Commit 136e0973 authored by Nathaniel Wesley Filardo's avatar Nathaniel Wesley Filardo
Browse files

Merge dev into release

While we intend our release strategy to be that we just fast-forward our
`release` branch to `dev`, things have come a little off the wheels.
This is a "git merge -s recursive -X theirs" of `dev` into `release`
instead.
parents 4f679277 c212b30a
# NTest
| Since | Origin / Contributor | Maintainer | Source |
| :----- | :-------------------- | :---------- | :------ |
| 2020-11-01 | [Gregor Hartmann](https://github.com/HHHartmann) | [Gregor Hartmann](https://github.com/HHHartmann) | [NTest.lua](NTest.lua) |
NTest is a test system for NodeMCU which is originally based on gambiarra. It is designed to run on chip but the selftest also runs on the host using luac.cross.
!!! warning
This module is too big to load by standard `require` function or compile on ESP8266 using `node.compile()`. The only option to load and use it is to use [LFS](../lfs.md).
## Example
``` Lua
-- Simple synchronous test
local tests = require("NTest")("first testrun")
tests.test('Check dogma', function()
ok(2+2 == 5, 'two plus two equals five')
end)
-- A more advanced asynchronous test
tests.testasync('Do it later', function(done)
someAsyncFn(function(result)
ok(result == expected)
done() -- this starts the next async test
end)
end)
-- An asynchronous test using a coroutine
tests.testco('Do it in place', function(getCB, waitCB)
someAsyncFn(getCB("callback"))
local CBName = waitCB()
ok(CBName, "callback")
end)
```
## API
`require('NTest')` returns an new function which must be called with a string.
``` Lua
local new = require('NTest')
```
`new(testrunname:string)` returns an object with the following functions:
``` Lua
local tests = new("first testrun")
```
`test(name:string, f:function)` allows you to define a new test:
``` Lua
tests.test('My sync test', function()
end)
```
`testasync(name:string, f:function(done:function))` allows you to define a new asynchronous test:
To tell NTest that the test is finished you need to call the function `done` which gets passed in.
In async scenarios the test function will usually terminate quickly but the test is still waiting for
some callback to be called before it is finished.
``` Lua
tests.testasync('My async test', function(done)
done()
end)
```
`testco(name:string, f:function(getCB:function, waitCB:function))` allows you to define a new asynchronous
test which runs in a coroutine:
This allows handling callbacks in the test in a linear way. simply use getCB to create a new callback stub.
waitCB blocks the test until the callback is called and returns the parameters.
``` Lua
tests.testco('My coroutine test', function(getCB, waitCB)
end)
tests.testco('My coroutine test with callback', function(getCB, waitCB)
local t = tmr.create();
t:alarm(200, tmr.ALARM_AUTO, getCB("timer"))
local name, tCB = waitCB()
ok(eq(name, "timer"), "CB name matches")
name, tCB = waitCB()
ok(eq(name, "timer"), "CB name matches again")
tCB:stop()
ok(true, "coroutine end")
end)
```
All test functions also define some helper functions that are added when the test is executed - `ok`, `nok`, `fail`, `eq` and `spy`.
`ok`, `nok`, `fail` are assert functions which will break the test if the condition is not met.
`ok(cond:bool, [msg:string])`. It takes any boolean condition and an optional assertion message. If no message is defined - current filename and line will be used. If the condition evaluetes to thuthy nothing happens.
If it is falsy the message is printed and the test is aborted. The next test will then be executed.
``` Lua
ok(1 == 2) -- prints 'foo.lua:42'
ok(1 == 2, 'one equals one') -- prints 'one equals one'
ok(1 == 1) -- prints nothing
```
`nok(cond:bool, [msg:string])` is a shorthand for `ok(not cond, 'msg')`.
``` Lua
nok(1 == 1) -- prints 'foo.lua:42'
nok(1 == 1, 'one equals one') -- prints 'one equals one'
```
`fail(func:function, [expected:string], [msg:string])` tests a function for failure. If expected is given the errormessage poduced by the function must also contain the given string else `fail` will fail the test. If no message is defined the current filename and line will be used.
``` Lua
local function f() error("my error") end
fail(f, "expected error", "Failed with incorrect error")
-- fails with 'Failed with incorrect error' and
-- 'expected errormessage "foo.lua:2: my error" to contain "expected error"'
```
`eq(a, b)` is a helper to deeply compare lua variables. It supports numbers, strings, booleans, nils, functions and tables. It's mostly useful within ok() and nok():
If the variables are equal it returns `true` else it returns `{msg='<reason>'}` This is recognized by `ok` and `nok` and results in also logging the reason for difference.
``` Lua
ok(eq(1, 1))
ok(eq({a='b',c='d'}, {c='d',a='b'})
ok(eq('foo', 'bar')) -- will fail
```
`spy([f])` creates function wrappers that remember each call (arguments, errors) but behaves much like the real function. Real function is optional, in this case spy will return nil, but will still record its calls.
Spies are most helpful when passing them as callbacks and testing that they were called with correct values.
``` Lua
local f = spy(function(s) return #s end)
ok(f('hello') == 5)
ok(f('foo') == 3)
ok(#f.called == 2)
ok(eq(f.called[1], {'hello'})
ok(eq(f.called[2], {'foo'})
f(nil)
ok(f.errors[3] ~= nil)
```
## Reports
Another useful feature is that you can customize test reports as you need. The default `outputhandler` just more or less prints out a basic report. You can easily override (or augment by wrapping, e.g.) this behavior as well as add any other information you need (number of passed/failed assertions, time the test took etc):
Events are:
`start` when testing starts
`finish` when all tests have finished
`begin` Will be called before each test
`end` Will be called after each test
`pass` Test has passed
`fail` Test has failed with not fulfilled assert (ok, nok, fail)
`except` Test has failed with unexpected error
``` Lua
local passed = 0
local failed = 0
tests.outputhandler = function(event, testfunc, msg)
if event == 'begin' then
print('Started test', testfunc)
passed = 0
failed = 0
elseif event == 'end' then
print('Finished test', testfunc, passed, failed)
elseif event == 'pass' then
passed = passed + 1
elseif event == 'fail' then
print('FAIL', testfunc, msg)
failed = failed + 1
elseif event == 'except' then
print('ERROR', testfunc, msg)
end
end
```
Additionally, you can pass a different environment to keep `_G` unpolluted:
You need to set it, so the helper functions mentioned above can be added before calling the test function.
``` Lua
local myenv = {}
tests.env = myenv
tests.test('Some test', function()
myenv.ok(myenv.eq(...))
local f = myenv.spy()
end)
```
You can restore `env` or `outputhandler` to their defaults by setting their values to `nil`.
## Appendix
This Library is for NodeMCU versions Lua 5.1 and Lua 5.3.
It is based on https://bitbucket.org/zserge/gambiarra and includes bugfixes, substantial extensions of functionality and adaptions to NodeMCU requirements.
local N = require('NTest')("selftest")
local orig_node = node
local metatest
local async
local expected = {}
local failed, passed = 0,0
local load_tests2
local function load_tests()
--
-- Basic tests
--
metatest('simple assert passes', function()
ok(2 == 2, '2==2')
end, {'2==2'}, {})
metatest('simple negative assert fails', function()
nok(2 == 2, '2==2')
nok(false, 'unreachable code')
end, {}, {'2==2'})
metatest('simple assert fails', function()
ok(1 == 2, '1~=2')
ok(true, 'unreachable code')
end, {}, {'1~=2'})
metatest('simple negative assert passes', function()
nok(1 == 2, '1~=2')
end, {'1~=2'}, {})
metatest('ok without a message', function()
ok(1 == 1)
ok(1 == 2)
end, {'NTest_NTest.lua:31'}, {'NTest_NTest.lua:32'})
metatest('nok without a message', function()
nok(1 == "")
nok(1 == 1)
end, {'NTest_NTest.lua:36'}, {'NTest_NTest.lua:37'})
--
-- Equality tests
--
metatest('eq nil', function()
ok(eq(nil, nil), 'nil == nil')
nok(eq("", nil), '"" != nil')
nok(eq(nil, ""), 'nil != ""')
end, {'nil == nil', '"" != nil', 'nil != ""'}, {})
metatest('eq primitives', function()
ok(eq('foo', 'foo'), 'str == str')
nok(eq('foo', 'bar'), 'str != str')
ok(eq(123, 123), 'num == num')
nok(eq(123, 12), 'num != num')
end, {'str == str', 'str != str', 'num == num', 'num != num'}, {})
metatest('eq arrays', function()
ok(eq({}, {}), 'empty')
ok(eq({1, 2}, {1, 2}), 'equal')
nok(eq({1, 2}, {2, 1}), 'swapped')
nok(eq({1, 2, 3}, {1, 2}), 'longer')
nok(eq({1, 2}, {1, 2, 3}), 'shorter')
end, {'empty', 'equal', 'swapped', 'longer', 'shorter'}, {})
metatest('eq objects', function()
ok(eq({}, {}), 'empty')
ok(eq({a=1,b=2}, {a=1,b=2}), 'equal')
ok(eq({b=2,a=1}, {a=1,b=2}), 'swapped')
nok(eq({a=1,b=2}, {a=1,b=3}), 'not equal')
end, {'empty', 'equal', 'swapped', 'not equal'}, {})
metatest('eq nested objects', function()
ok(eq({
['1'] = { name = 'mhc', age = 28 },
['2'] = { name = 'arb', age = 26 }
}, {
['1'] = { name = 'mhc', age = 28 },
['2'] = { name = 'arb', age = 26 }
}), 'equal')
ok(eq({
['1'] = { name = 'mhc', age = 28 },
['2'] = { name = 'arb', age = 26 }
}, {
['1'] = { name = 'mhc', age = 28 },
['2'] = { name = 'arb', age = 27 }
}), 'not equal')
end, {'equal'}, {'not equal', 'different numbers expected 26 vs. 27'})
metatest('eq functions', function()
ok(eq(function(x) return x end, function(x) return x end), 'equal')
nok(eq(function(z) return x + z end, function(z) return y + z end), 'wrong variable') -- luacheck: ignore
nok(eq(function(x) return x*2 end, function(x) return x+2 end), 'wrong code')
end, {'equal', 'wrong variable', 'wrong code'}, {})
metatest('eq different types', function()
local eqos = eq({a=1,b=2}, "text")
ok(eq(eqos.msg, "type 1 is table, type 2 is string"), 'object/string')
local eqfn = eq(function(x) return x end, 12)
ok(eq(eqfn.msg, "type 1 is function, type 2 is number"), 'function/int')
nok(eq(12, "Hallo"), 'int/string')
end, {"object/string", "function/int", 'int/string'}, {})
--
-- Spies
--
metatest('spy called', function()
local f = spy()
ok(not f.called or #f.called == 0, 'not called')
f()
ok(f.called, 'called')
ok(#f.called == 1, 'called once')
f()
ok(#f.called == 2, 'called twice')
end, {'not called', 'called', 'called once', 'called twice'}, {})
metatest('spy with arguments', function()
local x = 0
local function setX(n) x = n end
local f = spy(setX)
f(1)
ok(x == 1, 'x == 1')
ok(eq(f.called, {{1}}), 'called with 1')
f(42)
ok(x == 42, 'x == 42')
ok(eq(f.called, {{1}, {42}}), 'called with 42')
end, {'x == 1', 'called with 1', 'x == 42', 'called with 42'}, {})
metatest('spy with nils', function()
local function nils(a, _, b) return a, nil, b, nil end
local f = spy(nils)
local r1, r2, r3, r4 = f(1, nil, 2)
ok(eq(f.called, {{1, nil, 2}}), 'nil in args')
ok(r1 == 1 and r2 == nil and r3 == 2 and r4 == nil, 'nil in returns')
end, {'nil in args', 'nil in returns'}, {})
metatest('spy with exception', function()
local function throwSomething(s)
if s ~= 'nopanic' then error('panic: '..s) end
end
local f = spy(throwSomething)
f('nopanic')
ok(f.errors == nil, 'no errors yet')
f('foo')
ok(eq(f.called, {{'nopanic'}, {'foo'}}), 'args ok')
ok(f.errors[1] == nil and f.errors[2] ~= nil, 'thrown ok')
end, {'no errors yet', 'args ok', 'thrown ok'}, {})
metatest('another spy with exception', function()
local f = spy(function() local a = unknownVariable + 1 end) -- luacheck: ignore
f()
ok(f.errors[1], 'exception thrown')
end, {'exception thrown'}, {})
metatest('spy should return a value', function()
local f = spy(function() return 5 end)
ok(f() == 5, 'spy returns a value')
local g = spy()
ok(g() == nil, 'default spy returns undefined')
end, {'spy returns a value', 'default spy returns undefined'}, {})
--
-- fail tests
--
metatest('fail with correct errormessage', function()
fail(function() error("my error") end, "my error", "Failed with correct error")
ok(true, 'reachable code')
end, {'Failed with correct error', 'reachable code'}, {})
metatest('fail with incorrect errormessage', function()
fail(function() error("my error") end, "different error", "Failed with incorrect error")
ok(true, 'unreachable code')
end, {}, {'Failed with incorrect error',
'expected errormessage "NTest_NTest.lua:169: my error" to contain "different error"'})
metatest('fail with incorrect errormessage default message', function()
fail(function() error("my error") end, "different error")
ok(true, 'unreachable code')
end, {}, {'NTest_NTest.lua:175',
'expected errormessage "NTest_NTest.lua:175: my error" to contain "different error"'})
metatest('fail with not failing code', function()
fail(function() end, "my error", "did not fail")
ok(true, 'unreachable code')
end, {}, {"did not fail", 'Expected to fail with Error containing "my error"'})
metatest('fail with failing code', function()
fail(function() error("my error") end, nil, "Failed as expected")
ok(true, 'reachable code')
end, {'Failed as expected', 'reachable code'}, {})
metatest('fail with not failing code', function()
fail(function() end, nil , "did not fail")
ok(true, 'unreachable code')
end, {}, {"did not fail", 'Expected to fail with Error'})
metatest('fail with not failing code default message', function()
fail(function() end)
ok(true, 'unreachable code')
end, {}, {"NTest_NTest.lua:196", 'Expected to fail with Error'})
metatest('=== load more tests ===', function()
load_tests2()
end, {}, {})
end
load_tests2 = function()
--
-- except tests
--
metatest('error should panic', function()
error("lua error")
ok(true, 'unreachable code')
end, {}, {}, {'NTest_NTest.lua:211: lua error'})
--
-- called function except
--
local function subfuncerror()
error("lua error")
end
metatest('subroutine error should panic', function()
subfuncerror()
ok(true, 'unreachable code')
end, {}, {}, {'NTest_NTest.lua:220: lua error'})
local function subfuncok()
ok(false)
end
metatest('subroutine ok should fail', function()
subfuncok()
ok(true, 'unreachable code')
end, {}, {'NTest_NTest.lua:229'})
--drain_post_queue()
--
-- Async tests
--
metatest('async test', function(next)
async(function()
ok(true, 'bar')
async(function()
ok(true, 'baz')
next()
end)
end)
ok(true, 'foo')
end, {'foo', 'bar', 'baz'}, {}, {}, true)
metatest('async test without actually async', function(next)
ok(true, 'bar')
next()
end, {'bar'}, {}, {}, true)
metatest('async fail in main', function(--[[ next ]])
ok(false, "async fail")
ok(true, 'unreachable code')
end, {}, {'async fail'}, {}, true)
--drain_post_queue()
metatest('another async test after async queue drained', function(next)
async(function()
ok(true, 'bar')
next()
end)
ok(true, 'foo')
end, {'foo', 'bar'}, {}, {}, true)
--
-- except tests async
--
metatest('async except in main', function(--[[ next ]])
error("async except")
ok(true, 'unreachable code')
end, {}, {}, {'NTest_NTest.lua:277: async except'}, true)
metatest('async fail in callback', function(next)
async(function()
ok(false, "async fail")
next()
end)
ok(true, 'foo')
end, {'foo'}, {'async fail'}, {}, true)
metatest('async except in callback', function(next)
async(function()
error("async Lua error")
next()
end)
ok(true, 'foo')
end, {'foo'}, {}, {'NTest_NTest.lua:291: async Lua error'}, true)
--
-- sync after async test
--
local marker = 0
metatest('set marker async', function(next)
async(function()
marker = "marked"
ok(true, 'async bar')
next()
end)
ok(true, 'foo')
end, {'foo', 'async bar'}, {}, {}, true)
metatest('check marker async', function()
ok(eq(marker, "marked"), "marker check")
end, {"marker check"}, {})
--
-- coroutine async tests
--
metatest('coroutine pass', function(--[[ getCB, waitCB ]])
ok(true, "passing")
end, {"passing"}, {}, {}, "co")
metatest('coroutine fail in main', function(--[[ getCB, waitCB ]])
ok(false, "coroutine fail")
ok(true, 'unreachable code')
end, {}, {'coroutine fail'}, {}, "co")
metatest('coroutine fail in main', function(--[[ getCB, waitCB ]])
nok(true, "coroutine fail")
nok(false, 'unreachable code')
end, {}, {'coroutine fail'}, {}, "co")
metatest('coroutine fail error', function(--[[ getCB, waitCB ]])
fail(function() error("my error") end, "my error", "Failed with correct error")
fail(function() error("my error") end, "other error", "Failed with other error")
ok(true, 'unreachable code')
end, {'Failed with correct error'}, {'Failed with other error',
'expected errormessage "NTest_NTest.lua:333: my error" to contain "other error"'}, {}, "co")
metatest('coroutine except in main', function(--[[ getCB, waitCB ]])
error("coroutine except")
ok(true, 'unreachable code')
end, {}, {}, {'NTest_NTest.lua:339: coroutine except'}, "co")
--local function coasync(f) table.insert(post_queue, 1, f) end
local function coasync(f, p1, p2) node.task.post(node.task.MEDIUM_PRIORITY, function() f(p1,p2) end) end
metatest('coroutine with callback', function(getCB, waitCB)
coasync(getCB("testCb"))
local name = waitCB()
ok(eq(name, "testCb"), "cb")
end, {"cb"}, {}, {}, "co")
metatest('coroutine with callback with values', function(getCB, waitCB)
coasync(getCB("testCb"), "p1", 2)
local name, p1, p2 = waitCB()
ok(eq(name, "testCb"), "cb")
ok(eq(p1, "p1"), "p1")
ok(eq(p2, 2), "p2")
end, {"cb", "p1", "p2"}, {}, {}, "co")
metatest('coroutine fail after callback', function(getCB, waitCB)
coasync(getCB("testCb"))
local name = waitCB()
ok(eq(name, "testCb"), "cb")
ok(false, "fail")
ok(true, 'unreachable code')
end, {"cb"}, {"fail"}, {}, "co")
metatest('coroutine except after callback', function(getCB, waitCB)
coasync(getCB("testCb"))
local name = waitCB()
ok(eq(name, "testCb"), "cb")
error("error")
ok(true, 'unreachable code')
end, {"cb"}, {}, {"NTest_NTest.lua:372: error"}, "co")
--- detect stray callbacks after the test has finished
local strayCb
local function rewrap()
coasync(strayCb)
end
metatest('leave stray callback', function(getCB--[[ , waitCB ]])
strayCb = getCB("testa")
coasync(rewrap)
end, {}, {}, {}, "co")
metatest('test after stray cb', function(--[[ getCB, waitCB ]])
end, {}, {"Found stray Callback 'testa' from test 'leave stray callback'"}, {}, "co")
--
-- Finalize: check test results
--
metatest("finishing up pending tests", function()
for i = 1,#expected -1 do
print("--- FAIL "..expected[i].name..' (pending)')
failed = failed + 1
end
print("failed: "..failed, "passed: "..passed)
node = orig_node -- luacheck: ignore 121 (setting read-only global variable)
end, {}, {})
end -- load_tests()
local cbWrap = function(cb) return cb end
if not node.LFS then -- assume we run on host, not on MCU. node is already defined by NTest if running on host
cbWrap = function(cb)
return function(...)
local ok, p1,p2,p3,p4 = pcall(cb, ...)
if not ok then
if node.Host_Error_Func then -- luacheck: ignore 143
node.Host_Error_Func(p1) -- luacheck: ignore 143
else
print(p1, "::::::::::::: reboot :::::::::::::")
end
else
return p1,p2,p3,p4
end
end
end
end
-- Helper function to print arrays
local function stringify(t)
local s = ''
for i=1,#(t or {}) do
s = s .. '"' .. t[i] .. '"' .. ', '
end
return s:gsub('..$', '')
end
local pass
-- Set meta test handler
N.outputhandler = function(e, test, msg, errormsg)
local function consumemsg(msg, area) -- luacheck: ignore
if not expected[1][area][1] then
print("--- FAIL "..expected[1].name..' ('..area..'ed): unexpected "'..
msg..'"')
pass = false
return
end
if msg ~= expected[1][area][1] then
print("--- FAIL "..expected[1].name..' ('..area..'ed): expected "'..
expected[1][area][1]..'" vs "'..
msg..'"')
pass = false
end
table.remove(expected[1][area], 1)
end
local function consumeremainder(area)
if #expected[1][area] > 0 then
print("--- FAIL "..expected[1].name..' ('..area..'ed): expected ['..
stringify(expected[1][area])..']')
pass = false
end
end
if e == 'begin' then
pass = true
elseif e == 'end' then
consumeremainder("pass")
consumeremainder("fail")
consumeremainder("except")
if pass then
print("+++ Pass "..expected[1].name)
passed = passed + 1
else
failed = failed + 1
end
table.remove(expected, 1)
elseif e == 'pass' then
consumemsg(msg, "pass")
if errormsg then consumemsg(errormsg, "pass") end
elseif e == 'fail' then
consumemsg(msg, "fail")
if errormsg then consumemsg(errormsg, "fail") end
elseif e == 'except' then
consumemsg(msg, "except")
if errormsg then consumemsg(errormsg, "except") end
elseif e == 'start' or e == 'finish' then -- luacheck: ignore
-- ignore
else
print("Extra output: ", e, test, msg, errormsg)
end
end
local async_queue = {}
async = function(f) table.insert(async_queue, cbWrap(f)) end
local function async_next()
local f = table.remove(async_queue, 1)
if f then
f()
end
end
local function drain_async_queue()
while #async_queue > 0 do
async_next()
end
end
metatest = function(name, f, expectedPassed, expectedFailed, expectedExcept, asyncMode)
table.insert(expected, {
name = name,
pass = expectedPassed,
fail = expectedFailed,
except = expectedExcept or {}
})
local ff = f
if asyncMode then
ff = function(...)
f(...)
drain_async_queue()
end
if (asyncMode == "co") then
N.testco(name,ff)
else
N.testasync(name, ff)
end
else
N.test(name, ff)
end
end
load_tests()
-- Walk the ADC through a stepped triangle wave using the attached voltage
-- divider and I2C GPIO expander.
local N = ...
N = (N or require "NTest")("adc-env")
-- TODO: Preflight test that we are in the correct environment with an I2C
-- expander in the right place with the right connections.
-- TODO: Use the mcp23017 module in the main tree rather than hand-coding
-- the commands
N.test('setup', function()
-- Configure the ADC
if adc.force_init_mode(adc.INIT_ADC)
then
node.restart()
error "Must reboot to get to ADC mode"
end
-- Configure the I2C bus
i2c.setup(0, 2, 1, i2c.FAST)
-- Set the IO expander port B to channels 0 and 1 as outputs
i2c.start(0)
ok(i2c.address(0, 0x20, i2c.TRANSMITTER))
i2c.write(0, 0x01, 0xFC)
i2c.stop(0)
end)
-- set the two-bit voltage divider output value to v (in 0,1,2,3)
local function setv(v)
assert (0 <= v and v <= 3)
i2c.start(0)
i2c.address(0, 0x20, i2c.TRANSMITTER)
i2c.write(0, 0x15, v)
i2c.stop(0)
end
-- read out the ADC and compare to given range
local function checkadc(min, max)
local v = adc.read(0)
return ok(min <= v and v <= max, ("read adc: %d <= %d <= %d"):format(min,v,max))
end
-- Since we have a rail-to-rail 4-tap DAC, as it were, give us some one-sided
-- wiggle around either rail and some two-sided wiggle around both middle stops
local vmin = { 0, 300, 700, 1000 }
local vmax = { 24, 400, 800, 1024 }
-- Set the DAC, wait a brief while for physics, and then read the ADC
local function mktest(fs, i)
N.test(fs:format(i), function()
setv(i)
tmr.delay(10)
checkadc(vmin[i+1], vmax[i+1])
end)
end
-- test all four stops on the way up, and the three to come back down
for i=0,3 do mktest("%d up", i) end
for i=2,0,-1 do mktest("%d down", i) end
local N = ...
N = (N or require "NTest")("file")
local function cleanup()
file.remove("testfile")
file.remove("testfile2")
local testfiles = {"testfile1&", "testFILE2"}
for _, n in ipairs(testfiles) do
file.remove(n,n)
end
end
N.test('exist', function()
cleanup()
nok(file.exists("non existing file"), "non existing file")
file.putcontents("testfile", "testcontents")
ok(file.exists("testfile"), "existing file")
end)
N.test('fscfg', function()
cleanup()
local start, size = file.fscfg()
ok(start, "start")
ok(size, "size")
end)
N.test('fsinfo', function()
cleanup()
local remaining, used, total = file.fsinfo()
ok(remaining, "remaining")
ok(used, "used")
ok(total, "total")
ok(eq(remaining+used, total), "size maths")
end)
N.test('getcontents', function()
cleanup()
local testcontent = "some content \0 and more"
file.putcontents("testfile", testcontent)
local content = file.getcontents("testfile")
ok(eq(testcontent, content),"contents")
end)
N.test('getcontents non existent file', function()
cleanup()
nok(file.getcontents("non existing file"), "non existent file")
end)
N.test('getcontents more than 1K', function()
cleanup()
local f = file.open("testfile", "w")
for i = 1,100 do -- luacheck: ignore
f:write("some text to test")
end
f:close()
local content = file.getcontents("testfile")
ok(eq(#content, 1700), "long contents")
end)
N.test('read more than 1K', function()
cleanup()
local f = file.open("testfile", "w")
for i = 1,100 do -- luacheck: ignore
f:write("some text to test")
end
f:close()
f = file.open("testfile","r")
local buffer = f:read()
ok(eq(#buffer, 1024), "first block")
buffer = f:read()
f:close()
ok(eq(#buffer, 1700-1024), "second block")
end)
local function makefile(name, num128)
local s128 = "16 bytes written"
s128 = s128..s128..s128..s128
s128 = s128..s128
local f = file.open(name, "w")
for i = 1, num128 do -- luacheck: ignore
f:write(s128)
end
f:close()
end
N.test('read 7*128 bytes', function()
cleanup()
makefile("testfile", 7)
local f = file.open("testfile","r")
local buffer = f:read()
f:close()
nok(eq(buffer, nil), "nil first block")
ok(eq(#buffer, 128*7), "length first block")
end)
N.test('read 8*128 bytes long file', function()
cleanup()
makefile("testfile", 8)
local f = file.open("testfile","r")
local buffer = f:read()
nok(eq(buffer, nil), "nil first block")
ok(eq(#buffer, 128*8), "size first block")
buffer = f:read()
f:close()
ok(eq(buffer, nil), "nil second block")
end)
N.test('read 9*128 bytes', function()
cleanup()
makefile("testfile", 9)
local f = file.open("testfile","r")
local buffer = f:read()
nok(eq(buffer, nil), "nil first block")
ok(eq(#buffer, 1024), "size first block")
buffer = f:read()
f:close()
nok(eq(buffer, nil), "nil second block")
ok(eq(#buffer, 128*9-1024), "size second block")
end)
N.test('list', function()
cleanup()
local function count(files)
local filecount = 0
for _,_ in pairs(files) do filecount = filecount+1 end
return filecount
end
local files
local function testfile(name)
ok(eq(files[name],#name), "length matches name length")
end
local testfiles = {"testfile1&", "testFILE2"}
for _, n in ipairs(testfiles) do
file.putcontents(n,n)
end
files = file.list("testfile%.*")
ok(eq(count(files), 1), "found file")
testfile("testfile1&")
files = file.list("^%l*%u+%d%.-")
ok(eq(count(files), 1), "found file regexp")
testfile("testFILE2")
files = file.list()
ok(count(files) >= 2, "several files found")
end)
N.test('open non existing', function()
cleanup()
local function testopen(test, filename, mode)
test(file.open(filename, mode), mode)
file.close()
file.remove(filename)
end
testopen(nok, "testfile", "r")
testopen(ok, "testfile", "w")
testopen(ok, "testfile", "a")
testopen(nok, "testfile", "r+")
testopen(ok, "testfile", "w+")
testopen(ok, "testfile", "a+")
end)
N.test('open existing', function()
cleanup()
local function testopen(mode, position)
file.putcontents("testfile", "testcontent")
ok(file.open("testfile", mode), mode)
file.write("")
ok(eq(file.seek(), position), "seek check")
file.close()
end
testopen("r", 0)
testopen("w", 0)
testopen("a", 11)
testopen("r+", 0)
testopen("w+", 0)
testopen("a+", 11)
end)
N.test('remove', function()
cleanup()
file.putcontents("testfile", "testfile")
ok(file.remove("testfile") == nil, "existing file")
ok(file.remove("testfile") == nil, "non existing file")
end)
N.test('rename', function()
cleanup()
file.putcontents("testfile", "testfile")
ok(file.rename("testfile", "testfile2"), "rename existing")
nok(file.exists("testfile"), "old file removed")
ok(file.exists("testfile2"), "new file exists")
nok(file.rename("testfile", "testfile3"), "rename non existing")
file.putcontents("testfile", "testfile")
nok(file.rename("testfile", "testfile2"), "rename to existing")
ok(file.exists("testfile"), "from file exists")
ok(file.exists("testfile2"), "to file exists")
end)
N.test('stat existing file', function()
cleanup()
file.putcontents("testfile", "testfile")
local stat = file.stat("testfile")
ok(stat, "stat existing")
ok(eq(stat.size, 8), "size")
ok(eq(stat.name, "testfile"), "name")
ok(stat.time, "no time")
ok(eq(stat.time.year, 1970), "year")
ok(eq(stat.time.mon, 01), "mon")
ok(eq(stat.time.day, 01), "day")
ok(eq(stat.time.hour, 0), "hour")
ok(eq(stat.time.min, 0), "min")
ok(eq(stat.time.sec, 0), "sec")
nok(stat.is_dir, "is_dir")
nok(stat.is_rdonly, "is_rdonly")
nok(stat.is_hidden, "is_hidden")
nok(stat.is_sys, "is_sys")
nok(stat.is_arch, "is_arch")
end)
N.test('stat non existing file', function()
cleanup()
local stat = file.stat("not existing file")
ok(stat == nil, "stat empty")
end)
-- Walk the GPIO subsystem through its paces, using the attached I2C GPIO chip
--
-- Node GPIO 13 (index 7) is connected to I2C expander channel B6; node OUT
-- Node GPIO 15 (index 8) is connected to I2C expander channel B7; node IN
local N = ...
N = (N or require "NTest")("gpio-env")
-- TODO: Preflight test that we are in the correct environment with an I2C
-- expander in the right place with the right connections.
-- TODO: Use the mcp23017 module in the main tree rather than hand-coding
-- the commands
N.test('setup', function()
-- Set gpio pin directions
gpio.mode(8, gpio.INPUT)
gpio.mode(7, gpio.OUTPUT, gpio.FLOAT)
-- Configure the I2C bus
i2c.setup(0, 2, 1, i2c.FAST)
-- Set the IO expander port B to channel 7 as output, 6 as input
i2c.start(0)
ok(i2c.address(0, 0x20, i2c.TRANSMITTER))
i2c.write(0, 0x01, 0x7F)
i2c.stop(0)
end)
local function seti2cb7(v)
i2c.start(0)
i2c.address(0, 0x20, i2c.TRANSMITTER)
i2c.write(0, 0x15, v and 0x80 or 0x00)
i2c.stop(0)
end
local function geti2cb6()
i2c.start(0)
i2c.address(0, 0x20, i2c.TRANSMITTER)
i2c.write(0, 0x13)
i2c.start(0)
i2c.address(0, 0x20, i2c.RECEIVER)
local v = i2c.read(0, 1):byte(1)
i2c.stop(0)
return (bit.band(v,0x40) ~= 0)
end
N.test('gpio read 0', function()
seti2cb7(false)
ok(eq(0, gpio.read(8)))
end)
N.test('gpio read 1', function()
seti2cb7(true)
ok(eq(1, gpio.read(8)))
end)
N.test('i2c read 0', function()
gpio.write(7, 0)
ok(eq(false, geti2cb6()))
end)
N.test('i2c read 1', function()
gpio.write(7, 1)
ok(eq(true, geti2cb6()))
end)
N.testasync('gpio toggle trigger 1', function(next)
seti2cb7(false)
tmr.delay(10)
gpio.trig(8, "both", function(l,_,c)
ok(c == 1 and l == 1)
return next()
end)
seti2cb7(true)
end, true)
N.testasync('gpio toggle trigger 2', function(next)
gpio.trig(8, "both", function(l,_,c)
ok(c == 1 and l == 0)
return next()
end)
seti2cb7(false)
end, true)
N.test('gpio toggle trigger end', function()
gpio.trig(8, "none")
ok(true)
end)
-- Run LiquidCrystal through some basic tests. Requires `liquidcrystal.lua`
-- and `l2-i2c4bit.lua` available available to `require`.
--
-- This file ought to be named "NTest_liquidcrystal_i2c4bit" or something,
-- but it has its current name due to our default SPIFFS filename length limit.
local N = ...
N = (N or require "NTest")("liquidcrystal-i2c4bit")
local metalcd
local metaback
local backend
local lcd
collectgarbage()
print("HEAP init", node.heap())
metalcd = require "liquidcrystal"
collectgarbage() print("HEAP constructor imported ", node.heap())
metaback = require "lc-i2c4bit"
collectgarbage() print("HEAP backend imported ", node.heap())
backend = metaback({
address = 0x27,
id = 0,
speed = i2c.SLOW,
sda = 2,
scl = 1,
})
collectgarbage() print("HEAP backend built", node.heap())
lcd = metalcd(backend, false, true, 20)
collectgarbage() print("HEAP lcd built", node.heap())
print("waiting for LCD to be unbusy before testing...")
while lcd:busy() do end
N.test("custom character", function()
local glyph = { 0x1F, 0x15, 0x1B, 0x15, 0x1F, 0x10, 0x10, 0x0 }
lcd:customChar(0, glyph)
ok(eq(glyph,lcd:readCustom(0)), "read back")
end)
N.test("draw and readback", function()
lcd:cursorMove(0)
lcd:write("abc")
lcd:cursorMove(10,1)
lcd:write("de")
lcd:cursorMove(10,2)
lcd:write("fg")
lcd:cursorMove(12,3)
lcd:write("hi\000")
lcd:cursorMove(18,4)
lcd:write("jk")
lcd:home() ok(eq(0x61, lcd:read()), "read back 'a'")
ok(eq(0x62, lcd:read()), "read back 'b'")
lcd:cursorMove(11,1) ok(eq(0x65, lcd:read()), "read back 'e'")
lcd:cursorMove(11,2) ok(eq(0x67, lcd:read()), "read back 'g'")
lcd:cursorMove(13,3) ok(eq(0x69, lcd:read()), "read back 'i'")
lcd:cursorMove(14,3) ok(eq(0x00, lcd:read()), "read back 0" )
lcd:cursorMove(19,4) ok(eq(0x6B, lcd:read()), "read back 'k'")
end)
N.test("update home", function()
lcd:home() lcd:write("l")
lcd:home() ok(eq(0x6C, lcd:read()))
end)
N.testasync("clear", function(next)
-- clear and poll busy
lcd:clear()
tmr.create():alarm(5, tmr.ALARM_SEMI, function(tp)
if lcd:busy() then tp:start() else next() end
end)
lcd:home() -- work around busy polling incrementing position (XXX)
ok(eq(0x20, lcd:read()), "is space")
ok(eq(1, lcd:position())) -- having just read 1 from home, we should be at 1
end)
local N = require "NTest" ("Lua detail tests")
N.test('typeerror', function()
fail(function() math.abs("") end, "number expected, got string", "string")
fail(function() math.abs() end, "number expected, got no value", "no value")
end)
local N = ...
N = (N or require "NTest")("pixbuf")
local function initBuffer(buf, ...)
for i,v in ipairs({...}) do
buf:set(i, v, v*2, v*3, v*4)
end
return buf
end
N.test('initialize a buffer', function()
local buffer = pixbuf.newBuffer(9, 3)
nok(buffer == nil)
ok(eq(buffer:size(), 9), "check size")
ok(eq(buffer:dump(), string.char(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)), "initialize with 0")
fail(function() pixbuf.newBuffer(9, -1) end, "should be a positive integer")
fail(function() pixbuf.newBuffer(0, 3) end, "should be a positive integer")
fail(function() pixbuf.newBuffer(-1, 3) end, "should be a positive integer")
end)
N.test('have correct size', function()
local buffer = pixbuf.newBuffer(9, 3)
ok(eq(buffer:size(), 9), "check size")
buffer = pixbuf.newBuffer(9, 4)
ok(eq(buffer:size(), 9), "check size")
end)
N.test('fill a buffer with one color', function()
local buffer = pixbuf.newBuffer(3, 3)
buffer:fill(1,222,55)
ok(eq(buffer:dump(), string.char(1,222,55,1,222,55,1,222,55)), "RGB")
buffer = pixbuf.newBuffer(3, 4)
buffer:fill(1,222,55,77)
ok(eq(buffer:dump(), string.char(1,222,55,77,1,222,55,77,1,222,55,77)), "RGBW")
end)
N.test('replace correctly', function()
local buffer = pixbuf.newBuffer(5, 3)
buffer:replace(string.char(3,255,165,33,0,244,12,87,255))
ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW")
buffer = pixbuf.newBuffer(5, 3)
buffer:replace(string.char(3,255,165,33,0,244,12,87,255), 2)
ok(eq(buffer:dump(), string.char(0,0,0,3,255,165,33,0,244,12,87,255,0,0,0)), "RGBW")
buffer = pixbuf.newBuffer(5, 3)
buffer:replace(string.char(3,255,165,33,0,244,12,87,255), -5)
ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW")
fail(function() buffer:replace(string.char(3,255,165,33,0,244,12,87,255), 4) end,
"does not fit into destination")
end)
N.test('replace correctly issue #2921', function()
local buffer = pixbuf.newBuffer(5, 3)
buffer:replace(string.char(3,255,165,33,0,244,12,87,255), -7)
ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW")
end)
N.test('get/set correctly', function()
local buffer = pixbuf.newBuffer(3, 4)
buffer:fill(1,222,55,13)
ok(eq({buffer:get(2)},{1,222,55,13}))
buffer:set(2, 4,53,99,0)
ok(eq({buffer:get(1)},{1,222,55,13}))
ok(eq({buffer:get(2)},{4,53,99,0}))
ok(eq(buffer:dump(), string.char(1,222,55,13,4,53,99,0,1,222,55,13)), "RGBW")
fail(function() buffer:get(0) end, "index out of range")
fail(function() buffer:get(4) end, "index out of range")
fail(function() buffer:set(0,1,2,3,4) end, "index out of range")
fail(function() buffer:set(4,1,2,3,4) end, "index out of range")
fail(function() buffer:set(2,1,2,3) end, "number expected, got no value")
fail(function() buffer:set(2,1,2,3,4,5) end, "extra values given")
end)
N.test('get/set multiple with string', function()
-- verify that :set does indeed return its input
local buffer = pixbuf.newBuffer(4, 3):set(1,"ABCDEF")
buffer:set(3,"LMNOPQ")
ok(eq(buffer:dump(), "ABCDEFLMNOPQ"))
fail(function() buffer:set(4,"AAAAAA") end, "string size will exceed strip length")
fail(function() buffer:set(2,"AAAAA") end, "string does not contain whole LEDs")
end)
N.test('fade correctly', function()
local buffer = pixbuf.newBuffer(1, 3)
buffer:fill(1,222,55)
buffer:fade(2)
ok(buffer:dump() == string.char(0,111,27), "RGB")
buffer:fill(1,222,55)
buffer:fade(3, pixbuf.FADE_OUT)
ok(buffer:dump() == string.char(0,math.floor(222/3),math.floor(55/3)), "RGB")
buffer:fill(1,222,55)
buffer:fade(3, pixbuf.FADE_IN)
ok(buffer:dump() == string.char(3,255,165), "RGB")
buffer = pixbuf.newBuffer(1, 4)
buffer:fill(1,222,55, 77)
buffer:fade(2, pixbuf.FADE_OUT)
ok(eq(buffer:dump(), string.char(0,111,27,38)), "RGBW")
end)
N.test('mix correctly issue #1736', function()
local buffer1 = pixbuf.newBuffer(1, 3)
local buffer2 = pixbuf.newBuffer(1, 3)
buffer1:fill(10,22,54)
buffer2:fill(10,27,55)
buffer1:mix(256/8*7,buffer1,256/8,buffer2)
ok(eq({buffer1:get(1)}, {10,23,54}))
end)
N.test('mix saturation correctly ', function()
local buffer1 = pixbuf.newBuffer(1, 3)
local buffer2 = pixbuf.newBuffer(1, 3)
buffer1:fill(10,22,54)
buffer2:fill(10,27,55)
buffer1:mix(256/2,buffer1,-256,buffer2)
ok(eq({buffer1:get(1)}, {0,0,0}))
buffer1:fill(10,22,54)
buffer2:fill(10,27,55)
buffer1:mix(25600,buffer1,256/8,buffer2)
ok(eq({buffer1:get(1)}, {255,255,255}))
buffer1:fill(10,22,54)
buffer2:fill(10,27,55)
buffer1:mix(-257,buffer1,255,buffer2)
ok(eq({buffer1:get(1)}, {0,5,1}))
end)
N.test('power', function()
local buffer = pixbuf.newBuffer(2, 4)
buffer:fill(10,22,54,234)
ok(eq(buffer:power(), 2*(10+22+54+234)))
end)
N.test('shift LOGICAL', function()
local buffer1 = pixbuf.newBuffer(4, 4)
local buffer2 = pixbuf.newBuffer(4, 4)
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,0,0,7,8)
ok(buffer1 ~= buffer2, "disequality pre shift")
buffer1:shift(2)
ok(buffer1 == buffer2, "shift right")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,9,12,0,0)
buffer1:shift(-2)
ok(buffer1 == buffer2, "shift left")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,7,0,8,12)
buffer1:shift(1, nil, 2,3)
ok(buffer1 == buffer2, "shift middle right")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,7,9,0,12)
buffer1:shift(-1, nil, 2,3)
ok(buffer1 == buffer2, "shift middle left")
-- bounds checks, handle gracefully as string:sub does
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,8,9,12,0)
buffer1:shift(-1, pixbuf.SHIFT_LOGICAL, 0,5)
ok(buffer1 == buffer2, "shift left out of bound")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,0,7,8,9)
buffer1:shift(1, pixbuf.SHIFT_LOGICAL, 0,5)
ok(buffer1 == buffer2, "shift right out of bound")
end)
N.test('shift LOGICAL issue #2946', function()
local buffer1 = pixbuf.newBuffer(4, 4)
local buffer2 = pixbuf.newBuffer(4, 4)
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,0,0,0,0)
buffer1:shift(4)
ok(buffer1 == buffer2, "shift all right")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,0,0,0,0)
buffer1:shift(-4)
ok(buffer1 == buffer2, "shift all left")
fail(function() buffer1:shift(10) end, "shifting more elements than buffer size")
fail(function() buffer1:shift(-6) end, "shifting more elements than buffer size")
end)
N.test('shift CIRCULAR', function()
local buffer1 = pixbuf.newBuffer(4, 4)
local buffer2 = pixbuf.newBuffer(4, 4)
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,9,12,7,8)
buffer1:shift(2, pixbuf.SHIFT_CIRCULAR)
ok(buffer1 == buffer2, "shift right")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,9,12,7,8)
buffer1:shift(-2, pixbuf.SHIFT_CIRCULAR)
ok(buffer1 == buffer2, "shift left")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,7,9,8,12)
buffer1:shift(1, pixbuf.SHIFT_CIRCULAR, 2,3)
ok(buffer1 == buffer2, "shift middle right")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,7,9,8,12)
buffer1:shift(-1, pixbuf.SHIFT_CIRCULAR, 2,3)
ok(buffer1 == buffer2, "shift middle left")
-- bounds checks, handle gracefully as string:sub does
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,8,9,12,7)
buffer1:shift(-1, pixbuf.SHIFT_CIRCULAR, 0,5)
ok(buffer1 == buffer2, "shift left out of bound")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,12,7,8,9)
buffer1:shift(1, pixbuf.SHIFT_CIRCULAR, 0,5)
ok(buffer1 == buffer2, "shift right out of bound")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,12,7,8,9)
buffer1:shift(1, pixbuf.SHIFT_CIRCULAR, -12,12)
ok(buffer1 == buffer2, "shift right way out of bound")
end)
N.test('sub', function()
local buffer1 = pixbuf.newBuffer(4, 4)
initBuffer(buffer1,7,8,9,12)
buffer1 = buffer1:sub(4,3)
ok(eq(buffer1:size(), 0), "sub empty")
local buffer2 = pixbuf.newBuffer(2, 4)
buffer1 = pixbuf.newBuffer(4, 4)
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,9,12)
buffer1 = buffer1:sub(3,4)
ok(buffer1 == buffer2, "sub")
buffer1 = pixbuf.newBuffer(4, 4)
buffer2 = pixbuf.newBuffer(4, 4)
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,7,8,9,12)
buffer1 = buffer1:sub(-12,33)
ok(buffer1 == buffer2, "out of bounds")
end)
N.test('map', function()
local buffer1 = pixbuf.newBuffer(4, 4)
buffer1:fill(65,66,67,68)
buffer1:map(function(a,b,c,d) return b,a,c,d end)
ok(eq("BACDBACDBACDBACD", buffer1:dump()), "swizzle")
local buffer2 = pixbuf.newBuffer(4, 1)
buffer2:map(function(b,a,c,d) return c end, buffer1) -- luacheck: ignore
ok(eq("CCCC", buffer2:dump()), "projection")
local buffer3 = pixbuf.newBuffer(4, 3)
buffer3:map(function(b,a,c,d) return a,b,d end, buffer1) -- luacheck: ignore
ok(eq("ABDABDABDABD", buffer3:dump()), "projection 2")
buffer1:fill(70,71,72,73)
buffer1:map(function(c,a,b,d) return a,b,c,d end, buffer2, nil, nil, buffer3)
ok(eq("ABCDABCDABCDABCD", buffer1:dump()), "zip")
buffer1 = pixbuf.newBuffer(2, 4)
buffer1:fill(70,71,72,73)
buffer2:set(1,"ABCD")
buffer3:set(1,"EFGHIJKLM")
buffer1:map(function(c,a,b,d) return a,b,c,d end, buffer2, 1, 2, buffer3, 2)
ok(eq("HIAJKLBM", buffer1:dump()), "partial zip")
end)
--[[
pixbuf.buffer:__concat()
--]]
local N = ...
N = (N or require "NTest")("tmr")
N.testasync('SINGLE alarm', function(next)
local t = tmr.create();
local count = 0
t:alarm(200, tmr.ALARM_SINGLE,
function()
count = count + 1
ok(count <= 1, "only 1 invocation")
next()
end)
ok(true, "sync end")
end)
N.testasync('SEMI alarm', function(next)
local t = tmr.create();
local count = 0
t:alarm(200, tmr.ALARM_SEMI,
function(tp)
count = count + 1
if count <= 1 then
tp:start()
return
end
ok(eq(count, 2), "only 2 invocations")
next()
end)
ok(true, "sync end")
end)
N.testasync('AUTO alarm', function(next)
local t = tmr.create();
local count = 0
t:alarm(200, tmr.ALARM_AUTO,
function(tp)
count = count + 1
if count == 2 then
tp:stop()
return next()
end
ok(count < 2, "only 2 invocations")
end)
ok(true, "sync end")
end)
N.testco('SINGLE alarm coroutine', function(getCB, waitCB)
local t = tmr.create();
t:alarm(200, tmr.ALARM_SINGLE, getCB("timer"))
local name, timer = waitCB()
ok(eq("timer", name), "CB name matches")
ok(eq(t, timer), "CB tmr instance matches")
ok(true, "coroutine end")
end)
N.testco('SEMI alarm coroutine', function(getCB, waitCB)
local t = tmr.create();
t:alarm(200, tmr.ALARM_SEMI, getCB("timer"))
local name, timer = waitCB()
ok(eq("timer", name), "CB name matches")
ok(eq(t, timer), "CB tmr instance matches")
timer:start()
name, timer = waitCB()
ok(eq("timer", name), "CB name matches again")
ok(eq(t, timer), "CB tmr instance matches again")
ok(true, "coroutine end")
end)
N.testco('AUTO alarm coroutine', function(getCB, waitCB)
local t = tmr.create();
t:alarm(200, tmr.ALARM_AUTO, getCB("timer"))
local name, timer = waitCB()
ok(eq("timer", name), "CB name matches")
ok(eq(t, timer), "CB tmr instance matches")
name, timer = waitCB()
ok(eq("timer", name), "CB name matches again")
ok(eq(t, timer), "CB tmr instance matches again")
timer:stop()
ok(true, "coroutine end")
end)
N.test('softwd set positive and negative values', function()
tmr.softwd(22)
tmr.softwd(0)
tmr.softwd(-1) -- disable it again
tmr.softwd(-22) -- disable it again
end)
local N = ...
N = (N or require "NTest")("ws2812 buffers")
local buffer, buffer1, buffer2
local function initBuffer(buf, ...)
for i,v in ipairs({...}) do
buf:set(i, v, v*2, v*3, v*4)
end
return buf
end
local function equalsBuffer(buf1, buf2)
return eq(buf1:dump(), buf2:dump())
end
N.test('initialize a buffer', function()
buffer = ws2812.newBuffer(9, 3)
nok(buffer == nil)
ok(eq(buffer:size(), 9), "check size")
ok(eq(buffer:dump(), string.char(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)), "initialize with 0")
fail(function() ws2812.newBuffer(9, 0) end, "should be a positive integer")
fail(function() ws2812.newBuffer(9, -1) end, "should be a positive integer")
fail(function() ws2812.newBuffer(0, 3) end, "should be a positive integer")
fail(function() ws2812.newBuffer(-1, 3) end, "should be a positive integer")
end)
N.test('have correct size', function()
buffer = ws2812.newBuffer(9, 3)
ok(eq(buffer:size(), 9), "check size")
buffer = ws2812.newBuffer(9, 22)
ok(eq(buffer:size(), 9), "check size")
buffer = ws2812.newBuffer(13, 1)
ok(eq(buffer:size(), 13), "check size")
end)
N.test('fill a buffer with one color', function()
buffer = ws2812.newBuffer(3, 3)
buffer:fill(1,222,55)
ok(eq(buffer:dump(), string.char(1,222,55,1,222,55,1,222,55)), "RGB")
buffer = ws2812.newBuffer(3, 4)
buffer:fill(1,222,55, 77)
ok(eq(buffer:dump(), string.char(1,222,55,77,1,222,55,77,1,222,55,77)), "RGBW")
end)
N.test('replace correctly', function()
buffer = ws2812.newBuffer(5, 3)
buffer:replace(string.char(3,255,165,33,0,244,12,87,255))
ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW")
buffer = ws2812.newBuffer(5, 3)
buffer:replace(string.char(3,255,165,33,0,244,12,87,255), 2)
ok(eq(buffer:dump(), string.char(0,0,0,3,255,165,33,0,244,12,87,255,0,0,0)), "RGBW")
buffer = ws2812.newBuffer(5, 3)
buffer:replace(string.char(3,255,165,33,0,244,12,87,255), -5)
ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW")
fail(function() buffer:replace(string.char(3,255,165,33,0,244,12,87,255), 4) end, "does not fit into destination")
end)
N.test('replace correctly issue #2921', function()
buffer = ws2812.newBuffer(5, 3)
buffer:replace(string.char(3,255,165,33,0,244,12,87,255), -7)
ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW")
end)
N.test('get/set correctly', function()
buffer = ws2812.newBuffer(3, 4)
buffer:fill(1,222,55,13)
ok(eq({buffer:get(2)},{1,222,55,13}))
buffer:set(2, 4,53,99,0)
ok(eq({buffer:get(1)},{1,222,55,13}))
ok(eq({buffer:get(2)},{4,53,99,0}))
ok(eq(buffer:dump(), string.char(1,222,55,13,4,53,99,0,1,222,55,13)), "RGBW")
fail(function() buffer:get(0) end, "index out of range")
fail(function() buffer:get(4) end, "index out of range")
fail(function() buffer:set(0,1,2,3,4) end, "index out of range")
fail(function() buffer:set(4,1,2,3,4) end, "index out of range")
fail(function() buffer:set(2,1,2,3) end, "number expected, got no value")
-- fail(function() buffer:set(2,1,2,3,4,5) end, "extra values given")
end)
N.test('fade correctly', function()
buffer = ws2812.newBuffer(1, 3)
buffer:fill(1,222,55)
buffer:fade(2)
ok(buffer:dump() == string.char(0,111,27), "RGB")
buffer:fill(1,222,55)
buffer:fade(3, ws2812.FADE_OUT)
ok(buffer:dump() == string.char(0,math.floor(222/3),math.floor(55/3)), "RGB")
buffer:fill(1,222,55)
buffer:fade(3, ws2812.FADE_IN)
ok(buffer:dump() == string.char(3,255,165), "RGB")
buffer = ws2812.newBuffer(1, 4)
buffer:fill(1,222,55, 77)
buffer:fade(2, ws2812.FADE_OUT)
ok(eq(buffer:dump(), string.char(0,111,27,38)), "RGBW")
end)
N.test('mix correctly issue #1736', function()
buffer1 = ws2812.newBuffer(1, 3)
buffer2 = ws2812.newBuffer(1, 3)
buffer1:fill(10,22,54)
buffer2:fill(10,27,55)
buffer1:mix(256/8*7,buffer1,256/8,buffer2)
ok(eq({buffer1:get(1)}, {10,23,54}))
end)
N.test('mix saturation correctly ', function()
buffer1 = ws2812.newBuffer(1, 3)
buffer2 = ws2812.newBuffer(1, 3)
buffer1:fill(10,22,54)
buffer2:fill(10,27,55)
buffer1:mix(256/2,buffer1,-256,buffer2)
ok(eq({buffer1:get(1)}, {0,0,0}))
buffer1:fill(10,22,54)
buffer2:fill(10,27,55)
buffer1:mix(25600,buffer1,256/8,buffer2)
ok(eq({buffer1:get(1)}, {255,255,255}))
buffer1:fill(10,22,54)
buffer2:fill(10,27,55)
buffer1:mix(-257,buffer1,255,buffer2)
ok(eq({buffer1:get(1)}, {0,5,1}))
end)
N.test('power', function()
buffer = ws2812.newBuffer(2, 4)
buffer:fill(10,22,54,234)
ok(eq(buffer:power(), 2*(10+22+54+234)))
end)
N.test('shift LOGICAL', function()
buffer1 = ws2812.newBuffer(4, 4)
buffer2 = ws2812.newBuffer(4, 4)
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,0,0,7,8)
buffer1:shift(2)
ok(equalsBuffer(buffer1, buffer2), "shift right")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,9,12,0,0)
buffer1:shift(-2)
ok(equalsBuffer(buffer1, buffer2), "shift left")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,7,0,8,12)
buffer1:shift(1, nil, 2,3)
ok(equalsBuffer(buffer1, buffer2), "shift middle right")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,7,9,0,12)
buffer1:shift(-1, nil, 2,3)
ok(equalsBuffer(buffer1, buffer2), "shift middle left")
-- bounds checks, handle gracefully as string:sub does
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,8,9,12,0)
buffer1:shift(-1, ws2812.SHIFT_LOGICAL, 0,5)
ok(equalsBuffer(buffer1, buffer2), "shift left out of bound")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,0,7,8,9)
buffer1:shift(1, ws2812.SHIFT_LOGICAL, 0,5)
ok(equalsBuffer(buffer1, buffer2), "shift right out of bound")
end)
N.test('shift LOGICAL issue #2946', function()
buffer1 = ws2812.newBuffer(4, 4)
buffer2 = ws2812.newBuffer(4, 4)
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,0,0,0,0)
buffer1:shift(4)
ok(equalsBuffer(buffer1, buffer2), "shift all right")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,0,0,0,0)
buffer1:shift(-4)
ok(equalsBuffer(buffer1, buffer2), "shift all left")
fail(function() buffer1:shift(10) end, "shifting more elements than buffer size")
fail(function() buffer1:shift(-6) end, "shifting more elements than buffer size")
end)
N.test('shift CIRCULAR', function()
buffer1 = ws2812.newBuffer(4, 4)
buffer2 = ws2812.newBuffer(4, 4)
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,9,12,7,8)
buffer1:shift(2, ws2812.SHIFT_CIRCULAR)
ok(equalsBuffer(buffer1, buffer2), "shift right")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,9,12,7,8)
buffer1:shift(-2, ws2812.SHIFT_CIRCULAR)
ok(equalsBuffer(buffer1, buffer2), "shift left")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,7,9,8,12)
buffer1:shift(1, ws2812.SHIFT_CIRCULAR, 2,3)
ok(equalsBuffer(buffer1, buffer2), "shift middle right")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,7,9,8,12)
buffer1:shift(-1, ws2812.SHIFT_CIRCULAR, 2,3)
ok(equalsBuffer(buffer1, buffer2), "shift middle left")
-- bounds checks, handle gracefully as string:sub does
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,8,9,12,7)
buffer1:shift(-1, ws2812.SHIFT_CIRCULAR, 0,5)
ok(equalsBuffer(buffer1, buffer2), "shift left out of bound")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,12,7,8,9)
buffer1:shift(1, ws2812.SHIFT_CIRCULAR, 0,5)
ok(equalsBuffer(buffer1, buffer2), "shift right out of bound")
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,12,7,8,9)
buffer1:shift(1, ws2812.SHIFT_CIRCULAR, -12,12)
ok(equalsBuffer(buffer1, buffer2), "shift right way out of bound")
end)
N.test('sub', function()
buffer1 = ws2812.newBuffer(4, 4)
buffer2 = ws2812.newBuffer(4, 4)
initBuffer(buffer1,7,8,9,12)
buffer1 = buffer1:sub(4,3)
ok(eq(buffer1:size(), 0), "sub empty")
buffer1 = ws2812.newBuffer(4, 4)
buffer2 = ws2812.newBuffer(2, 4)
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,9,12)
buffer1 = buffer1:sub(3,4)
ok(equalsBuffer(buffer1, buffer2), "sub")
buffer1 = ws2812.newBuffer(4, 4)
buffer2 = ws2812.newBuffer(4, 4)
initBuffer(buffer1,7,8,9,12)
initBuffer(buffer2,7,8,9,12)
buffer1 = buffer1:sub(-12,33)
ok(equalsBuffer(buffer1, buffer2), "out of bounds")
end)
--[[
ws2812.buffer:__concat()
--]]
local N = ...
N = (N or require "NTest")("ws2812_effects")
local buffer
N.test('set_speed', function()
buffer = ws2812.newBuffer(9, 3)
ws2812_effects.init(buffer)
ws2812_effects.set_speed(0)
ws2812_effects.set_speed(255)
fail(function() ws2812_effects.set_speed(-1) end, "should be")
fail(function() ws2812_effects.set_speed(256) end, "should be")
end)
N.test('set_brightness', function()
buffer = ws2812.newBuffer(9, 3)
ws2812_effects.init(buffer)
ws2812_effects.set_brightness(0)
ws2812_effects.set_brightness(255)
fail(function() ws2812_effects.set_brightness(-1) end, "should be")
fail(function() ws2812_effects.set_brightness(256) end, "should be")
end)
# Introduction
Welcome to the NodeMCU self-test suite. Here you will find our growing effort
to ensure that our software behaves as we think it should and that we do not
regress against earlier versions.
Our tests are written using [NTest](./NTest/NTest.md), a lightweight yet
featureful framework for specifying unit tests.
# Building and Running Test Software on NodeMCU Devices
Naturally, to test NodeMCU on its intended hardware, you will need one or more
NodeMCU-capable boards. At present, the test environment is specified using
two ESP8266 Devices Under Test (DUTs), but we envision expanding this to mixed
ESP8266/ESP32 environments as well.
Test programs live beside this file. While many test programs run on the
NodeMCU DUTs, but there is reason to want to orchestrate DUTs and the
environment using the host. Files matching the glob `NTest_*.lua` are intended
for on-DUT execution.
## Manual Test Invocation
At the moment, the testing regime and host-based orchestration is still in
development, and so things are a little more manual than perhaps desired. The
`NTest`-based test programs all assume that they can `require "NTest"`, and so
the easiest route to success is to
* build an LFS image containing
* [package.loader support for LFS](../lua_examples/lfs/_init.lua)
* [NTest itself](./NTest/NTest.lua)
* Any additional Lua support modules required (e.g., [mcp23017
support](../lua_modules/mcp23017/mcp23017.lua) )
* build a firmware with the appropriate C modules
* program the board with your firmware and LFS images
* ensure that `package.loader` is patched appropriately on startup
* transfer the `NTest_foo` program you wish to run to the device SPIFFS
(or have included it in the LFS).
* at the interpreter prompt, say `dofile("NTest_foo.lua")` (or
`node.LFS.get("NTest_foo")()`) to run the `foo` test program.
## Experimental Host Orchestration
Enthusiastic testers are encouraged to try using our very new, very
experimental host test runner, [tap-driver.expect](./tap-driver.expect). To
use this program, in addition to the above, the LFS environment should contain
[NTestTapOut](./tests/utils/NTestTapOut.lua), an output adapter for `NTest`,
making it speak a slight variant of the [Test Anything
Protocol](https://testanything.org/). This structured output is scanned for
by the script on the host.
You'll need `expect` and TCL and some TCL libraries available; on Debian, that
amounts to
apt install tcl tcllib tclx8.4 expect
This program should be invoked from beside this file with something like
TCLLIBPATH=./expectnmcu ./tap-driver.expect -serial /dev/ttyUSB3 -lfs ./lfs.img NTest_file.lua
This will...
* transfer and install the specified LFS module (and reboot the device to load LFS)
* transfer the test program
* run the test program with `NTest` shimmed to use the `NTestTapOut` output
handler
* summarize the results
* return 0 if and only if all tests have passed
This tool is quite flexible and takes a number of other options and flags
controlling aspects of its behavior:
* Additional files, Lua or otherwise, may be transferred by specifing them
before the test to run (e.g., `./tap-driver.expect a.lua b.lua
NTest_foo.lua`); dually, a `-noxfer` flag will suppress transferring even the
last file. All transferred files are moved byte-for-byte to the DUT's
SPIFFS with names, but not directory components, preserved.
* The `-lfs LFS.img` option need not be specified and, if not given, any
existing `LFS` image will remain on the device for use by the test.
* A `-nontestshim` flag will skip attempting to shim the given test program
with `NTestTapOut`; the test program is expected to provide its own TAP
output. The `-tpfx` argument can be used to override the leading `TAP: `
sigil used by the `NTestTapOut` output handler.
* A `-runfunc` option indicates that the last argument is not a file to
transfer but rather a function to be run. It will be invoked at the REPL
with a single argument, the shimmed `NTest` constructor, unless `-nontestshim`
is given, in which case the argument will be `nil`.
* A `-notests` option suppresses running tests (making the tool merely another
option for loading files to the device).
Transfers will be significantly faster if
[pipeutils](../lua_examples/pipeutils.lua) is available to `require` on the
DUT, but a fallback strategy exists if not. We suggest either including
`pipeutils` in LFS images, in SPIFFS, or as the first file to be transferred.
# NodeMCU Testing Environment
Herein we define the environment our testing framework expects to see
when it runs. It is composed of two ESP8266 devices, each capable of
holding an entire NodeMCU firmware, LFS image, and SPIFFS file system,
as well as additional peripheral hardware. It is designed to fit
comfortably on a breadboard and so should be easily replicated and
integrated into any firmware validation testing.
The test harness runs from a dedicated host computer, which is expected
to have reset- and programming-capable UART links to both ESP8266
devices, as found on almost all ESP8266 boards with USB to UART
adapters, but the host does not necessarily need to use USB to connect,
so long as TXD, RXD, DTR, and RTS are wired across.
A particular implementation of this can be found at [Test Harness](HardwareTestHarness.html).
## Peripherals
### I2C Bus
There is an I2C bus hanging off DUT 0. Attached hardware is used both as
tests of modules directly and also to facilitate testing other modules
(e.g., gpio).
#### MCP23017: I/O Expander
At address 0x20. An 16-bit tristate GPIO expander, this chip is used to
test I2C, GPIO, and ADC functionality. This chip's interconnections are
as follows:
MPC23017 | Purpose
---------|--------------------------------------------------------------
/RESET |DUT0 reset. This resets the chip whenever the host computer resets DUT 0 over its serial link (using DTR/RTS).
B 0 |4K7 resistor to DUT 0 ADC.
B 1 |2K2 resistor to DUT 0 ADC.
B 5 |DUT1 GPIO16/WAKE via 4K7 resitor
B 6 |DUT0 GPIO13 via 4K7 resistor and DUT1 GPIO15 via 4K7 resistor
B 7 |DUT0 GPIO15 via 4K7 resistor and DUT1 GPIO13 via 4K7 resistor
Notes:
- DUT 0's ADC pin is connected via a 2K2 reistor to this chip's port
B, pin 1 and via a 4K7 resistor to port B, pin 0. This gives us the
ability to produce approximately 0 (both pins low), 1.1 (pin 0 high,
pin 1 low), 2.2 (pin 1 high, pin 0 low), and 3.3V (both pins high)
on the ADC pin.
- Port B pins 6 and 7 sit on the UART cross-wiring between DUT 0 and
DUT 1. The 23017 will be tristated for inter-DUT UART tests, but
these
- Port B pins 2, 3, and 4, as well as all of port A, remain available
for expansion.
- The interrupt pins are not yet routed, but could be. We reserve DUT
0 GPIO 2 for this purpose with the understanding that the 23017's
interrupt functionality will be disabled (INTA, INTB set to
open-drain, GPINTEN set to 0) when not explicitly under test.
ESP8266 Device 0 Connections
----------------------------
ESP | Usage
----------|----------------------------------------------------------
GPIO 0 |Used to enter programming mode; otherwise unused in test environment.
GPIO 1 |Primary UART transmit; reserved for host communication
GPIO 2 |[reserved for 1-Wire] [+ reserved for 23017 INT[AB] connections]
GPIO 3 |Primary UART recieve; reserved for host communication
GPIO 4 |I2C SDA
GPIO 5 |I2C SCL
GPIO 6 |[Reserved for on-chip flash]
GPIO 7 |[Reserved for on-chip flash]
GPIO 8 |[Reserved for on-chip flash]
GPIO 9 |[Reserved for on-chip flash]
GPIO 10 |[Reserved for on-chip flash]
GPIO 11 |[Reserved for on-chip flash]
GPIO 12 |
GPIO 13 |Secondary UART RX; DUT 1 GPIO 15, I/O expander B 6
GPIO 14 |
GPIO 15 |Secondary UART TX; DUT 1 GPIO 13, I/O expander B 7
GPIO 16 |
ADC 0 |Resistor divider with I/O expander
ESP8266 Device 1 Connections
----------------------------
ESP | Usage
----------|----------------------------------------------------------
GPIO 0 |Used to enter programming mode; otherwise unused in test environment.
GPIO 1 |Primary UART transmit; reserved for host communication
GPIO 2 |[Reserved for WS2812]
GPIO 3 |Primary UART recieve; reserved for host communication
GPIO 4 |
GPIO 5 |
GPIO 6 |[Reserved for on-chip flash]
GPIO 7 |[Reserved for on-chip flash]
GPIO 8 |[Reserved for on-chip flash]
GPIO 9 |[Reserved for on-chip flash]
GPIO 10 |[Reserved for on-chip flash]
GPIO 11 |[Reserved for on-chip flash]
GPIO 12 |HSPI MISO
GPIO 13 |Secondary UART RX; DUT 0 GPIO 15, I/O exp B 7 via 4K7 Also used as HSPI MOSI for SPI tests
GPIO 14 |HSPI CLK
GPIO 15 |Secondary UART TX; DUT 0 GPIO 13, I/O exp B 6 via 4K7 Also used as HSPI /CS for SPI tests
GPIO 16 |I/O expander B 5 via 4K7 resistor, for deep-sleep tests
ADC 0 |
namespace eval expectnmcu::core {
set panicre "powered by Lua \[0-9.\]+ on SDK \[0-9.\]+"
set promptstr "\n> "
namespace export reboot waitboot connect
namespace export send_exp_prompt send_exp_res_prompt send_exp_prompt_c
}
package require cmdline
# Use DTR/RTS signaling to reboot the device
## I'm not sure why we have to keep resetting the mode, but so it goes.
proc ::expectnmcu::core::reboot { dev } {
set victimfd [open ${dev} ]
set mode [fconfigure ${victimfd} -mode ]
fconfigure ${victimfd} -mode ${mode} -ttycontrol {DTR 0 RTS 1}
sleep 0.1
fconfigure ${victimfd} -mode ${mode} -ttycontrol {DTR 0 RTS 0}
close ${victimfd}
}
proc ::expectnmcu::core::waitboot { victim } {
expect {
-i ${victim} "Formatting file system" {
set timeout 120
exp_continue
}
-i ${victim} "powered by Lua" { }
timeout { return -code error "Timeout" }
}
# Catch nwf's system bootup, in case we're testing an existing system,
# rather than a blank firmware.
expect {
-i ${victim} -re "Reset delay!.*${::expectnmcu::core::promptstr}" {
send -i ${victim} "stop(true)\n"
expect -i ${victim} -ex ${::expectnmcu::core::promptstr}
}
-i ${victim} -ex ${::expectnmcu::core::promptstr} { }
timeout { return -code error "Timeout" }
}
# Do a little more active synchronization with the DUT: send it a command
# and wait for the side-effect of that command to happen, thereby ensuring
# that the next prompt we see is after this point in the input.
send -i ${victim} "print(\"a\",\"z\")\n"
expect {
-i ${victim} -ex "a\tz" { }
}
expect {
-i ${victim} -ex ${::expectnmcu::core::promptstr} { }
timeout { return -code error "Timeout" }
}
}
# Establish a serial connection to the device via socat. Takes
# -baud=N, -reboot=0/1/dontwait, -waitboot=0/1 optional parameters
proc ::expectnmcu::core::connect { dev args } {
set opts {
{ baud.arg 115200 }
{ reboot.arg 1 }
}
array set arg [::cmdline::getoptions args $opts]
spawn "socat" "STDIO" "${dev},b${arg(baud)},raw,crnl"
close -onexec 1 -i ${spawn_id}
set victim ${spawn_id}
# XXX?
set victimfd [open ${dev} ]
set mode [fconfigure ${victimfd} -mode ${arg(baud)},n,8,1 ]
if { ${arg(reboot)} != 0 } {
::expectnmcu::core::reboot ${dev}
if { ${arg(reboot)} != "dontwait" } {
::expectnmcu::core::waitboot ${victim}
}
}
close ${victimfd}
return ${victim}
}
# This one is somewhat "for experts only" -- it expects that you have either
# consumed whatever command you flung at the node or that you have some reason
# to not be concerned with its echo (and return)
proc ::expectnmcu::core::exp_prompt { sid } {
expect {
-i ${sid} -ex ${::expectnmcu::core::promptstr} { }
-i ${sid} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" }
timeout { return -code error "Timeout" }
}
}
proc ::expectnmcu::core::send_exp_prompt { sid cmd } {
send -i ${sid} -- "${cmd}\n"
expect {
-i ${sid} -ex "${cmd}" { }
timeout { return -code error "Timeout" }
}
expect {
-i ${sid} -ex ${::expectnmcu::core::promptstr} { }
-i ${sid} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" }
timeout { return -code error "Timeout" }
}
}
proc ::expectnmcu::core::send_exp_res_prompt { sid cmd res } {
send -i ${sid} -- "${cmd}\n"
expect {
-i ${sid} -ex "${cmd}" { }
timeout { return -code error "Timeout" }
}
expect {
-i ${sid} -re "${res}.*${::expectnmcu::core::promptstr}" { }
-i ${sid} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" }
-i ${sid} -ex ${::expectnmcu::core::promptstr} { return -code error "Prompt before expected response" }
timeout { return -code error "Timeout" }
}
}
proc ::expectnmcu::core::send_exp_prompt_c { sid cmd } {
send -i ${sid} -- "${cmd}\n"
expect {
-i ${sid} -ex "${cmd}" { }
timeout { return -code error "Timeout" }
}
expect {
-i ${sid} -ex "\n>> " { }
-i ${sid} -ex ${::expectnmcu::core::promptstr} { return -code error "Non-continuation prompt" }
-i ${sid} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" }
timeout { return -code error "Timeout" }
}
}
package provide expectnmcu::core 1.0
# Tcl package index file, version 1.1
# This file is generated by the "pkg_mkIndex" command
# and sourced either when an application starts up or
# by a "package unknown" script. It invokes the
# "package ifneeded" command to set up package-related
# information so that packages will be loaded automatically
# in response to "package require" commands. When this
# script is sourced, the variable $dir must contain the
# full path name of this file's directory.
package ifneeded expectnmcu::core 1.0 [list source [file join $dir core.tcl]]
package ifneeded expectnmcu::xfer 1.0 [list source [file join $dir xfer.tcl]]
namespace eval expectnmcu::xfer {
}
package require expectnmcu::core
# Open remote file `which` on `dev` in `mode` as Lua object `dfh`
proc ::expectnmcu::xfer::open { dev dfh which mode } {
::expectnmcu::core::send_exp_prompt ${dev} "${dfh} = nil"
::expectnmcu::core::send_exp_prompt ${dev} "${dfh} = file.open(\"${which}\",\"${mode}\")"
::expectnmcu::core::send_exp_res_prompt ${dev} "=type(${dfh})" "userdata"
}
# Close Lua file object `dfh` on `dev`
proc ::expectnmcu::xfer::close { dev dfh } {
::expectnmcu::core::send_exp_prompt ${dev} "${dfh}:close()"
}
# Write to `dfh` on `dev` at `where` `what`, using base64 as transport
#
# This does not split lines; write only short amounts of data.
proc ::expectnmcu::xfer::pwrite { dev dfh where what } {
send -i ${dev} -- [string cat \
"do local d,e = encoder.fromBase64(\"[binary encode base64 -maxlen 0 ${what}]\");" \
"${dfh}:seek(\"set\",${where});" \
"print(${dfh}:write(d));" \
"end\n" \
]
expect {
-i ${dev} -re "true\[\r\n\]+> " { }
-i ${dev} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" }
-i ${dev} -ex "\n> " { return -code error "Bad result from pwrite" }
timeout { return -code error "Timeout while waiting for pwrite" }
}
}
# Read `howmuch` byetes from `dfh` on `dev` at `where`, using base64
# as transport. This buffers the whole data and its base64 encoding
# in device RAM; read only short strings.
proc ::expectnmcu::xfer::pread { dev dfh where howmuch } {
send -i ${dev} -- "${dfh}:seek(\"set\",${where}); print(encoder.toBase64(${dfh}:read(${howmuch})))\n"
expect {
-i ${dev} -re "\\)\\)\\)\[\r\n\]+(\[^\r\n\]+)\[\r\n\]+> " {
return [binary decode base64 ${expect_out(1,string)}]
}
-i ${dev} -ex "\n> " { return -code error "No reply to pread" }
-i ${dev} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" }
timeout { return -code error "Timeout while pread-ing" }
}
}
# Check for pipeutils on the target device
proc ::expectnmcu::xfer::haspipeutils { dev } {
send -i ${dev} -- "local ok, pu = pcall(require, \"pipeutils\"); print(ok and type(pu) == \"table\" and pu.chunker and pu.debase64 and true or false)\n"
expect {
-i ${dev} -re "\[\r\n\]+false\[\r\n\]+> " { return 0 }
-i ${dev} -re "\[\r\n\]+true\[\r\n\]+> " { return 1 }
-i ${dev} -ex "\n> " { return -code error "No reply to pipeutils probe" }
-i ${dev} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" }
timeout { return -code error "Timeout while probing for pipeutils" }
}
}
# Send local file `lfn` to the remote filesystem on `dev` and name it `rfn`.
# Use `dfo` as the Lua handle to the remote file for the duration of writing,
# (and `nil` it out afterwards)
proc ::expectnmcu::xfer::sendfile { dev lfn rfn {dfo "xfo"} } {
package require sha256
set has_pipeutils [::expectnmcu::xfer::haspipeutils ${dev} ]
set ltf [::open ${lfn} ]
fconfigure ${ltf} -translation binary
file stat ${lfn} lfstat
::expectnmcu::xfer::open ${dev} ${dfo} "${rfn}.sf" "w+"
if { ${has_pipeutils} } {
# Send over a loader program
::expectnmcu::core::send_exp_prompt_c ${dev} "do"
::expectnmcu::core::send_exp_prompt_c ${dev} " local pu = require \"pipeutils\""
::expectnmcu::core::send_exp_prompt_c ${dev} " local ch = pu.chunker(function(d) ${dfo}:write(d) end, 256)"
::expectnmcu::core::send_exp_prompt_c ${dev} " local db = pu.debase64(ch.write, function(ed,ee)"
::expectnmcu::core::send_exp_prompt_c ${dev} " if ed:match(\"^%.\[\\r\\n\]*$\") then ch.flush() print(\"F I N\")"
::expectnmcu::core::send_exp_prompt_c ${dev} " else print(\"ABORT\", ee, ed) end"
::expectnmcu::core::send_exp_prompt_c ${dev} " uart.on(\"data\") end)"
# TODO: make echo use CRC not full string; probably best add to crypto module
::expectnmcu::core::send_exp_prompt_c ${dev} " uart.on(\"data\", \"\\n\", function(x) db.write(x); uart.write(0, \"OK: \", x) end, 0)"
::expectnmcu::core::send_exp_prompt ${dev} "end"
set xln 90
} else {
set xln 48
}
set lho [sha2::SHA256Init]
set fpos 0
while { 1 } {
send_user ">> xfer ${fpos} of ${lfstat(size)}\n"
set data [read ${ltf} ${xln}]
sha2::SHA256Update ${lho} ${data}
if { ${has_pipeutils} } {
set estr [binary encode base64 -maxlen 0 ${data}]
send -i ${dev} -- "${estr}\n"
expect {
-i ${dev} -ex "OK: ${estr}" { expect -i ${dev} -re "\[\r\n\]+" {} }
-i ${dev} -ex "\n> " { return -code error "Prompt while sending data" }
-i ${dev} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" }
timeout { return -code error "Timeout while sending data" }
}
} else {
::expectnmcu::xfer::pwrite ${dev} ${dfo} ${fpos} ${data}
}
set fpos [expr $fpos + ${xln}]
if { [string length ${data}] != ${xln} } { break }
}
if { ${has_pipeutils} } {
send -i ${dev} -- ".\n"
expect {
-i ${dev} -re "F I N\[\r\n\]+" { }
-i ${dev} -ex "\n> " { return -code error "Prompt while awaiting acknowledgement" }
-i ${dev} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" }
timeout { return -code error "Timeout while awaiting acknowledgement" }
}
}
::close ${ltf}
::expectnmcu::xfer::close ${dev} ${dfo}
::expectnmcu::core::send_exp_prompt ${dev} "${dfo} = nil"
set exphash [sha2::Hex [sha2::SHA256Final ${lho}]]
send -i ${dev} "=encoder.toHex(crypto.fhash(\"sha256\",\"${rfn}.sf\"))\n"
expect {
-i ${dev} -re "\[\r\n\]+(\[a-f0-9\]+)\[\r\n\]+> " {
if { ${expect_out(1,string)} != ${exphash} } {
return -code error \
"Sendfile checksum mismatch: ${expect_out(1,string)} != ${exphash}"
}
}
-i ${dev} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" }
timeout { return -code error "Timeout while verifying checksum" }
}
::expectnmcu::core::send_exp_prompt ${dev} "file.remove(\"${rfn}\")"
::expectnmcu::core::send_exp_res_prompt ${dev} "=file.rename(\"${rfn}.sf\", \"${rfn}\")" "true"
}
package provide expectnmcu::xfer 1.0
#!/usr/bin/env expect
# Push a file to the device, run it, and watch the tests run
#
# A typical invocation looks like:
# TCLLIBPATH=./expectnmcu ./tap-driver.expect -serial /dev/ttyUSB3 ./mispec.lua ./mispec_file.lua
#
# For debugging the driver itself, it may be useful to invoke expect with -d,
# which will give a great deal of diagnostic information about the expect state
# machine's internals:
#
# TCLLIBPATH=./expectnmcu expect -d ./tap-driver.expect ...
#
# The -debug option will turn on some additional reporting from this driver program, as well.
package require expectnmcu::core
package require expectnmcu::xfer
package require cmdline
set cmd_parameters {
{ serial.arg "/dev/ttyUSB0" "Set the serial interface name" }
{ tpfx.arg "TAP: " "Set the expected TAP test prefix" }
{ lfs.arg "" "Flash a file to LFS" }
{ noxfer "Do not send files, just run script" }
{ runfunc "Last argument is function, not file" }
{ notests "Don't run tests, just xfer files" }
{ nontestshim "Don't shim NTest when testing" }
{ debug "Enable debugging reporting" }
}
set cmd_usage "- A NodeMCU Lua-based-test runner"
if {[catch {array set cmdopts [cmdline::getoptions ::argv $cmd_parameters $cmd_usage]}]} {
send_user [cmdline::usage $cmd_parameters $cmd_usage]
send_user "\n Additional arguments should be files be transferred\n"
send_user " The last file transferred will be run with `dofile`\n"
exit 0
}
if { ${cmdopts(noxfer)} } {
if { [ llength ${::argv} ] > 1 } {
send_user "No point in more than one argument if noxfer given\n"
exit 1
}
} {
set xfers ${::argv}
if { ${cmdopts(runfunc)} } {
# Last argument is command, not file to xfer
set xfers [lreplace xfers end end]
}
foreach arg ${xfers} {
if { ! [file exists ${arg}] } {
send_user "File ${arg} does not exist\n"
exit 1
}
}
}
if { ${cmdopts(lfs)} ne "" } {
if { ! [file exists ${cmdopts(lfs)}] } {
send_user "LFS file does not exist\n"
exit 1
}
}
proc sus { what } { send_user "\n===> ${what} <===\n" }
proc sui { what } { send_user "\n---> ${what} <---\n" }
proc sud { what } {
upvar 1 cmdopts cmdopts
if { ${cmdopts(debug)} } { send_user "\n~~~> ${what} <~~~\n" }
}
set victim [::expectnmcu::core::connect ${cmdopts(serial)}]
sus "Machine has booted"
if { ${cmdopts(lfs)} ne "" } {
::expectnmcu::xfer::sendfile ${victim} ${cmdopts(lfs)} "tap-driver.lfs"
send -i ${victim} "=node.LFS.reload(\"tap-driver.lfs\")\n"
::expectnmcu::core::waitboot ${victim}
}
if { ! ${cmdopts(noxfer)} } {
foreach arg ${xfers} {
::expectnmcu::xfer::sendfile ${victim} ${arg} [file tail ${arg}]
}
}
set tfn [file tail [lindex ${::argv} end ] ]
if { ${cmdopts(notests)} || ${tfn} eq "" } {
sus "No tests requested, and so operations are completed"
exit 0
}
sus "Files transferred; running ${tfn}"
if { ! ${cmdopts(nontestshim)} } {
::expectnmcu::core::send_exp_prompt_c ${victim} "function ntshim(...)"
::expectnmcu::core::send_exp_prompt_c ${victim} " local test = (require \"NTest\")(...)"
::expectnmcu::core::send_exp_prompt_c ${victim} " test.outputhandler = require\"NTestTapOut\""
::expectnmcu::core::send_exp_prompt_c ${victim} " return test"
::expectnmcu::core::send_exp_prompt ${victim} "end"
} else {
sui "Not shimming NTest output; test must report its own TAP messages"
}
# ntshim may be nil at this point if -nontestshim was given; that's fine
if { ${cmdopts(runfunc)} } {
send -i ${victim} "[ lindex ${::argv} end ](ntshim)\n"
expect -i ${victim} -re "\\(ntshim\\)\[\r\n\]+" { }
} else {
send -i ${victim} "assert(loadfile(\"${tfn}\"))(ntshim)\n"
expect -i ${victim} -re "assert\\(loadfile\\(\"${tfn}\"\\)\\)\\(ntshim\\)\[\r\n\]+" { }
}
set tpfx ${cmdopts(tpfx)}
set toeol "\[^\n\]*(?=\n)"
# Wait for the test to start and tell us how many
# success lines we should expect
set ntests 0
set timeout 10
expect {
-i ${victim} -re "${tpfx}1\\.\\.(\\d+)(?=\r?\n)" {
global ntests
set ntests $expect_out(1,string)
}
-i ${victim} -re "${tpfx}Bail out!${toeol}" {
sus "Bail out before start"
exit 2
}
-i ${victim} -re ${::expectnmcu::core::panicre} {
sus "Panic!"
exit 2
}
# A prefixed line other than a plan (1..N) or bailout means we've not got
# a plan. Leave ${ntests} at 0 and proceed to run the protocol.
-i ${victim} -notransfer -re "${tpfx}${toeol}" { }
# -i ${victim} -ex "\n> " {
# sus "Prompt before start!"
# exit 2
# }
# Consume other outputs and discard as if they were comments
# This must come as the last pattern that looks at input
-i ${victim} -re "(?p).${toeol}" { exp_continue }
timeout {
send_user "Failure: time out getting started\n"
exit 2
}
}
if { ${ntests} == 0 } {
sus "System did not report plan; will look for summary at end"
} else {
sus "Expecting ${ntests} test results"
}
set timeout 60
set exitwith 0
set failures 0
for {set this 1} {${ntests} == 0 || ${this} <= ${ntests}} {incr this} {
expect {
-i ${victim} -re "${tpfx}#${toeol}" {
sud "Harness got comment: ${expect_out(buffer)}"
exp_continue
}
-i ${victim} -re "${tpfx}ok (\\d+)\\D${toeol}" {
sud "Harness acknowledge OK! ${this} ${expect_out(1,string)}"
set tid ${expect_out(1,string)}
if { ${tid} != "" && ${tid} != ${this} } {
sui "WARNING: Test reporting misaligned at ${this} (got ${tid})"
}
}
-i ${victim} -re "${tpfx}ok #${toeol}" {
sud "Harness acknowledge anonymous ok! ${this}"
}
-i ${victim} -re "${tpfx}not ok (\\d+)\\D${toeol}" {
sud "Failure in simulation after ${this} ${expect_out(1,string)}"
set tid ${expect_out(1,string)}
if { ${tid} != "" && ${tid} != ${this} } {
sui "WARNING: Test reporting misaligned at ${this}"
}
set exitwith [expr max(${exitwith},1)]
incr failures
}
-i ${victim} -re "${tpfx}not ok #${toeol}" {
sud "Failure (anonymous) in simulation after ${this}"
set exitwith [expr max(${exitwith},1)]
incr failures
}
-i ${victim} -re "${tpfx}Bail out!${toeol}" {
sus "Bail out after ${this} tests"
exit 2
}
-i ${victim} -re "${tpfx}POST 1\\.\\.(\\d+)(?=\r?\n)" {
# A post-factual plan; this must be the end of testing
global ntests
set ntests ${expect_out(1,string)}
if { ${ntests} != ${this} } {
sus "Postfix plan claimed ${ntests} but we saw ${this}"
set exitwith [expr max(${exitwith},2)]
incr failures
}
# break out of for loop
set this ${ntests}
}
-i ${victim} -re "${tpfx}${toeol}" {
sus "TAP line not understood!"
exit 2
}
# -i ${victim} -ex ${::expectnmcu::core::promptstr} {
# sus "Prompt while running tests!"
# exit 2
# }
-i ${victim} -re ${::expectnmcu::core::panicre} {
sus "Panic!"
exit 2
}
# Consume other outputs and discard as if they were comments
# This must come as the last pattern that looks at input
-re "(?p).${toeol}" { exp_continue }
timeout {
send_user "Failure: time out\n"
exit 2
}
}
}
# We think we're done running tests; send a final command for synchronization
send -i ${victim} "print(\"f\",\"i\",\"n\")\n"
expect -i ${victim} -re "print\\(\"f\",\"i\",\"n\"\\)\[\r\n\]+" { }
expect {
-i ${victim} -ex "f\ti\tn" { }
-i ${victim} -re "${tpfx}#${toeol}" {
sud "Harness got comment: ${expect_out(buffer)}"
exp_continue
}
-i ${victim} -re "${tpfx}Bail out!${toeol}" {
sus "Bail out after all tests finished"
exit 2
}
-i ${victim} -re "${tpfx}${toeol}" {
sus "Unexpected TAP output after tests finished"
exit 2
}
-i ${victim} -re ${::expectnmcu::core::panicre} {
sus "Panic!"
exit 2
}
-re "(?p).${toeol}" { exp_continue }
timeout {
send_user "Failure: time out\n"
exit 2
}
}
if { ${exitwith} == 0 } {
sus "All tests reported in OK"
} else {
sus "${failures} TEST FAILURES; REVIEW LOGS"
}
exit ${exitwith}
-- This is a NTest output handler that formats its output in a way that
-- resembles the Test Anything Protocol (though prefixed with "TAP: " so we can
-- more readily find it in comingled output streams).
local nrun
return function(e, test, msg, err)
msg = msg or ""
err = err or ""
if e == "pass" then
print(("\nTAP: ok %d %s # %s"):format(nrun, test, msg))
nrun = nrun + 1
elseif e == "fail" then
print(("\nTAP: not ok %d %s # %s: %s"):format(nrun, test, msg, err))
nrun = nrun + 1
elseif e == "except" then
print(("\nTAP: not ok %d %s # exn; %s: %s"):format(nrun, test, msg, err))
nrun = nrun + 1
elseif e == "abort" then
print(("\nTAP: Bail out! %d %s # exn; %s: %s"):format(nrun, test, msg, err))
elseif e == "start" then
-- We don't know how many tests we plan to run, so emit a comment instead
print(("\nTAP: # STARTUP %s"):format(test))
nrun = 1
elseif e == "finish" then
-- Ah, now, here we go; we know how many tests we ran, so signal completion
print(("\nTAP: POST 1..%d"):format(nrun))
elseif #msg ~= 0 or #err ~= 0 then
print(("\nTAP: # %s: %s: %s"):format(test, msg, err))
end
end
#!/bin/bash
# get all linked module docs for mkdocs.yml
grep "modules/" mkdocs.yml | sed "s/ *- .*: *'//" | sed "s/'//" | sort > /tmp/doc
# get all module and lua_module *.md files
find docs/modules/ docs/lua-modules/ -name "*.md" | sed "sxdocs/xx" | sort > /tmp/files
diff /tmp/doc /tmp/files && echo "all *.md files are reflected in mkdocs.yml"
stds.nodemcu_libs = {}
loadfile ("tools/luacheck_config.lua")(stds)
local empty = { }
stds.nodemcu_libs.read_globals.ok = empty
stds.nodemcu_libs.read_globals.nok = empty
stds.nodemcu_libs.read_globals.eq = empty
stds.nodemcu_libs.read_globals.fail = empty
stds.nodemcu_libs.read_globals.spy = empty
std = "lua51+lua53+nodemcu_libs"
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment