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
-4
View File
@@ -74,10 +74,6 @@ jobs:
name: luals
run: cmake --build build --target luals
- if: success() || failure() && steps.abort_job.outputs.status == 'success'
name: luacheck
run: cmake --build build --target lintlua-luacheck
- if: success() || failure() && steps.abort_job.outputs.status == 'success'
name: lintsh
run: cmake --build build --target lintsh
-3
View File
@@ -4,9 +4,6 @@
"version": "LuaJIT"
},
"workspace": {
"library": [
"${3rd}/busted/library"
],
"ignoreDir": [
".deps",
"build",
+1 -1
View File
@@ -569,7 +569,7 @@ Note that the C++ compiler is explicitly set so that it can be found when the de
See "Available System Integrations" in `zig build -h` to see available system integrations. Enabling an integration, e.g. `zig build -fsys=utf8proc` will use the system's installation of utf8proc.
`zig build --system deps_dir` will enable all integrations and turn off dependency fetching. This requires you to pre-download the dependencies which don't have a system integration into `deps_dir` (at the time of writing these are ziglua, [`lua_dev_deps`](https://github.com/neovim/deps/blob/master/opt/lua-dev-deps.tar.gz), and the built-in tree-sitter parsers). You have to create subdirectories whose names are the respective package's hash under `deps_dir` and unpack the dependencies inside that directory - ziglua should go under `deps_dir/zlua-0.1.0-hGRpC1dCBQDf-IqqUifYvyr8B9-4FlYXqY8cl7HIetrC` and so on. Hashes should be taken from `build.zig.zon`.
`zig build --system deps_dir` will enable all integrations and turn off dependency fetching. This requires you to pre-download the dependencies which don't have a system integration into `deps_dir` (at the time of writing these are ziglua and the built-in tree-sitter parsers). You have to create subdirectories whose names are the respective package's hash under `deps_dir` and unpack the dependencies inside that directory - ziglua should go under `deps_dir/zlua-0.1.0-hGRpC1dCBQDf-IqqUifYvyr8B9-4FlYXqY8cl7HIetrC` and so on. Hashes should be taken from `build.zig.zon`.
See the `prepare` function of [this `PKGBUILD`](https://git.sr.ht/~chinmay/nvim_build/tree/26364a4cf9b4819f52a3e785fa5a43285fb9cea2/item/PKGBUILD#L90) for an example.
+1 -25
View File
@@ -243,15 +243,6 @@ mark_as_advanced(TS_QUERY_LS_PRG)
set(STYLUA_DIRS runtime scripts src test contrib)
add_glob_target(
TARGET lintlua-luacheck
COMMAND $<TARGET_FILE:nvim_bin>
FLAGS -ll ${PROJECT_SOURCE_DIR}/test/lua_runner.lua ${CMAKE_BINARY_DIR}/usr/share/lua/5.1 luacheck -q
GLOB_DIRS runtime scripts src test
GLOB_PAT *.lua
TOUCH_STRATEGY PER_DIR)
add_dependencies(lintlua-luacheck lua_dev_deps)
add_glob_target(
TARGET lintlua-stylua
COMMAND ${STYLUA_PRG}
@@ -271,7 +262,7 @@ add_custom_target(lintlua-stylua2
add_dependencies(lintlua-stylua lintlua-stylua2)
add_custom_target(lintlua)
add_dependencies(lintlua lintlua-luacheck lintlua-stylua)
add_dependencies(lintlua lintlua-stylua)
add_glob_target(
TARGET lintsh
@@ -355,21 +346,6 @@ ExternalProject_Add(uncrustify
EXCLUDE_FROM_ALL TRUE
${EXTERNALPROJECT_OPTIONS})
option(USE_BUNDLED_BUSTED "Use bundled busted" ON)
if(USE_BUNDLED_BUSTED)
get_externalproject_options(lua_dev_deps ${DEPS_IGNORE_SHA})
ExternalProject_Add(lua_dev_deps
DOWNLOAD_DIR ${DEPS_DOWNLOAD_DIR}/lua_dev_deps
SOURCE_DIR ${DEPS_SHARE_DIR}
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
EXCLUDE_FROM_ALL TRUE
${EXTERNALPROJECT_OPTIONS})
else()
add_custom_target(lua_dev_deps)
endif()
if (CMAKE_SYSTEM_PROCESSOR MATCHES "arm|aarch")
set(LUALS_ARCH arm64)
else()
+1 -4
View File
@@ -545,8 +545,6 @@ pub fn build(b: *std.Build) !void {
const gen_runtime = try runtime.nvim_gen_runtime(b, nlua0, funcs_data);
const lua_dev_deps = b.dependency("lua_dev_deps", .{});
const test_deps = b.step("test_deps", "test prerequisites");
test_deps.dependOn(&nvim_exe_install.step);
// running tests doesn't require copying the static runtime, only the generated stuff
@@ -673,7 +671,6 @@ pub fn build(b: *std.Build) !void {
b,
nvim_exe,
test_deps,
lua_dev_deps.path("."),
test_config_step.getDirectory(),
unit_headers,
);
@@ -804,7 +801,7 @@ pub fn test_config(b: *std.Build) ![]u8 {
\\M.test_source_path = "{[src_path]s}"
\\M.test_lua_prg = ""
\\M.test_luajit_prg = ""
\\ -- include path passed on the cmdline, see test/lua_runner.lua
\\ -- include path passed on the cmdline, see test/runner.lua
\\M.include_paths = _G.c_include_path or {{}}
\\
\\return M
-4
View File
@@ -41,10 +41,6 @@
.hash = "libiconv-1.18.0-p9sJwWnqAACzVYeWgXB5r5lOQ74XwTPlptixV0JPRO28",
.lazy = true,
},
.lua_dev_deps = .{
.url = "https://github.com/neovim/deps/raw/06ef2b58b0876f8de1a3f5a710473dcd7afff251/opt/lua-dev-deps.tar.gz",
.hash = "N-V-__8AAGevEQCHAkCozca5AIdN9DFc3Luf3g3r2AcbyOrm",
},
.treesitter_c = .{
.url = "git+https://github.com/tree-sitter/tree-sitter-c?ref=v0.24.1#7fa1be1b694b6e763686793d97da01f36a0e5c12",
.hash = "N-V-__8AANxPSABzw3WBTSH_YkwaGAfrK6PBqAMqQedkDDim",
-3
View File
@@ -51,6 +51,3 @@ WASMTIME_SHA256 c4a3c596a07c02ba6adce503154a2095fd98037a1e50d56add9773f0269ec9b7
UNCRUSTIFY_URL https://github.com/uncrustify/uncrustify/archive/uncrustify-0.82.0.tar.gz
UNCRUSTIFY_SHA256 e05f8d5ee36aaa1acfa032fe97546b7be46b1f4620e7c38037f8a42e25fe676f
# This is where we get busted, luassert, ...
LUA_DEV_DEPS_URL https://github.com/neovim/deps/raw/06ef2b58b0876f8de1a3f5a710473dcd7afff251/opt/lua-dev-deps.tar.gz
LUA_DEV_DEPS_SHA256 49f8399e453103064a23c65534f266f3067cda716b6502f016bfafeed5799354
-1
View File
@@ -1,7 +1,6 @@
set(DEPS_INSTALL_DIR "${CMAKE_BINARY_DIR}/usr")
set(DEPS_BIN_DIR "${DEPS_INSTALL_DIR}/bin")
set(DEPS_LIB_DIR "${DEPS_INSTALL_DIR}/lib")
set(DEPS_SHARE_DIR "${DEPS_INSTALL_DIR}/share/lua/5.1")
set(DEPS_BUILD_DIR "${CMAKE_BINARY_DIR}/build")
set(DEPS_DOWNLOAD_DIR "${DEPS_BUILD_DIR}/downloads")
+8 -8
View File
@@ -60,18 +60,18 @@ if(IS_ABSOLUTE ${TEST_PATH})
file(RELATIVE_PATH TEST_PATH "${ROOT_DIR}" "${TEST_PATH}")
endif()
separate_arguments(BUSTED_ARGS NATIVE_COMMAND $ENV{BUSTED_ARGS})
separate_arguments(TEST_ARGS NATIVE_COMMAND $ENV{TEST_ARGS})
if(DEFINED ENV{TEST_TAG} AND NOT "$ENV{TEST_TAG}" STREQUAL "")
list(APPEND BUSTED_ARGS --tags $ENV{TEST_TAG})
list(APPEND TEST_ARGS --tags $ENV{TEST_TAG})
endif()
if(DEFINED ENV{TEST_FILTER} AND NOT "$ENV{TEST_FILTER}" STREQUAL "")
list(APPEND BUSTED_ARGS --filter $ENV{TEST_FILTER})
list(APPEND TEST_ARGS --filter $ENV{TEST_FILTER})
endif()
if(DEFINED ENV{TEST_FILTER_OUT} AND NOT "$ENV{TEST_FILTER_OUT}" STREQUAL "")
list(APPEND BUSTED_ARGS --filter-out $ENV{TEST_FILTER_OUT})
list(APPEND TEST_ARGS --filter-out $ENV{TEST_FILTER_OUT})
endif()
# TMPDIR: for testutil.tmpname() and Nvim tempname().
@@ -95,14 +95,14 @@ endif()
execute_process(
# Note: because of "-ll" (low-level interpreter mode), some modules like
# _core/editor.lua are not loaded.
COMMAND ${NVIM_PRG} -ll ${ROOT_DIR}/test/lua_runner.lua ${DEPS_INSTALL_DIR}/share/lua/5.1/ busted -v -o test.busted.outputHandlers.nvim
-Xoutput "{\"test_path\": \"${TEST_PATH}\", \"summary_file\": \"${TEST_SUMMARY_FILE}\"}"
--lazy --helper=${TEST_DIR}/${TEST_TYPE}/preload.lua
COMMAND ${NVIM_PRG} -ll ${ROOT_DIR}/test/runner.lua -v
--summary-file=${TEST_SUMMARY_FILE}
--helper=${TEST_DIR}/${TEST_TYPE}/preload.lua
--lpath=${BUILD_DIR}/?.lua
--lpath=${ROOT_DIR}/src/?.lua
--lpath=${ROOT_DIR}/runtime/lua/?.lua
--lpath=?.lua
${BUSTED_ARGS}
${TEST_ARGS}
${TEST_PATH}
TIMEOUT $ENV{TEST_TIMEOUT}
WORKING_DIRECTORY ${TEST_XDG_PREFIX}
-1
View File
@@ -41,7 +41,6 @@
# Uncomment these entries to instead use system-wide installations of
# them.
#
# DEPS_CMAKE_FLAGS += -DUSE_BUNDLED_BUSTED=OFF
# DEPS_CMAKE_FLAGS += -DUSE_BUNDLED_GETTEXT=OFF
# DEPS_CMAKE_FLAGS += -DUSE_BUNDLED_LIBICONV=OFF
# DEPS_CMAKE_FLAGS += -DUSE_BUNDLED_LIBUV=OFF
+25 -13
View File
@@ -37,12 +37,25 @@ You can learn [Lua concepts 15 minutes](https://learnxinyminutes.com/docs/lua/),
see also |lua-guide|. Use any existing test as a template to start writing new
tests, or see |dev-quickstart|.
Tests are run by the `/cmake/RunTests.cmake` script using `busted` (a Lua test-runner).
Tests are run by the `/cmake/RunTests.cmake` script using the repo-local Lua
test harness in `/test/harness.lua`.
For some failures, `./build/nvim.log` (or `$NVIM_LOG_FILE`) may provide insight.
Depending on the presence of binaries (e.g., `xclip`) some tests will be
skipped.
Harness isolation *dev-test-harness-isolation*
The Lua harness runs all selected spec files in one low-level `nvim -ll`
process, then restores a baseline before each file. For each suite iteration,
the file boundary baseline is captured after `--helper` is loaded, so
helper-provided modules and defaults persist across files.
Helper files are preload-only: they may require modules, set defaults, and
register suite-end cleanup, but they do not define tests or hooks.
The harness restores `_G`, `package.loaded`, `package.preload`, `arg`, and the
process environment with the same shallow file boundary model that busted uses.
==============================================================================
Test Layout
@@ -56,7 +69,8 @@ Test Layout
- `/test/includes` : include-files for use by luajit `ffi.cdef` C definitions
parser: normally used to make macros not accessible via this mechanism
accessible the other way.
- `/test/*/preload.lua` : modules preloaded by busted `--helper` option
- `/test/*/preload.lua` : modules and defaults preloaded by the harness
`--helper` option
- `/test/**/testutil.lua` : common utility functions in the context of the test
runner
- `/test/**/testnvim.lua` : common utility functions in the context of the
@@ -145,9 +159,7 @@ need to install and configure:
2. [local-lua-debugger-vscode](https://github.com/mfussenegger/nvim-dap/wiki/Debug-Adapter-installation#local-lua-debugger-vscode)
3. [nlua](https://github.com/mfussenegger/nlua)
4. [one-small-step-for-vimkind](https://github.com/jbyuki/one-small-step-for-vimkind) (called `osv`)
5. A `nbusted` command in `$PATH`. This command can be a copy of `busted` with
`exec '/usr/bin/lua5.1'"` replaced with `"exec '/usr/bin/nlua'"` (or the
path to your `nlua`)
5. An `nlua` command in `$PATH`.
The setup roughly looks like this: >
@@ -163,7 +175,7 @@ The setup roughly looks like this: >
│ │
▼ │
┌─────────┐ │
nbusted │ │
nlua │ │
└─────────┘ │
│ │
▼ │
@@ -206,11 +218,11 @@ With these installed you can use a configuration like this: >
request = "launch",
cwd = "${workspaceFolder}",
program = {
command = "nbusted",
command = "nlua",
},
args = {
"--ignore-lua",
"--lazy",
"-ll",
"test/runner.lua",
"--helper=test/functional/preload.lua",
"--lpath=build/?.lua",
"--lpath=?.lua",
@@ -274,7 +286,7 @@ Limitations:
if os.getenv("LOCAL_LUA_DEBUGGER_VSCODE") == "1" then
require("lldebugger").start()
end
< This is a [local-lua-debugger limitation](https://github.com/tomblind/local-lua-debugger-vscode?tab=readme-ov-file#busted)
< This is a local-lua-debugger limitation.
- You cannot step into code of files which get baked into the nvim binary
(such as `_core/*.lua` and `inspect.lua`).
@@ -314,10 +326,10 @@ or >
cmake -E env "TEST_FILE=test/functional/foo.lua" cmake --build build --target functionaltest
To _repeat_ a test: >
BUSTED_ARGS="--repeat=100 --no-keep-going" TEST_FILE=test/functional/foo_spec.lua make functionaltest
TEST_ARGS="--repeat=100 --no-keep-going" TEST_FILE=test/functional/foo_spec.lua make functionaltest
or >
cmake -E env "TEST_FILE=test/functional/foo_spec.lua" cmake -E env BUSTED_ARGS="--repeat=100 --no-keep-going" cmake --build build --target functionaltest
cmake -E env "TEST_FILE=test/functional/foo_spec.lua" cmake -E env TEST_ARGS="--repeat=100 --no-keep-going" cmake --build build --target functionaltest
FILTER BY TAG
@@ -430,7 +442,7 @@ Test behaviour is affected by environment variables. Currently supported
treated as Integer; when defined, treated as String; when defined, treated as
Number; !must be defined to function properly):
- `BUSTED_ARGS` (F) (U): arguments forwarded to `busted`.
- `TEST_ARGS` (F) (U): arguments forwarded to the local test harness.
- `CC` (U) (S): specifies which C compiler to use to preprocess files.
Currently only compilers with gcc-compatible arguments are supported.
+2 -2
View File
@@ -388,7 +388,7 @@ Gdb *dev-tools-gdb*
USING GDB TO STEP THROUGH FUNCTIONAL TESTS
Use `TEST_TAG` to run tests matching busted tags (of the form `#foo` e.g.
Use `TEST_TAG` to run tests matching `#foo` tags (e.g.
`it("test #foo ...", ...)`):
>bash
GDB=1 TEST_TAG=foo make functionaltest
@@ -403,7 +403,7 @@ Then, in another terminal:
USING LLDB TO STEP THROUGH UNIT TESTS
>
lldb .deps/usr/bin/luajit -- .deps/usr/bin/busted --lpath="./build/?.lua" test/unit/
lldb build/bin/nvim -- -ll test/runner.lua --lpath=./build/?.lua test/unit/
<
USING GDB
+27
View File
@@ -64,6 +64,31 @@ function vim.deepcopy(orig, noref)
return deepcopy(orig, not noref and {} or nil)
end
--- Returns a shallow copy of `orig`.
---
--- Non-table values are returned as-is. Table keys and values are copied by
--- reference, and the original metatable is preserved. Use |vim.deepcopy()|
--- for a recursive copy.
---
--- @nodoc
--- @generic T
--- @param orig T
--- @return T
function vim._copy(orig)
if type(orig) ~= 'table' then
return orig
end
--- @cast orig table<any,any>
local copy = {} --- @type table<any,any>
for k, v in pairs(orig) do
copy[k] = v
end
return setmetatable(copy, getmetatable(orig))
end
--- @class vim.gsplit.Opts
--- @inlinedoc
---
@@ -678,6 +703,8 @@ local function deep_equal(left, right, seen)
return false
end
---@cast left table<any, any>
---@cast right table<any, any>
seen = seen or {}
local seen_left = seen[left]
if seen_left and seen_left[right] ~= nil then
+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json",
"format": {
"externalTool": {
"program": "stylua",
"args": [
"-",
"--stdin-filepath",
"${file}"
]
}
},
"diagnostics": {
"disable": [
"unnecessary-if"
]
},
"codeAction": {
"insertSpace": true
},
"strict": {
"typeCall": true,
"arrayIndex": true
}
}
+1 -6
View File
@@ -6,12 +6,7 @@
"workspace": {
"library": [
"../runtime/lua",
"../src",
"../build/usr/share/lua/5.1",
"../build",
"${3rd}/busted/library",
"${3rd}/luassert/library",
"${3rd}/luv/library"
"../src"
],
"checkThirdParty": "Disable"
},
+4 -5
View File
@@ -7,7 +7,6 @@ set(TEST_OPTIONS
-D BUILD_DIR=${CMAKE_BINARY_DIR}
-D CIRRUS_CI=$ENV{CIRRUS_CI}
-D CI_BUILD=${CI_BUILD}
-D DEPS_INSTALL_DIR=${DEPS_INSTALL_DIR}
-D NVIM_PRG=$<TARGET_FILE:nvim_bin>
-D TEST_DIR=${TEST_DIR}
-D ROOT_DIR=${PROJECT_SOURCE_DIR})
@@ -20,7 +19,7 @@ if(LUA_HAS_FFI)
${TEST_OPTIONS}
-P ${PROJECT_SOURCE_DIR}/cmake/RunTests.cmake
USES_TERMINAL)
add_dependencies(unittest lua_dev_deps nvim)
add_dependencies(unittest nvim)
else()
message(WARNING "disabling unit tests: no Luajit FFI in ${LUA_PRG}")
endif()
@@ -36,7 +35,7 @@ add_custom_target(benchmark
-P ${PROJECT_SOURCE_DIR}/cmake/RunTests.cmake
DEPENDS tty-test
USES_TERMINAL)
add_dependencies(benchmark lua_dev_deps nvim)
add_dependencies(benchmark nvim)
add_custom_target(functionaltest
COMMAND ${CMAKE_COMMAND}
@@ -45,7 +44,7 @@ add_custom_target(functionaltest
-P ${PROJECT_SOURCE_DIR}/cmake/RunTests.cmake
DEPENDS printenv-test printargs-test shell-test pwsh-test streams-test tty-test
USES_TERMINAL)
add_dependencies(functionaltest lua_dev_deps nvim)
add_dependencies(functionaltest nvim)
# Create multiple targets for groups of functional tests to enable parallel testing.
set(group_targets "")
@@ -66,7 +65,7 @@ foreach(test_file ${test_files})
${TEST_OPTIONS}
-P ${PROJECT_SOURCE_DIR}/cmake/RunTests.cmake
DEPENDS printenv-test printargs-test shell-test pwsh-test streams-test tty-test)
add_dependencies(${group_target} lua_dev_deps nvim)
add_dependencies(${group_target} nvim)
list(APPEND group_targets ${group_target})
list(APPEND summary_files ${summary_file})
endif()
+23 -8
View File
@@ -1,10 +1,25 @@
--- @meta
do -- Mark block as optional
---Mark a test as placeholder.
---
---This will not fail or pass, it will simply be marked as "pending".
---@param name string
---@param block? fun()
function pending(name, block) end
end
--- @param name string
--- @param fn? fun()
function it(name, fn) end
--- @param name string
--- @param fn fun()
function describe(name, fn) end
--- @param name? string
--- @param block? fun()|string
function pending(name, block) end
--- @param fn fun()
function setup(fn) end
--- @param fn fun()
function before_each(fn) end
--- @param fn fun()
function after_each(fn) end
--- @param fn fun()
function teardown(fn) end
+102
View File
@@ -0,0 +1,102 @@
--- @class test.assert
local M = {}
local FORMAT_DEPTH = 100
--- @param value any
--- @return string
local function format_value(value)
if type(value) == 'string' then
return string.format('%q', value)
end
local ok, inspected = pcall(vim.inspect, value, { depth = FORMAT_DEPTH })
if ok then
return inspected
end
return tostring(value)
end
--- @param condition boolean
--- @param value any
--- @param context any
--- @param fallback string
local function assert_value(condition, value, context, fallback)
if not condition then
local message = context ~= nil and tostring(context) or fallback
error(message, 0)
end
return value
end
--- @param expected any
--- @param actual any
--- @param comparator string
--- @return string
local function comparison_message(expected, actual, comparator)
return ('Expected values to be %s.\nExpected:\n%s\nActual:\n%s'):format(
comparator,
format_value(expected),
format_value(actual)
)
end
--- @param expected any
--- @param actual any
--- @param context? any
--- @return any
function M.eq(expected, actual, context)
return assert_value(
vim.deep_equal(expected, actual),
actual,
context,
comparison_message(expected, actual, 'equal')
)
end
--- @param expected any
--- @param actual any
--- @param context? any
--- @return any
function M.neq(expected, actual, context)
return assert_value(
not vim.deep_equal(expected, actual),
actual,
context,
('Expected values to differ.\nValue:\n%s'):format(format_value(actual))
)
end
--- @param value any
--- @param context? any
--- @return any
function M.is_true(value, context)
return M.eq(true, value, context)
end
--- @param value any
--- @param context? any
--- @return any
function M.is_false(value, context)
return M.eq(false, value, context)
end
-- TODO(lewis6991): remove these aliases
M.True = M.is_true
M.False = M.is_false
M.equals = M.eq
M.Equal = M.eq
return setmetatable(M, {
--- @param condition any
--- @param message? string
--- @param level? integer
__call = function(_, condition, message, level, ...)
if condition then
return condition, message, level, ...
end
error(message or 'assertion failed!', (type(level) == 'number' and level or 1) + 1)
end,
})
+3 -3
View File
@@ -1,4 +1,4 @@
-- Modules loaded here will not be cleared and reloaded by Busted.
-- Busted started doing this to help provide more isolation. See issue #62
-- for more information about this.
-- Modules loaded here will not be cleared and reloaded by the local harness.
-- Keeping these preloaded preserves cross-file setup while still resetting
-- non-helper modules between files.
require('test.functional.testnvim')()
-16
View File
@@ -1,16 +0,0 @@
-- Extends the upstream TAP handler, to display the log with suiteEnd.
local t_global = require('test.testutil')
return function(options)
local busted = require 'busted'
local handler = require 'busted.outputHandlers.TAP'(options)
local suiteEnd = function()
io.write(t_global.read_nvim_log(nil, true))
return nil, true
end
busted.subscribe({ 'suite', 'end' }, suiteEnd)
return handler
end
-359
View File
@@ -1,359 +0,0 @@
local pretty = require 'pl.pretty'
local t_global = require('test.testutil')
local colors = setmetatable({}, {
__index = function()
return function(s)
return s == nil and '' or tostring(s)
end
end,
})
local enable_colors = true
if os.getenv 'TEST_COLORS' then
local test_colors = os.getenv('TEST_COLORS'):lower()
local disable_colors = test_colors == 'false'
or test_colors == '0'
or test_colors == 'no'
or test_colors == 'off'
enable_colors = not disable_colors
end
if enable_colors then
colors = require 'term.colors'
end
return function(options)
local busted = require 'busted'
local handler = require 'busted.outputHandlers.base'()
local args = options.arguments
args = vim.json.decode(#args > 0 and table.concat(args, ',') or '{}')
local c = {
succ = function(s)
return colors.bright(colors.green(s))
end,
skip = function(s)
return colors.bright(colors.yellow(s))
end,
fail = function(s)
return colors.bright(colors.magenta(s))
end,
errr = function(s)
return colors.bright(colors.red(s))
end,
test = tostring,
file = colors.cyan,
time = colors.dim,
note = colors.yellow,
sect = function(s)
return colors.green(colors.dim(s))
end,
nmbr = colors.bright,
}
local repeatSuiteString = '\nRepeating all tests (run %d of %d) . . .\n\n'
local randomizeString = c.note('Note: Randomizing test order with a seed of %d.\n')
local globalSetup = c.sect('--------') .. ' Global test environment setup.\n'
local fileStartString = c.sect('--------') .. ' Running tests from ' .. c.file('%s') .. '\n'
local runString = c.sect('RUN ') .. ' ' .. c.test('%s') .. ': '
local successString = c.succ('OK') .. '\n'
local skippedString = c.skip('SKIP') .. '\n'
local failureString = c.fail('FAIL') .. '\n'
local errorString = c.errr('ERR') .. '\n'
local fileEndString = c.sect('--------')
.. ' '
.. c.nmbr('%d')
.. ' %s from '
.. c.file('%s')
.. ' '
.. c.time('(%.2f ms total)')
.. '\n\n'
local globalTeardown = c.sect('--------') .. ' Global test environment teardown.\n'
local suiteEndString = c.sect('========')
.. ' '
.. c.nmbr('%d')
.. ' %s from '
.. c.nmbr('%d')
.. ' test %s ran. '
.. c.time('(%.2f ms total)')
.. '\n'
local successStatus = c.succ('PASSED ') .. ' ' .. c.nmbr('%d') .. ' %s.\n'
local timeString = c.time('%.2f ms')
local summaryStrings = {
skipped = {
header = c.skip('SKIPPED ') .. ' ' .. c.nmbr('%d') .. ' %s, listed below:\n',
test = c.skip('SKIPPED ') .. ' %s\n',
footer = ' ' .. c.nmbr('%d') .. ' SKIPPED %s\n',
},
failure = {
header = c.fail('FAILED ') .. ' ' .. c.nmbr('%d') .. ' %s, listed below:\n',
test = c.fail('FAILED ') .. ' %s\n',
footer = ' ' .. c.nmbr('%d') .. ' FAILED %s\n',
},
error = {
header = c.errr('ERROR ') .. ' ' .. c.nmbr('%d') .. ' %s, listed below:\n',
test = c.errr('ERROR ') .. ' %s\n',
footer = ' ' .. c.nmbr('%d') .. ' %s\n',
},
}
local fileCount = 0
local fileTestCount = 0
local testCount = 0
local successCount = 0
local skippedCount = 0
local failureCount = 0
local errorCount = 0
local naCheck = function(pending)
if vim.list_contains(vim.split(pending.name, '[ :]'), 'N/A') then
return true
end
if type(pending.message) ~= 'string' then
return false
end
if vim.list_contains(vim.split(pending.message, '[ :]'), 'N/A') then
return true
end
return false
end
local pendingDescription = function(pending)
local string = ''
if type(pending.message) == 'string' then
string = string .. pending.message .. '\n'
elseif pending.message ~= nil then
string = string .. pretty.write(pending.message) .. '\n'
end
return string
end
local failureDescription = function(failure)
local string = failure.randomseed and ('Random seed: ' .. failure.randomseed .. '\n') or ''
if type(failure.message) == 'string' then
string = string .. failure.message
elseif failure.message == nil then
string = string .. 'Nil error'
else
string = string .. pretty.write(failure.message)
end
string = string .. '\n'
if options.verbose and failure.trace and failure.trace.traceback then
string = string .. failure.trace.traceback .. '\n'
end
return string
end
local getFileLine = function(element)
local fileline = ''
if element.trace or element.trace.short_src then
local fname = vim.fs.normalize(element.trace.short_src)
fileline = colors.cyan(fname) .. ' @ ' .. colors.cyan(element.trace.currentline) .. ': '
end
return fileline
end
local getTestList = function(status, count, list, getDescription)
local string = ''
local header = summaryStrings[status].header
if count > 0 and header then
local tests = (count == 1 and 'test' or 'tests')
local errors = (count == 1 and 'error' or 'errors')
string = header:format(count, status == 'error' and errors or tests)
local testString = summaryStrings[status].test
if testString then
local naCount = 0
for _, t in ipairs(list) do
if status == 'skipped' and naCheck(t) then
naCount = naCount + 1
else
local fullname = getFileLine(t.element) .. colors.bright(t.name)
string = string .. testString:format(fullname)
string = string .. getDescription(t)
end
end
if naCount > 0 then
string = string
.. colors.bright(
('%d N/A %s not shown\n'):format(naCount, naCount == 1 and 'test' or 'tests')
)
end
end
end
return string
end
local getSummary = function(status, count)
local string = ''
local footer = summaryStrings[status].footer
if count > 0 and footer then
local tests = (count == 1 and 'TEST' or 'TESTS')
local errors = (count == 1 and 'ERROR' or 'ERRORS')
string = footer:format(count, status == 'error' and errors or tests)
end
return string
end
local getSummaryString = function()
local tests = (successCount == 1 and 'test' or 'tests')
local string = successStatus:format(successCount, tests)
string = string .. getTestList('skipped', skippedCount, handler.pendings, pendingDescription)
string = string .. getTestList('failure', failureCount, handler.failures, failureDescription)
string = string .. getTestList('error', errorCount, handler.errors, failureDescription)
string = string .. ((skippedCount + failureCount + errorCount) > 0 and '\n' or '')
string = string .. getSummary('skipped', skippedCount)
string = string .. getSummary('failure', failureCount)
string = string .. getSummary('error', errorCount)
return string
end
handler.suiteReset = function()
fileCount = 0
fileTestCount = 0
testCount = 0
successCount = 0
skippedCount = 0
failureCount = 0
errorCount = 0
return nil, true
end
handler.suiteStart = function(_suite, count, total, randomseed)
if total > 1 then
io.write(repeatSuiteString:format(count, total))
end
if randomseed then
io.write(randomizeString:format(randomseed))
end
io.write(globalSetup)
io.flush()
return nil, true
end
local function getElapsedTime(tbl)
if tbl.duration then
return tbl.duration * 1000
else
return tonumber('nan')
end
end
handler.suiteEnd = function(suite, _count, _total)
local elapsedTime_ms = getElapsedTime(suite)
local tests = (testCount == 1 and 'test' or 'tests')
local files = (fileCount == 1 and 'file' or 'files')
if type(args.test_path) == 'string' then
files = files .. ' of ' .. args.test_path
end
local sf = type(args.summary_file) == 'string'
and args.summary_file ~= '-'
and io.open(args.summary_file, 'w')
or io.stdout
io.write(globalTeardown)
io.flush()
sf:write('\n')
sf:write(suiteEndString:format(testCount, tests, fileCount, files, elapsedTime_ms))
sf:write(getSummaryString())
if failureCount > 0 or errorCount > 0 then
sf:write(t_global.read_nvim_log(nil, true))
end
sf:flush()
if sf ~= io.stdout then
sf:close()
end
return nil, true
end
handler.fileStart = function(file)
fileTestCount = 0
io.write(fileStartString:format(vim.fs.normalize(file.name)))
io.flush()
return nil, true
end
handler.fileEnd = function(file)
local elapsedTime_ms = getElapsedTime(file)
local tests = (fileTestCount == 1 and 'test' or 'tests')
fileCount = fileCount + 1
io.write(
fileEndString:format(fileTestCount, tests, vim.fs.normalize(file.name), elapsedTime_ms)
)
io.flush()
return nil, true
end
handler.testStart = function(element, _parent)
local testid = _G._nvim_test_id or ''
local desc = ('%s %s'):format(testid, handler.getFullName(element))
io.write(runString:format(desc))
io.flush()
return nil, true
end
local function write_status(element, string)
io.write(timeString:format(getElapsedTime(element)) .. ' ' .. string)
io.flush()
end
handler.testEnd = function(element, _parent, status, _debug)
local string
fileTestCount = fileTestCount + 1
testCount = testCount + 1
if status == 'success' then
successCount = successCount + 1
string = successString
elseif status == 'pending' then
skippedCount = skippedCount + 1
string = skippedString
elseif status == 'failure' then
failureCount = failureCount + 1
string = failureString .. failureDescription(handler.failures[#handler.failures])
elseif status == 'error' then
errorCount = errorCount + 1
string = errorString .. failureDescription(handler.errors[#handler.errors])
else
string = 'unexpected test status! (' .. status .. ')'
end
write_status(element, string)
return nil, true
end
handler.error = function(element, _parent, _message, _debug)
if element.descriptor ~= 'it' then
write_status(element, failureDescription(handler.errors[#handler.errors]))
errorCount = errorCount + 1
end
return nil, true
end
busted.subscribe({ 'suite', 'reset' }, handler.suiteReset)
busted.subscribe({ 'suite', 'start' }, handler.suiteStart)
busted.subscribe({ 'suite', 'end' }, handler.suiteEnd)
busted.subscribe({ 'file', 'start' }, handler.fileStart)
busted.subscribe({ 'file', 'end' }, handler.fileEnd)
busted.subscribe({ 'test', 'start' }, handler.testStart, { predicate = handler.cancelOnPending })
busted.subscribe({ 'test', 'end' }, handler.testEnd, { predicate = handler.cancelOnPending })
busted.subscribe({ 'failure' }, handler.error)
busted.subscribe({ 'error' }, handler.error)
return handler
end
+1 -3
View File
@@ -1,5 +1,3 @@
local luaassert = require('luassert')
local M = {}
local SUBTBL = {
@@ -124,7 +122,7 @@ function M.format_luav(v, indent, opts)
else
print(type(v))
-- Not implemented yet
luaassert(false)
assert(false)
end
return ret
end
+1 -1
View File
@@ -12,6 +12,6 @@ describe('autocmd FileType', function()
command('let g:foo = 0')
command('autocmd FileType help let g:foo = g:foo + 1')
command('help help')
assert.same(1, eval('g:foo'))
assert.eq(1, eval('g:foo'))
end)
end)
+28
View File
@@ -0,0 +1,28 @@
local assert = require('test.assert')
describe('test.assert', function()
it('ignores aliasing differences', function()
local shared = {}
assert.eq({ 1, shared, 1, shared }, { 1, {}, 1, {} })
assert.eq({ 1, {}, 1, {} }, { 1, shared, 1, shared })
end)
it('handles cyclic tables', function()
local expected = {}
local actual = {}
expected[1] = expected
actual[1] = actual
assert.eq(expected, actual)
end)
it('still rejects different structures', function()
local expected = {}
expected[1] = expected
assert.neq(expected, { {} })
end)
end)
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -8,7 +8,6 @@ local api = n.api
local fn = n.fn
local clear = n.clear
local eq = t.eq
local fail = t.fail
local exec_lua = n.exec_lua
local feed = n.feed
local expect_events = t.expect_events
@@ -507,7 +506,7 @@ describe('lua: nvim_buf_attach on_bytes', function()
for _, event in ipairs(events) do
for _, elem in ipairs(event) do
if type(elem) == 'number' and elem < 0 then
fail(string.format('Received event has negative values'))
error('Received event has negative values')
end
end
+1 -1
View File
@@ -199,7 +199,7 @@ describe('vim.net.request', function()
return result
]])
assert.is_table(headers)
t.eq('table', type(headers), 'Expected headers to be a table')
-- httpbingo.org/request returns each header as a list in the returned value
t.eq(headers.Authorization[1], 'Bearer test-token', 'Expected Authorization header')
t.eq(headers['X-Custom-Header'][1], 'custom-value', 'Expected X-Custom-Header')
+25 -1
View File
@@ -834,6 +834,26 @@ describe('lua stdlib', function()
)
end)
it('vim._copy', function()
ok(exec_lua([[
local inner = { x = 1 }
local mt = { tag = true }
local a = setmetatable({ inner = inner }, mt)
local b = vim._copy(a)
local c = vim.empty_dict()
c.inner = inner
local d = vim._copy(c)
return b ~= a
and b.inner == inner
and getmetatable(b) == mt
and d ~= c
and d.inner == inner
and not vim.islist(d)
]]))
end)
it('vim.pesc', function()
eq('foo%-bar', exec_lua([[return vim.pesc('foo-bar')]]))
eq('foo%%%-bar', exec_lua([[return vim.pesc(vim.pesc('foo-bar'))]]))
@@ -1249,7 +1269,11 @@ describe('lua stdlib', function()
eq(true, exec_lua [[ return vim.deep_equal({a={b=1}}, {a={b=1}}) ]])
eq(true, exec_lua [[ return vim.deep_equal({a={b={nil}}}, {a={b={}}}) ]])
eq(true, exec_lua [[ return vim.deep_equal({a=1, [5]=5}, {nil,nil,nil,nil,5,a=1}) ]])
eq(true, exec_lua [[ local shared = {}; return vim.deep_equal({ 1, shared, 1, shared }, { 1, {}, 1, {} }) ]])
eq(
true,
exec_lua [[ local shared = {}; return vim.deep_equal({ 1, shared, 1, shared }, { 1, {}, 1, {} }) ]]
)
-- cyclic table
eq(true, exec_lua [[ local a,b={},{}; a[1]=a; b[1]=b; return vim.deep_equal(a, b) ]])
eq(false, exec_lua [[ return vim.deep_equal(1, {nil,nil,nil,nil,5,a=1}) ]])
eq(false, exec_lua [[ return vim.deep_equal(1, 3) ]])
+3 -3
View File
@@ -1,6 +1,6 @@
-- Modules loaded here will NOT be cleared and reloaded by Busted.
-- Busted started doing this to help provide more isolation. See issue #62
-- for more information about this.
-- Modules loaded here will not be cleared and reloaded by the local harness.
-- Keeping these preloaded preserves cross-file setup while still resetting
-- non-helper modules between files.
local t = require('test.testutil')
require('test.functional.testnvim')()
require('test.functional.ui.screen')
+2 -2
View File
@@ -80,14 +80,14 @@ describe(':terminal', function()
send_osc_with_terminator(BEL)
--- @type string
assert.same(
assert.eq(
{ sequence = OSC_PREFIX .. '10;?', terminator = BEL },
exec_lua([[return _G.osc10_response]])
)
send_osc_with_terminator(ST)
--- @type string
assert.same(
assert.eq(
{ sequence = OSC_PREFIX .. '10;?', terminator = ST },
exec_lua([[return _G.osc10_response]])
)
+3 -2
View File
@@ -1,6 +1,7 @@
local uv = vim.uv
local t = require('test.testutil')
local busted = require('busted')
---@type test.harness
local harness = require('test.harness')
local Session = require('test.client.session')
local uv_stream = require('test.client.uv_stream')
@@ -528,7 +529,7 @@ function M.new_session(keep, ...)
return new_session
end
busted.subscribe({ 'suite', 'end' }, function()
harness.on_suite_end(function()
M.check_close(true)
local timed_out = false
local timer = assert(vim.uv.new_timer())
+3 -4
View File
@@ -47,7 +47,6 @@
local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local busted = require('busted')
local uv = vim.uv
local deepcopy = vim.deepcopy
@@ -911,7 +910,7 @@ between asynchronous (feed(), nvim_input()) and synchronous API calls.
if eof then
err = err .. '\n\n' .. eof[2]
end
busted.fail(err .. '\n\nSnapshot:\n' .. self:_print_snapshot(), 3)
error(err .. '\n\nSnapshot:\n' .. self:_print_snapshot(), 3)
elseif did_warn then
if eof then
print(eof[2])
@@ -922,9 +921,9 @@ between asynchronous (feed(), nvim_input()) and synchronous API calls.
end
if flags.intermediate and not intermediate_seen then
busted.fail('Expected intermediate screen state before final screen state', 3)
error('Expected intermediate screen state before final screen state', 3)
elseif flags.unchanged and intermediate_seen then
busted.fail(
error(
'Expected screen state to be unchanged.\nIntermediate screen state:\n'
.. intermediate_state_snapshot,
3
+1694
View File
File diff suppressed because it is too large Load Diff
-91
View File
@@ -1,91 +0,0 @@
local platform = vim.uv.os_uname()
local deps_install_dir = table.remove(_G.arg, 1)
_G.c_include_path = {}
while vim.startswith(_G.arg[1], '-I') do
table.insert(_G.c_include_path, string.sub(table.remove(_G.arg, 1), 3))
end
local subcommand = table.remove(_G.arg, 1)
local suffix = (platform and platform.sysname:lower():find 'windows') and '.dll' or '.so'
package.path = (deps_install_dir .. '/?.lua;')
.. (deps_install_dir .. '/?/init.lua;')
.. package.path
package.cpath = deps_install_dir .. '/?' .. suffix .. ';' .. package.cpath
local uv = vim.uv
-- we use busted and luacheck and their lua dependencies
-- But installing their binary dependencies with luarocks is very
-- slow, replace them with vim.uv wrappers
local system = {}
package.loaded['system.core'] = system
function system.monotime()
uv.update_time()
return uv.now() * 1e-3
end
function system.gettime()
local sec, usec = uv.gettimeofday()
return sec + usec * 1e-6
end
function system.sleep(sec)
uv.sleep(sec * 1e3)
end
local term = {}
package.loaded['term.core'] = term
function term.isatty(_)
return uv.guess_handle(1) == 'tty'
end
local lfs = { _VERSION = 'fake' }
package.loaded['lfs'] = lfs
function lfs.attributes(path, attr)
local stat = uv.fs_stat(path)
if attr == 'mode' then
return stat and stat.type or ''
elseif attr == 'modification' then
if not stat then
return nil
end
local mtime = stat.mtime
return mtime.sec + mtime.nsec * 1e-9
else
error('not implemented')
end
end
function lfs.currentdir()
return uv.cwd()
end
function lfs.chdir(dir)
local status, err = pcall(uv.chdir, dir)
if status then
return true
else
return nil, err
end
end
function lfs.dir(path)
local fs = uv.fs_scandir(path)
return function()
if not fs then
return
end
return uv.fs_scandir_next(fs)
end
end
function lfs.mkdir(dir)
return uv.fs_mkdir(dir, 493) -- octal 755
end
if subcommand == 'busted' then
require 'busted.runner'({ standalone = false })
elseif subcommand == 'luacheck' then
require 'luacheck.main'
else
error 'unknown subcommand'
end
+461
View File
@@ -0,0 +1,461 @@
--- @alias test.reporter.SummaryStatus 'skipped' | 'failure' | 'error'
--- @class test.reporter.ColorMap
--- @field bright fun(s: any): string
--- @field green fun(s: any): string
--- @field yellow fun(s: any): string
--- @field magenta fun(s: any): string
--- @field red fun(s: any): string
--- @field cyan fun(s: any): string
--- @field dim fun(s: any): string
--- @class test.reporter.FileElement
--- @field name string
--- @field duration? number
--- @class test.reporter.Options
--- @field verbose boolean
--- @field summary_file string
--- @class test.base_reporter
--- @field new fun(opts: test.reporter.Options): test.base_reporter
--- @field suite_start fun(self: test.base_reporter, repeat_index?: integer, repeat_count?: integer)
--- @field file_start fun(self: test.base_reporter, file: test.reporter.FileElement)
--- @field test_start fun(self: test.base_reporter, name: string)
--- @field test_end fun(self: test.base_reporter, record: test.harness.Record)
--- @field file_end fun(self: test.base_reporter, file: test.reporter.FileElement, test_count: integer)
--- @field suite_end fun(self: test.base_reporter, duration: number, run_summary: test.harness.RunSummary, failure_output?: string)
--- @class test.reporter : test.base_reporter
--- @field opts test.reporter.Options
--- @field colors test.reporter.ColorMap
local M = {}
M.__index = M
--- @return test.reporter.ColorMap
local function identity_colors()
return setmetatable({}, {
__index = function()
return function(s)
return s == nil and '' or tostring(s)
end
end,
})
end
--- @param code string
--- @return fun(s: any): string
local function ansi_color(code)
return function(s)
return ('\27[%sm%s\27[0m'):format(code, s == nil and '' or tostring(s))
end
end
local ansi_colors = {
bright = ansi_color('1'),
green = ansi_color('32'),
yellow = ansi_color('33'),
magenta = ansi_color('35'),
red = ansi_color('31'),
cyan = ansi_color('36'),
dim = ansi_color('2'),
}
--- @return boolean
local function use_colors()
local test_colors = os.getenv('TEST_COLORS')
if not test_colors then
return true
end
local value = test_colors:lower()
return not (value == 'false' or value == '0' or value == 'no' or value == 'off')
end
--- @param path? string
--- @return file
local function open_summary_file(path)
if type(path) ~= 'string' or path == '-' then
return io.stdout
end
return (assert(io.open(path, 'w')))
end
--- @param opts test.reporter.Options
--- @return test.reporter
function M.new(opts)
local colors = identity_colors()
if use_colors() then
colors = ansi_colors
end
--- @type test.reporter
local self = setmetatable({
opts = opts,
colors = colors,
}, M)
return self
end
--- @param element? { duration?: number }
--- @return number
local function get_elapsed_time_ms(element)
if element and element.duration then
return element.duration * 1000
end
return tonumber('nan')
end
--- @private
--- @param s any
--- @return string
function M:succ(s)
return self.colors.bright(self.colors.green(s))
end
--- @private
--- @param s any
--- @return string
function M:skip(s)
return self.colors.bright(self.colors.yellow(s))
end
--- @private
--- @param s any
--- @return string
function M:fail(s)
return self.colors.bright(self.colors.magenta(s))
end
--- @private
--- @param s any
--- @return string
function M:errr(s)
return self.colors.bright(self.colors.red(s))
end
--- @private
--- @param s any
--- @return string
function M:fpath(s)
return self.colors.cyan(s)
end
--- @private
--- @param s any
--- @return string
function M:time(s)
return self.colors.dim(s)
end
--- @private
--- @param s any
--- @return string
function M:sect(s)
return self.colors.green(self.colors.dim(s))
end
--- @private
--- @param s any
--- @return string
function M:nmbr(s)
return self.colors.bright(s)
end
--- @private
--- @param status test.harness.ResultStatus
--- @return string
function M:result_text(status)
if status == 'success' then
return ('%s\n'):format(self:succ('OK'))
elseif status == 'pending' then
return ('%s\n'):format(self:skip('SKIP'))
elseif status == 'failure' then
return ('%s\n'):format(self:fail('FAIL'))
end
return ('%s\n'):format(self:errr('ERR'))
end
--- @private
--- @param status test.reporter.SummaryStatus
--- @return string
function M:summary_label(status)
if status == 'skipped' then
return self:skip('SKIPPED ')
elseif status == 'failure' then
return self:fail('FAILED ')
end
return self:errr('ERROR ')
end
--- @private
--- @param status test.reporter.SummaryStatus
--- @param count integer
--- @return string
function M:summary_header_noun(status, count)
if status == 'error' then
return count == 1 and 'error' or 'errors'
end
return count == 1 and 'test' or 'tests'
end
--- @private
--- @param status test.reporter.SummaryStatus
--- @param count integer
--- @return string
function M:summary_footer_noun(status, count)
if status == 'skipped' then
return count == 1 and 'SKIPPED TEST' or 'SKIPPED TESTS'
elseif status == 'failure' then
return count == 1 and 'FAILED TEST' or 'FAILED TESTS'
elseif status == 'error' then
return count == 1 and 'ERROR' or 'ERRORS'
end
return count == 1 and 'TEST' or 'TESTS'
end
--- @param message any
--- @return string
local function stringify_message(message)
if type(message) == 'string' then
return message
elseif message == nil then
return ''
end
return vim.inspect(message)
end
--- @private
--- @param pending test.harness.Record
--- @return boolean
function M:na_check(pending)
if pending.name and vim.list_contains(vim.split(pending.name, '[ :]'), 'N/A') then
return true
end
if type(pending.message) == 'string' then
return vim.list_contains(vim.split(pending.message, '[ :]'), 'N/A')
end
return false
end
--- @private
--- @param pending test.harness.Record
--- @return string
function M:pending_description(pending)
local message = stringify_message(pending.message)
if message == '' then
return ''
end
return table.concat({ message, '\n' })
end
--- @private
--- @param failure test.harness.Record
--- @return string
function M:failure_description(failure)
local message = stringify_message(failure.message)
if message == '' then
message = 'Nil error'
end
local parts = { message, '\n' }
if self.opts.verbose and failure.traceback then
parts[#parts + 1] = failure.traceback
parts[#parts + 1] = '\n'
end
return table.concat(parts)
end
--- @private
--- @param trace? test.harness.Trace
--- @return string
function M:get_file_line(trace)
if not trace or not trace.short_src then
return ''
end
local source = vim.fs.normalize(trace.short_src)
local line = trace.currentline or 0
return self:fpath(source) .. ' @ ' .. self:fpath(line) .. ': '
end
--- @private
--- @param status test.reporter.SummaryStatus
--- @param count integer
--- @param list test.harness.Record[]
--- @param describe fun(self: test.reporter, item: test.harness.Record): string
--- @return string
function M:get_test_list(status, count, list, describe)
if count == 0 then
return ''
end
local label = self:summary_label(status)
local parts = {
('%s %s %s, listed below:\n'):format(
label,
self:nmbr(count),
self:summary_header_noun(status, count)
),
}
local na_count = 0
for _, item in ipairs(list) do
if status == 'skipped' and self:na_check(item) then
na_count = na_count + 1
else
local fullname = self:get_file_line(item.trace) .. self:nmbr(item.name)
parts[#parts + 1] = ('%s %s\n'):format(label, fullname)
parts[#parts + 1] = describe(self, item)
end
end
if na_count > 0 then
parts[#parts + 1] =
self:nmbr(('%d N/A %s not shown\n'):format(na_count, na_count == 1 and 'test' or 'tests'))
end
return table.concat(parts)
end
--- @private
--- @param status test.reporter.SummaryStatus
--- @param count integer
--- @return string
function M:get_summary(status, count)
if count == 0 then
return ''
end
return (' %s %s\n'):format(self:nmbr(count), self:summary_footer_noun(status, count))
end
--- @private
--- @return string
--- @param summary test.harness.RunSummary
function M:get_summary_string(summary)
local tests = summary.success_count == 1 and 'test' or 'tests'
local parts = {
('%s %s %s.\n'):format(self:succ('PASSED '), self:nmbr(summary.success_count), tests),
self:get_test_list('skipped', summary.skipped_count, summary.pendings, M.pending_description),
self:get_test_list('failure', summary.failure_count, summary.failures, M.failure_description),
self:get_test_list('error', summary.error_count, summary.errors, M.failure_description),
}
if (summary.skipped_count + summary.failure_count + summary.error_count) > 0 then
parts[#parts + 1] = '\n'
end
parts[#parts + 1] = self:get_summary('skipped', summary.skipped_count)
parts[#parts + 1] = self:get_summary('failure', summary.failure_count)
parts[#parts + 1] = self:get_summary('error', summary.error_count)
return table.concat(parts)
end
--- @return nil
--- @param repeat_index? integer
--- @param repeat_count? integer
function M:suite_start(repeat_index, repeat_count)
if repeat_count and repeat_count > 1 and repeat_index then
io.write(('\nRepeating all tests (run %d of %d) . . .\n\n'):format(repeat_index, repeat_count))
end
io.write(('%s Global test environment setup.\n'):format(self:sect('--------')))
io.flush()
end
--- @param file test.reporter.FileElement
function M:file_start(file)
io.write(
('%s Running tests from %s\n'):format(
self:sect('--------'),
self:fpath(vim.fs.normalize(file.name))
)
)
io.flush()
end
--- @param name string
function M:test_start(name)
local desc = ('%s %s'):format(_G._nvim_test_id or '', name)
io.write(('%s %s: '):format(self:sect('RUN '), desc))
io.flush()
end
--- @private
--- @param record { duration?: number }
--- @param text string
function M:write_status(record, text)
io.write(('%s %s'):format(self:time(('%.2f ms'):format(get_elapsed_time_ms(record))), text))
io.flush()
end
--- @param record test.harness.Record
function M:test_end(record)
local text = self:result_text(record.status)
if record.status == 'failure' or record.status == 'error' then
text = text .. self:failure_description(record)
end
self:write_status(record, text)
end
--- @param file test.reporter.FileElement
--- @param test_count integer
function M:file_end(file, test_count)
io.write(
('%s %s %s from %s %s\n\n'):format(
self:sect('--------'),
self:nmbr(test_count),
test_count == 1 and 'test' or 'tests',
self:fpath(vim.fs.normalize(file.name)),
self:time(('(%.2f ms total)'):format(get_elapsed_time_ms(file)))
)
)
io.flush()
end
--- @param duration number
--- @param run_summary test.harness.RunSummary
--- @param failure_output? string
function M:suite_end(duration, run_summary, failure_output)
local tests = run_summary.test_count == 1 and 'test' or 'tests'
local files = run_summary.file_count == 1 and 'file' or 'files'
io.write(('%s Global test environment teardown.\n'):format(self:sect('--------')))
io.flush()
local summary_file = open_summary_file(self.opts.summary_file)
summary_file:write('\n')
summary_file:write(
('%s %s %s from %s test %s ran. %s\n'):format(
self:sect('========'),
self:nmbr(run_summary.test_count),
tests,
self:nmbr(run_summary.file_count),
files,
self:time(('(%.2f ms total)'):format(duration * 1000))
)
)
summary_file:write(self:get_summary_string(run_summary))
if failure_output then
summary_file:write(failure_output)
end
summary_file:flush()
if summary_file ~= io.stdout then
summary_file:close()
end
end
return M
+8 -10
View File
@@ -1,24 +1,22 @@
const std = @import("std");
const LazyPath = std.Build.LazyPath;
pub fn testStep(b: *std.Build, kind: []const u8, nvim_bin: *std.Build.Step.Compile, lua_deps: LazyPath, config_dir: LazyPath, include_path: ?[]const LazyPath) !*std.Build.Step.Run {
pub fn testStep(b: *std.Build, kind: []const u8, nvim_bin: *std.Build.Step.Compile, config_dir: LazyPath, include_path: ?[]const LazyPath) !*std.Build.Step.Run {
const test_step = b.addRunArtifact(nvim_bin);
test_step.addArg("-ll");
test_step.addFileArg(b.path("./test/lua_runner.lua"));
test_step.addDirectoryArg(lua_deps);
test_step.addFileArg(b.path("./test/runner.lua"));
if (include_path) |paths| {
for (paths) |path| {
test_step.addPrefixedDirectoryArg("-I", path);
}
}
test_step.addArgs(&.{ "busted", "-v", "-o", "test.busted.outputHandlers.nvim", "--lazy" });
// TODO(bfredl): a bit funky with paths, should work even if we run "zig build" in a nested dir
test_step.addArg(b.fmt("./test/{s}/preload.lua", .{kind}));
test_step.addArg("-v");
test_step.addArg(b.fmt("--helper=./test/{s}/preload.lua", .{kind}));
test_step.addArg("--lpath=./src/?.lua");
test_step.addArg("--lpath=./runtime/lua/?.lua");
test_step.addArg("--lpath=./?.lua");
test_step.addPrefixedFileArg("--lpath=", config_dir.path(b, "?.lua")); // FULING: not a real file but works anyway?
// TODO(bfredl): look into $BUSTED_ARGS user hook, TEST_TAG, TEST_FILTER
// TODO(bfredl): look into a TEST_ARGS user hook, TEST_TAG, TEST_FILTER.
if (b.args) |args| {
test_step.addArgs(args); // accept TEST_FILE as a positional argument
} else {
@@ -40,12 +38,12 @@ pub fn testStep(b: *std.Build, kind: []const u8, nvim_bin: *std.Build.Step.Compi
return test_step;
}
pub fn test_steps(b: *std.Build, nvim_bin: *std.Build.Step.Compile, depend_on: *std.Build.Step, lua_deps: LazyPath, config_dir: LazyPath, unit_paths: ?[]const LazyPath) !void {
pub fn test_steps(b: *std.Build, nvim_bin: *std.Build.Step.Compile, depend_on: *std.Build.Step, config_dir: LazyPath, unit_paths: ?[]const LazyPath) !void {
const empty_dir = b.addWriteFiles();
_ = empty_dir.add(".touch", "");
const tmpdir_create = b.addInstallDirectory(.{ .source_dir = empty_dir.getDirectory(), .install_dir = .prefix, .install_subdir = "Xtest_tmpdir/" });
const functional_tests = try testStep(b, "functional", nvim_bin, lua_deps, config_dir, null);
const functional_tests = try testStep(b, "functional", nvim_bin, config_dir, null);
functional_tests.step.dependOn(depend_on);
functional_tests.step.dependOn(&tmpdir_create.step);
@@ -53,7 +51,7 @@ pub fn test_steps(b: *std.Build, nvim_bin: *std.Build.Step.Compile, depend_on: *
functionaltest_step.dependOn(&functional_tests.step);
if (unit_paths) |paths| {
const unit_tests = try testStep(b, "unit", nvim_bin, lua_deps, config_dir, paths);
const unit_tests = try testStep(b, "unit", nvim_bin, config_dir, paths);
unit_tests.step.dependOn(depend_on);
unit_tests.step.dependOn(&tmpdir_create.step);
+35
View File
@@ -0,0 +1,35 @@
local uv = vim.uv
---@return string
local function repo_root()
local source = debug.getinfo(1, 'S').source
assert(type(source) == 'string' and vim.startswith(source, '@'), 'failed to resolve runner path')
local script_path = assert(uv.fs_realpath(source:sub(2)), 'failed to resolve runner path')
return vim.fs.dirname(vim.fs.dirname(script_path))
end
---@param roots string[]
local function prepend_package_roots(roots)
local entries = {}
for _, root in ipairs(roots) do
entries[#entries + 1] = root .. '/?.lua'
entries[#entries + 1] = root .. '/?/init.lua'
end
package.path = table.concat(entries, ';') .. ';' .. package.path
end
_G.c_include_path = {}
while _G.arg[1] and vim.startswith(_G.arg[1], '-I') do
table.insert(_G.c_include_path, string.sub(table.remove(_G.arg, 1), 3))
end
local root = repo_root()
prepend_package_roots({ root, root .. '/test', '.', './test' })
local exit_code = require('test.harness').main(_G.arg)
io.stdout:flush()
io.stderr:flush()
-- Close the standalone Lua state before exit so sanitizers see Lua-owned cleanup.
os.exit(exit_code, true)
+11 -40
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
+2 -2
View File
@@ -76,8 +76,8 @@ describe('fs.c', function()
uv.fs_symlink('test.file', 'unit-test-directory/test_link.file')
uv.fs_symlink('non_existing_file.file', 'unit-test-directory/test_broken_link.file')
-- The tests are invoked with an absolute path to `busted` executable.
absolute_executable = arg[0]
-- The tests are invoked with an absolute executable path in arg[0].
absolute_executable = vim.fs.normalize(assert(uv.exepath()))
-- Split the absolute_executable path into a directory and filename.
directory, executable_name = string.match(absolute_executable, '^(.*)/(.*)$')
end)
-8
View File
@@ -397,14 +397,6 @@ describe('path.c', function()
setup(function()
mkdir('unit-test-directory')
io.open('unit-test-directory/test.file', 'w'):close()
-- Since the tests are executed, they are called by an executable. We use
-- that executable for several asserts.
local absolute_executable = arg[0]
-- Split absolute_executable into a directory and the actual file name for
-- later usage.
local directory, executable_name = string.match(absolute_executable, '^(.*)/(.*)$') -- luacheck: ignore
end)
teardown(function()
+3 -3
View File
@@ -1,6 +1,6 @@
-- Modules loaded here will not be cleared and reloaded by Busted.
-- Busted started doing this to help provide more isolation. See issue #62
-- for more information about this.
-- Modules loaded here will not be cleared and reloaded by the local harness.
-- Keeping these preloaded preserves cross-file setup while still resetting
-- non-helper modules between files.
require('ffi')
require('test.unit.testutil')
require('test.unit.preprocess')
+1 -1
View File
@@ -46,7 +46,7 @@ describe('tempfile related functions', function()
itp('generate name of non-existing file', function()
local file = vim_tempname()
assert.truthy(file)
assert(file)
assert.False(lib.os_path_exists(file))
end)
+1 -2
View File
@@ -1,5 +1,4 @@
local t = require('test.unit.testutil')
local assert = require('luassert')
local itp = t.gen_itp(it)
@@ -11,7 +10,7 @@ if os.getenv('NVIM_TEST_RUN_TESTTEST') ~= '1' then
end
describe('test code', function()
itp('does not hang when working with lengthy errors', function()
assert.just_fail(('x'):rep(65536))
error(('x'):rep(65536), 0)
end)
itp('shows trace after exiting abnormally', function()
sc.exit(0)
+3 -17
View File
@@ -4,8 +4,7 @@ local Set = require('test.unit.set')
local Preprocess = require('test.unit.preprocess')
local t_global = require('test.testutil')
local paths = t_global.paths
local assert = require('luassert')
local say = require('say')
local assert = require('test.assert')
local check_cores = t_global.check_cores
local dedent = t_global.dedent
@@ -538,19 +537,6 @@ if os.getenv('NVIM_TEST_PRINT_SYSCALLS') == '1' then
end
end
local function just_fail(_)
return false
end
say:set('assertion.just_fail.positive', '%s')
say:set('assertion.just_fail.negative', '%s')
assert:register(
'assertion',
'just_fail',
just_fail,
'assertion.just_fail.positive',
'assertion.just_fail.negative'
)
local hook_fnamelen = 30
local hook_sfnamelen = 30
local hook_numlen = 5
@@ -769,7 +755,7 @@ local function check_child_err(rd)
end
end
if err ~= '' then
assert.just_fail(err)
error(err, 0)
end
end
end
@@ -810,7 +796,7 @@ local function gen_itp(it)
end
-- Pre-emptively calculating error location, wasteful, ugh!
-- But the way this code messes around with busted implies the real location is strictly
-- But the way this code wraps the local harness means the real location is strictly
-- not available in the parent when an actual error occurs. so we have to do this here.
local location = debug.traceback()
it(name, function()
+7
View File
@@ -275,6 +275,13 @@ local function push(input, vt)
vterm.vterm_input_write(vt, input, string.len(input))
end
-- vterm_input_write() can synchronously invoke the Lua callbacks installed
-- above. LuaJIT must not JIT-compile an FFI call that re-enters Lua, or the
-- test can panic with "bad callback".
if jit then
jit.off(push, true)
end
local function expect(expected)
local actual = read_rm()
t.eq(expected .. '\n', actual)