forked from nodemcu/nodemcu-firmware
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create NodeMCU test system based on gambiarra (nodemcu#2984)
* Create mispec_file.lua * Initial commit of gambiarra * Adapt gambiarra to NodeMCU * adapt to NodeMCU spacing and add nok functionality * Some refactoring to make it easier to add new functionality * Add methode `fail` to check failing code and pass error messages to output - fail can be called with a function that should fail and a string which should be contained in the errormessage. - Pass failed check reasons to output. * Create gambiarra_file.lua * Add reporting of tests that failed with Lua error * ok, nok and fail will terminate the running test * Add capability to run sync and async tests in mixed order and have a task.post inbetween them * fix gambiarra self test to also run on device (not only host) Use less ram in checking tests directly after they ran. Use nateie task.post to tame watchdog. * Update file tests + add async tmr tests * Another fix in executing async test * Catch errors in callbacks using node.setonerror * change interface to return an object with several test methods * Update README.md * Change interface of Gambiarra + add reason for failed eq * Update gambiarra documentation * Add coroutine testcases to gambiarra * Delete mispec_file.lua as it is superseeded by gambiarra_file.lua * improve regexp for stack frame extraction * Use Lua 53 debug capabilities * move actual tests upfront * remove debug code + optimization * Show errors immediately instead of at the end of the test, freeing memory earlier * Split tests to be run in 2 tranches * rename to NTest and move to new location * Add tests to checking mechanisms * Add luacheck to tests * Some pushing around of files * more (last) fixes and file juggling * Minor tweaks and forgotten checkin * Add NTest selftest to travis * Trying how to master travis * another try * restrict NTest selftest to linux
- Loading branch information
1 parent
acbd7dd
commit 37cba66
Showing
10 changed files
with
1,425 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,301 @@ | ||
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 | ||
env.ok = function (cond, msg1, msg2) wrap(assertok, false, cond, msg1, msg2) end | ||
env.nok = function(cond, msg1, msg2) wrap(assertok, true, cond, msg1, msg2) end | ||
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 | ||
|
Oops, something went wrong.