generated from eblume/project-template
140 lines
4 KiB
Lua
140 lines
4 KiB
Lua
|
|
--- A tiny, dependency-free busted-compatible test runner (tech-spec §9 sanctions
|
||
|
|
--- "drive nvim ... from the test runner"). Provides the `describe`/`it`/
|
||
|
|
--- `before_each`/`after_each` globals and a luassert-style `assert` table, then
|
||
|
|
--- runs `*_spec.lua` files in headless Neovim with proper exit codes — no
|
||
|
|
--- external plugins, no network, nothing vendored.
|
||
|
|
|
||
|
|
local M = {}
|
||
|
|
|
||
|
|
-- Registration state (module-local; the installed globals close over these).
|
||
|
|
local cases = {}
|
||
|
|
local scope_stack = {}
|
||
|
|
|
||
|
|
local function full_name(leaf)
|
||
|
|
local parts = {}
|
||
|
|
for _, s in ipairs(scope_stack) do
|
||
|
|
if s.name then
|
||
|
|
parts[#parts + 1] = s.name
|
||
|
|
end
|
||
|
|
end
|
||
|
|
parts[#parts + 1] = leaf
|
||
|
|
return table.concat(parts, " ")
|
||
|
|
end
|
||
|
|
|
||
|
|
--- Install the spec globals. `assert` becomes a callable luassert-ish table:
|
||
|
|
--- `assert(cond, msg)` still works (so harness code using the builtin is fine),
|
||
|
|
--- plus `assert.are.equal`, `assert.is_true/is_false/is_truthy/is_falsy`.
|
||
|
|
function M.install_globals()
|
||
|
|
_G.describe = function(name, fn)
|
||
|
|
table.insert(scope_stack, { name = name, befores = {}, afters = {} })
|
||
|
|
fn()
|
||
|
|
table.remove(scope_stack)
|
||
|
|
end
|
||
|
|
_G.before_each = function(fn)
|
||
|
|
table.insert(scope_stack[#scope_stack].befores, fn)
|
||
|
|
end
|
||
|
|
_G.after_each = function(fn)
|
||
|
|
table.insert(scope_stack[#scope_stack].afters, fn)
|
||
|
|
end
|
||
|
|
_G.it = function(name, fn)
|
||
|
|
-- Snapshot the active before/after chain (outermost-first for befores,
|
||
|
|
-- innermost-first for afters), as busted runs them per-test.
|
||
|
|
local befores, afters = {}, {}
|
||
|
|
for _, s in ipairs(scope_stack) do
|
||
|
|
for _, b in ipairs(s.befores) do
|
||
|
|
befores[#befores + 1] = b
|
||
|
|
end
|
||
|
|
end
|
||
|
|
for i = #scope_stack, 1, -1 do
|
||
|
|
for _, a in ipairs(scope_stack[i].afters) do
|
||
|
|
afters[#afters + 1] = a
|
||
|
|
end
|
||
|
|
end
|
||
|
|
table.insert(cases, { name = full_name(name), fn = fn, befores = befores, afters = afters })
|
||
|
|
end
|
||
|
|
|
||
|
|
local function fail(msg, level)
|
||
|
|
error(msg, (level or 1) + 1)
|
||
|
|
end
|
||
|
|
local A = setmetatable({}, {
|
||
|
|
__call = function(_, cond, msg)
|
||
|
|
if not cond then
|
||
|
|
fail(msg or "assertion failed")
|
||
|
|
end
|
||
|
|
return cond
|
||
|
|
end,
|
||
|
|
})
|
||
|
|
local function eq(a, b, msg)
|
||
|
|
if a ~= b then
|
||
|
|
fail(msg or string.format("expected %s, got %s", vim.inspect(b), vim.inspect(a)))
|
||
|
|
end
|
||
|
|
end
|
||
|
|
A.are = { equal = eq, equals = eq }
|
||
|
|
A.equals = eq
|
||
|
|
A.equal = eq
|
||
|
|
A.is_true = function(x, msg)
|
||
|
|
if x ~= true then
|
||
|
|
fail(msg or ("expected true, got " .. vim.inspect(x)))
|
||
|
|
end
|
||
|
|
end
|
||
|
|
A.is_false = function(x, msg)
|
||
|
|
if x ~= false then
|
||
|
|
fail(msg or ("expected false, got " .. vim.inspect(x)))
|
||
|
|
end
|
||
|
|
end
|
||
|
|
A.is_truthy = function(x, msg)
|
||
|
|
if not x then
|
||
|
|
fail(msg or ("expected truthy, got " .. vim.inspect(x)))
|
||
|
|
end
|
||
|
|
end
|
||
|
|
A.is_falsy = function(x, msg)
|
||
|
|
if x then
|
||
|
|
fail(msg or ("expected falsy, got " .. vim.inspect(x)))
|
||
|
|
end
|
||
|
|
end
|
||
|
|
_G.assert = A
|
||
|
|
end
|
||
|
|
|
||
|
|
--- Run each spec file, returning the number of failed tests. Prints TAP-ish
|
||
|
|
--- lines so failures are obvious in CI logs.
|
||
|
|
function M.run_files(files)
|
||
|
|
local passed, failed = 0, 0
|
||
|
|
for _, file in ipairs(files) do
|
||
|
|
cases = {}
|
||
|
|
scope_stack = {}
|
||
|
|
local loaded, lerr = pcall(dofile, file)
|
||
|
|
if not loaded then
|
||
|
|
failed = failed + 1
|
||
|
|
print("not ok - load " .. file)
|
||
|
|
print(" " .. tostring(lerr):gsub("\n", "\n "))
|
||
|
|
end
|
||
|
|
for _, c in ipairs(cases) do
|
||
|
|
local ok, err = true, nil
|
||
|
|
for _, b in ipairs(c.befores) do
|
||
|
|
local bok, berr = pcall(b)
|
||
|
|
if not bok then
|
||
|
|
ok, err = false, berr
|
||
|
|
break
|
||
|
|
end
|
||
|
|
end
|
||
|
|
if ok then
|
||
|
|
ok, err = pcall(c.fn)
|
||
|
|
end
|
||
|
|
for _, a in ipairs(c.afters) do
|
||
|
|
pcall(a) -- teardown always runs, even after a failure
|
||
|
|
end
|
||
|
|
if ok then
|
||
|
|
passed = passed + 1
|
||
|
|
print("ok - " .. c.name)
|
||
|
|
else
|
||
|
|
failed = failed + 1
|
||
|
|
print("not ok - " .. c.name)
|
||
|
|
print(" " .. tostring(err):gsub("\n", "\n "))
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
print(string.format("\n%d passed, %d failed", passed, failed))
|
||
|
|
return failed
|
||
|
|
end
|
||
|
|
|
||
|
|
return M
|