Initial commit
168
games/devtest/mods/unittests/async_env.lua
Normal file
@@ -0,0 +1,168 @@
|
||||
-- helper
|
||||
|
||||
core.register_async_dofile(core.get_modpath(core.get_current_modname()) ..
|
||||
DIR_DELIM .. "inside_async_env.lua")
|
||||
|
||||
local function deepequal(a, b)
|
||||
if type(a) == "function" then
|
||||
return type(b) == "function"
|
||||
elseif type(a) ~= "table" then
|
||||
return a == b
|
||||
elseif type(b) ~= "table" then
|
||||
return false
|
||||
end
|
||||
for k, v in pairs(a) do
|
||||
if not deepequal(v, b[k]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
for k, v in pairs(b) do
|
||||
if not deepequal(a[k], v) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Object Passing / Serialization
|
||||
|
||||
local test_object = {
|
||||
name = "stairs:stair_glass",
|
||||
type = "node",
|
||||
groups = {oddly_breakable_by_hand = 3, cracky = 3, stair = 1},
|
||||
description = "Glass Stair",
|
||||
sounds = {
|
||||
dig = {name = "default_glass_footstep", gain = 0.5},
|
||||
footstep = {name = "default_glass_footstep", gain = 0.3},
|
||||
dug = {name = "default_break_glass", gain = 1}
|
||||
},
|
||||
node_box = {
|
||||
fixed = {
|
||||
{-0.5, -0.5, -0.5, 0.5, 0, 0.5},
|
||||
{-0.5, 0, 0, 0.5, 0.5, 0.5}
|
||||
},
|
||||
type = "fixed"
|
||||
},
|
||||
tiles = {
|
||||
{name = "stairs_glass_split.png", backface_culling = true},
|
||||
{name = "default_glass.png", backface_culling = true},
|
||||
{name = "stairs_glass_stairside.png^[transformFX", backface_culling = true}
|
||||
},
|
||||
on_place = function(itemstack, placer)
|
||||
return core.is_player(placer)
|
||||
end,
|
||||
sunlight_propagates = true,
|
||||
is_ground_content = false,
|
||||
light_source = 0,
|
||||
}
|
||||
|
||||
local function test_object_passing()
|
||||
local tmp = core.serialize_roundtrip(test_object)
|
||||
assert(deepequal(test_object, tmp))
|
||||
|
||||
local circular_key = {"foo", "bar"}
|
||||
circular_key[circular_key] = true
|
||||
tmp = core.serialize_roundtrip(circular_key)
|
||||
assert(tmp[1] == "foo")
|
||||
assert(tmp[2] == "bar")
|
||||
assert(tmp[tmp] == true)
|
||||
|
||||
local circular_value = {"foo"}
|
||||
circular_value[2] = circular_value
|
||||
tmp = core.serialize_roundtrip(circular_value)
|
||||
assert(tmp[1] == "foo")
|
||||
assert(tmp[2] == tmp)
|
||||
|
||||
-- Two-segment cycle
|
||||
local cycle_seg_1, cycle_seg_2 = {}, {}
|
||||
cycle_seg_1[1] = cycle_seg_2
|
||||
cycle_seg_2[1] = cycle_seg_1
|
||||
tmp = core.serialize_roundtrip(cycle_seg_1)
|
||||
assert(tmp[1][1] == tmp)
|
||||
|
||||
-- Duplicated value without a cycle
|
||||
local acyclic_dup_holder = {}
|
||||
tmp = ItemStack("")
|
||||
acyclic_dup_holder[tmp] = tmp
|
||||
tmp = core.serialize_roundtrip(acyclic_dup_holder)
|
||||
for k, v in pairs(tmp) do
|
||||
assert(rawequal(k, v))
|
||||
end
|
||||
end
|
||||
unittests.register("test_object_passing", test_object_passing)
|
||||
|
||||
local function test_userdata_passing(_, pos)
|
||||
-- basic userdata passing
|
||||
local obj = table.copy(test_object.tiles[1])
|
||||
obj.test = ItemStack("default:cobble 99")
|
||||
local tmp = core.serialize_roundtrip(obj)
|
||||
assert(type(tmp.test) == "userdata")
|
||||
assert(obj.test:to_string() == tmp.test:to_string())
|
||||
|
||||
-- object can't be passed, should error
|
||||
obj = core.raycast(pos, pos)
|
||||
assert(not pcall(core.serialize_roundtrip, obj))
|
||||
|
||||
-- VManip
|
||||
local vm = core.get_voxel_manip(pos, pos)
|
||||
local expect = vm:get_node_at(pos)
|
||||
local vm2 = core.serialize_roundtrip(vm)
|
||||
assert(deepequal(vm2:get_node_at(pos), expect))
|
||||
end
|
||||
unittests.register("test_userdata_passing", test_userdata_passing, {map=true})
|
||||
|
||||
-- Asynchronous jobs
|
||||
|
||||
local function test_handle_async(cb)
|
||||
-- Basic test including mod name tracking and unittests.async_test()
|
||||
-- which is defined inside_async_env.lua
|
||||
local func = function(x)
|
||||
return core.get_last_run_mod(), _VERSION, unittests[x]()
|
||||
end
|
||||
local expect = {core.get_last_run_mod(), _VERSION, true}
|
||||
|
||||
core.handle_async(func, function(...)
|
||||
if not deepequal(expect, {...}) then
|
||||
return cb("Values did not equal")
|
||||
end
|
||||
if core.get_last_run_mod() ~= expect[1] then
|
||||
return cb("Mod name not tracked correctly")
|
||||
end
|
||||
|
||||
-- Test passing of nil arguments and return values
|
||||
core.handle_async(function(a, b)
|
||||
return a, b
|
||||
end, function(a, b)
|
||||
if b ~= 123 then
|
||||
return cb("Argument went missing")
|
||||
end
|
||||
cb()
|
||||
end, nil, 123)
|
||||
end, "async_test")
|
||||
end
|
||||
unittests.register("test_handle_async", test_handle_async, {async=true})
|
||||
|
||||
local function test_userdata_passing2(cb, _, pos)
|
||||
-- VManip: check transfer into other env
|
||||
local vm = core.get_voxel_manip(pos, pos)
|
||||
local expect = vm:get_node_at(pos)
|
||||
|
||||
core.handle_async(function(vm_, pos_)
|
||||
return vm_:get_node_at(pos_)
|
||||
end, function(ret)
|
||||
if not deepequal(expect, ret) then
|
||||
return cb("Node data mismatch (one-way)")
|
||||
end
|
||||
|
||||
-- VManip: test a roundtrip
|
||||
core.handle_async(function(vm_)
|
||||
return vm_
|
||||
end, function(vm2)
|
||||
if not deepequal(expect, vm2:get_node_at(pos)) then
|
||||
return cb("Node data mismatch (roundtrip)")
|
||||
end
|
||||
cb()
|
||||
end, vm)
|
||||
end, vm, pos)
|
||||
end
|
||||
unittests.register("test_userdata_passing2", test_userdata_passing2, {map=true, async=true})
|
||||
112
games/devtest/mods/unittests/crafting.lua
Normal file
@@ -0,0 +1,112 @@
|
||||
dofile(core.get_modpath(core.get_current_modname()) .. "/crafting_prepare.lua")
|
||||
|
||||
-- Test minetest.clear_craft function
|
||||
local function test_clear_craft()
|
||||
-- Clearing by output
|
||||
minetest.register_craft({
|
||||
output = "foo",
|
||||
recipe = {{"bar"}}
|
||||
})
|
||||
minetest.register_craft({
|
||||
output = "foo 4",
|
||||
recipe = {{"foo", "bar"}}
|
||||
})
|
||||
assert(#minetest.get_all_craft_recipes("foo") == 2)
|
||||
minetest.clear_craft({output="foo"})
|
||||
assert(minetest.get_all_craft_recipes("foo") == nil)
|
||||
-- Clearing by input
|
||||
minetest.register_craft({
|
||||
output = "foo 4",
|
||||
recipe = {{"foo", "bar"}}
|
||||
})
|
||||
assert(#minetest.get_all_craft_recipes("foo") == 1)
|
||||
minetest.clear_craft({recipe={{"foo", "bar"}}})
|
||||
assert(minetest.get_all_craft_recipes("foo") == nil)
|
||||
end
|
||||
unittests.register("test_clear_craft", test_clear_craft)
|
||||
|
||||
-- Test minetest.get_craft_result function
|
||||
local function test_get_craft_result()
|
||||
-- normal
|
||||
local input = {
|
||||
method = "normal",
|
||||
width = 2,
|
||||
items = {"", "unittests:coal_lump", "", "unittests:stick"}
|
||||
}
|
||||
minetest.log("info", "[unittests] torch crafting input: "..dump(input))
|
||||
local output, decremented_input = minetest.get_craft_result(input)
|
||||
minetest.log("info", "[unittests] torch crafting output: "..dump(output))
|
||||
minetest.log("info", "[unittests] torch crafting decremented input: "..dump(decremented_input))
|
||||
assert(output.item)
|
||||
minetest.log("info", "[unittests] torch crafting output.item:to_table(): "..dump(output.item:to_table()))
|
||||
assert(output.item:get_name() == "unittests:torch")
|
||||
assert(output.item:get_count() == 4)
|
||||
|
||||
-- fuel
|
||||
input = {
|
||||
method = "fuel",
|
||||
width = 1,
|
||||
items = {"unittests:coal_lump"}
|
||||
}
|
||||
minetest.log("info", "[unittests] coal fuel input: "..dump(input))
|
||||
output, decremented_input = minetest.get_craft_result(input)
|
||||
minetest.log("info", "[unittests] coal fuel output: "..dump(output))
|
||||
minetest.log("info", "[unittests] coal fuel decremented input: "..dump(decremented_input))
|
||||
assert(output.time)
|
||||
assert(output.time > 0)
|
||||
|
||||
-- cooking
|
||||
input = {
|
||||
method = "cooking",
|
||||
width = 1,
|
||||
items = {"unittests:iron_lump"}
|
||||
}
|
||||
minetest.log("info", "[unittests] iron lump cooking input: "..dump(output))
|
||||
output, decremented_input = minetest.get_craft_result(input)
|
||||
minetest.log("info", "[unittests] iron lump cooking output: "..dump(output))
|
||||
minetest.log("info", "[unittests] iron lump cooking decremented input: "..dump(decremented_input))
|
||||
assert(output.time)
|
||||
assert(output.time > 0)
|
||||
assert(output.item)
|
||||
minetest.log("info", "[unittests] iron lump cooking output.item:to_table(): "..dump(output.item:to_table()))
|
||||
assert(output.item:get_name() == "unittests:steel_ingot")
|
||||
assert(output.item:get_count() == 1)
|
||||
|
||||
-- tool repair (repairable)
|
||||
input = {
|
||||
method = "normal",
|
||||
width = 2,
|
||||
-- Using a wear of 60000
|
||||
items = {"unittests:repairable_tool 1 60000", "unittests:repairable_tool 1 60000"}
|
||||
}
|
||||
minetest.log("info", "[unittests] repairable tool crafting input: "..dump(input))
|
||||
output, decremented_input = minetest.get_craft_result(input)
|
||||
minetest.log("info", "[unittests] repairable tool crafting output: "..dump(output))
|
||||
minetest.log("info", "[unittests] repairable tool crafting decremented input: "..dump(decremented_input))
|
||||
assert(output.item)
|
||||
minetest.log("info", "[unittests] repairable tool crafting output.item:to_table(): "..dump(output.item:to_table()))
|
||||
assert(output.item:get_name() == "unittests:repairable_tool")
|
||||
-- Test the wear value.
|
||||
-- See src/craftdef.cpp in Minetest source code for the formula. The formula to calculate
|
||||
-- the value 51187 is:
|
||||
-- 65536 - ((65536-60000)+(65536-60000)) + floor(additonal_wear * 65536 + 0.5) = 51187
|
||||
-- where additional_wear = 0.05
|
||||
assert(output.item:get_wear() == 51187)
|
||||
assert(output.item:get_count() == 1)
|
||||
|
||||
-- failing tool repair (unrepairable)
|
||||
input = {
|
||||
method = "normal",
|
||||
width = 2,
|
||||
items = {"unittests:unrepairable_tool 1 60000", "unittests:unrepairable_tool 1 60000"}
|
||||
}
|
||||
minetest.log("info", "[unittests] unrepairable tool crafting input: "..dump(input))
|
||||
output, decremented_input = minetest.get_craft_result(input)
|
||||
minetest.log("info", "[unittests] unrepairable tool crafting output: "..dump(output))
|
||||
minetest.log("info", "[unittests] unrepairable tool crafting decremented input: "..dump(decremented_input))
|
||||
assert(output.item)
|
||||
minetest.log("info", "[unittests] unrepairable tool crafting output.item:to_table(): "..dump(output.item:to_table()))
|
||||
-- unrepairable tool must not yield any output
|
||||
assert(output.item:is_empty())
|
||||
end
|
||||
unittests.register("test_get_craft_result", test_get_craft_result)
|
||||
94
games/devtest/mods/unittests/crafting_prepare.lua
Normal file
@@ -0,0 +1,94 @@
|
||||
-- Registering some dummy items and recipes for the crafting tests
|
||||
|
||||
minetest.register_craftitem("unittests:torch", {
|
||||
description = "Crafting Test Item: Torch",
|
||||
inventory_image = "unittests_torch.png",
|
||||
|
||||
groups = { dummy = 1 },
|
||||
})
|
||||
minetest.register_craftitem("unittests:coal_lump", {
|
||||
description = "Crafting Test Item: Coal Lump",
|
||||
inventory_image = "unittests_coal_lump.png",
|
||||
|
||||
groups = { dummy = 1 },
|
||||
})
|
||||
minetest.register_craftitem("unittests:stick", {
|
||||
description = "Crafting Test Item: Stick",
|
||||
inventory_image = "unittests_stick.png",
|
||||
|
||||
groups = { dummy = 1 },
|
||||
})
|
||||
minetest.register_craftitem("unittests:iron_lump", {
|
||||
description = "Crafting Test Item: Iron Lump",
|
||||
inventory_image = "unittests_iron_lump.png",
|
||||
|
||||
groups = { dummy = 1 },
|
||||
})
|
||||
minetest.register_craftitem("unittests:steel_ingot", {
|
||||
description = "Crafting Test Item: Steel Ingot",
|
||||
inventory_image = "unittests_steel_ingot.png",
|
||||
|
||||
groups = { dummy = 1 },
|
||||
})
|
||||
|
||||
-- Use aliases in recipes for more complete testing
|
||||
|
||||
minetest.register_alias("unittests:steel_ingot_alias", "unittests:steel_ingot")
|
||||
minetest.register_alias("unittests:coal_lump_alias", "unittests:coal_lump")
|
||||
minetest.register_alias("unittests:iron_lump_alias", "unittests:iron_lump")
|
||||
|
||||
-- Recipes for tests: Normal crafting, cooking and fuel
|
||||
|
||||
minetest.register_craft({
|
||||
output = 'unittests:torch 4',
|
||||
recipe = {
|
||||
{'unittests:coal_lump_alias'},
|
||||
{'unittests:stick'},
|
||||
}
|
||||
})
|
||||
|
||||
minetest.register_craft({
|
||||
type = "cooking",
|
||||
output = "unittests:steel_ingot_alias",
|
||||
recipe = "unittests:iron_lump_alias",
|
||||
})
|
||||
|
||||
minetest.register_craft({
|
||||
type = "fuel",
|
||||
recipe = "unittests:coal_lump_alias",
|
||||
burntime = 40,
|
||||
})
|
||||
|
||||
-- Test tool repair
|
||||
minetest.register_craft({
|
||||
type = "toolrepair",
|
||||
additional_wear = -0.05,
|
||||
})
|
||||
|
||||
-- Test the disable_repair=1 group
|
||||
minetest.register_tool("unittests:unrepairable_tool", {
|
||||
description = "Crafting Test Item: Unrepairable Tool",
|
||||
inventory_image = "unittests_unrepairable_tool.png",
|
||||
tool_capabilities = {
|
||||
groupcaps = {
|
||||
cracky = {
|
||||
times = {3, 2, 1},
|
||||
}
|
||||
}
|
||||
},
|
||||
groups = { disable_repair = 1, dummy = 1 }
|
||||
})
|
||||
|
||||
minetest.register_tool("unittests:repairable_tool", {
|
||||
description = "Crafting Test Item: Repairable Tool",
|
||||
inventory_image = "unittests_repairable_tool.png",
|
||||
tool_capabilities = {
|
||||
groupcaps = {
|
||||
cracky = {
|
||||
times = {3, 2, 1},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
groups = { dummy = 1 },
|
||||
})
|
||||
132
games/devtest/mods/unittests/entity.lua
Normal file
@@ -0,0 +1,132 @@
|
||||
local log = {}
|
||||
|
||||
local function insert_log(...)
|
||||
log[#log+1] = string.format(...)
|
||||
end
|
||||
|
||||
local function objref_str(self, ref)
|
||||
if ref and ref:is_player() then
|
||||
return "player"
|
||||
end
|
||||
return self.object == ref and "self" or tostring(ref)
|
||||
end
|
||||
|
||||
core.register_entity("unittests:callbacks", {
|
||||
initial_properties = {
|
||||
hp_max = 5,
|
||||
visual = "upright_sprite",
|
||||
textures = { "unittests_stick.png" },
|
||||
static_save = false,
|
||||
},
|
||||
|
||||
on_activate = function(self, staticdata, dtime_s)
|
||||
self.object:set_armor_groups({test = 100})
|
||||
assert(self.object:get_hp() == self.initial_properties.hp_max)
|
||||
insert_log("on_activate(%d)", #staticdata)
|
||||
end,
|
||||
on_deactivate = function(self, removal)
|
||||
insert_log("on_deactivate(%s)", tostring(removal))
|
||||
end,
|
||||
on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir, damage)
|
||||
insert_log("on_punch(%s, %.1f, %d)", objref_str(self, puncher),
|
||||
time_from_last_punch, damage)
|
||||
end,
|
||||
on_death = function(self, killer)
|
||||
assert(self.object:get_hp() == 0)
|
||||
insert_log("on_death(%s)", objref_str(self, killer))
|
||||
end,
|
||||
on_rightclick = function(self, clicker)
|
||||
insert_log("on_rightclick(%s)", objref_str(self, clicker))
|
||||
end,
|
||||
on_attach_child = function(self, child)
|
||||
insert_log("on_attach_child(%s)", objref_str(self, child))
|
||||
end,
|
||||
on_detach_child = function(self, child)
|
||||
insert_log("on_detach_child(%s)", objref_str(self, child))
|
||||
end,
|
||||
on_detach = function(self, parent)
|
||||
insert_log("on_detach(%s)", objref_str(self, parent))
|
||||
end,
|
||||
get_staticdata = function(self)
|
||||
assert(false)
|
||||
end,
|
||||
})
|
||||
|
||||
--
|
||||
|
||||
local function check_log(expect)
|
||||
if #expect ~= #log then
|
||||
error("Log mismatch: " .. core.write_json(log))
|
||||
end
|
||||
for i, s in ipairs(expect) do
|
||||
if log[i] ~= s then
|
||||
error("Log mismatch at " .. i .. ": " .. core.write_json(log))
|
||||
end
|
||||
end
|
||||
log = {} -- clear it for next time
|
||||
end
|
||||
|
||||
local function test_entity_lifecycle(_, pos)
|
||||
log = {}
|
||||
|
||||
-- with binary in staticdata
|
||||
local obj = core.add_entity(pos, "unittests:callbacks", "abc\000def")
|
||||
check_log({"on_activate(7)"})
|
||||
|
||||
obj:set_hp(0)
|
||||
check_log({"on_death(nil)", "on_deactivate(true)"})
|
||||
|
||||
-- objectref must be invalid now
|
||||
assert(obj:get_velocity() == nil)
|
||||
end
|
||||
unittests.register("test_entity_lifecycle", test_entity_lifecycle, {map=true})
|
||||
|
||||
local function test_entity_interact(_, pos)
|
||||
log = {}
|
||||
|
||||
local obj = core.add_entity(pos, "unittests:callbacks")
|
||||
check_log({"on_activate(0)"})
|
||||
|
||||
-- rightclick
|
||||
obj:right_click(obj)
|
||||
check_log({"on_rightclick(self)"})
|
||||
|
||||
-- useless punch
|
||||
obj:punch(obj, 0.5, {})
|
||||
check_log({"on_punch(self, 0.5, 0)"})
|
||||
|
||||
-- fatal punch
|
||||
obj:punch(obj, 1.9, {
|
||||
full_punch_interval = 1.0,
|
||||
damage_groups = { test = 10 },
|
||||
})
|
||||
check_log({
|
||||
-- does 10 damage even though we only have 5 hp
|
||||
"on_punch(self, 1.9, 10)",
|
||||
"on_death(self)",
|
||||
"on_deactivate(true)"
|
||||
})
|
||||
end
|
||||
unittests.register("test_entity_interact", test_entity_interact, {map=true})
|
||||
|
||||
local function test_entity_attach(player, pos)
|
||||
log = {}
|
||||
|
||||
local obj = core.add_entity(pos, "unittests:callbacks")
|
||||
check_log({"on_activate(0)"})
|
||||
|
||||
-- attach player to entity
|
||||
player:set_attach(obj)
|
||||
check_log({"on_attach_child(player)"})
|
||||
player:set_detach()
|
||||
check_log({"on_detach_child(player)"})
|
||||
|
||||
-- attach entity to player
|
||||
obj:set_attach(player)
|
||||
check_log({})
|
||||
obj:set_detach()
|
||||
check_log({"on_detach(player)"})
|
||||
|
||||
obj:remove()
|
||||
end
|
||||
unittests.register("test_entity_attach", test_entity_attach, {player=true, map=true})
|
||||
202
games/devtest/mods/unittests/init.lua
Normal file
@@ -0,0 +1,202 @@
|
||||
unittests = {}
|
||||
|
||||
unittests.list = {}
|
||||
|
||||
-- name: Name of the test
|
||||
-- func:
|
||||
-- for sync: function(player, pos), should error on failure
|
||||
-- for async: function(callback, player, pos)
|
||||
-- MUST call callback() or callback("error msg") in case of error once test is finished
|
||||
-- this means you cannot use assert() in the test implementation
|
||||
-- opts: {
|
||||
-- player = false, -- Does test require a player?
|
||||
-- map = false, -- Does test require map access?
|
||||
-- async = false, -- Does the test run asynchronously? (read notes above!)
|
||||
-- }
|
||||
function unittests.register(name, func, opts)
|
||||
local def = table.copy(opts or {})
|
||||
def.name = name
|
||||
def.func = func
|
||||
table.insert(unittests.list, def)
|
||||
end
|
||||
|
||||
function unittests.on_finished(all_passed)
|
||||
-- free to override
|
||||
end
|
||||
|
||||
-- Calls invoke with a callback as argument
|
||||
-- Suspends coroutine until that callback is called
|
||||
-- Return values are passed through
|
||||
local function await(invoke)
|
||||
local co = coroutine.running()
|
||||
assert(co)
|
||||
local called_early = true
|
||||
invoke(function(...)
|
||||
if called_early == true then
|
||||
called_early = {...}
|
||||
else
|
||||
coroutine.resume(co, ...)
|
||||
co = nil
|
||||
end
|
||||
end)
|
||||
if called_early ~= true then
|
||||
-- callback was already called before yielding
|
||||
return unpack(called_early)
|
||||
end
|
||||
called_early = nil
|
||||
return coroutine.yield()
|
||||
end
|
||||
|
||||
function unittests.run_one(idx, counters, out_callback, player, pos)
|
||||
local def = unittests.list[idx]
|
||||
if not def.player then
|
||||
player = nil
|
||||
elseif player == nil then
|
||||
out_callback(false)
|
||||
return false
|
||||
end
|
||||
if not def.map then
|
||||
pos = nil
|
||||
elseif pos == nil then
|
||||
out_callback(false)
|
||||
return false
|
||||
end
|
||||
|
||||
local tbegin = core.get_us_time()
|
||||
local function done(status, err)
|
||||
local tend = core.get_us_time()
|
||||
local ms_taken = (tend - tbegin) / 1000
|
||||
|
||||
if not status then
|
||||
core.log("error", err)
|
||||
end
|
||||
print(string.format("[%s] %s - %dms",
|
||||
status and "PASS" or "FAIL", def.name, ms_taken))
|
||||
counters.time = counters.time + ms_taken
|
||||
counters.total = counters.total + 1
|
||||
if status then
|
||||
counters.passed = counters.passed + 1
|
||||
end
|
||||
end
|
||||
|
||||
if def.async then
|
||||
core.log("info", "[unittest] running " .. def.name .. " (async)")
|
||||
def.func(function(err)
|
||||
done(err == nil, err)
|
||||
out_callback(true)
|
||||
end, player, pos)
|
||||
else
|
||||
core.log("info", "[unittest] running " .. def.name)
|
||||
local status, err = pcall(def.func, player, pos)
|
||||
done(status, err)
|
||||
out_callback(true)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function wait_for_player(callback)
|
||||
if #core.get_connected_players() > 0 then
|
||||
return callback(core.get_connected_players()[1])
|
||||
end
|
||||
local first = true
|
||||
core.register_on_joinplayer(function(player)
|
||||
if first then
|
||||
callback(player)
|
||||
first = false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function wait_for_map(player, callback)
|
||||
local check = function()
|
||||
if core.get_node_or_nil(player:get_pos()) ~= nil then
|
||||
callback()
|
||||
else
|
||||
core.after(0, check)
|
||||
end
|
||||
end
|
||||
check()
|
||||
end
|
||||
|
||||
function unittests.run_all()
|
||||
-- This runs in a coroutine so it uses await().
|
||||
local counters = { time = 0, total = 0, passed = 0 }
|
||||
|
||||
-- Run standalone tests first
|
||||
for idx = 1, #unittests.list do
|
||||
local def = unittests.list[idx]
|
||||
def.done = await(function(cb)
|
||||
unittests.run_one(idx, counters, cb, nil, nil)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Wait for a player to join, run tests that require a player
|
||||
local player = await(wait_for_player)
|
||||
for idx = 1, #unittests.list do
|
||||
local def = unittests.list[idx]
|
||||
if not def.done then
|
||||
def.done = await(function(cb)
|
||||
unittests.run_one(idx, counters, cb, player, nil)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Wait for the world to generate/load, run tests that require map access
|
||||
await(function(cb)
|
||||
wait_for_map(player, cb)
|
||||
end)
|
||||
local pos = vector.round(player:get_pos())
|
||||
for idx = 1, #unittests.list do
|
||||
local def = unittests.list[idx]
|
||||
if not def.done then
|
||||
def.done = await(function(cb)
|
||||
unittests.run_one(idx, counters, cb, player, pos)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Print stats
|
||||
assert(#unittests.list == counters.total)
|
||||
print(string.rep("+", 80))
|
||||
print(string.format("Unit Test Results: %s",
|
||||
counters.total == counters.passed and "PASSED" or "FAILED"))
|
||||
print(string.format(" %d / %d failed tests.",
|
||||
counters.total - counters.passed, counters.total))
|
||||
print(string.format(" Testing took %dms total.", counters.time))
|
||||
print(string.rep("+", 80))
|
||||
unittests.on_finished(counters.total == counters.passed)
|
||||
return counters.total == counters.passed
|
||||
end
|
||||
|
||||
--------------
|
||||
|
||||
local modpath = core.get_modpath("unittests")
|
||||
dofile(modpath .. "/misc.lua")
|
||||
dofile(modpath .. "/player.lua")
|
||||
dofile(modpath .. "/crafting.lua")
|
||||
dofile(modpath .. "/itemdescription.lua")
|
||||
dofile(modpath .. "/async_env.lua")
|
||||
dofile(modpath .. "/entity.lua")
|
||||
|
||||
--------------
|
||||
|
||||
if core.settings:get_bool("devtest_unittests_autostart", false) then
|
||||
core.after(0, function()
|
||||
coroutine.wrap(unittests.run_all)()
|
||||
end)
|
||||
else
|
||||
core.register_chatcommand("unittests", {
|
||||
privs = {basic_privs=true},
|
||||
description = "Runs devtest unittests (may modify player or map state)",
|
||||
func = function(name, param)
|
||||
unittests.on_finished = function(ok)
|
||||
core.chat_send_player(name,
|
||||
(ok and "All tests passed." or "There were test failures.") ..
|
||||
" Check the console for detailed output.")
|
||||
end
|
||||
coroutine.wrap(unittests.run_all)()
|
||||
return true, ""
|
||||
end,
|
||||
})
|
||||
end
|
||||
25
games/devtest/mods/unittests/inside_async_env.lua
Normal file
@@ -0,0 +1,25 @@
|
||||
unittests = {}
|
||||
|
||||
core.log("info", "Hello World")
|
||||
|
||||
local function do_tests()
|
||||
assert(core == minetest)
|
||||
-- stuff that should not be here
|
||||
assert(not core.get_player_by_name)
|
||||
assert(not core.set_node)
|
||||
assert(not core.object_refs)
|
||||
-- stuff that should be here
|
||||
assert(ItemStack)
|
||||
assert(core.registered_items[""])
|
||||
-- alias handling
|
||||
assert(core.registered_items["unittests:steel_ingot_alias"].name ==
|
||||
"unittests:steel_ingot")
|
||||
end
|
||||
|
||||
function unittests.async_test()
|
||||
local ok, err = pcall(do_tests)
|
||||
if not ok then
|
||||
core.log("error", err)
|
||||
end
|
||||
return ok
|
||||
end
|
||||
42
games/devtest/mods/unittests/itemdescription.lua
Normal file
@@ -0,0 +1,42 @@
|
||||
local full_description = "Description Test Item\nFor testing item decription"
|
||||
minetest.register_tool("unittests:description_test", {
|
||||
description = full_description,
|
||||
inventory_image = "unittests_description_test.png",
|
||||
})
|
||||
|
||||
minetest.register_chatcommand("item_description", {
|
||||
param = "",
|
||||
description = "Show the short and full description of the wielded item.",
|
||||
func = function(name)
|
||||
local player = minetest.get_player_by_name(name)
|
||||
local item = player:get_wielded_item()
|
||||
return true, string.format("short_description: %s\ndescription: %s",
|
||||
item:get_short_description(), item:get_description())
|
||||
end
|
||||
})
|
||||
|
||||
local function test_short_desc()
|
||||
local function get_short_description(item)
|
||||
return ItemStack(item):get_short_description()
|
||||
end
|
||||
|
||||
local stack = ItemStack("unittests:description_test")
|
||||
assert(stack:get_short_description() == "Description Test Item")
|
||||
assert(get_short_description("unittests:description_test") == "Description Test Item")
|
||||
assert(minetest.registered_items["unittests:description_test"].short_description == nil)
|
||||
assert(stack:get_description() == full_description)
|
||||
assert(stack:get_description() == minetest.registered_items["unittests:description_test"].description)
|
||||
|
||||
stack:get_meta():set_string("description", "Hello World")
|
||||
assert(stack:get_short_description() == "Hello World")
|
||||
assert(stack:get_description() == "Hello World")
|
||||
assert(get_short_description(stack) == "Hello World")
|
||||
assert(get_short_description("unittests:description_test") == "Description Test Item")
|
||||
|
||||
stack:get_meta():set_string("short_description", "Foo Bar")
|
||||
assert(stack:get_short_description() == "Foo Bar")
|
||||
assert(stack:get_description() == "Hello World")
|
||||
|
||||
return true
|
||||
end
|
||||
unittests.register("test_short_desc", test_short_desc)
|
||||
82
games/devtest/mods/unittests/misc.lua
Normal file
@@ -0,0 +1,82 @@
|
||||
local function test_random()
|
||||
-- Try out PseudoRandom
|
||||
local pseudo = PseudoRandom(13)
|
||||
assert(pseudo:next() == 22290)
|
||||
assert(pseudo:next() == 13854)
|
||||
end
|
||||
unittests.register("test_random", test_random)
|
||||
|
||||
local function test_dynamic_media(cb, player)
|
||||
if core.get_player_information(player:get_player_name()).protocol_version < 40 then
|
||||
core.log("warning", "test_dynamic_media: Client too old, skipping test.")
|
||||
return cb()
|
||||
end
|
||||
|
||||
-- Check that the client acknowledges media transfers
|
||||
local path = core.get_worldpath() .. "/test_media.obj"
|
||||
local f = io.open(path, "w")
|
||||
f:write("# contents don't matter\n")
|
||||
f:close()
|
||||
|
||||
local call_ok = false
|
||||
local ok = core.dynamic_add_media({
|
||||
filepath = path,
|
||||
to_player = player:get_player_name(),
|
||||
}, function(name)
|
||||
if not call_ok then
|
||||
return cb("impossible condition")
|
||||
end
|
||||
cb()
|
||||
end)
|
||||
if not ok then
|
||||
return cb("dynamic_add_media() returned error")
|
||||
end
|
||||
call_ok = true
|
||||
|
||||
-- if the callback isn't called this test will just hang :shrug:
|
||||
end
|
||||
unittests.register("test_dynamic_media", test_dynamic_media, {async=true, player=true})
|
||||
|
||||
local function test_v3f_metatable(player)
|
||||
assert(vector.check(player:get_pos()))
|
||||
end
|
||||
unittests.register("test_v3f_metatable", test_v3f_metatable, {player=true})
|
||||
|
||||
local function test_v3s16_metatable(player, pos)
|
||||
local node = minetest.get_node(pos)
|
||||
local found_pos = minetest.find_node_near(pos, 0, node.name, true)
|
||||
assert(vector.check(found_pos))
|
||||
end
|
||||
unittests.register("test_v3s16_metatable", test_v3s16_metatable, {map=true})
|
||||
|
||||
local function test_clear_meta(_, pos)
|
||||
local ref = core.get_meta(pos)
|
||||
|
||||
for way = 1, 3 do
|
||||
ref:set_string("foo", "bar")
|
||||
assert(ref:contains("foo"))
|
||||
|
||||
if way == 1 then
|
||||
ref:from_table({})
|
||||
elseif way == 2 then
|
||||
ref:from_table(nil)
|
||||
else
|
||||
ref:set_string("foo", "")
|
||||
end
|
||||
|
||||
assert(#core.find_nodes_with_meta(pos, pos) == 0, "clearing failed " .. way)
|
||||
end
|
||||
end
|
||||
unittests.register("test_clear_meta", test_clear_meta, {map=true})
|
||||
|
||||
local on_punch_called
|
||||
minetest.register_on_punchnode(function()
|
||||
on_punch_called = true
|
||||
end)
|
||||
unittests.register("test_punch_node", function(_, pos)
|
||||
minetest.place_node(pos, {name="basenodes:dirt"})
|
||||
on_punch_called = false
|
||||
minetest.punch_node(pos)
|
||||
minetest.remove_node(pos)
|
||||
-- currently failing: assert(on_punch_called)
|
||||
end, {map=true})
|
||||
3
games/devtest/mods/unittests/mod.conf
Normal file
@@ -0,0 +1,3 @@
|
||||
name = unittests
|
||||
description = Adds automated unit tests for the engine
|
||||
depends = basenodes
|
||||
70
games/devtest/mods/unittests/player.lua
Normal file
@@ -0,0 +1,70 @@
|
||||
--
|
||||
-- HP Change Reasons
|
||||
--
|
||||
local expect = nil
|
||||
minetest.register_on_player_hpchange(function(player, hp, reason)
|
||||
if expect == nil then
|
||||
return
|
||||
end
|
||||
|
||||
for key, value in pairs(reason) do
|
||||
assert(expect[key] == value)
|
||||
end
|
||||
for key, value in pairs(expect) do
|
||||
assert(reason[key] == value)
|
||||
end
|
||||
|
||||
expect = nil
|
||||
end)
|
||||
|
||||
local function run_hpchangereason_tests(player)
|
||||
local old_hp = player:get_hp()
|
||||
|
||||
player:set_hp(20)
|
||||
expect = { type = "set_hp", from = "mod" }
|
||||
player:set_hp(3)
|
||||
assert(expect == nil)
|
||||
|
||||
expect = { a = 234, type = "set_hp", from = "mod" }
|
||||
player:set_hp(7, { a= 234 })
|
||||
assert(expect == nil)
|
||||
|
||||
expect = { df = 3458973454, type = "fall", from = "mod" }
|
||||
player:set_hp(10, { type = "fall", df = 3458973454 })
|
||||
assert(expect == nil)
|
||||
|
||||
player:set_hp(old_hp)
|
||||
end
|
||||
unittests.register("test_hpchangereason", run_hpchangereason_tests, {player=true})
|
||||
|
||||
--
|
||||
-- Player meta
|
||||
--
|
||||
local function run_player_meta_tests(player)
|
||||
local meta = player:get_meta()
|
||||
meta:set_string("foo", "bar")
|
||||
assert(meta:contains("foo"))
|
||||
assert(meta:get_string("foo") == "bar")
|
||||
assert(meta:get("foo") == "bar")
|
||||
|
||||
local meta2 = player:get_meta()
|
||||
assert(meta2:get_string("foo") == "bar")
|
||||
assert(meta2:get("foo") == "bar")
|
||||
assert(meta:equals(meta2))
|
||||
|
||||
meta:set_string("bob", "dillan")
|
||||
assert(meta:get_string("foo") == "bar")
|
||||
assert(meta:get_string("bob") == "dillan")
|
||||
assert(meta:get("bob") == "dillan")
|
||||
assert(meta2:get_string("foo") == "bar")
|
||||
assert(meta2:get_string("bob") == "dillan")
|
||||
assert(meta2:get("bob") == "dillan")
|
||||
assert(meta:equals(meta2))
|
||||
|
||||
meta:set_string("foo", "")
|
||||
assert(not meta:contains("foo"))
|
||||
assert(meta:get("foo") == nil)
|
||||
assert(meta:get_string("foo") == "")
|
||||
assert(meta:equals(meta2))
|
||||
end
|
||||
unittests.register("test_player_meta", run_player_meta_tests, {player=true})
|
||||
BIN
games/devtest/mods/unittests/textures/default_dirt.png
Normal file
|
After Width: | Height: | Size: 790 B |
BIN
games/devtest/mods/unittests/textures/unittests_coal_lump.png
Normal file
|
After Width: | Height: | Size: 160 B |
|
After Width: | Height: | Size: 268 B |
BIN
games/devtest/mods/unittests/textures/unittests_iron_lump.png
Normal file
|
After Width: | Height: | Size: 154 B |
|
After Width: | Height: | Size: 160 B |
BIN
games/devtest/mods/unittests/textures/unittests_steel_ingot.png
Normal file
|
After Width: | Height: | Size: 159 B |
BIN
games/devtest/mods/unittests/textures/unittests_stick.png
Normal file
|
After Width: | Height: | Size: 147 B |
BIN
games/devtest/mods/unittests/textures/unittests_torch.png
Normal file
|
After Width: | Height: | Size: 155 B |
|
After Width: | Height: | Size: 157 B |