diff --git a/.github/workflows/check-release.yml b/.github/workflows/check-release.yml index 368507f..af49de0 100644 --- a/.github/workflows/check-release.yml +++ b/.github/workflows/check-release.yml @@ -33,3 +33,6 @@ jobs: - name: run mesecons_mvps tests working-directory: ./mesecons_mvps/ run: $HOME/.luarocks/bin/mineunit -q + - name: run mesecons_fpga tests + working-directory: ./mesecons_fpga/ + run: $HOME/.luarocks/bin/mineunit -q diff --git a/.luacheckrc b/.luacheckrc index 897ba25..d93fb87 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -36,9 +36,13 @@ files["mesecons/actionqueue.lua"] = { } files["*/spec/**/*.lua"] = { - read_globals = {"assert", "fixture", "mineunit", "sourcefile", "world"}, + read_globals = {"assert", "fixture", "mineunit", "Player", "sourcefile", "world"}, } files["mesecons/spec/fixtures/voxelmanip.lua"] = { globals = {"minetest.get_voxel_manip"}, } + +files["mesecons_fpga/spec/fixtures/mesecons_fpga.lua"] = { + globals = {"minetest.register_on_player_receive_fields"}, +} diff --git a/mesecons_fpga/spec/fixtures/mesecons_fpga.lua b/mesecons_fpga/spec/fixtures/mesecons_fpga.lua new file mode 100644 index 0000000..2e3c565 --- /dev/null +++ b/mesecons_fpga/spec/fixtures/mesecons_fpga.lua @@ -0,0 +1,57 @@ +mineunit("player") + +fixture("mesecons") + +local registered_on_player_receive_fields = {} +local old_register_on_player_receive_fields = minetest.register_on_player_receive_fields +function minetest.register_on_player_receive_fields(func) + old_register_on_player_receive_fields(func) + table.insert(registered_on_player_receive_fields, func) +end + +mineunit:set_current_modname("mesecons_fpga") +mineunit:set_modpath("mesecons_fpga", "../mesecons_fpga") +sourcefile("../mesecons_fpga/init") + +local fpga_user = Player("mesecons_fpga_user") + +function mesecon._test_program_fpga(pos, program) + local node = minetest.get_node(pos) + assert.equal("mesecons_fpga:fpga", node.name:sub(1, 18)) + + local fields = {program = true} + for i, instr in ipairs(program) do + local op1, act, op2, dst + if #instr == 3 then + act, op2, dst = unpack(instr) + else + assert.equal(4, #instr) + op1, act, op2, dst = unpack(instr) + end + fields[i .. "op1"] = op1 + fields[i .. "act"] = (" "):rep(4 - #act) .. act + fields[i .. "op2"] = op2 + fields[i .. "dst"] = dst + end + + minetest.registered_nodes[node.name].on_rightclick(pos, node, fpga_user) + + for _, func in ipairs(registered_on_player_receive_fields) do + if func(fpga_user, "mesecons:fpga", fields) then + break + end + end +end + +function mesecon._test_copy_fpga_program(pos) + fpga_user:get_inventory():set_stack("main", 1, "mesecons_fpga:programmer") + local pt = {type = "node", under = vector.new(pos), above = vector.offset(pos, 0, 1, 0)} + fpga_user:do_place(pt) + return fpga_user:get_wielded_item() +end + +function mesecon._test_paste_fpga_program(pos, tool) + fpga_user:get_inventory():set_stack("main", 1, tool) + local pt = {type = "node", under = vector.new(pos), above = vector.offset(pos, 0, 1, 0)} + fpga_user:do_use(pt) +end diff --git a/mesecons_fpga/spec/fixtures/screwdriver.lua b/mesecons_fpga/spec/fixtures/screwdriver.lua new file mode 100644 index 0000000..1a98de0 --- /dev/null +++ b/mesecons_fpga/spec/fixtures/screwdriver.lua @@ -0,0 +1,6 @@ +mineunit:set_current_modname("screwdriver") + +screwdriver = {} + +screwdriver.ROTATE_FACE = 1 +screwdriver.ROTATE_AXIS = 2 diff --git a/mesecons_fpga/spec/helper_spec.lua b/mesecons_fpga/spec/helper_spec.lua new file mode 100644 index 0000000..d577d07 --- /dev/null +++ b/mesecons_fpga/spec/helper_spec.lua @@ -0,0 +1,134 @@ +require("mineunit") + +fixture("mesecons_fpga") +fixture("screwdriver") + +local pos = {x = 0, y = 0, z = 0} +local pos_a = {x = -1, y = 0, z = 0} +local pos_b = {x = 0, y = 0, z = 1} +local pos_c = {x = 1, y = 0, z = 0} +local pos_d = {x = 0, y = 0, z = -1} + +describe("FPGA rotation", function() + before_each(function() + world.set_node(pos, "mesecons_fpga:fpga0000") + end) + + after_each(function() + mesecon._test_reset() + world.clear() + end) + + it("rotates I/O operands clockwise", function() + mesecon._test_program_fpga(pos, {{"A", "OR", "B", "C"}}) + + local node = world.get_node(pos) + minetest.registered_nodes[node.name].on_rotate(pos, node, nil, screwdriver.ROTATE_FACE) + + mesecon._test_place(pos_b, "mesecons:test_receptor_on") + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons_fpga:fpga1000", world.get_node(pos).name) + + mesecon._test_dig(pos_b) + mesecon._test_place(pos_c, "mesecons:test_receptor_on") + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons_fpga:fpga1000", world.get_node(pos).name) + end) + + it("rotates I/O operands counterclockwise", function() + mesecon._test_program_fpga(pos, {{"A", "OR", "B", "C"}}) + + local node = world.get_node(pos) + minetest.registered_nodes[node.name].on_rotate(pos, node, nil, screwdriver.ROTATE_AXIS) + + mesecon._test_place(pos_d, "mesecons:test_receptor_on") + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons_fpga:fpga0010", world.get_node(pos).name) + + mesecon._test_dig(pos_d) + mesecon._test_place(pos_a, "mesecons:test_receptor_on") + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons_fpga:fpga0010", world.get_node(pos).name) + end) + + it("updates ports", function() + mesecon._test_program_fpga(pos, {{"NOT", "A", "B"}}) + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons_fpga:fpga0010", world.get_node(pos).name) + + local node = world.get_node(pos) + minetest.registered_nodes[node.name].on_rotate(pos, node, nil, screwdriver.ROTATE_AXIS) + assert.equal("mesecons_fpga:fpga0001", world.get_node(pos).name) + end) +end) + +-- mineunit does not support deprecated ItemStack:get_metadata() +pending("FPGA programmer", function() + local pos2 = {x = 10, y = 0, z = 0} + + before_each(function() + world.set_node(pos, "mesecons_fpga:fpga0000") + world.set_node(pos2, "mesecons_fpga:fpga0000") + end) + + after_each(function() + mesecon._test_reset() + world.clear() + end) + + it("transfers instructions", function() + mesecon._test_program_fpga(pos2, {{"NOT", "A", "B"}}) + + mesecon._test_paste_fpga_program(pos, mesecon._test_copy_fpga_program(pos2)) + + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons_fpga:fpga0010", world.get_node(pos).name) + end) + + it("does not copy from new FPGAs", function() + mesecon._test_program_fpga(pos, {{"NOT", "A", "B"}}) + + mineunit:execute_globalstep() + mineunit:execute_globalstep() + + mesecon._test_paste_fpga_program(pos, mesecon._test_copy_fpga_program(pos2)) + + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons_fpga:fpga0010", world.get_node(pos).name) + end) + + it("does not copy from cleared FPGAs", function() + mesecon._test_program_fpga(pos, {{"NOT", "A", "B"}}) + + mineunit:execute_globalstep() + mineunit:execute_globalstep() + + mesecon._test_program_fpga(pos2, {{"=", "A", "B"}}) + mesecon._test_program_fpga(pos2, {}) + mesecon._test_paste_fpga_program(pos, mesecon._test_copy_fpga_program(pos2)) + + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons_fpga:fpga0010", world.get_node(pos).name) + end) + + it("does not copy from non-FPGA nodes", function() + mesecon._test_program_fpga(pos, {{"NOT", "A", "B"}}) + + mineunit:execute_globalstep() + mineunit:execute_globalstep() + + mesecon._test_paste_fpga_program(pos, mesecon._test_copy_fpga_program(vector.add(pos2, 1))) + + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons_fpga:fpga0010", world.get_node(pos).name) + end) +end) diff --git a/mesecons_fpga/spec/logic_spec.lua b/mesecons_fpga/spec/logic_spec.lua new file mode 100644 index 0000000..e79fce0 --- /dev/null +++ b/mesecons_fpga/spec/logic_spec.lua @@ -0,0 +1,195 @@ +require("mineunit") + +fixture("mesecons_fpga") + +describe("FPGA logic", function() + local pos = {x = 0, y = 0, z = 0} + local pos_a = {x = -1, y = 0, z = 0} + local pos_b = {x = 0, y = 0, z = 1} + local pos_c = {x = 1, y = 0, z = 0} + local pos_d = {x = 0, y = 0, z = -1} + + local fpga_set = false + + local function set_fpga() + if not fpga_set then + world.set_node(pos, "mesecons_fpga:fpga0000") + fpga_set = true + end + end + before_each(set_fpga) + + local function reset_world() + if fpga_set then + mesecon._test_reset() + world.clear() + fpga_set = false + end + end + after_each(reset_world) + + local function test_program(inputs, outputs, program) + set_fpga() + + mesecon._test_program_fpga(pos, program) + + if inputs.a then mesecon._test_place(pos_a, "mesecons:test_receptor_on") end + if inputs.b then mesecon._test_place(pos_b, "mesecons:test_receptor_on") end + if inputs.c then mesecon._test_place(pos_c, "mesecons:test_receptor_on") end + if inputs.d then mesecon._test_place(pos_d, "mesecons:test_receptor_on") end + mineunit:execute_globalstep() + mineunit:execute_globalstep() + + local expected_name = "mesecons_fpga:fpga" + .. (outputs.d and 1 or 0) .. (outputs.c and 1 or 0) + .. (outputs.b and 1 or 0) .. (outputs.a and 1 or 0) + assert.equal(expected_name, world.get_node(pos).name) + + reset_world() + end + + it("operator and", function() + local prog = {{"A", "AND", "B", "C"}} + test_program({}, {}, prog) + test_program({a = true}, {}, prog) + test_program({b = true}, {}, prog) + test_program({a = true, b = true}, {c = true}, prog) + end) + + it("operator or", function() + local prog = {{"A", "OR", "B", "C"}} + test_program({}, {}, prog) + test_program({a = true}, {c = true}, prog) + test_program({b = true}, {c = true}, prog) + test_program({a = true, b = true}, {c = true}, prog) + end) + + it("operator not", function() + local prog = {{"NOT", "A", "B"}} + test_program({}, {b = true}, prog) + test_program({a = true}, {}, prog) + end) + + it("operator xor", function() + local prog = {{"A", "XOR", "B", "C"}} + test_program({}, {}, prog) + test_program({a = true}, {c = true}, prog) + test_program({b = true}, {c = true}, prog) + test_program({a = true, b = true}, {}, prog) + end) + + it("operator nand", function() + local prog = {{"A", "NAND", "B", "C"}} + test_program({}, {c = true}, prog) + test_program({a = true}, {c = true}, prog) + test_program({b = true}, {c = true}, prog) + test_program({a = true, b = true}, {}, prog) + end) + + it("operator buf", function() + local prog = {{"=", "A", "B"}} + test_program({}, {}, prog) + test_program({a = true}, {b = true}, prog) + end) + + it("operator xnor", function() + local prog = {{"A", "XNOR", "B", "C"}} + test_program({}, {c = true}, prog) + test_program({a = true}, {}, prog) + test_program({b = true}, {}, prog) + test_program({a = true, b = true}, {c = true}, prog) + end) + + it("operator nor", function() + local prog = {{"A", "NOR", "B", "C"}} + test_program({}, {c = true}, prog) + test_program({a = true}, {}, prog) + test_program({b = true}, {}, prog) + test_program({a = true, b = true}, {}, prog) + end) + + it("rejects duplicate operands", function() + test_program({a = true}, {}, {{"A", "OR", "A", "B"}}) + test_program({a = true}, {}, {{"=", "A", "0"}, {"0", "OR", "0", "B"}}) + end) + + it("rejects unassigned memory operands", function() + test_program({a = true}, {}, {{"A", "OR", "0", "B"}}) + test_program({a = true}, {}, {{"0", "OR", "A", "B"}}) + end) + + it("rejects double memory assignment", function() + test_program({a = true}, {}, {{"=", "A", "0"}, {"=", "A", "0"}, {"=", "0", "B"}}) + end) + + it("rejects assignment to memory operand", function() + test_program({a = true}, {}, {{"=", "A", "0"}, {"A", "OR", "0", "0"}, {"=", "0", "B"}}) + end) + + it("allows double port assignment", function() + test_program({a = true}, {b = true}, {{"=", "A", "B"}, {"=", "A", "B"}}) + end) + + it("allows assignment to port operand", function() + test_program({a = true}, {b = true}, {{"A", "OR", "B", "B"}}) + end) + + it("preserves initial pin states", function() + test_program({a = true}, {b = true}, {{"=", "A", "B"}, {"=", "B", "C"}}) + end) + + it("rejects binary operations with single operands", function() + test_program({a = true}, {}, {{"=", "A", "B"}, {" ", "OR", "A", "C"}}) + test_program({a = true}, {}, {{"=", "A", "B"}, {"A", "OR", " ", "C"}}) + end) + + it("rejects unary operations with first operands", function() + test_program({a = true}, {}, {{"=", "A", "B"}, {"A", "=", " ", "C"}}) + end) + + it("rejects operations without destinations", function() + test_program({a = true}, {}, {{"=", "A", "B"}, {"=", "A", " "}}) + end) + + it("allows blank statements", function() + test_program({a = true}, {b = true, c = true}, { + {" ", " ", " ", " "}, + {"=", "A", "B"}, + {" ", " ", " ", " "}, + {" ", " ", " ", " "}, + {"=", "A", "C"}, + }) + end) + + it("considers past outputs in determining inputs", function() + -- Memory cell: Turning on A turns on C; turning on B turns off C. + mesecon._test_program_fpga(pos, { + {"A", "OR", "C", "0"}, + {"B", "OR", "D", "1"}, + {"NOT", "A", "2"}, + {"NOT", "B", "3"}, + {"0", "AND", "3", "C"}, + {"1", "AND", "2", "D"}, + }) + + mesecon._test_place(pos_a, "mesecons:test_receptor_on") + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons_fpga:fpga0100", world.get_node(pos).name) + + mesecon._test_dig(pos_a) + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons_fpga:fpga0100", world.get_node(pos).name) + + mesecon._test_place(pos_b, "mesecons:test_receptor_on") + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons_fpga:fpga1000", world.get_node(pos).name) + + mesecon._test_dig(pos_b) + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons_fpga:fpga1000", world.get_node(pos).name) + end) +end) diff --git a/mesecons_fpga/spec/mineunit.conf b/mesecons_fpga/spec/mineunit.conf new file mode 100644 index 0000000..b6d7b31 --- /dev/null +++ b/mesecons_fpga/spec/mineunit.conf @@ -0,0 +1 @@ +fixture_paths = {"../mesecons/spec/fixtures"}