1
0

init.lua 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. unittests = {}
  2. unittests.list = {}
  3. -- name: Name of the test
  4. -- func:
  5. -- for sync: function(player, pos), should error on failure
  6. -- for async: function(callback, player, pos)
  7. -- MUST call callback() or callback("error msg") in case of error once test is finished
  8. -- this means you cannot use assert() in the test implementation
  9. -- opts: {
  10. -- player = false, -- Does test require a player?
  11. -- map = false, -- Does test require map access?
  12. -- async = false, -- Does the test run asynchronously? (read notes above!)
  13. -- }
  14. function unittests.register(name, func, opts)
  15. local def = table.copy(opts or {})
  16. def.name = name
  17. def.func = func
  18. table.insert(unittests.list, def)
  19. end
  20. function unittests.on_finished(all_passed)
  21. -- free to override
  22. end
  23. -- Calls invoke with a callback as argument
  24. -- Suspends coroutine until that callback is called
  25. -- Return values are passed through
  26. local function await(invoke)
  27. local co = coroutine.running()
  28. assert(co)
  29. local called_early = true
  30. invoke(function(...)
  31. if called_early == true then
  32. called_early = {...}
  33. else
  34. coroutine.resume(co, ...)
  35. co = nil
  36. end
  37. end)
  38. if called_early ~= true then
  39. -- callback was already called before yielding
  40. return unpack(called_early)
  41. end
  42. called_early = nil
  43. return coroutine.yield()
  44. end
  45. function unittests.run_one(idx, counters, out_callback, player, pos)
  46. local def = unittests.list[idx]
  47. if not def.player then
  48. player = nil
  49. elseif player == nil then
  50. out_callback(false)
  51. return false
  52. end
  53. if not def.map then
  54. pos = nil
  55. elseif pos == nil then
  56. out_callback(false)
  57. return false
  58. end
  59. local tbegin = core.get_us_time()
  60. local function done(status, err)
  61. local tend = core.get_us_time()
  62. local ms_taken = (tend - tbegin) / 1000
  63. if not status then
  64. core.log("error", err)
  65. end
  66. print(string.format("[%s] %s - %dms",
  67. status and "PASS" or "FAIL", def.name, ms_taken))
  68. counters.time = counters.time + ms_taken
  69. counters.total = counters.total + 1
  70. if status then
  71. counters.passed = counters.passed + 1
  72. end
  73. end
  74. if def.async then
  75. core.log("info", "[unittest] running " .. def.name .. " (async)")
  76. def.func(function(err)
  77. done(err == nil, err)
  78. out_callback(true)
  79. end, player, pos)
  80. else
  81. core.log("info", "[unittest] running " .. def.name)
  82. local status, err = pcall(def.func, player, pos)
  83. done(status, err)
  84. out_callback(true)
  85. end
  86. return true
  87. end
  88. local function wait_for_player(callback)
  89. if #core.get_connected_players() > 0 then
  90. return callback(core.get_connected_players()[1])
  91. end
  92. local first = true
  93. core.register_on_joinplayer(function(player)
  94. if first then
  95. callback(player)
  96. first = false
  97. end
  98. end)
  99. end
  100. local function wait_for_map(pos, callback)
  101. local function check()
  102. if core.get_node(pos).name ~= "ignore" then
  103. callback()
  104. else
  105. core.after(0, check)
  106. end
  107. end
  108. check()
  109. end
  110. -- This runs in a coroutine so it uses await()
  111. function unittests.run_all()
  112. local counters = { time = 0, total = 0, passed = 0 }
  113. -- Run standalone tests first
  114. for idx = 1, #unittests.list do
  115. local def = unittests.list[idx]
  116. def.done = await(function(cb)
  117. unittests.run_one(idx, counters, cb, nil, nil)
  118. end)
  119. end
  120. -- Wait for a player to join, run tests that require a player
  121. local player = await(wait_for_player)
  122. for idx = 1, #unittests.list do
  123. local def = unittests.list[idx]
  124. if not def.done then
  125. def.done = await(function(cb)
  126. unittests.run_one(idx, counters, cb, player, nil)
  127. end)
  128. end
  129. end
  130. -- Wait for the world to generate/load, run tests that require map access
  131. local pos = player:get_pos():round():offset(0, 5, 0)
  132. core.forceload_block(pos, true, -1)
  133. await(function(cb)
  134. wait_for_map(pos, cb)
  135. end)
  136. for idx = 1, #unittests.list do
  137. local def = unittests.list[idx]
  138. if not def.done then
  139. def.done = await(function(cb)
  140. unittests.run_one(idx, counters, cb, player, pos)
  141. end)
  142. end
  143. end
  144. -- Print stats
  145. assert(#unittests.list == counters.total)
  146. print(string.rep("+", 80))
  147. print(string.format("Devtest Unit Test Results: %s",
  148. counters.total == counters.passed and "PASSED" or "FAILED"))
  149. print(string.format(" %d / %d failed tests.",
  150. counters.total - counters.passed, counters.total))
  151. print(string.format(" Testing took %dms total.", counters.time))
  152. print(string.rep("+", 80))
  153. unittests.on_finished(counters.total == counters.passed)
  154. return counters.total == counters.passed
  155. end
  156. --------------
  157. local modpath = core.get_modpath("unittests")
  158. dofile(modpath .. "/misc.lua")
  159. dofile(modpath .. "/player.lua")
  160. dofile(modpath .. "/crafting.lua")
  161. dofile(modpath .. "/itemdescription.lua")
  162. dofile(modpath .. "/async_env.lua")
  163. dofile(modpath .. "/entity.lua")
  164. dofile(modpath .. "/get_version.lua")
  165. dofile(modpath .. "/itemstack_equals.lua")
  166. dofile(modpath .. "/content_ids.lua")
  167. dofile(modpath .. "/metadata.lua")
  168. dofile(modpath .. "/raycast.lua")
  169. dofile(modpath .. "/inventory.lua")
  170. dofile(modpath .. "/load_time.lua")
  171. dofile(modpath .. "/on_shutdown.lua")
  172. dofile(modpath .. "/color.lua")
  173. --------------
  174. local function send_results(name, ok)
  175. core.chat_send_player(name,
  176. core.colorize(ok and "green" or "red",
  177. (ok and "All devtest unit tests passed." or
  178. "There were devtest unit test failures.") ..
  179. " Check the console for detailed output."))
  180. end
  181. if core.settings:get_bool("devtest_unittests_autostart", false) then
  182. local test_results = nil
  183. core.after(0, function()
  184. -- CI adds a mod which sets `unittests.on_finished`
  185. -- to write status information to the filesystem
  186. local old_on_finished = unittests.on_finished
  187. unittests.on_finished = function(ok)
  188. for _, player in ipairs(core.get_connected_players()) do
  189. send_results(player:get_player_name(), ok)
  190. end
  191. test_results = ok
  192. old_on_finished(ok)
  193. end
  194. coroutine.wrap(unittests.run_all)()
  195. end)
  196. core.register_on_joinplayer(function(player)
  197. if test_results == nil then
  198. return -- tests haven't completed yet
  199. end
  200. send_results(player:get_player_name(), test_results)
  201. end)
  202. else
  203. core.register_chatcommand("unittests", {
  204. privs = {basic_privs=true},
  205. description = "Runs devtest unittests (may modify player or map state)",
  206. func = function(name, param)
  207. unittests.on_finished = function(ok)
  208. send_results(name, ok)
  209. end
  210. coroutine.wrap(unittests.run_all)()
  211. return true, ""
  212. end,
  213. })
  214. end