]> git.lizzy.rs Git - minetest.git/blob - games/devtest/mods/unittests/init.lua
96651a8787380c1279c7f77c5e1019c663ce29e6
[minetest.git] / games / devtest / mods / unittests / init.lua
1 unittests = {}
2
3 unittests.list = {}
4
5 -- name: Name of the test
6 -- func:
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
11 -- opts: {
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!)
15 -- }
16 function unittests.register(name, func, opts)
17         local def = table.copy(opts or {})
18         def.name = name
19         def.func = func
20         table.insert(unittests.list, def)
21 end
22
23 function unittests.on_finished(all_passed)
24         -- free to override
25 end
26
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()
32         assert(co)
33         local called_early = true
34         invoke(function(...)
35                 if called_early == true then
36                         called_early = {...}
37                 else
38                         coroutine.resume(co, ...)
39                 end
40         end)
41         if called_early ~= true then
42                 -- callback was already called before yielding
43                 return unpack(called_early)
44         end
45         called_early = nil
46         return coroutine.yield()
47 end
48
49 function unittests.run_one(idx, counters, out_callback, player, pos)
50         local def = unittests.list[idx]
51         if not def.player then
52                 player = nil
53         elseif player == nil then
54                 out_callback(false)
55                 return false
56         end
57         if not def.map then
58                 pos = nil
59         elseif pos == nil then
60                 out_callback(false)
61                 return false
62         end
63
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
68
69                 if not status then
70                         core.log("error", err)
71                 end
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
76                 if status then
77                         counters.passed = counters.passed + 1
78                 end
79         end
80
81         if def.async then
82                 core.log("info", "[unittest] running " .. def.name .. " (async)")
83                 def.func(function(err)
84                         done(err == nil, err)
85                         out_callback(true)
86                 end, player, pos)
87         else
88                 core.log("info", "[unittest] running " .. def.name)
89                 local status, err = pcall(def.func, player, pos)
90                 done(status, err)
91                 out_callback(true)
92         end
93         
94         return true
95 end
96
97 local function wait_for_player(callback)
98         if #core.get_connected_players() > 0 then
99                 return callback(core.get_connected_players()[1])
100         end
101         local first = true
102         core.register_on_joinplayer(function(player)
103                 if first then
104                         callback(player)
105                         first = false
106                 end
107         end)
108 end
109
110 local function wait_for_map(player, callback)
111         local check = function()
112                 if core.get_node_or_nil(player:get_pos()) ~= nil then
113                         callback()
114                 else
115                         core.after(0, check)
116                 end
117         end
118         check()
119 end
120
121 function unittests.run_all()
122         -- This runs in a coroutine so it uses await().
123         local counters = { time = 0, total = 0, passed = 0 }
124
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)
130                 end)
131         end
132
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]
137                 if not def.done then
138                         def.done = await(function(cb)
139                                 unittests.run_one(idx, counters, cb, player, nil)
140                         end)
141                 end
142         end
143
144         -- Wait for the world to generate/load, run tests that require map access
145         await(function(cb)
146                 wait_for_map(player, cb)
147         end)
148         local pos = vector.round(player:get_pos())
149         for idx = 1, #unittests.list do
150                 local def = unittests.list[idx]
151                 if not def.done then
152                         def.done = await(function(cb)
153                                 unittests.run_one(idx, counters, cb, player, pos)
154                         end)
155                 end
156         end
157
158         -- Print stats
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
169 end
170
171 --------------
172
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")
180
181 --------------
182
183 if core.settings:get_bool("devtest_unittests_autostart", false) then
184         core.after(0, function()
185                 coroutine.wrap(unittests.run_all)()
186         end)
187 else
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.")
196                         end
197                         coroutine.wrap(unittests.run_all)()
198                         return true, ""
199                 end,
200         })
201 end