diff --git a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java index dfe529acd..18d04dac8 100644 --- a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java +++ b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java @@ -97,7 +97,7 @@ public class ComputerTestDelegate if( REPORT_PATH.delete() ) ComputerCraft.log.info( "Deleted previous coverage report." ); - Terminal term = new Terminal( 78, 20 ); + Terminal term = new Terminal( 80, 30 ); IWritableMount mount = new FileMount( new File( "test-files/mount" ), 10_000_000 ); // Remove any existing files diff --git a/src/test/resources/test-rom/spec/apis/rednet_spec.lua b/src/test/resources/test-rom/spec/apis/rednet_spec.lua index f6c942e33..c2b98590d 100644 --- a/src/test/resources/test-rom/spec/apis/rednet_spec.lua +++ b/src/test/resources/test-rom/spec/apis/rednet_spec.lua @@ -83,4 +83,90 @@ describe("The rednet library", function() expect(rednet.lookup("a_protocol", "a_hostname")):eq(os.getComputerID()) end) end) + + describe("on a fake computers", function() + local fake_computer = require "support.fake_computer" + + local function computer_with_rednet(id, fn, options) + local computer = fake_computer.make_computer(id, function(_ENV) + local fns = { _ENV.rednet.run } + if options and options.rep then + fns[#fns + 1] = function() _ENV.dofile("rom/programs/rednet/repeat.lua") end + end + + if fn then + fns[#fns + 1] = function() + if options and options.open then + _ENV.rednet.open("back") + _ENV.os.queueEvent("x") _ENV.os.pullEvent("x") + end + return fn(_ENV.rednet, _ENV) + end + end + + return parallel.waitForAny(table.unpack(fns)) + end) + local modem = fake_computer.add_modem(computer, "back") + fake_computer.add_api(computer, "rom/apis/rednet.lua") + return computer, modem + end + + it("opens and closes channels", function() + local id = math.random(256) + local computer = computer_with_rednet(id, function(rednet) + expect(rednet.isOpen()):eq(false) + + rednet.open("back") + rednet.open("front") + + expect(rednet.isOpen()):eq(true) + expect(rednet.isOpen("back")):eq(true) + expect(rednet.isOpen("front")):eq(true) + + rednet.close("back") + expect(rednet.isOpen("back")):eq(false) + expect(rednet.isOpen("front")):eq(true) + expect(rednet.isOpen()):eq(true) + + rednet.close() + + expect(rednet.isOpen("back")):eq(false) + expect(rednet.isOpen("front")):eq(false) + expect(rednet.isOpen()):eq(false) + end) + fake_computer.add_modem(computer, "front") + + fake_computer.run_all { computer } + end) + + it("sends and receives rednet messages", function() + local computer_1, modem_1 = computer_with_rednet(1, function(rednet, _ENV) + rednet.send(2, "Hello") + end, { open = true }) + local computer_2, modem_2 = computer_with_rednet(2, function(rednet) + local id, message = rednet.receive() + expect(id):eq(1) + expect(message):eq("Hello") + end, { open = true }) + fake_computer.add_modem_edge(modem_1, modem_2) + + fake_computer.run_all { computer_1, computer_2 } + end) + + it("repeats messages between computers", function() + local computer_1, modem_1 = computer_with_rednet(1, function(rednet, _ENV) + rednet.send(3, "Hello") + end, { open = true }) + local computer_2, modem_2 = computer_with_rednet(2, nil, { open = true, rep = true }) + local computer_3, modem_3 = computer_with_rednet(3, function(rednet) + local id, message = rednet.receive() + expect(id):eq(1) + expect(message):eq("Hello") + end, { open = true }) + fake_computer.add_modem_edge(modem_1, modem_2) + fake_computer.add_modem_edge(modem_2, modem_3) + + fake_computer.run_all({ computer_1, computer_2, computer_3 }, { computer_1, computer_3 }) + end) + end) end) diff --git a/src/test/resources/test-rom/spec/support/fake_computer.lua b/src/test/resources/test-rom/spec/support/fake_computer.lua new file mode 100644 index 000000000..151b536ae --- /dev/null +++ b/src/test/resources/test-rom/spec/support/fake_computer.lua @@ -0,0 +1,146 @@ +local function keys(tbl) + local keys = {} + for k in pairs(tbl) do keys[#keys + 1] = k end + return keys +end + +local safe_globals = { + "assert", "bit32", "coroutine", "debug", "error", "fs", "getmetatable", "io", "ipairs", "math", "next", "pairs", + "pcall", "print", "printError", "rawequal", "rawget", "rawlen", "rawset", "select", "setmetatable", "string", + "table", "term", "tonumber", "tostring", "type", "utf8", "xpcall", +} + +--- Create a fake computer. +local function make_computer(id, fn) + local env = setmetatable({}, _G) + + local peripherals = {} + local events = { { n = 1, env } } + local function queue_event(...) events[#events + 1] = table.pack(...) end + + for _, k in pairs(safe_globals) do env[k] = _G[k] end + env.peripheral = { + getNames = function() return keys(peripherals) end, + isPresent = function(name) return peripherals[name] ~= nil end, + getType = function(name) return peripherals[name] and getmetatable(peripherals[name]).type end, + getMethods = function(name) return peripherals[name] and keys(peripherals[name]) end, + call = function(name, method, ...) + local p = peripherals[name] + if p then return p[method](...) end + return nil + end, + wrap = function(name) return peripherals[name] end, + } + env.os = { + getComputerID = function() return id end, + queueEvent = queue_event, + pullEventRaw = coroutine.yield, + pullEvent = function(filter) + local event_data = table.pack(coroutine.yield(filter)) + if event_data[1] == "terminate" then error("Terminated", 0) end + return table.unpack(event_data, 1, event_data.n) + end, + startTimer = function() return 0 end, + clock = function() return 0 end, + } + env.dofile = function(path) + local fn, err = loadfile(path, nil, env) + if fn then return fn() else error(err, 2) end + end + + local co = coroutine.create(fn) + local filter = nil + local function step() + while true do + if #events == 0 or coroutine.status(co) == "dead" then return false end + + local ev = table.remove(events, 1) + if filter == nil or ev[1] == filter or ev[1] == "terminated" then + local ok, result = coroutine.resume(co, table.unpack(ev, 1, ev.n)) + if not ok then + if type(result) == "table" and result.trace == nil then result.trace = debug.traceback(co) end + error(result, 0) + end + filter = result + return true + end + end + end + + return { env = env, peripherals = peripherals, queue_event = queue_event, step = step, co = co } +end + +--- Add a modem to a computer on a particular side +local function add_modem(owner, side) + local open, adjacent = {}, {} + local peripheral = setmetatable({ + open = function(channel) open[channel] = true end, + close = function(channel) open[channel] = false end, + closeAll = function(channel) open = {} end, + isOpen = function(channel) return open[channel] == true end, + transmit = function(channel, reply_channel, payload) + for _, adjacent in pairs(adjacent) do + if adjacent.open[channel] then + adjacent.owner.queue_event("modem_message", adjacent.side, channel, reply_channel, payload, 123) + end + end + end, + }, { type = "modem" }) + owner.peripherals[side] = peripheral + return { adjacent = adjacent, side = side, owner = owner, open = open } +end + +local function add_modem_edge(modem1, modem2) + table.insert(modem1.adjacent, modem2) + table.insert(modem2.adjacent, modem1) +end + +--- Load an API into the computer's environment. +local function add_api(computer, path) + local name = fs.getName(path) + if name:sub(-4) == ".lua" then name = name:sub(1, -5) end + + local child_env = {} + setmetatable(child_env, { __index = computer.env }) + assert(loadfile(path, nil, child_env))() + + local api = {} + for k, v in pairs(child_env) do api[k] = v end + + computer.env[name] = api +end + +--- Step all computers forward by one event. +local function step_all(computers) + local any = false + for _, computer in pairs(computers) do + if computer.step() then any = true end + end + return any +end + +--- Run all computers until their event queue is empty. +local function run_all(computers, require_done) + while step_all(computers) do end + + if require_done ~= false then + if type(require_done) == "table" then + for _, v in ipairs(require_done) do require_done[v] = true end + end + + for _, computer in pairs(computers) do + if coroutine.status(computer.co) ~= "dead" and (type(require_done) ~= "table" or require_done[computer]) then + error(debug.traceback(computer.co, "Computer did not shutdown"), 0) + end + end + end +end + +return { + make_computer = make_computer, + add_modem = add_modem, + add_modem_edge = add_modem_edge, + add_api = add_api, + step_all = step_all, + run_all = run_all, +}