init.lua 6.4 KB

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