193 lines
5.4 KiB
Lua
193 lines
5.4 KiB
Lua
if (...) ~= nil and (...).type == "group" then return end -- prevent recursive harness call
|
|
|
|
local ok = pcall(require, "lb:task")
|
|
if not ok then error("lua test harness requires 'lb:task'") end
|
|
local ok, time = pcall(require, "lb:time")
|
|
if not ok then error("lua test harness requires 'lb:time'") end
|
|
local ok, fs = pcall(require, "lb:fs")
|
|
if not ok then error("lua test harness requires 'lb:fs'") end
|
|
|
|
local global = _G
|
|
local color = {
|
|
reset = "\x1b[0m",
|
|
pass = "\x1b[32;1m", -- green
|
|
fail = "\x1b[31;1m", -- red
|
|
faint = "\x1b[2;39;49m", -- faint
|
|
}
|
|
|
|
local icon = {
|
|
check = "\u{2713}",
|
|
cross = "\u{00d7}",
|
|
chevron = "\u{203a}",
|
|
}
|
|
|
|
local function style(name, s)
|
|
return ("%s%s%s"):format(color[name], s, color.reset)
|
|
end
|
|
|
|
local function rjust(s, w)
|
|
if w == nil then w = 12 end
|
|
if #s >= w then return s end
|
|
return (" "):rep(w - #s) .. s
|
|
end
|
|
|
|
local function create_test(name, f, group)
|
|
local test = { type = "test", name = name or "", group = group, state = "pending", f = f }
|
|
local fenv = setmetatable({}, { __index = global, __newindex = global })
|
|
setfenv(f, fenv)
|
|
return test
|
|
end
|
|
|
|
local function create_group(name, f, parent)
|
|
local group = { type = "group", name = name or "", parent = parent, items = {} }
|
|
local fenv = setmetatable({
|
|
describe = function(name, f)
|
|
local item = create_group(name, f, group)
|
|
table.insert(group.items, item)
|
|
return item
|
|
end,
|
|
|
|
test = function(name, f)
|
|
local item = create_test(name, f, group)
|
|
table.insert(group.items, item)
|
|
return item
|
|
end,
|
|
}, { __index = global, __newindex = global })
|
|
|
|
setfenv(f, fenv)
|
|
f(group)
|
|
return group
|
|
end
|
|
|
|
local function name_test(test)
|
|
local name = test.name
|
|
local group = test.group
|
|
while group ~= nil do
|
|
if group.name ~= "" then name = ("%s %s %s"):format(group.name, icon.chevron, name) end
|
|
group = group.parent
|
|
end
|
|
return name
|
|
end
|
|
|
|
local function trace(msg)
|
|
return style("fail", msg) .. debug.traceback("", 2):sub(("\nstack traceback:"):len() + 1)
|
|
end
|
|
|
|
local function run_test(test)
|
|
local ok, trace = xpcall(test.f, trace, test)
|
|
if ok then
|
|
test.state = "pass"
|
|
print(("%s %s"):format(style("pass", rjust("PASS")), name_test(test)))
|
|
else
|
|
test.state = "fail"
|
|
print(("%s %s\n\n%s\n"):format(style("fail", rjust("!!! FAIL")), name_test(test), trace))
|
|
end
|
|
collectgarbage() -- gc after each test to test destructors
|
|
return test
|
|
end
|
|
|
|
local function start(cx, item)
|
|
if item.type == "test" then
|
|
table.insert(cx.tasks, spawn(run_test, item))
|
|
elseif item.type == "group" then
|
|
for _, item in ipairs(item.items) do
|
|
start(cx, item)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function check_refs()
|
|
-- ensure all refs were properly unref'ed
|
|
local registry = debug.getregistry()
|
|
local count = #registry
|
|
local ref = 0 -- FREELIST_REF
|
|
while type(registry[ref]) == "number" do
|
|
local next = registry[ref]
|
|
registry[ref], ref = nil, next
|
|
end
|
|
for i = 1, count do
|
|
local value = registry[i]
|
|
if type(value) ~= "thread" then -- ignore threads pinned by the runtime
|
|
assert(rawequal(registry[i], nil), ("ref %d not unref'ed: %s"):format(i, registry[i]))
|
|
end
|
|
end
|
|
end
|
|
|
|
local function main(item)
|
|
local cx = { tasks = {} }
|
|
local time, pass, fail = time.instant(), 0, 0
|
|
start(cx, item)
|
|
for _, task in ipairs(cx.tasks) do
|
|
if task:await().state == "pass" then
|
|
pass = pass + 1
|
|
else
|
|
fail = fail + 1
|
|
end
|
|
end
|
|
local elapsed = time:elapsed_secs()
|
|
local retcode
|
|
if fail == 0 then
|
|
print(style("pass", ("\t%s %d tests passed"):format(icon.check, pass)))
|
|
retcode = 0
|
|
else
|
|
print(
|
|
("\t%s, %s"):format(
|
|
style("pass", ("%s %d tests passed"):format(icon.check, pass)),
|
|
style("fail", ("%s %d tests failed"):format(icon.cross, fail))
|
|
)
|
|
)
|
|
retcode = 1
|
|
end
|
|
if elapsed < 1000 then
|
|
print(style("faint", ("\t%s completed in %.2f ms"):format(icon.chevron, elapsed * 1000)))
|
|
else
|
|
print(style("faint", ("\t%s completed in %.2f s"):format(icon.chevron, elapsed)))
|
|
end
|
|
cx = nil
|
|
collectgarbage()
|
|
check_refs() -- check that all refs were properly unref'ed in destructors
|
|
return retcode -- report error to cargo
|
|
end
|
|
|
|
return main(create_group("", function()
|
|
local function include(path, pat)
|
|
for entry in fs.glob_dir(path, pat) do
|
|
local path = entry:path()
|
|
local f, err = loadfile(path)
|
|
if not f then error(err) end
|
|
describe(path, f)
|
|
end
|
|
end
|
|
|
|
local function include_doctest(path, pat)
|
|
for entry in fs.glob_dir(path, pat) do
|
|
local line, doctest = 0, nil
|
|
for s in fs.read(entry:path()):gmatch("([^\n]*)\n?") do
|
|
line = line + 1
|
|
local prefix = s:match("^%s*///")
|
|
s = prefix and s:sub(#prefix + 1)
|
|
if s and not s:match("^%s*```%s*$") then
|
|
if s:match("^%s*```lua$") then
|
|
doctest = { line = line, col = #prefix + 2 }
|
|
elseif doctest then
|
|
table.insert(doctest, (" "):rep(#prefix) .. s)
|
|
end
|
|
else
|
|
if doctest then
|
|
local name = ("%s:%d:%d"):format(entry:path(), doctest.line, doctest.col)
|
|
local f, err = loadstring(table.concat(doctest, "\n"), "@" .. name)
|
|
if not f then error(err) end
|
|
test(("%s %s"):format(name, style("faint", "(doctest)")), f)
|
|
end
|
|
doctest = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
include("tests", "**/*.lua")
|
|
include("crates", "*/tests/**/*.lua")
|
|
include_doctest("src", "**/*.rs")
|
|
include_doctest("crates", "*/src/**/*.rs")
|
|
end))
|