5 -- name: Name of the test
7 -- for sync: function(player, pos), should error on failure
8 -- for async: function(callback, player, pos)
9 -- MUST call callback() or callback("error msg") in case of error once test is finished
10 -- this means you cannot use assert() in the test implementation
12 -- player = false, -- Does test require a player?
13 -- map = false, -- Does test require map access?
14 -- async = false, -- Does the test run asynchronously? (read notes above!)
16 function unittests.register(name, func, opts)
17 local def = table.copy(opts or {})
20 table.insert(unittests.list, def)
23 function unittests.on_finished(all_passed)
27 -- Calls invoke with a callback as argument
28 -- Suspends coroutine until that callback is called
29 -- Return values are passed through
30 local function await(invoke)
31 local co = coroutine.running()
33 local called_early = true
35 if called_early == true then
38 coroutine.resume(co, ...)
41 if called_early ~= true then
42 -- callback was already called before yielding
43 return unpack(called_early)
46 return coroutine.yield()
49 function unittests.run_one(idx, counters, out_callback, player, pos)
50 local def = unittests.list[idx]
51 if not def.player then
53 elseif player == nil then
59 elseif pos == nil then
64 local tbegin = core.get_us_time()
65 local function done(status, err)
66 local tend = core.get_us_time()
67 local ms_taken = (tend - tbegin) / 1000
70 core.log("error", err)
72 print(string.format("[%s] %s - %dms",
73 status and "PASS" or "FAIL", def.name, ms_taken))
74 counters.time = counters.time + ms_taken
75 counters.total = counters.total + 1
77 counters.passed = counters.passed + 1
82 core.log("info", "[unittest] running " .. def.name .. " (async)")
83 def.func(function(err)
88 core.log("info", "[unittest] running " .. def.name)
89 local status, err = pcall(def.func, player, pos)
97 local function wait_for_player(callback)
98 if #core.get_connected_players() > 0 then
99 return callback(core.get_connected_players()[1])
102 core.register_on_joinplayer(function(player)
110 local function wait_for_map(player, callback)
111 local check = function()
112 if core.get_node_or_nil(player:get_pos()) ~= nil then
121 function unittests.run_all()
122 -- This runs in a coroutine so it uses await().
123 local counters = { time = 0, total = 0, passed = 0 }
125 -- Run standalone tests first
126 for idx = 1, #unittests.list do
127 local def = unittests.list[idx]
128 def.done = await(function(cb)
129 unittests.run_one(idx, counters, cb, nil, nil)
133 -- Wait for a player to join, run tests that require a player
134 local player = await(wait_for_player)
135 for idx = 1, #unittests.list do
136 local def = unittests.list[idx]
138 def.done = await(function(cb)
139 unittests.run_one(idx, counters, cb, player, nil)
144 -- Wait for the world to generate/load, run tests that require map access
146 wait_for_map(player, cb)
148 local pos = vector.round(player:get_pos())
149 for idx = 1, #unittests.list do
150 local def = unittests.list[idx]
152 def.done = await(function(cb)
153 unittests.run_one(idx, counters, cb, player, pos)
159 assert(#unittests.list == counters.total)
160 print(string.rep("+", 80))
161 print(string.format("Unit Test Results: %s",
162 counters.total == counters.passed and "PASSED" or "FAILED"))
163 print(string.format(" %d / %d failed tests.",
164 counters.total - counters.passed, counters.total))
165 print(string.format(" Testing took %dms total.", counters.time))
166 print(string.rep("+", 80))
167 unittests.on_finished(counters.total == counters.passed)
168 return counters.total == counters.passed
173 local modpath = core.get_modpath("unittests")
174 dofile(modpath .. "/misc.lua")
175 dofile(modpath .. "/player.lua")
176 dofile(modpath .. "/crafting.lua")
177 dofile(modpath .. "/itemdescription.lua")
178 dofile(modpath .. "/async_env.lua")
179 dofile(modpath .. "/entity.lua")
183 if core.settings:get_bool("devtest_unittests_autostart", false) then
184 core.after(0, function()
185 coroutine.wrap(unittests.run_all)()
188 core.register_chatcommand("unittests", {
189 privs = {basic_privs=true},
190 description = "Runs devtest unittests (may modify player or map state)",
191 func = function(name, param)
192 unittests.on_finished = function(ok)
193 core.chat_send_player(name,
194 (ok and "All tests passed." or "There were test failures.") ..
195 " Check the console for detailed output.")
197 coroutine.wrap(unittests.run_all)()