2020-11-08 15:31:11 +01:00
|
|
|
local function TERMINAL_HANDLER(e, test, msg, errormsg)
|
|
|
|
if errormsg then
|
|
|
|
errormsg = ": "..errormsg
|
|
|
|
else
|
|
|
|
errormsg = ""
|
|
|
|
end
|
|
|
|
if e == 'start' then
|
|
|
|
print("######## "..e.."ed "..test.." tests")
|
|
|
|
elseif e == 'pass' then
|
|
|
|
print(" "..e.." "..test..': '..msg)
|
|
|
|
elseif e == 'fail' then
|
|
|
|
print(" ==> "..e.." "..test..': '..msg..errormsg)
|
|
|
|
elseif e == 'except' then
|
|
|
|
print(" ==> "..e.." "..test..': '..msg..errormsg)
|
|
|
|
elseif e == 'finish' then
|
|
|
|
print("######## "..e.."ed "..test.." tests")
|
|
|
|
else
|
|
|
|
print(e.." "..test)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
--[[
|
|
|
|
if equal returns true
|
|
|
|
if different returns {msg = "<reason>"}
|
|
|
|
this will be handled spechially by ok and nok
|
|
|
|
--]]
|
|
|
|
local function deepeq(a, b)
|
|
|
|
local function notEqual(m)
|
|
|
|
return { msg=m }
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Different types: false
|
|
|
|
if type(a) ~= type(b) then return notEqual("type 1 is "..type(a)..", type 2 is "..type(b)) end
|
|
|
|
-- Functions
|
|
|
|
if type(a) == 'function' then
|
|
|
|
if string.dump(a) == string.dump(b) then
|
|
|
|
return true
|
|
|
|
else
|
|
|
|
return notEqual("functions differ")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
-- Primitives and equal pointers
|
|
|
|
if a == b then return true end
|
|
|
|
-- Only equal tables could have passed previous tests
|
|
|
|
if type(a) ~= 'table' then return notEqual("different "..type(a).."s expected "..a.." vs. "..b) end
|
|
|
|
-- Compare tables field by field
|
|
|
|
for k,v in pairs(a) do
|
|
|
|
if b[k] == nil then return notEqual("key "..k.."only contained in left part") end
|
|
|
|
local result = deepeq(v, b[k])
|
|
|
|
if type(result) == 'table' then return result end
|
|
|
|
end
|
|
|
|
for k,v in pairs(b) do
|
|
|
|
if a[k] == nil then return notEqual("key "..k.."only contained in right part") end
|
|
|
|
local result = deepeq(a[k], v)
|
|
|
|
if type(result) == 'table' then return result end
|
|
|
|
end
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Compatibility for Lua 5.1 and Lua 5.2
|
|
|
|
local function args(...)
|
|
|
|
return {n=select('#', ...), ...}
|
|
|
|
end
|
|
|
|
|
|
|
|
local function spy(f)
|
|
|
|
local mt = {}
|
|
|
|
setmetatable(mt, {__call = function(s, ...)
|
|
|
|
s.called = s.called or {}
|
|
|
|
local a = args(...)
|
|
|
|
table.insert(s.called, {...})
|
|
|
|
if f then
|
|
|
|
local r
|
|
|
|
r = args(pcall(f, unpack(a, 1, a.n)))
|
|
|
|
if not r[1] then
|
|
|
|
s.errors = s.errors or {}
|
|
|
|
s.errors[#s.called] = r[2]
|
|
|
|
else
|
|
|
|
return unpack(r, 2, r.n)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end})
|
|
|
|
return mt
|
|
|
|
end
|
|
|
|
|
|
|
|
local function getstackframe()
|
|
|
|
-- debug.getinfo() does not exist in NodeMCU Lua 5.1
|
|
|
|
if debug.getinfo then
|
|
|
|
return debug.getinfo(5, 'S').short_src:match("([^\\/]*)$")..":"..debug.getinfo(5, 'l').currentline
|
|
|
|
end
|
|
|
|
|
|
|
|
local msg
|
|
|
|
msg = debug.traceback()
|
|
|
|
msg = msg:match("\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t([^\t]*): in") -- Get 5th stack frame
|
|
|
|
msg = msg:match(".-([^\\/]*)$") -- cut off path of filename
|
|
|
|
return msg
|
|
|
|
end
|
|
|
|
|
|
|
|
local function assertok(handler, name, invert, cond, msg)
|
|
|
|
local errormsg
|
|
|
|
-- check if cond is return object of 'eq' call
|
|
|
|
if type(cond) == 'table' and cond.msg then
|
|
|
|
errormsg = cond.msg
|
|
|
|
cond = false
|
|
|
|
end
|
|
|
|
if not msg then
|
|
|
|
msg = getstackframe()
|
|
|
|
end
|
|
|
|
|
|
|
|
if invert then
|
|
|
|
cond = not cond
|
|
|
|
end
|
|
|
|
if cond then
|
|
|
|
handler('pass', name, msg)
|
|
|
|
else
|
|
|
|
handler('fail', name, msg, errormsg)
|
|
|
|
error('_*_TestAbort_*_')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function fail(handler, name, func, expected, msg)
|
|
|
|
local status, err = pcall(func)
|
|
|
|
if not msg then
|
|
|
|
msg = getstackframe()
|
|
|
|
end
|
|
|
|
if status then
|
|
|
|
local messageParts = {"Expected to fail with Error"}
|
|
|
|
if expected then
|
|
|
|
messageParts[2] = " containing \"" .. expected .. "\""
|
|
|
|
end
|
|
|
|
handler('fail', name, msg, table.concat(messageParts, ""))
|
|
|
|
error('_*_TestAbort_*_')
|
|
|
|
end
|
|
|
|
if (expected and not string.find(err, expected)) then
|
|
|
|
err = err:match(".-([^\\/]*)$") -- cut off path of filename
|
|
|
|
handler('fail', name, msg, "expected errormessage \"" .. err .. "\" to contain \"" .. expected .. "\"")
|
|
|
|
error('_*_TestAbort_*_')
|
|
|
|
end
|
|
|
|
handler('pass', name, msg)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function NTest(testrunname, failoldinterface)
|
|
|
|
|
|
|
|
if failoldinterface then error("The interface has changed. Please see documentstion.") end
|
|
|
|
|
|
|
|
local pendingtests = {}
|
|
|
|
local env = _G
|
|
|
|
local outputhandler = TERMINAL_HANDLER
|
|
|
|
local started
|
|
|
|
|
|
|
|
local function runpending()
|
|
|
|
if pendingtests[1] ~= nil then
|
|
|
|
node.task.post(node.task.LOW_PRIORITY, function()
|
|
|
|
pendingtests[1](runpending)
|
|
|
|
end)
|
|
|
|
else
|
|
|
|
outputhandler('finish', testrunname)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function copyenv(dest, src)
|
|
|
|
dest.eq = src.eq
|
|
|
|
dest.spy = src.spy
|
|
|
|
dest.ok = src.ok
|
|
|
|
dest.nok = src.nok
|
|
|
|
dest.fail = src.fail
|
|
|
|
end
|
|
|
|
|
|
|
|
local function testimpl(name, f, async)
|
|
|
|
local testfn = function(next)
|
|
|
|
|
|
|
|
local prev = {}
|
|
|
|
copyenv(prev, env)
|
|
|
|
|
|
|
|
local handler = outputhandler
|
|
|
|
|
|
|
|
local restore = function(err)
|
|
|
|
if err then
|
|
|
|
err = err:match(".-([^\\/]*)$") -- cut off path of filename
|
|
|
|
if not err:match('_*_TestAbort_*_') then
|
|
|
|
handler('except', name, err)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if node then node.setonerror() end
|
|
|
|
copyenv(env, prev)
|
|
|
|
outputhandler('end', name)
|
|
|
|
table.remove(pendingtests, 1)
|
|
|
|
collectgarbage()
|
|
|
|
if next then next() end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function wrap(method, ...)
|
|
|
|
method(handler, name, ...)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function cbError(err)
|
|
|
|
err = err:match(".-([^\\/]*)$") -- cut off path of filename
|
|
|
|
if not err:match('_*_TestAbort_*_') then
|
|
|
|
handler('except', name, err)
|
|
|
|
end
|
|
|
|
restore()
|
|
|
|
end
|
|
|
|
|
|
|
|
env.eq = deepeq
|
|
|
|
env.spy = spy
|
2020-11-15 16:01:15 +01:00
|
|
|
env.ok = function (cond, msg) wrap(assertok, false, cond, msg) end
|
|
|
|
env.nok = function(cond, msg) wrap(assertok, true, cond, msg) end
|
2020-11-08 15:31:11 +01:00
|
|
|
env.fail = function (func, expected, msg) wrap(fail, func, expected, msg) end
|
|
|
|
|
|
|
|
handler('begin', name)
|
|
|
|
node.setonerror(cbError)
|
|
|
|
local ok, err = pcall(f, async and restore)
|
|
|
|
if not ok then
|
|
|
|
err = err:match(".-([^\\/]*)$") -- cut off path of filename
|
|
|
|
if not err:match('_*_TestAbort_*_') then
|
|
|
|
handler('except', name, err)
|
|
|
|
end
|
|
|
|
if async then
|
|
|
|
restore()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if not async then
|
|
|
|
restore()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if not started then
|
|
|
|
outputhandler('start', testrunname)
|
|
|
|
started = true
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
table.insert(pendingtests, testfn)
|
|
|
|
if #pendingtests == 1 then
|
|
|
|
runpending()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function test(name, f)
|
|
|
|
testimpl(name, f)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function testasync(name, f)
|
|
|
|
testimpl(name, f, true)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function report(f, envP)
|
|
|
|
outputhandler = f or outputhandler
|
|
|
|
env = envP or env
|
|
|
|
end
|
|
|
|
|
|
|
|
local currentCoName
|
|
|
|
|
|
|
|
local function testco(name, func)
|
|
|
|
-- local t = tmr.create();
|
|
|
|
local co
|
|
|
|
testasync(name, function(Next)
|
|
|
|
currentCoName = name
|
|
|
|
|
|
|
|
local function getCB(cbName)
|
|
|
|
return function(...) -- upval: co, cbName
|
|
|
|
local result, err = coroutine.resume(co, cbName, ...)
|
|
|
|
if (not result) then
|
|
|
|
if (name == currentCoName) then
|
|
|
|
currentCoName = nil
|
|
|
|
Next(err)
|
|
|
|
else
|
|
|
|
outputhandler('fail', name, "Found stray Callback '"..cbName.."' from test '"..name.."'")
|
|
|
|
end
|
|
|
|
elseif coroutine.status(co) == "dead" then
|
|
|
|
currentCoName = nil
|
|
|
|
Next()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function waitCb()
|
|
|
|
return coroutine.yield()
|
|
|
|
end
|
|
|
|
|
|
|
|
co = coroutine.create(function(wr, wa)
|
|
|
|
func(wr, wa)
|
|
|
|
end)
|
|
|
|
|
|
|
|
local result, err = coroutine.resume(co, getCB, waitCb)
|
|
|
|
if (not result) then
|
|
|
|
currentCoName = nil
|
|
|
|
Next(err)
|
|
|
|
elseif coroutine.status(co) == "dead" then
|
|
|
|
currentCoName = nil
|
|
|
|
Next()
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
return {test = test, testasync = testasync, testco = testco, report = report}
|
|
|
|
end
|
|
|
|
|
|
|
|
return NTest
|
|
|
|
|