hephaestus/heph.nvim/tests/e2e/runner.lua

140 lines
4 KiB
Lua
Raw Normal View History

--- 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