diff --git a/.luacheckrc b/.luacheckrc index c8cccda..897ba25 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -34,3 +34,11 @@ globals = {"mesecon"} files["mesecons/actionqueue.lua"] = { globals = {"minetest.registered_globalsteps"}, } + +files["*/spec/**/*.lua"] = { + read_globals = {"assert", "fixture", "mineunit", "sourcefile", "world"}, +} + +files["mesecons/spec/fixtures/voxelmanip.lua"] = { + globals = {"minetest.get_voxel_manip"}, +} diff --git a/mesecons/spec/action_spec.lua b/mesecons/spec/action_spec.lua new file mode 100644 index 0000000..22539ff --- /dev/null +++ b/mesecons/spec/action_spec.lua @@ -0,0 +1,62 @@ +require("mineunit") + +fixture("mesecons") + +describe("action queue", function() + local layout = { + {{x = 1, y = 0, z = 0}, "mesecons:test_receptor_off"}, + {{x = 0, y = 0, z = 0}, "mesecons:test_conductor_off"}, + {{x = -1, y = 0, z = 0}, "mesecons:test_conductor_off"}, + {{x = 0, y = 1, z = 0}, "mesecons:test_effector"}, + {{x = -1, y = 1, z = 0}, "mesecons:test_effector"}, + } + + before_each(function() + world.layout(layout) + end) + + after_each(function() + mesecon._test_reset() + world.clear() + end) + + it("executes in order", function() + mesecon.swap_node_force(layout[1][1], "mesecons:test_receptor_on") + mesecon.receptor_on(layout[1][1], mesecon.rules.alldirs) + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal(2, #mesecon._test_effector_events) + assert.same({"on", layout[4][1]}, mesecon._test_effector_events[1]) + assert.same({"on", layout[5][1]}, mesecon._test_effector_events[2]) + + mesecon.swap_node_force(layout[1][1], "mesecons:test_receptor_off") + mesecon.receptor_off(layout[1][1], mesecon.rules.alldirs) + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal(4, #mesecon._test_effector_events) + assert.same({"off", layout[4][1]}, mesecon._test_effector_events[3]) + assert.same({"off", layout[5][1]}, mesecon._test_effector_events[4]) + end) + + it("ignores overwritten actions", function() + mesecon.swap_node_force(layout[1][1], "mesecons:test_receptor_on") + mesecon.receptor_on(layout[1][1], mesecon.rules.alldirs) + mesecon.swap_node_force(layout[1][1], "mesecons:test_receptor_off") + mesecon.receptor_off(layout[1][1], mesecon.rules.alldirs) + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal(0, #mesecon._test_effector_events) + end) + + it("delays actions", function() + mesecon.swap_node_force(layout[1][1], "mesecons:test_receptor_on") + mesecon.queue:add_action(layout[1][1], "receptor_on", {mesecon.rules.alldirs}, 1, nil) + mineunit:execute_globalstep(0.1) + mineunit:execute_globalstep(1) + assert.equal(0, #mesecon._test_effector_events) + mineunit:execute_globalstep() + assert.equal(0, #mesecon._test_effector_events) + mineunit:execute_globalstep() + assert.equal(2, #mesecon._test_effector_events) + end) +end) diff --git a/mesecons/spec/fixtures/mesecons.lua b/mesecons/spec/fixtures/mesecons.lua new file mode 100644 index 0000000..ec0fbb2 --- /dev/null +++ b/mesecons/spec/fixtures/mesecons.lua @@ -0,0 +1,128 @@ +mineunit("core") +mineunit("server") + +fixture("voxelmanip") + +sourcefile("init") + +do + local off_spec = {conductor = { + state = mesecon.state.off, + rules = mesecon.rules.alldirs, + onstate = "mesecons:test_conductor_on", + }} + local on_spec = {conductor = { + state = mesecon.state.on, + rules = mesecon.rules.alldirs, + offstate = "mesecons:test_conductor_off", + }} + mesecon.register_node("mesecons:test_conductor", { + description = "Test Conductor", + }, {mesecons = off_spec}, {mesecons = on_spec}) +end + +do + local off_spec = {receptor = { + state = mesecon.state.off, + rules = mesecon.rules.alldirs, + }} + local on_spec = {receptor = { + state = mesecon.state.on, + rules = mesecon.rules.alldirs, + }} + mesecon.register_node("mesecons:test_receptor", { + description = "Test Receptor", + }, {mesecons = off_spec}, {mesecons = on_spec}) +end + +do + mesecon._test_effector_events = {} + local function action_on(pos, node) + table.insert(mesecon._test_effector_events, {"on", pos}) + node.param2 = node.param2 % 64 + 128 + minetest.swap_node(pos, node) + end + local function action_off(pos, node) + table.insert(mesecon._test_effector_events, {"off", pos}) + node.param2 = node.param2 % 64 + minetest.swap_node(pos, node) + end + local function action_change(pos, node, rule_name, new_state) + if mesecon.do_overheat(pos) then + table.insert(mesecon._test_effector_events, {"overheat", pos}) + minetest.remove_node(pos) + return + end + local bit = tonumber(rule_name.name, 2) + local bits_above = node.param2 - node.param2 % (bit * 2) + local bits_below = node.param2 % bit + local bits_flipped = new_state == mesecon.state.on and bit or 0 + node.param2 = bits_above + bits_flipped + bits_below + minetest.swap_node(pos, node) + end + minetest.register_node("mesecons:test_effector", { + description = "Test Effector", + mesecons = {effector = { + action_on = action_on, + action_off = action_off, + action_change = action_change, + rules = { + {x = 1, y = 0, z = 0, name = "000001"}, + {x = -1, y = 0, z = 0, name = "000010"}, + {x = 0, y = 1, z = 0, name = "000100"}, + {x = 0, y = -1, z = 0, name = "001000"}, + {x = 0, y = 0, z = 1, name = "010000"}, + {x = 0, y = 0, z = -1, name = "100000"}, + } + }}, + }) +end + +do + local mesecons_spec = {conductor = { + rules = { + {{x = 1, y = 0, z = 0}, {x = 0, y = -1, z = 0}}, + {{x = 0, y = 1, z = 0}, {x = 0, y = 0, z = -1}}, + {{x = 0, y = 0, z = 1}, {x = -1, y = 0, z = 0}}, + }, + states = { + "mesecons:test_multiconductor_off", "mesecons:test_multiconductor_001", + "mesecons:test_multiconductor_010", "mesecons:test_multiconductor_011", + "mesecons:test_multiconductor_100", "mesecons:test_multiconductor_101", + "mesecons:test_multiconductor_110", "mesecons:test_multiconductor_on", + }, + }} + for _, state in ipairs(mesecons_spec.conductor.states) do + minetest.register_node(state, { + description = "Test Multiconductor", + mesecons = mesecons_spec, + }) + end +end + +mesecon._test_autoconnects = {} +mesecon.register_autoconnect_hook("test", function(pos, node) + table.insert(mesecon._test_autoconnects, {pos, node}) +end) + +function mesecon._test_dig(pos) + local node = minetest.get_node(pos) + minetest.remove_node(pos) + mesecon.on_dignode(pos, node) +end + +function mesecon._test_place(pos, node) + world.set_node(pos, node) + mesecon.on_placenode(pos, minetest.get_node(pos)) +end + +function mesecon._test_reset() + for i = 1, 30 do + mineunit:execute_globalstep(60) + end + mesecon.queue.actions = {} + mesecon._test_effector_events = {} + mesecon._test_autoconnects = {} +end + +mineunit:execute_globalstep(mesecon.setting("resumetime", 4) + 1) diff --git a/mesecons/spec/fixtures/voxelmanip.lua b/mesecons/spec/fixtures/voxelmanip.lua new file mode 100644 index 0000000..b7a8b44 --- /dev/null +++ b/mesecons/spec/fixtures/voxelmanip.lua @@ -0,0 +1,91 @@ +mineunit("world") +mineunit("common/vector") +mineunit("game/misc") + +local VoxelManip = {} + +local function create(p1, p2) + local vm = setmetatable({nodes = {}}, VoxelManip) + + if type(p1) == "table" and type(p2) == "table" then + vm:read_from_map(p1, p2) + end + + return vm +end + +function VoxelManip:read_from_map(p1, p2) + assert.same(p1, p2) + assert.is_nil(self.emin) + + local blockpos = vector.floor(vector.divide(p1, minetest.MAP_BLOCKSIZE)) + local emin = vector.multiply(blockpos, minetest.MAP_BLOCKSIZE) + local emax = vector.add(emin, minetest.MAP_BLOCKSIZE - 1) + self.emin, self.emax = emin, emax + + local p = vector.new(emin) + while p.z <= emax.z do + while p.y <= emax.y do + while p.x <= emax.x do + local node = world.get_node(p) + if node then + self.nodes[minetest.hash_node_position(p)] = node + end + p.x = p.x + 1 + end + p.x = emin.x + p.y = p.y + 1 + end + p.y = emin.y + p.z = p.z + 1 + end +end + +function VoxelManip:get_node_at(pos) + local node = self.nodes[minetest.hash_node_position(pos)] + if node then + return {name = node.name, param1 = node.param1, param2 = node.param2} + else + return {name = "ignore", param1 = 0, param2 = 0} + end +end + +function VoxelManip:set_node_at(pos, node) + local emin, emax = self.emin, self.emax + if pos.x < emin.x or pos.y < emin.y or pos.z < emin.z or pos.x > emax.x or pos.y > emax.y or pos.z > emax.z then + return + end + self.nodes[minetest.hash_node_position(pos)] = {name = node.name, param1 = node.param1, param2 = node.param2} +end + +function VoxelManip:write_to_map() + local emin, emax = self.emin, self.emax + local p = vector.new(emin) + while p.z <= emax.z do + while p.y <= emax.y do + while p.x <= emax.x do + local node = self.nodes[minetest.hash_node_position(p)] + if node ~= nil or world.get_node(p) ~= nil then + world.swap_node(p, node) + end + p.x = p.x + 1 + end + p.x = emin.x + p.y = p.y + 1 + end + p.y = emin.y + p.z = p.z + 1 + end +end + +function VoxelManip.update_map() +end + +function minetest.get_voxel_manip(p1, p2) + return create(p1, p2) +end + +mineunit.export_object(VoxelManip, { + name = "VoxelManip", + constructor = create, +}) diff --git a/mesecons/spec/mineunit.conf b/mesecons/spec/mineunit.conf new file mode 100644 index 0000000..e69de29 diff --git a/mesecons/spec/service_spec.lua b/mesecons/spec/service_spec.lua new file mode 100644 index 0000000..c5eb315 --- /dev/null +++ b/mesecons/spec/service_spec.lua @@ -0,0 +1,141 @@ +require("mineunit") + +fixture("mesecons") + +describe("placement/digging service", function() + local layout = { + {{x = 1, y = 0, z = 0}, "mesecons:test_receptor_on"}, + {{x = 0, y = 0, z = 0}, "mesecons:test_conductor_on"}, + {{x = -1, y = 0, z = 0}, "mesecons:test_conductor_on"}, + {{x = 0, y = 1, z = 0}, "mesecons:test_effector"}, + {{x = -2, y = 0, z = 0}, "mesecons:test_effector"}, + {{x = 2, y = 0, z = 0}, "mesecons:test_effector"}, + } + + before_each(function() + world.layout(layout) + end) + + after_each(function() + mesecon._test_reset() + world.clear() + end) + + it("updates components when a receptor changes", function() + mesecon._test_dig(layout[1][1]) + mineunit:execute_globalstep() + assert.equal("mesecons:test_conductor_off", world.get_node(layout[2][1]).name) + mineunit:execute_globalstep() + assert.equal(3, #mesecon._test_effector_events) + + mesecon._test_place(layout[1][1], "mesecons:test_receptor_on") + mineunit:execute_globalstep() + assert.equal("mesecons:test_conductor_on", world.get_node(layout[2][1]).name) + mineunit:execute_globalstep() + assert.equal(6, #mesecon._test_effector_events) + end) + + it("updates components when a conductor changes", function() + mesecon._test_dig(layout[2][1]) + mineunit:execute_globalstep() + assert.equal("mesecons:test_conductor_off", world.get_node(layout[3][1]).name) + mineunit:execute_globalstep() + assert.equal(2, #mesecon._test_effector_events) + + mesecon._test_place(layout[2][1], "mesecons:test_conductor_off") + assert.equal("mesecons:test_conductor_on", world.get_node(layout[2][1]).name) + assert.equal("mesecons:test_conductor_on", world.get_node(layout[3][1]).name) + mineunit:execute_globalstep() + assert.equal(4, #mesecon._test_effector_events) + end) + + it("updates effectors on placement", function() + local pos = {x = 0, y = 0, z = 1} + mesecon._test_place(pos, "mesecons:test_effector") + mineunit:execute_globalstep() + assert.equal(tonumber("10100000", 2), world.get_node(pos).param2) + end) + + it("updates multiconductors on placement", function() + local pos = {x = 0, y = 0, z = 1} + mesecon._test_place(pos, "mesecons:test_multiconductor_off") + assert.equal("mesecons:test_multiconductor_010", world.get_node(pos).name) + end) + + it("turns off conductors on placement", function() + local pos = {x = 3, y = 0, z = 0} + mesecon._test_place(pos, "mesecons:test_conductor_on") + assert.equal("mesecons:test_conductor_off", world.get_node(pos).name) + end) + + -- Will work once #584 is merged. + pending("turns off multiconductors on placement", function() + local pos = {x = 3, y = 0, z = 0} + mesecon._test_place(pos, "mesecons:test_multiconductor_on") + assert.equal("mesecons:test_multiconductor_off", world.get_node(pos).name) + end) + + it("triggers autoconnect hooks", function() + mesecon._test_dig(layout[2][1]) + mineunit:execute_globalstep() + assert.equal(1, #mesecon._test_autoconnects) + + mesecon._test_place(layout[2][1], layout[2][2]) + assert.equal(2, #mesecon._test_autoconnects) + end) +end) + +describe("overheating service", function() + local layout = { + {{x = 0, y = 0, z = 0}, "mesecons:test_receptor_off"}, + {{x = 1, y = 0, z = 0}, "mesecons:test_effector"}, + {{x = 2, y = 0, z = 0}, "mesecons:test_receptor_on"}, + } + + before_each(function() + world.layout(layout) + end) + + after_each(function() + mesecon._test_reset() + world.clear() + end) + + it("tracks heat", function() + mesecon.do_overheat(layout[2][1]) + assert.equal(1, mesecon.get_heat(layout[2][1])) + mesecon.do_cooldown(layout[2][1]) + assert.equal(0, mesecon.get_heat(layout[2][1])) + end) + + it("cools over time", function() + mesecon.do_overheat(layout[2][1]) + assert.equal(1, mesecon.get_heat(layout[2][1])) + mineunit:execute_globalstep(60) + mineunit:execute_globalstep(60) + mineunit:execute_globalstep(60) + assert.equal(0, mesecon.get_heat(layout[2][1])) + end) + + it("tracks movement", function() + local oldpos = layout[2][1] + local pos = vector.offset(oldpos, 0, 1, 0) + mesecon.do_overheat(oldpos) + mesecon.move_hot_nodes({{pos = pos, oldpos = oldpos}}) + assert.equal(0, mesecon.get_heat(oldpos)) + assert.equal(1, mesecon.get_heat(pos)) + end) + + it("causes overheating", function() + repeat + if mesecon.flipstate(layout[1][1], minetest.get_node(layout[1][1])) == "on" then + mesecon.receptor_on(layout[1][1], mesecon.rules.alldirs) + else + mesecon.receptor_off(layout[1][1], mesecon.rules.alldirs) + end + mineunit:execute_globalstep(0) + until minetest.get_node(layout[2][1]).name ~= "mesecons:test_effector" + assert.same({"overheat", layout[2][1]}, mesecon._test_effector_events[#mesecon._test_effector_events]) + assert.equal(0, mesecon.get_heat(layout[2][1])) + end) +end) diff --git a/mesecons/spec/state_spec.lua b/mesecons/spec/state_spec.lua new file mode 100644 index 0000000..74e0e39 --- /dev/null +++ b/mesecons/spec/state_spec.lua @@ -0,0 +1,116 @@ +require("mineunit") + +fixture("mesecons") + +describe("state", function() + local layout = { + {{x = 1, y = 0, z = 0}, "mesecons:test_receptor_off"}, + {{x = 0, y = 1, z = 0}, "mesecons:test_receptor_off"}, + {{x = 0, y = 0, z = 0}, "mesecons:test_conductor_off"}, + {{x = -1, y = 0, z = 0}, "mesecons:test_effector"}, + {{x = 2, y = 0, z = 0}, "mesecons:test_effector"}, + {{x = 0, y = -1, z = 0}, "mesecons:test_effector"}, + } + + before_each(function() + world.layout(layout) + end) + + after_each(function() + mesecon._test_reset() + world.clear() + end) + + it("turns on", function() + mesecon.swap_node_force(layout[1][1], "mesecons:test_receptor_on") + mesecon.receptor_on(layout[1][1], mesecon.rules.alldirs) + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons:test_conductor_on", world.get_node(layout[3][1]).name) + assert.equal(tonumber("10000001", 2), world.get_node(layout[4][1]).param2) + assert.equal(tonumber("10000010", 2), world.get_node(layout[5][1]).param2) + assert.equal(tonumber("10000100", 2), world.get_node(layout[6][1]).param2) + + mesecon.swap_node_force(layout[2][1], "mesecons:test_receptor_on") + mesecon.receptor_on(layout[2][1], mesecon.rules.alldirs) + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons:test_conductor_on", world.get_node(layout[3][1]).name) + assert.equal(tonumber("10000001", 2), world.get_node(layout[4][1]).param2) + assert.equal(tonumber("10000010", 2), world.get_node(layout[5][1]).param2) + assert.equal(tonumber("10000100", 2), world.get_node(layout[6][1]).param2) + end) + + it("turns off", function() + mesecon.swap_node_force(layout[1][1], "mesecons:test_receptor_on") + mesecon.swap_node_force(layout[2][1], "mesecons:test_receptor_on") + mesecon.receptor_on(layout[1][1], mesecon.rules.alldirs) + mesecon.receptor_on(layout[2][1], mesecon.rules.alldirs) + mineunit:execute_globalstep() + + mesecon.swap_node_force(layout[1][1], "mesecons:test_receptor_off") + mesecon.receptor_off(layout[1][1], mesecon.rules.alldirs) + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons:test_conductor_on", world.get_node(layout[3][1]).name) + assert.equal(tonumber("10000001", 2), world.get_node(layout[4][1]).param2) + assert.equal(tonumber("00000000", 2), world.get_node(layout[5][1]).param2) + assert.equal(tonumber("10000100", 2), world.get_node(layout[6][1]).param2) + + mesecon.swap_node_force(layout[2][1], "mesecons:test_receptor_off") + mesecon.receptor_off(layout[2][1], mesecon.rules.alldirs) + mineunit:execute_globalstep() + mineunit:execute_globalstep() + assert.equal("mesecons:test_conductor_off", world.get_node(layout[3][1]).name) + assert.equal(tonumber("00000000", 2), world.get_node(layout[4][1]).param2) + assert.equal(tonumber("00000000", 2), world.get_node(layout[5][1]).param2) + assert.equal(tonumber("00000000", 2), world.get_node(layout[6][1]).param2) + end) +end) + +describe("multiconductor", function() + local layout = { + {{x = 1, y = 0, z = 0}, "mesecons:test_receptor_off"}, + {{x = 0, y = 1, z = 0}, "mesecons:test_receptor_off"}, + {{x = 0, y = 0, z = 1}, "mesecons:test_receptor_off"}, + {{x = 0, y = 0, z = 0}, "mesecons:test_multiconductor_off"}, + } + + before_each(function() + world.layout(layout) + end) + + after_each(function() + world.clear() + mesecon._test_reset() + end) + + it("separates its subparts", function() + mesecon.swap_node_force(layout[1][1], "mesecons:test_receptor_on") + mesecon.receptor_on(layout[1][1], mesecon.rules.alldirs) + mineunit:execute_globalstep() + assert.equal("mesecons:test_multiconductor_001", world.get_node(layout[4][1]).name) + + mesecon.swap_node_force(layout[2][1], "mesecons:test_receptor_on") + mesecon.receptor_on(layout[2][1], mesecon.rules.alldirs) + mineunit:execute_globalstep() + assert.equal("mesecons:test_multiconductor_011", world.get_node(layout[4][1]).name) + + mesecon.swap_node_force(layout[3][1], "mesecons:test_receptor_on") + mesecon.receptor_on(layout[3][1], mesecon.rules.alldirs) + mineunit:execute_globalstep() + assert.equal("mesecons:test_multiconductor_on", world.get_node(layout[4][1]).name) + end) + + it("loops through itself", function() + -- Make a loop. + world.set_node({x = 0, y = -1, z = 0}, "mesecons:test_conductor_off") + world.set_node({x = -1, y = -1, z = 0}, "mesecons:test_conductor_off") + world.set_node({x = -1, y = 0, z = 0}, "mesecons:test_conductor_off") + + mesecon.swap_node_force(layout[1][1], "mesecons:test_receptor_on") + mesecon.receptor_on(layout[1][1], mesecon.rules.alldirs) + mineunit:execute_globalstep() + assert.equal("mesecons:test_multiconductor_101", world.get_node(layout[4][1]).name) + end) +end)