1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
|
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, ...)
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
minetest.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 = minetest.get_modpath("unittests")
dofile(modpath .. "/misc.lua")
dofile(modpath .. "/player.lua")
dofile(modpath .. "/crafting.lua")
dofile(modpath .. "/itemdescription.lua")
dofile(modpath .. "/async_env.lua")
--------------
if core.settings:get_bool("devtest_unittests_autostart", false) then
core.after(0, function()
coroutine.wrap(unittests.run_all)()
end)
else
minetest.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
|