15383b8cce
A runaway test spins at full CPU instead of failing, stalling the whole run. A debug hook enforces a per-test deadline; the JIT stays off so the hook is reachable from compiled code.
85 lines
2.1 KiB
Lua
85 lines
2.1 KiB
Lua
local M = { passed = 0, failed = 0, failures = {} }
|
|
|
|
local uv = vim.uv or vim.loop
|
|
local current = '?'
|
|
|
|
M.timeout_ms = tonumber(vim.env.ARTI_PACK_TEST_TIMEOUT_MS) or 5000
|
|
|
|
-- LuaJIT never reaches a debug hook from inside a compiled trace, so the
|
|
-- interpreter has to stay in charge for the budget below to be enforceable.
|
|
if _G.jit then
|
|
_G.jit.off()
|
|
_G.jit.flush()
|
|
end
|
|
|
|
local HOOK_INTERVAL = 20000
|
|
|
|
local function bounded(fn)
|
|
local deadline = uv.hrtime() + M.timeout_ms * 1e6
|
|
debug.sethook(function()
|
|
if uv.hrtime() > deadline then
|
|
debug.sethook()
|
|
error(string.format('timed out after %dms', M.timeout_ms), 0)
|
|
end
|
|
end, '', HOOK_INTERVAL)
|
|
local ok, err = pcall(fn)
|
|
debug.sethook()
|
|
return ok, err
|
|
end
|
|
|
|
function M.describe(name, fn)
|
|
current = name
|
|
fn()
|
|
end
|
|
|
|
function M.it(name, fn)
|
|
local ok, err = bounded(fn)
|
|
if ok then
|
|
M.passed = M.passed + 1
|
|
else
|
|
M.failed = M.failed + 1
|
|
table.insert(M.failures, string.format('%s > %s\n %s', current, name, tostring(err)))
|
|
end
|
|
end
|
|
|
|
local function render(v)
|
|
if type(v) == 'table' then
|
|
return vim.inspect(v)
|
|
end
|
|
return string.format('%q', tostring(v))
|
|
end
|
|
|
|
function M.eq(expected, actual, msg)
|
|
if not vim.deep_equal(expected, actual) then
|
|
error(string.format('%sexpected %s, got %s', msg and (msg .. ': ') or '', render(expected), render(actual)), 2)
|
|
end
|
|
end
|
|
|
|
function M.truthy(value, msg)
|
|
if not value then
|
|
error((msg or 'expected a truthy value') .. ', got ' .. render(value), 2)
|
|
end
|
|
end
|
|
|
|
function M.falsy(value, msg)
|
|
if value then
|
|
error((msg or 'expected a falsy value') .. ', got ' .. render(value), 2)
|
|
end
|
|
end
|
|
|
|
function M.matches(pattern, actual, msg)
|
|
if type(actual) ~= 'string' or not actual:find(pattern) then
|
|
error(string.format('%sexpected %s to match %q', msg and (msg .. ': ') or '', render(actual), pattern), 2)
|
|
end
|
|
end
|
|
|
|
function M.report()
|
|
for _, failure in ipairs(M.failures) do
|
|
io.stderr:write('FAIL ' .. failure .. '\n')
|
|
end
|
|
io.stdout:write(string.format('\n%d passed, %d failed\n', M.passed, M.failed))
|
|
return M.failed == 0
|
|
end
|
|
|
|
return M
|