mirror of
https://github.com/luanti-org/luanti.git
synced 2025-06-27 16:36:03 +00:00
252 lines
6.5 KiB
Lua
252 lines
6.5 KiB
Lua
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!)
|
|
-- random = false, -- Does the test use math.random directly or indirectly?
|
|
-- }
|
|
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
|
|
|
|
local function printf(fmt, ...)
|
|
print(fmt:format(...))
|
|
end
|
|
|
|
function unittests.run_one(idx, counters, out_callback, player, pos)
|
|
local def = unittests.list[idx]
|
|
local seed
|
|
if def.random then
|
|
seed = core.get_us_time()
|
|
math.randomseed(seed)
|
|
end
|
|
|
|
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(err)
|
|
local tend = core.get_us_time()
|
|
local ms_taken = (tend - tbegin) / 1000
|
|
|
|
if err then
|
|
core.log("error", err)
|
|
end
|
|
printf("[%s] %s - %dms", err and "FAIL" or "PASS", def.name, ms_taken)
|
|
if seed and err then
|
|
printf("Random was seeded to %d", seed)
|
|
end
|
|
counters.time = counters.time + ms_taken
|
|
counters.total = counters.total + 1
|
|
counters.passed = counters.passed + (err and 0 or 1)
|
|
end
|
|
|
|
if def.async then
|
|
core.log("info", "[unittest] running " .. def.name .. " (async)")
|
|
def.func(function(err)
|
|
done(err)
|
|
out_callback(true)
|
|
end, player, pos)
|
|
else
|
|
core.log("info", "[unittest] running " .. def.name)
|
|
local err
|
|
xpcall(function() return def.func(player, pos) end, function(e)
|
|
err = e .. "\n" .. debug.traceback()
|
|
end)
|
|
done(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(pos, callback)
|
|
local function check()
|
|
if core.get_node(pos).name ~= "ignore" then
|
|
callback()
|
|
else
|
|
core.after(0, check)
|
|
end
|
|
end
|
|
check()
|
|
end
|
|
|
|
-- This runs in a coroutine so it uses await()
|
|
function unittests.run_all()
|
|
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
|
|
local pos = player:get_pos():round():offset(0, 5, 0)
|
|
core.forceload_block(pos, true, -1)
|
|
await(function(cb)
|
|
wait_for_map(pos, cb)
|
|
end)
|
|
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))
|
|
local passed = counters.total == counters.passed
|
|
printf("Devtest Unit Test Results: %s", passed and "PASSED" or "FAILED")
|
|
printf(" %d / %d failed tests.",
|
|
counters.total - counters.passed, counters.total)
|
|
printf(" 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")
|
|
dofile(modpath .. "/version.lua")
|
|
dofile(modpath .. "/itemstack_equals.lua")
|
|
dofile(modpath .. "/content_ids.lua")
|
|
dofile(modpath .. "/metadata.lua")
|
|
dofile(modpath .. "/raycast.lua")
|
|
dofile(modpath .. "/inventory.lua")
|
|
dofile(modpath .. "/load_time.lua")
|
|
dofile(modpath .. "/on_shutdown.lua")
|
|
dofile(modpath .. "/color.lua")
|
|
|
|
local bustitute = dofile(modpath .. "/bustitute.lua")
|
|
bustitute.register("matrix4")
|
|
|
|
--------------
|
|
|
|
local function send_results(name, ok)
|
|
core.chat_send_player(name,
|
|
core.colorize(ok and "green" or "red",
|
|
(ok and "All devtest unit tests passed." or
|
|
"There were devtest unit test failures.") ..
|
|
" Check the console for detailed output."))
|
|
end
|
|
|
|
if core.settings:get_bool("devtest_unittests_autostart", false) then
|
|
local test_results = nil
|
|
core.after(0, function()
|
|
-- CI adds a mod which sets `unittests.on_finished`
|
|
-- to write status information to the filesystem
|
|
local old_on_finished = unittests.on_finished
|
|
unittests.on_finished = function(ok)
|
|
for _, player in ipairs(core.get_connected_players()) do
|
|
send_results(player:get_player_name(), ok)
|
|
end
|
|
test_results = ok
|
|
old_on_finished(ok)
|
|
end
|
|
coroutine.wrap(unittests.run_all)()
|
|
end)
|
|
core.register_on_joinplayer(function(player)
|
|
if test_results == nil then
|
|
return -- tests haven't completed yet
|
|
end
|
|
send_results(player:get_player_name(), test_results)
|
|
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)
|
|
send_results(name, ok)
|
|
end
|
|
coroutine.wrap(unittests.run_all)()
|
|
return true, ""
|
|
end,
|
|
})
|
|
end
|