test: replace busted with local harness

Replace the busted-based Lua test runner with a repo-local harness.

The new harness runs spec files directly under `nvim -ll`, ships its own
reporter and lightweight `luassert` shim, and keeps the helper/preload
flow used by the functional and unit test suites.

Keep the file boundary model shallow and busted-like by restoring `_G`,
`package.loaded`, `package.preload`, `arg`, and the process environment
between files, without carrying extra reset APIs or custom assertion
machinery.

Update the build and test entrypoints to use the new runner, add
black-box coverage for the harness itself, and drop the bundled
busted/luacheck dependency path.

AI-assisted: Codex
This commit is contained in:
Lewis Russell
2026-03-25 13:33:17 +00:00
parent e289f9579c
commit 55f9c2136e
45 changed files with 3899 additions and 659 deletions

View File

@@ -1,10 +1,9 @@
local luaassert = require('luassert')
local busted = require('busted')
local test_assert = require('test.assert')
---@type test.harness
local harness = require('test.harness')
local uv = vim.uv
local Paths = require('test.cmakeconfig.paths')
luaassert:set_parameter('TableFormatLevel', 100)
--- Functions executing in the context of the test runner (not the current nvim test session).
--- @class test.testutil
local M = {
@@ -86,7 +85,7 @@ function M.retry(max, max_ms, fn)
end
uv.update_time() -- Update cached value of luv.now() (libuv: uv_now()).
if (max and tries >= max) or (uv.now() - start_time > timeout) then
busted.fail(string.format('retry() attempts: %d\n%s', tries, tostring(result)), 2)
error(string.format('retry() attempts: %d\n%s', tries, tostring(result)), 2)
end
tries = tries + 1
uv.sleep(20) -- Avoid hot loop...
@@ -100,10 +99,10 @@ local check_logs_useless_lines = {
}
function M.eq(expected, actual, context)
return luaassert.are.same(expected, actual, context)
return test_assert.eq(expected, actual, context)
end
function M.neq(expected, actual, context)
return luaassert.are_not.same(expected, actual, context)
return test_assert.neq(expected, actual, context)
end
--- Compare paths after resolving symlinks with realpath.
@@ -125,15 +124,6 @@ function M.ok(cond, expected, actual)
return assert(cond, msg)
end
local function epicfail(state, arguments, _)
state.failure_message = arguments[1]
return false
end
luaassert:register('assertion', 'epicfail', epicfail)
function M.fail(msg)
return luaassert.epicfail(msg)
end
--- @param pat string
--- @param actual string
--- @return boolean
@@ -365,7 +355,7 @@ function M.check_logs()
end
end
end
luaassert(
test_assert(
0 == #runtime_errors,
string.format('Found runtime errors in logfile(s): %s', table.concat(runtime_errors, ', '))
)
@@ -789,34 +779,15 @@ end
--- @param name? 'cirrus'|'github'
--- @return boolean
function M.is_ci(name)
local any = (name == nil)
assert(any or name == 'github' or name == 'cirrus')
local gh = ((any or name == 'github') and nil ~= os.getenv('GITHUB_ACTIONS'))
local cirrus = ((any or name == 'cirrus') and nil ~= os.getenv('CIRRUS_CI'))
return gh or cirrus
return harness.is_ci(name)
end
-- Gets the (tail) contents of `logfile`.
-- Also moves the file to "${NVIM_LOG_FILE}.displayed" on CI environments.
function M.read_nvim_log(logfile, ci_rename)
logfile = logfile or os.getenv('NVIM_LOG_FILE') or 'nvim.log'
assert(uv.fs_stat(logfile), ('logfile not found: %q'):format(logfile))
local is_ci = M.is_ci()
local keep = is_ci and 100 or 10
local lines = M.read_file_list(logfile, -keep) or {}
local log = (
('-'):rep(78)
.. '\n'
.. string.format('$NVIM_LOG_FILE: %s\n', logfile)
.. (#lines > 0 and '(last ' .. tostring(keep) .. ' lines)\n' or '(empty)\n')
)
for _, line in ipairs(lines) do
log = log .. line .. '\n'
end
log = log .. ('-'):rep(78) .. '\n'
if is_ci and ci_rename then
os.rename(logfile, logfile .. '.displayed')
end
local log = harness.read_nvim_log(logfile, ci_rename)
assert(log, ('logfile not found: %q'):format(logfile))
return log
end
@@ -843,7 +814,7 @@ function M.expect_events(expected, received, kind)
for _, e in ipairs(expected) do
msg = msg .. ' ' .. vim.inspect(e) .. ';\n'
end
M.fail(msg)
error(msg, 2)
end
return received
end