commit 8d1cdf7514e369e2974dd205c6bbe046f7f823af Author: Rokas Puzonas Date: Thu May 11 21:50:02 2023 +0300 initial commit diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..99bd5c9 --- /dev/null +++ b/.luarc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", + "Lua.diagnostics.globals": [ + "peripheral" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6caf9ce --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Fake CC peripherals + +Create fake CC peripherals, useful for testing libraries which directly interact with peripherals diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..6b81847 --- /dev/null +++ b/init.lua @@ -0,0 +1,221 @@ +local PeripheralAPIFake = {} +PeripheralAPIFake.__index = PeripheralAPIFake + +local expect = require("cc.expect").expect + +local function isPeripheral(peripheral) + if type(peripheral) ~= "table" then return false end + local meta = getmetatable(peripheral) + + if type(meta) ~= "table" then return false end + if meta.__name ~= "peripheral" then return false end + if type(meta.name) ~= "string" then return false end + if type(meta.type) ~= "string" then return false end + if type(meta.types) ~= "table" then return false end + for _, ty in ipairs(meta.types) do + if not meta.types[ty] then return false end + end + + return true +end + +-- TODO: Add descriptive error messages to asserts +local function expectPeripheral(index, peripheral) + if not isPeripheral(peripheral) then + error(("bad argument #%d (table is not a peripheral)"):format(index), 3) + end +end + +--- Performs a deep clone of a table. +local function cloneTable(t) + local mt = getmetatable(t) + setmetatable(t, nil) + local cloned = {} + for k, v in pairs(t) do + if type(v) == "table" then + cloned[k] = cloneTable(v) + else + cloned[k] = v + end + end + setmetatable(t, mt) + return cloned +end + +local function doesTableContain(t, value) + for _, v in pairs(t) do + if v == value then + return true + end + end + return false +end + +local function getPeripheralName(peripheral) + return getmetatable(peripheral).name +end + +local function getPeripheralTypes(peripheral) + return getmetatable(peripheral).types +end + +function PeripheralAPIFake.new(peripherals) + expect(1, peripherals, "table", "nil") + + local self = setmetatable({}, PeripheralAPIFake) + self.peripherals = {} + + if peripherals then + for _, peripheral in ipairs(peripherals) do + self:addPeripehral(peripheral) + end + end + + return self +end + +function PeripheralAPIFake:newExternalAPI() + local api = {} + + function api.call(name, method, ...) + expect(1, name, "string") + expect(2, method, "string") + local peripheral = self.peripherals[name] + if not peripheral then return end + local func = peripheral[method] + if type(func) ~= "function" then + error(("No such method %s"):format(method), 2) + end + return func(...) + end + + function api.find(ty, filter) + expect(1, ty, "string") + expect(2, filter, "function", "nil") + local peripherals = {} + for name, peripheral in pairs(self.peripherals) do + local types = getPeripheralTypes(peripheral) + if doesTableContain(types, ty) then + local wrapped = api.wrap(name) + if not filter or filter(name, wrapped) then + table.insert(peripherals, wrapped) + end + end + end + return peripherals + end + + function api.getMethods(name) + expect(1, name, "string") + local peripheral = self:getPeripheral(name) + if not peripheral then return end + + local methods = {} + for methodName, method in pairs(peripheral) do + if type(method) == "function" then + table.insert(methods, methodName) + end + end + return methods + end + + function api.getName(peripheral) + expect(1, peripheral, "table", "string") + expectPeripheral(1, peripheral) + return getPeripheralName(peripheral) + end + + function api.getNames() + return self:listPeripherals() + end + + function api.getType(peripheral) + expect(1, peripheral, "table", "string") + if type(peripheral) == "table" then + expectPeripheral(1, peripheral) + end + local p = self:getPeripheral(peripheral) + return table.unpack(getPeripheralTypes(p)) + end + + function api.hasType(peripheral, peripheral_type) + expect(1, peripheral, "table", "string") + if type(peripheral) == "table" then + expectPeripheral(1, peripheral) + end + expect(2, peripheral_type, "string") + local p = self:getPeripheral(peripheral) + if not p then return end + + local types = getPeripheralTypes(peripheral) + return doesTableContain(types, peripheral_type) + end + + function api.isPresent(name) + expect(1, name, "string") + return self:hasPeripheral(name) + end + + function api.wrap(name) + expect(1, name, "string") + local peripheral = self.peripherals[name] + if not peripheral then return end + return cloneTable(peripheral) + end + + return api +end + +function PeripheralAPIFake:addPeripehral(peripheral) + expect(1, peripheral, "table") + expectPeripheral(1, peripheral) + + local name = getPeripheralName(peripheral) + assert(not self.peripherals[name], "Expected peripheral name to be unique, it's not unique") + assert(not self:hasPeripheral(peripheral), "Peripheral already added to system") + + self.peripherals[name] = peripheral +end + +function PeripheralAPIFake:removePeripheral(peripheral) + expect(1, peripheral, "table", "string") + + if type(peripheral) == "string" then + self.peripherals[peripheral] = nil + else + expectPeripheral(1, peripheral) + local name = getPeripheralName(peripheral) + self.peripherals[name] = nil + end +end + +function PeripheralAPIFake:hasPeripheral(peripheral) + expect(1, peripheral, "table", "string") + return self:getPeripheral(peripheral) ~= nil +end + +function PeripheralAPIFake:getPeripheral(peripheral) + expect(1, peripheral, "table", "string") + + if type(peripheral) == "string" then + return self.peripherals[peripheral] + else + expectPeripheral(1, peripheral) + local name = getPeripheralName(peripheral) + return self.peripherals[name] + end +end + +function PeripheralAPIFake:getPeripheralName(peripheral) + return getPeripheralName(self:getPeripheral(peripheral)) +end + +function PeripheralAPIFake:listPeripherals() + local names = {} + for _, peripheral in pairs(self.peripherals) do + table.insert(names, getPeripheralName(peripheral)) + end + return names +end + +return PeripheralAPIFake diff --git a/init.test.lua b/init.test.lua new file mode 100644 index 0000000..ba9fa13 --- /dev/null +++ b/init.test.lua @@ -0,0 +1,195 @@ +local lust = require("lust").nocolor() +local PeripheralAPIMock = require("init") +local describe, it, expect = lust.describe, lust.it, lust.expect + +local function newPeripheralStub(name, types, methodNames) + local peripheral = {} + local metaTypes = {} + for i, ty in ipairs(types) do + metaTypes[i] = ty + metaTypes[ty] = true + end + + setmetatable(peripheral, { + __name = "peripheral", + name = name, + type = types[1], + types = metaTypes + }) + + for _, methodName in pairs(methodNames) do + peripheral[methodName] = function() end + end + + return peripheral +end + +local function newInventoryStub(type, id) + return newPeripheralStub(("%s_%d"):format(type, id), {type, "inventory"}, { + "size", + "list", + "getItemDetail", + "getItemLimit", + "pushItems", + "pullItems" + }) +end + +local function pprint(...) + local pretty = require("cc.pretty") + pretty.print(pretty.pretty(...)) +end + +describe("peripherals mock", function() + it("initialize with no peripherals by default", function() + local peripheral = PeripheralAPIMock.new() + expect(peripheral:listPeripherals()).to.equal{} + end) + + it("initialize with initial peripherals", function() + local chest1 = newInventoryStub("minecraft:chest", 1) + local chest2 = newInventoryStub("minecraft:chest", 2) + local peripheral = PeripheralAPIMock.new{ chest1, chest2 } + expect(peripheral:listPeripherals()).to.equal{ + "minecraft:chest_1", + "minecraft:chest_2", + } + end) + + it("throw error if given table is not a valid peripheral", function() + local chest = { } + expect(function() + PeripheralAPIMock.new{ chest } + end).to.fail() + end) + + it("check if peripheral is added", function() + local chest = newInventoryStub("minecraft:chest", 1) + local peripheral = PeripheralAPIMock.new{ chest } + expect(peripheral:hasPeripheral(chest)).to.be.truthy() + end) + + it("check if peripheral is removed", function() + local chest = newInventoryStub("minecraft:chest", 1) + local peripheral = PeripheralAPIMock.new{ chest } + expect(peripheral:hasPeripheral(chest)).to.be.truthy() + peripheral:removePeripheral(chest) + expect(peripheral:hasPeripheral(chest)).to_not.be.truthy() + end) + + it("listPeripherals returns a new list every time", function() + local peripheral = PeripheralAPIMock.new() + expect(peripheral:listPeripherals()).to_not.be(peripheral:listPeripherals()) + end) + + describe("external api", function() + local chest1, chest2, api + lust.before(function() + chest1 = newInventoryStub("minecraft:chest", 1) + chest2 = newInventoryStub("minecraft:chest", 2) + local peripheral = PeripheralAPIMock.new{ chest1, chest2 } + api = peripheral:newExternalAPI() + end) + + it("has the same methods as the real api", function() + for methodName in pairs(peripheral) do + assert(type(api[methodName]) == "function", ("Missing method %s"):format(methodName)) + end + expect(#api).to.be(#peripheral) + end) + + it("using .call and .wrap, invoke the same function", function() + local spy = lust.spy(chest1, "size") + api.call("minecraft:chest_1", "size") + expect(#spy).to.be(1) + api.wrap("minecraft:chest_1").size() + expect(#spy).to.be(2) + end) + + it(".call dosen't supply self to methods", function() + local spy = lust.spy(chest1, "size") + api.call("minecraft:chest_1", "size", 1, 2, 3) + expect(spy).to.equal{{1, 2, 3}} + end) + + it(".call returns nil if peripheral was not found", function() + expect(api.call("foobar", "size")).to.be(nil) + end) + + it(".call throws error if peripheral dosen't have requested method", function() + expect(function() + api.call("minecraft:chest_1", "foobar") + end).to.fail() + end) + + it(".wrap returns a new table each time, but have the same contents", function() + local name = "minecraft:chest_1" + expect(api.wrap(name)).to.equal(api.wrap(name)) + expect(api.wrap(name)).to_not.be(api.wrap(name)) + end) + + it(".wrap returns nil if peripheral was not found", function() + expect(api.wrap("foobar")).to.be(nil) + end) + + it("check if peripheral exists if .isPresent", function() + expect(api.isPresent("minecraft:chest_1")).to.be(true) + expect(api.isPresent("foobar")).to.be(false) + end) + + it(".getType throws error if given table is not a peripheral", function() + expect(function () + api.getType({}) + end).to.fail() + + expect(function () + api.getType(chest1) + end).to_not.fail() + end) + + it(".getType returns multiple types if that peripheral has it", function() + local types = {api.getType("minecraft:chest_1")} + expect(types).to.equal{"minecraft:chest", "inventory"} + end) + + it(".getNames returns a new table each time, but have the same contents", function() + expect(api.getNames()).to.equal(api.getNames()) + expect(api.getNames()).to_not.be(api.getNames()) + end) + + it(".getName throws error if table is not a valid peripheral", function() + expect(function() + api.getName({}) + end).to.fail() + + expect(function() + api.getName(chest1) + end).to_not.fail() + end) + + it(".getMethods returns nil if peripheral is not found", function() + expect(api.getMethods("foobar")).to.be(nil) + end) + + it(".hasType returns nil if peripheral is not found", function() + expect(api.hasType("foobar", "inventory")).to.be(nil) + end) + + it(".hasType returns true if peripheral has target type", function() + expect(api.hasType(chest1, "inventory")).to.be(true) + end) + + it(".find returns a list of wrapped peripherals", function() + local peripherals = api.find("inventory") + expect(peripherals).to.equal{ chest1, chest2 } + end) + + it(".find with filter returns a list of wrapped peripherals", function() + local peripherals = api.find("inventory", function(_, wrapped) + return wrapped.size ~= nil + end) + expect(peripherals).to.equal{ chest1, chest2 } + end) + end) +end) + diff --git a/lust.lua b/lust.lua new file mode 100644 index 0000000..791c409 --- /dev/null +++ b/lust.lua @@ -0,0 +1,231 @@ +-- lust v0.1.0 - Lua test framework +-- https://github.com/bjornbytes/lust +-- MIT LICENSE + +local lust = {} +lust.level = 0 +lust.passes = 0 +lust.errors = 0 +lust.befores = {} +lust.afters = {} + +local red = string.char(27) .. '[31m' +local green = string.char(27) .. '[32m' +local normal = string.char(27) .. '[0m' +local function indent(level) return string.rep('\t', level or lust.level) end + +function lust.nocolor() + red, green, normal = '', '', '' + return lust +end + +function lust.describe(name, fn) + print(indent() .. name) + lust.level = lust.level + 1 + fn() + lust.befores[lust.level] = {} + lust.afters[lust.level] = {} + lust.level = lust.level - 1 +end + +function lust.it(name, fn) + for level = 1, lust.level do + if lust.befores[level] then + for i = 1, #lust.befores[level] do + lust.befores[level][i](name) + end + end + end + + local success, err = pcall(fn) + if success then lust.passes = lust.passes + 1 + else lust.errors = lust.errors + 1 end + local color = success and green or red + local label = success and 'PASS' or 'FAIL' + print(indent() .. color .. label .. normal .. ' ' .. name) + if err then + print(indent(lust.level + 1) .. red .. err .. normal) + end + + for level = 1, lust.level do + if lust.afters[level] then + for i = 1, #lust.afters[level] do + lust.afters[level][i](name) + end + end + end +end + +function lust.before(fn) + lust.befores[lust.level] = lust.befores[lust.level] or {} + table.insert(lust.befores[lust.level], fn) +end + +function lust.after(fn) + lust.afters[lust.level] = lust.afters[lust.level] or {} + table.insert(lust.afters[lust.level], fn) +end + +-- Assertions +local function isa(v, x) + if type(x) == 'string' then + return type(v) == x, + 'expected ' .. tostring(v) .. ' to be a ' .. x, + 'expected ' .. tostring(v) .. ' to not be a ' .. x + elseif type(x) == 'table' then + if type(v) ~= 'table' then + return false, + 'expected ' .. tostring(v) .. ' to be a ' .. tostring(x), + 'expected ' .. tostring(v) .. ' to not be a ' .. tostring(x) + end + + local seen = {} + local meta = v + while meta and not seen[meta] do + if meta == x then return true end + seen[meta] = true + meta = getmetatable(meta) and getmetatable(meta).__index + end + + return false, + 'expected ' .. tostring(v) .. ' to be a ' .. tostring(x), + 'expected ' .. tostring(v) .. ' to not be a ' .. tostring(x) + end + + error('invalid type ' .. tostring(x)) +end + +local function has(t, x) + for k, v in pairs(t) do + if v == x then return true end + end + return false +end + +local function strict_eq(t1, t2) + if type(t1) ~= type(t2) then return false end + if type(t1) ~= 'table' then return t1 == t2 end + for k, _ in pairs(t1) do + if not strict_eq(t1[k], t2[k]) then return false end + end + for k, _ in pairs(t2) do + if not strict_eq(t2[k], t1[k]) then return false end + end + return true +end + +local paths = { + [''] = { 'to', 'to_not' }, + to = { 'have', 'equal', 'be', 'exist', 'fail' }, + to_not = { 'have', 'equal', 'be', 'exist', 'fail', chain = function(a) a.negate = not a.negate end }, + a = { test = isa }, + an = { test = isa }, + be = { 'a', 'an', 'truthy', + test = function(v, x) + return v == x, + 'expected ' .. tostring(v) .. ' and ' .. tostring(x) .. ' to be equal', + 'expected ' .. tostring(v) .. ' and ' .. tostring(x) .. ' to not be equal' + end + }, + exist = { + test = function(v) + return v ~= nil, + 'expected ' .. tostring(v) .. ' to exist', + 'expected ' .. tostring(v) .. ' to not exist' + end + }, + truthy = { + test = function(v) + return v, + 'expected ' .. tostring(v) .. ' to be truthy', + 'expected ' .. tostring(v) .. ' to not be truthy' + end + }, + equal = { + test = function(v, x) + return strict_eq(v, x), + 'expected ' .. tostring(v) .. ' and ' .. tostring(x) .. ' to be exactly equal', + 'expected ' .. tostring(v) .. ' and ' .. tostring(x) .. ' to not be exactly equal' + end + }, + have = { + test = function(v, x) + if type(v) ~= 'table' then + error('expected ' .. tostring(v) .. ' to be a table') + end + + return has(v, x), + 'expected ' .. tostring(v) .. ' to contain ' .. tostring(x), + 'expected ' .. tostring(v) .. ' to not contain ' .. tostring(x) + end + }, + fail = { + test = function(v) + return not pcall(v), + 'expected ' .. tostring(v) .. ' to fail', + 'expected ' .. tostring(v) .. ' to not fail' + end + } +} + +function lust.expect(v) + local assertion = {} + assertion.val = v + assertion.action = '' + assertion.negate = false + + setmetatable(assertion, { + __index = function(t, k) + if has(paths[rawget(t, 'action')], k) then + rawset(t, 'action', k) + local chain = paths[rawget(t, 'action')].chain + if chain then chain(t) end + return t + end + return rawget(t, k) + end, + __call = function(t, ...) + if paths[t.action].test then + local res, err, nerr = paths[t.action].test(t.val, ...) + if assertion.negate then + res = not res + err = nerr or err + end + if not res then + error(err or 'unknown failure', 2) + end + end + end + }) + + return assertion +end + +function lust.spy(target, name, run) + local spy = {} + local subject + + local function capture(...) + table.insert(spy, {...}) + return subject(...) + end + + if type(target) == 'table' then + subject = target[name] + target[name] = capture + else + run = name + subject = target or function() end + end + + setmetatable(spy, {__call = function(_, ...) return capture(...) end}) + + if run then run() end + + return spy +end + +lust.test = lust.it +lust.paths = paths + +return lust