NTest.lua 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. local function TERMINAL_HANDLER(e, test, msg, errormsg)
  2. if errormsg then
  3. errormsg = ": "..errormsg
  4. else
  5. errormsg = ""
  6. end
  7. if e == 'start' then
  8. print("######## "..e.."ed "..test.." tests")
  9. elseif e == 'pass' then
  10. print(" "..e.." "..test..': '..msg)
  11. elseif e == 'fail' then
  12. print(" ==> "..e.." "..test..': '..msg..errormsg)
  13. elseif e == 'except' then
  14. print(" ==> "..e.." "..test..': '..msg..errormsg)
  15. elseif e == 'finish' then
  16. print("######## "..e.."ed "..test.." tests")
  17. else
  18. print(e.." "..test)
  19. end
  20. end
  21. --[[
  22. if equal returns true
  23. if different returns {msg = "<reason>"}
  24. this will be handled spechially by ok and nok
  25. --]]
  26. local function deepeq(a, b)
  27. local function notEqual(m)
  28. return { msg=m }
  29. end
  30. -- Different types: false
  31. if type(a) ~= type(b) then return notEqual("type 1 is "..type(a)..", type 2 is "..type(b)) end
  32. -- Functions
  33. if type(a) == 'function' then
  34. if string.dump(a) == string.dump(b) then
  35. return true
  36. else
  37. return notEqual("functions differ")
  38. end
  39. end
  40. -- Primitives and equal pointers
  41. if a == b then return true end
  42. -- Only equal tables could have passed previous tests
  43. if type(a) ~= 'table' then return notEqual("different "..type(a).."s expected "..a.." vs. "..b) end
  44. -- Compare tables field by field
  45. for k,v in pairs(a) do
  46. if b[k] == nil then return notEqual("key "..k.."only contained in left part") end
  47. local result = deepeq(v, b[k])
  48. if type(result) == 'table' then return result end
  49. end
  50. for k,v in pairs(b) do
  51. if a[k] == nil then return notEqual("key "..k.."only contained in right part") end
  52. local result = deepeq(a[k], v)
  53. if type(result) == 'table' then return result end
  54. end
  55. return true
  56. end
  57. -- Compatibility for Lua 5.1 and Lua 5.2
  58. local function args(...)
  59. return {n=select('#', ...), ...}
  60. end
  61. local function spy(f)
  62. local mt = {}
  63. setmetatable(mt, {__call = function(s, ...)
  64. s.called = s.called or {}
  65. local a = args(...)
  66. table.insert(s.called, {...})
  67. if f then
  68. local r
  69. r = args(pcall(f, unpack(a, 1, a.n)))
  70. if not r[1] then
  71. s.errors = s.errors or {}
  72. s.errors[#s.called] = r[2]
  73. else
  74. return unpack(r, 2, r.n)
  75. end
  76. end
  77. end})
  78. return mt
  79. end
  80. local function getstackframe()
  81. -- debug.getinfo() does not exist in NodeMCU Lua 5.1
  82. if debug.getinfo then
  83. return debug.getinfo(5, 'S').short_src:match("([^\\/]*)$")..":"..debug.getinfo(5, 'l').currentline
  84. end
  85. local msg
  86. msg = debug.traceback()
  87. msg = msg:match("\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t([^\t]*): in") -- Get 5th stack frame
  88. msg = msg:match(".-([^\\/]*)$") -- cut off path of filename
  89. return msg
  90. end
  91. local function assertok(handler, name, invert, cond, msg)
  92. local errormsg
  93. -- check if cond is return object of 'eq' call
  94. if type(cond) == 'table' and cond.msg then
  95. errormsg = cond.msg
  96. cond = false
  97. end
  98. if not msg then
  99. msg = getstackframe()
  100. end
  101. if invert then
  102. cond = not cond
  103. end
  104. if cond then
  105. handler('pass', name, msg)
  106. else
  107. handler('fail', name, msg, errormsg)
  108. error('_*_TestAbort_*_')
  109. end
  110. end
  111. local function fail(handler, name, func, expected, msg)
  112. local status, err = pcall(func)
  113. if not msg then
  114. msg = getstackframe()
  115. end
  116. if status then
  117. local messageParts = {"Expected to fail with Error"}
  118. if expected then
  119. messageParts[2] = " containing \"" .. expected .. "\""
  120. end
  121. handler('fail', name, msg, table.concat(messageParts, ""))
  122. error('_*_TestAbort_*_')
  123. end
  124. if (expected and not string.find(err, expected)) then
  125. err = err:match(".-([^\\/]*)$") -- cut off path of filename
  126. handler('fail', name, msg, "expected errormessage \"" .. err .. "\" to contain \"" .. expected .. "\"")
  127. error('_*_TestAbort_*_')
  128. end
  129. handler('pass', name, msg)
  130. end
  131. local function NTest(testrunname, failoldinterface)
  132. if failoldinterface then error("The interface has changed. Please see documentstion.") end
  133. local pendingtests = {}
  134. local env = _G
  135. local outputhandler = TERMINAL_HANDLER
  136. local started
  137. local function runpending()
  138. if pendingtests[1] ~= nil then
  139. node.task.post(node.task.LOW_PRIORITY, function()
  140. pendingtests[1](runpending)
  141. end)
  142. else
  143. outputhandler('finish', testrunname)
  144. end
  145. end
  146. local function copyenv(dest, src)
  147. dest.eq = src.eq
  148. dest.spy = src.spy
  149. dest.ok = src.ok
  150. dest.nok = src.nok
  151. dest.fail = src.fail
  152. end
  153. local function testimpl(name, f, async)
  154. local testfn = function(next)
  155. local prev = {}
  156. copyenv(prev, env)
  157. local handler = outputhandler
  158. local restore = function(err)
  159. if err then
  160. err = err:match(".-([^\\/]*)$") -- cut off path of filename
  161. if not err:match('_*_TestAbort_*_') then
  162. handler('except', name, err)
  163. end
  164. end
  165. if node then node.setonerror() end
  166. copyenv(env, prev)
  167. outputhandler('end', name)
  168. table.remove(pendingtests, 1)
  169. collectgarbage()
  170. if next then next() end
  171. end
  172. local function wrap(method, ...)
  173. method(handler, name, ...)
  174. end
  175. local function cbError(err)
  176. err = err:match(".-([^\\/]*)$") -- cut off path of filename
  177. if not err:match('_*_TestAbort_*_') then
  178. handler('except', name, err)
  179. end
  180. restore()
  181. end
  182. env.eq = deepeq
  183. env.spy = spy
  184. env.ok = function (cond, msg1, msg2) wrap(assertok, false, cond, msg1, msg2) end
  185. env.nok = function(cond, msg1, msg2) wrap(assertok, true, cond, msg1, msg2) end
  186. env.fail = function (func, expected, msg) wrap(fail, func, expected, msg) end
  187. handler('begin', name)
  188. node.setonerror(cbError)
  189. local ok, err = pcall(f, async and restore)
  190. if not ok then
  191. err = err:match(".-([^\\/]*)$") -- cut off path of filename
  192. if not err:match('_*_TestAbort_*_') then
  193. handler('except', name, err)
  194. end
  195. if async then
  196. restore()
  197. end
  198. end
  199. if not async then
  200. restore()
  201. end
  202. end
  203. if not started then
  204. outputhandler('start', testrunname)
  205. started = true
  206. end
  207. table.insert(pendingtests, testfn)
  208. if #pendingtests == 1 then
  209. runpending()
  210. end
  211. end
  212. local function test(name, f)
  213. testimpl(name, f)
  214. end
  215. local function testasync(name, f)
  216. testimpl(name, f, true)
  217. end
  218. local function report(f, envP)
  219. outputhandler = f or outputhandler
  220. env = envP or env
  221. end
  222. local currentCoName
  223. local function testco(name, func)
  224. -- local t = tmr.create();
  225. local co
  226. testasync(name, function(Next)
  227. currentCoName = name
  228. local function getCB(cbName)
  229. return function(...) -- upval: co, cbName
  230. local result, err = coroutine.resume(co, cbName, ...)
  231. if (not result) then
  232. if (name == currentCoName) then
  233. currentCoName = nil
  234. Next(err)
  235. else
  236. outputhandler('fail', name, "Found stray Callback '"..cbName.."' from test '"..name.."'")
  237. end
  238. elseif coroutine.status(co) == "dead" then
  239. currentCoName = nil
  240. Next()
  241. end
  242. end
  243. end
  244. local function waitCb()
  245. return coroutine.yield()
  246. end
  247. co = coroutine.create(function(wr, wa)
  248. func(wr, wa)
  249. end)
  250. local result, err = coroutine.resume(co, getCB, waitCb)
  251. if (not result) then
  252. currentCoName = nil
  253. Next(err)
  254. elseif coroutine.status(co) == "dead" then
  255. currentCoName = nil
  256. Next()
  257. end
  258. end)
  259. end
  260. return {test = test, testasync = testasync, testco = testco, report = report}
  261. end
  262. return NTest