test: add headless test framework

This commit is contained in:
2026-05-07 02:07:35 +02:00
parent 55effc0a67
commit e9d13627c8
10 changed files with 398 additions and 257 deletions
+16 -2
View File
@@ -2,7 +2,7 @@
"$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json", "$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json",
"runtime": { "runtime": {
"version": "LuaJIT", "version": "LuaJIT",
"requirePattern": ["lua/?.lua", "lua/?/init.lua", "test/?.lua"] "requirePattern": ["lua/?.lua", "lua/?/init.lua"]
}, },
"diagnostics": { "diagnostics": {
"disable": ["unnecessary-if", "preferred-local-alias", "redefined-local"] "disable": ["unnecessary-if", "preferred-local-alias", "redefined-local"]
@@ -10,7 +10,21 @@
"workspace": { "workspace": {
"library": [ "library": [
"/usr/share/nvim/runtime", "/usr/share/nvim/runtime",
"~/.local/share/nvim/site/pack/core/opt" "/usr/share/nvim/runtime/pack/dist/opt/nvim.undotree",
"~/.local/share/nvim/site/pack/core/opt/onedark.nvim",
"~/.local/share/nvim/site/pack/core/opt/fzf-lua",
"~/.local/share/nvim/site/pack/core/opt/nvim-lspconfig",
"~/.local/share/nvim/site/pack/core/opt/mason.nvim",
"~/.local/share/nvim/site/pack/core/opt/mason-auto-install.nvim",
"~/.local/share/nvim/site/pack/core/opt/nvim-dap",
"~/.local/share/nvim/site/pack/core/opt/Comment.nvim",
"~/.local/share/nvim/site/pack/core/opt/gitsigns.nvim",
"~/.local/share/nvim/site/pack/core/opt/grug-far.nvim",
"~/.local/share/nvim/site/pack/core/opt/nvim-tree.lua",
"~/.local/share/nvim/site/pack/core/opt/oil.nvim",
"~/.local/share/nvim/site/pack/core/opt/outline.nvim",
"~/.local/share/nvim/site/pack/core/opt/blink.cmp",
"./test"
] ]
} }
} }
+11
View File
@@ -0,0 +1,11 @@
.PHONY: all check lint test
all: check
check: lint test
test:
@scripts/test
lint:
@scripts/lint
Executable
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -uo pipefail
cd "$(dirname "$0")/.." || exit 1
emmylua_check --output-format=json . 2> >(grep -v '^Check finished$' >&2) \
| jq -r '
.[]
| .file as $f
| .diagnostics[]
| "\($f):\(.range.start.line + 1):\(.range.start.character + 1)"
+ ": \(["error","warning","info","hint"][.severity-1])"
+ ": \(.message | rtrimstr(" ")) [\(.code)]"
' \
| sed "s|^$PWD/||"
exit "${PIPESTATUS[0]}"
Executable
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -u
usage() {
cat <<EOF
Usage: ${0##*/} [--help] [TARGET ...]
Run Neovim integration tests in a single 'nvim --headless' instance.
With no targets, runs every test/**/*_test.lua. Each TARGET may be a
test file or a directory; directories expand to all _test.lua files
beneath them.
Exit status is 0 if all tests passed, non-zero otherwise.
EOF
}
if [[ ${1:-} == --help || ${1:-} == -h ]]; then
usage
exit 0
fi
cd "$(dirname "$0")/.." || exit 1
if [[ -t 1 ]]; then
export TEST_COLOR=1
fi
exec nvim --headless --clean -l test/runner.lua "$@"
+98 -55
View File
@@ -1,24 +1,78 @@
local h = require("helpers") local t = require("test")
require("git").init() require("git").init()
---Run the cursor-restore autocmd that was responsible for the original ---@param dir string
---cursor-jump bug. Replicating it lets the regression test exercise the ---@param ... string
---same interaction the user had in their config. local function git(dir, ...)
local r = vim.system({ "git", ... }, { cwd = dir, text = true }):wait()
if r.code ~= 0 then
error(
string.format(
"git %s failed: %s",
table.concat({ ... }, " "),
vim.trim(r.stderr or "")
),
2
)
end
return r
end
---Build a temporary git repo with the given committed contents and
---queue cleanup (stop fs watchers, drop test buffers, delete the dir).
---@param files table<string, string>?
---@return string dir
local function make_repo(files)
local dir = vim.fn.tempname()
vim.fn.mkdir(dir, "p")
git(dir, "init", "-q", "-b", "main")
git(dir, "config", "user.email", "t@t.com")
git(dir, "config", "user.name", "t")
if files and next(files) then
for path, content in pairs(files) do
t.write(dir, path, content)
end
git(dir, "add", ".")
git(dir, "commit", "-q", "-m", "init")
end
t.defer(function()
pcall(vim.cmd.cd, "/tmp")
pcall(function()
require("git.repo").stop_all()
end)
vim.wait(60)
for _, b in ipairs(vim.api.nvim_list_bufs()) do
local name = vim.api.nvim_buf_get_name(b)
if name:find(dir, 1, true) or name:match("^git[a-z]*://") then
pcall(vim.api.nvim_buf_delete, b, { force = true })
end
end
vim.fn.delete(dir, "rf")
end)
return dir
end
---Replicate the user's global cursor-restore autocmd. Scoped to a
---named augroup + cleanup so it doesn't leak between tests.
local function install_cursor_restore_autocmd() local function install_cursor_restore_autocmd()
local group =
vim.api.nvim_create_augroup("test.cursor_restore", { clear = true })
vim.api.nvim_create_autocmd("BufReadPost", { vim.api.nvim_create_autocmd("BufReadPost", {
group = group,
pattern = "*", pattern = "*",
command = 'silent! normal! g`"zv', command = 'silent! normal! g`"zv',
}) })
t.defer(function()
pcall(vim.api.nvim_del_augroup_by_name, "test.cursor_restore")
end)
end end
---@param sidebar_buf integer ---@param sidebar_buf integer
---@param needle string ---@param needle string
---@return integer? ---@return integer?
local function find_line(sidebar_buf, needle) local function find_line(sidebar_buf, needle)
for i, l in for i, l in ipairs(vim.api.nvim_buf_get_lines(sidebar_buf, 0, -1, false)) do
ipairs(vim.api.nvim_buf_get_lines(sidebar_buf, 0, -1, false))
do
if l:match(needle) then if l:match(needle) then
return i return i
end end
@@ -51,8 +105,6 @@ local function find_diff_win(role)
end end
end end
---Set up a repo with one tracked-and-modified file, open the sidebar, and
---return the sidebar window plus the line of the file's entry.
---@param file_path string ---@param file_path string
---@param committed_content string ---@param committed_content string
---@param worktree_content string ---@param worktree_content string
@@ -63,8 +115,8 @@ local function setup_sidebar_with_unstaged_file(
committed_content, committed_content,
worktree_content worktree_content
) )
local repo = h.make_repo({ [file_path] = committed_content }) local repo = make_repo({ [file_path] = committed_content })
h.write(repo, file_path, worktree_content) t.write(repo, file_path, worktree_content)
vim.cmd("cd " .. repo) vim.cmd("cd " .. repo)
require("git.status_view").open({ placement = "sidebar" }) require("git.status_view").open({ placement = "sidebar" })
@@ -96,43 +148,40 @@ end
---@param cond fun(): boolean ---@param cond fun(): boolean
---@param msg string ---@param msg string
local function wait_for(cond, msg) local function wait_for(cond, msg)
h.truthy(vim.wait(1000, cond), "timed out waiting for: " .. msg) t.truthy(vim.wait(1000, cond), "timed out waiting for: " .. msg)
end end
h.test( t.test("stage with diff open: sidebar cursor stays put", function()
"stage with diff open: sidebar cursor stays put", install_cursor_restore_autocmd()
function() local sidebar_win, line = setup_sidebar_with_unstaged_file(
install_cursor_restore_autocmd() "zsh/rc",
local sidebar_win, line = setup_sidebar_with_unstaged_file( "ZSH=true\n",
"zsh/rc", "ZSH=true\nmodified\n"
"ZSH=true\n", )
"ZSH=true\nmodified\n"
)
vim.api.nvim_set_current_win(sidebar_win) vim.api.nvim_set_current_win(sidebar_win)
vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 }) vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 })
press("<Tab>") press("<Tab>")
wait_for(function() wait_for(function()
return find_diff_win("left") ~= nil return find_diff_win("left") ~= nil
end, "diff windows to appear") end, "diff windows to appear")
local r = assert(require("git.repo").find(vim.fn.getcwd())) local r = assert(require("git.repo").find(vim.fn.getcwd()))
vim.api.nvim_set_current_win(sidebar_win) vim.api.nvim_set_current_win(sidebar_win)
press("s") press("s")
wait_for(function() wait_for(function()
return #r.status:by_kind("staged") > 0 return #r.status:by_kind("staged") > 0
end, "stage to propagate to repo state") end, "stage to propagate to repo state")
h.eq( t.eq(
vim.api.nvim_win_get_cursor(sidebar_win), vim.api.nvim_win_get_cursor(sidebar_win),
{ line, 0 }, { line, 0 },
"sidebar cursor should remain at the entry's original line" "sidebar cursor should remain at the entry's original line"
) )
end end)
)
h.test( t.test(
"stage with diff open: diff foldmethod is preserved on refresh", "stage with diff open: diff foldmethod is preserved on refresh",
function() function()
local sidebar_win, line = setup_sidebar_with_unstaged_file( local sidebar_win, line = setup_sidebar_with_unstaged_file(
@@ -149,7 +198,7 @@ h.test(
return find_diff_win("left") ~= nil return find_diff_win("left") ~= nil
end, "diff windows to appear") end, "diff windows to appear")
local left_win = assert(find_diff_win("left")) local left_win = assert(find_diff_win("left"))
h.eq( t.eq(
vim.wo[left_win].foldmethod, vim.wo[left_win].foldmethod,
"diff", "diff",
"left diff foldmethod should be 'diff' after Tab" "left diff foldmethod should be 'diff' after Tab"
@@ -162,7 +211,7 @@ h.test(
return #r.status:by_kind("staged") > 0 return #r.status:by_kind("staged") > 0
end, "stage to propagate to repo state") end, "stage to propagate to repo state")
h.eq( t.eq(
vim.wo[left_win].foldmethod, vim.wo[left_win].foldmethod,
"diff", "diff",
"left diff foldmethod should still be 'diff' after stage refresh" "left diff foldmethod should still be 'diff' after stage refresh"
@@ -170,12 +219,9 @@ h.test(
end end
) )
h.test("refresh on stage updates the index URI buffer's content", function() t.test("refresh on stage updates the index URI buffer's content", function()
local sidebar_win, line = setup_sidebar_with_unstaged_file( local sidebar_win, line =
"foo.txt", setup_sidebar_with_unstaged_file("foo.txt", "v1\n", "v2\n")
"v1\n",
"v2\n"
)
vim.api.nvim_set_current_win(sidebar_win) vim.api.nvim_set_current_win(sidebar_win)
vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 }) vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 })
@@ -186,7 +232,7 @@ h.test("refresh on stage updates the index URI buffer's content", function()
local left_win = assert(find_diff_win("left")) local left_win = assert(find_diff_win("left"))
local index_buf = vim.api.nvim_win_get_buf(left_win) local index_buf = vim.api.nvim_win_get_buf(left_win)
h.eq( t.eq(
vim.api.nvim_buf_get_lines(index_buf, 0, -1, false), vim.api.nvim_buf_get_lines(index_buf, 0, -1, false),
{ "v1" }, { "v1" },
"index pane should initially show committed content" "index pane should initially show committed content"
@@ -195,16 +241,13 @@ h.test("refresh on stage updates the index URI buffer's content", function()
vim.api.nvim_set_current_win(sidebar_win) vim.api.nvim_set_current_win(sidebar_win)
press("s") press("s")
wait_for(function() wait_for(function()
local first = local first = vim.api.nvim_buf_get_lines(index_buf, 0, -1, false)[1]
vim.api.nvim_buf_get_lines(index_buf, 0, -1, false)[1]
return first == "v2" return first == "v2"
end, "index pane to refresh to staged content") end, "index pane to refresh to staged content")
h.eq( t.eq(
vim.api.nvim_buf_get_lines(index_buf, 0, -1, false), vim.api.nvim_buf_get_lines(index_buf, 0, -1, false),
{ "v2" }, { "v2" },
"index pane should reflect staged content after refresh" "index pane should reflect staged content after refresh"
) )
end) end)
h.report()
+23 -21
View File
@@ -1,47 +1,49 @@
local h = require("helpers") local t = require("test")
local util = require("git.util") local util = require("git.util")
h.test("set_buf_lines preserves modifiable=false", function() local function fresh_buf()
local buf = vim.api.nvim_create_buf(false, true) local buf = vim.api.nvim_create_buf(false, true)
t.defer(function()
pcall(vim.api.nvim_buf_delete, buf, { force = true })
end)
return buf
end
t.test("set_buf_lines preserves modifiable=false", function()
local buf = fresh_buf()
vim.bo[buf].modifiable = false vim.bo[buf].modifiable = false
util.set_buf_lines(buf, 0, -1, { "a", "b", "c" }) util.set_buf_lines(buf, 0, -1, { "a", "b", "c" })
h.eq( t.eq(
vim.api.nvim_buf_get_lines(buf, 0, -1, false), vim.api.nvim_buf_get_lines(buf, 0, -1, false),
{ "a", "b", "c" }, { "a", "b", "c" },
"lines should be replaced" "lines should be replaced"
) )
h.falsy(vim.bo[buf].modifiable, "modifiable should stay false") t.falsy(vim.bo[buf].modifiable, "modifiable should stay false")
h.falsy(vim.bo[buf].modified, "modified should be cleared") t.falsy(vim.bo[buf].modified, "modified should be cleared")
vim.api.nvim_buf_delete(buf, { force = true })
end) end)
h.test("set_buf_lines preserves modifiable=true", function() t.test("set_buf_lines preserves modifiable=true", function()
local buf = vim.api.nvim_create_buf(false, true) local buf = fresh_buf()
vim.bo[buf].modifiable = true vim.bo[buf].modifiable = true
util.set_buf_lines(buf, 0, -1, { "a", "b" }) util.set_buf_lines(buf, 0, -1, { "a", "b" })
h.truthy(vim.bo[buf].modifiable, "modifiable should stay true") t.truthy(vim.bo[buf].modifiable, "modifiable should stay true")
h.falsy(vim.bo[buf].modified, "modified should be cleared") t.falsy(vim.bo[buf].modified, "modified should be cleared")
vim.api.nvim_buf_delete(buf, { force = true })
end) end)
h.test("set_buf_lines partial range update", function() t.test("set_buf_lines partial range update", function()
local buf = vim.api.nvim_create_buf(false, true) local buf = fresh_buf()
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "a", "b", "c", "d" }) vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "a", "b", "c", "d" })
util.set_buf_lines(buf, 1, 3, { "X", "Y", "Z" }) util.set_buf_lines(buf, 1, 3, { "X", "Y", "Z" })
h.eq( t.eq(
vim.api.nvim_buf_get_lines(buf, 0, -1, false), vim.api.nvim_buf_get_lines(buf, 0, -1, false),
{ "a", "X", "Y", "Z", "d" }, { "a", "X", "Y", "Z", "d" },
"lines [1, 3) should be replaced" "lines [1, 3) should be replaced"
) )
vim.api.nvim_buf_delete(buf, { force = true })
end) end)
h.test("set_buf_lines errors on out-of-bounds (strict_indexing)", function() t.test("set_buf_lines errors on out-of-bounds (strict_indexing)", function()
local buf = vim.api.nvim_create_buf(false, true) local buf = fresh_buf()
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "a", "b" }) vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "a", "b" })
local ok = pcall(util.set_buf_lines, buf, 100, 200, { "x" }) local ok = pcall(util.set_buf_lines, buf, 100, 200, { "x" })
h.falsy(ok, "out-of-bounds index should error") t.falsy(ok, "out-of-bounds index should error")
vim.api.nvim_buf_delete(buf, { force = true })
end) end)
h.report()
+46 -90
View File
@@ -1,7 +1,8 @@
local M = {} local M = {}
local stats = { passed = 0, failed = 0, errors = {} } local stats = { passed = 0, failed = 0, errors = {} }
local pending_cleanup = {} local defers = {}
local label = "?"
local started = false local started = false
local color_on = vim.env.TEST_COLOR == "1" local color_on = vim.env.TEST_COLOR == "1"
@@ -12,11 +13,9 @@ local function color(code, str)
end end
return str return str
end end
local function red(s) local function red(s)
return color("31", s) return color("31", s)
end end
local function green(s) local function green(s)
return color("32", s) return color("32", s)
end end
@@ -26,51 +25,24 @@ local function ensure_started()
return return
end end
started = true started = true
io.stdout:write( io.stdout:write(string.format("-> %s ", label))
string.format("-> %s ", vim.env.TEST_FILE_LABEL or "?")
)
io.stdout:flush() io.stdout:flush()
end end
---Tear down repos created during the current test. Stops fs watchers first, ---Begin a new test file. Resets per-file stats and the cleanup queue
---drains any scheduled callbacks, wipes buffers tied to the repo, then nukes ---and stages the per-file header for the next `M.test` call.
---the directory. ---@param path string
---@param dir string function M.start_file(path)
function M.cleanup(dir) label = path
pcall(vim.cmd.cd, "/tmp") started = false
pcall(function() stats = { passed = 0, failed = 0, errors = {} }
require("git.repo").stop_all() defers = {}
end)
vim.wait(60)
for _, b in ipairs(vim.api.nvim_list_bufs()) do
local name = vim.api.nvim_buf_get_name(b)
if name:find(dir, 1, true) or name:match("^git[a-z]*://") then
pcall(vim.api.nvim_buf_delete, b, { force = true })
end
end
vim.fn.delete(dir, "rf")
end
---@param name string
---@param fn fun()
function M.test(name, fn)
ensure_started()
local ok, err = pcall(fn)
while #pending_cleanup > 0 do
local dir = table.remove(pending_cleanup)
pcall(M.cleanup, dir)
end
if ok then
stats.passed = stats.passed + 1
io.stdout:write(".")
else
stats.failed = stats.failed + 1
table.insert(stats.errors, { name = name, err = err })
io.stdout:write(red("F"))
end
io.stdout:flush()
end end
---Print the per-file summary and return the counts so the runner can
---accumulate totals across files.
---@return integer passed
---@return integer failed
function M.report() function M.report()
ensure_started() ensure_started()
if stats.failed > 0 then if stats.failed > 0 then
@@ -88,12 +60,37 @@ function M.report()
else else
io.stdout:write(" " .. green("OK") .. "\n") io.stdout:write(" " .. green("OK") .. "\n")
end end
io.stdout:write( return stats.passed, stats.failed
string.format("RESULTS %d %d\n", stats.passed, stats.failed) end
)
if stats.failed > 0 then ---Queue a function to run when the current `M.test` completes (whether
vim.cmd("cquit 1") ---it passed or failed). Deferred calls run in LIFO order.
---@param fn fun()
function M.defer(fn)
table.insert(defers, fn)
end
---@param name string
---@param fn fun()
function M.test(name, fn)
ensure_started()
local saved_cwd = vim.fn.getcwd()
local ok, err = pcall(fn)
while #defers > 0 do
pcall(table.remove(defers))
end end
if vim.fn.getcwd() ~= saved_cwd then
pcall(vim.cmd.cd, saved_cwd)
end
if ok then
stats.passed = stats.passed + 1
io.stdout:write(".")
else
stats.failed = stats.failed + 1
table.insert(stats.errors, { name = name, err = err })
io.stdout:write(red("F"))
end
io.stdout:flush()
end end
local function fmt_value(v) local function fmt_value(v)
@@ -136,56 +133,15 @@ function M.falsy(val, msg)
end end
end end
---@param dir string
---@param ... string
local function git(dir, ...)
local r = vim.system({ "git", ... }, { cwd = dir, text = true }):wait()
if r.code ~= 0 then
error(
string.format(
"git %s failed: %s",
table.concat({ ... }, " "),
vim.trim(r.stderr or "")
),
2
)
end
return r
end
---@param dir string ---@param dir string
---@param path string ---@param path string
---@param content string ---@param content string
function M.write(dir, path, content) function M.write(dir, path, content)
local full = vim.fs.joinpath(dir, path) local full = vim.fs.joinpath(dir, path)
vim.fn.mkdir(vim.fs.dirname(full), "p") vim.fn.mkdir(vim.fs.dirname(full), "p")
local f = assert(io.open(full, "w")) local f = assert(io.open(full --[[@as string]], "w"))
f:write(content) f:write(content)
f:close() f:close()
end end
---Create a temporary git repo and seed it with files committed on `main`.
---The directory is auto-cleaned at the end of the current `M.test(...)`.
---@param files table<string, string>? path -> content
---@return string dir
---@return fun(...): vim.SystemCompleted runner
function M.make_repo(files)
local dir = vim.fn.tempname()
vim.fn.mkdir(dir, "p")
git(dir, "init", "-q", "-b", "main")
git(dir, "config", "user.email", "t@t.com")
git(dir, "config", "user.name", "t")
if files and next(files) then
for path, content in pairs(files) do
M.write(dir, path, content)
end
git(dir, "add", ".")
git(dir, "commit", "-q", "-m", "init")
end
table.insert(pending_cleanup, dir)
return dir, function(...)
return git(dir, ...)
end
end
return M return M
+76
View File
@@ -0,0 +1,76 @@
local pack = require("pack")
local t = require("test")
---Build a fake plugin tree under a temp dir. `files` maps module names
---(e.g. "foo", "foo.bar") to a string body — the file path is derived by
---swapping `.` for `/` and adding `.lua`. Cleanup is queued so the dir
---is removed after the current test.
---@param files table<string, string>
---@return string plugin_path
local function fake_plugin(files)
local dir = vim.fn.tempname()
vim.fn.mkdir(dir, "p")
for module, body in pairs(files) do
local rel = module:gsub("%.", "/") .. ".lua"
t.write(dir, "lua/" .. rel, body)
end
t.defer(function()
vim.fn.delete(dir, "rf")
end)
return dir
end
---Pre-populate `package.loaded` and queue cleanup so the entry doesn't
---leak across tests.
local function preload(name)
package.loaded[name] = { _marker = name }
t.defer(function()
package.loaded[name] = nil
end)
end
t.test("unload clears top-level module", function()
local dir = fake_plugin({ foo = "return {}" })
preload("foo")
pack.unload(dir)
t.eq(package.loaded.foo, nil, "foo should be uncached")
end)
t.test("unload clears submodules via dotted names", function()
local dir = fake_plugin({
foo = "return {}",
["foo.bar"] = "return {}",
["foo.baz.qux"] = "return {}",
})
preload("foo")
preload("foo.bar")
preload("foo.baz.qux")
pack.unload(dir)
t.eq(package.loaded.foo, nil, "foo cleared")
t.eq(package.loaded["foo.bar"], nil, "foo.bar cleared")
t.eq(package.loaded["foo.baz.qux"], nil, "foo.baz.qux cleared")
end)
t.test("unload leaves unrelated modules untouched", function()
local dir = fake_plugin({ foo = "return {}" })
preload("foo")
preload("unrelated")
preload("foo_neighbor")
pack.unload(dir)
t.eq(package.loaded.foo, nil, "foo cleared")
t.truthy(package.loaded.unrelated, "unrelated kept")
t.truthy(package.loaded.foo_neighbor, "foo_neighbor kept")
end)
t.test("unload handles init.lua submodules", function()
local dir = fake_plugin({ ["pkg.init"] = "return {}" })
preload("pkg")
pack.unload(dir)
t.eq(package.loaded.pkg, nil, "pkg cleared via init.lua")
end)
t.test("unload on non-existent path is a no-op", function()
preload("foo")
pack.unload("/nonexistent/path/to/nowhere")
t.truthy(package.loaded.foo, "foo kept")
end)
-89
View File
@@ -1,89 +0,0 @@
#!/usr/bin/env bash
set -u
usage() {
cat <<EOF
Usage: ${0##*/} [--help] [TARGET ...]
Run Neovim integration tests. Each test file is invoked via
'nvim --headless --clean' with the project's runtimepath added.
With no targets, runs every test/**/*_test.lua. Each TARGET may be a
test file (test/git/util_test.lua) or a directory (test/git); a
directory expands to all _test.lua files beneath it.
Exit status is 0 if all tests passed, non-zero otherwise.
EOF
}
if [[ ${1:-} == --help || ${1:-} == -h ]]; then
usage
exit 0
fi
cd "$(dirname "$0")/.." || exit 1
ROOT=$PWD
if [[ -t 1 ]]; then
red=$'\033[31m'
green=$'\033[32m'
reset=$'\033[0m'
export TEST_COLOR=1
else
red=''
green=''
reset=''
fi
if [[ $# -gt 0 ]]; then
targets=("$@")
else
mapfile -d '' -t targets < <(find test -type f -name '*_test.lua' -print0 | sort -z)
fi
exit_code=0
total_passed=0
total_failed=0
for arg in "${targets[@]}"; do
if [[ -d "$arg" ]]; then
mapfile -d '' -t files < <(find "$arg" -type f -name '*_test.lua' -print0 | sort -z)
elif [[ -f "$arg" ]]; then
files=("$arg")
else
printf 'no such test target: %s\n' "$arg" >&2
exit_code=2
continue
fi
for f in "${files[@]}"; do
out=$(TEST_FILE_LABEL="$f" nvim --headless --clean \
-c "set rtp+=$ROOT" \
-c "lua package.path = package.path .. ';$ROOT/test/?.lua'" \
-c "luafile $f" \
-c "qa!" \
2>&1)
rc=$?
# Split the per-file results from any output.
results_line=$(printf '%s\n' "$out" | grep '^RESULTS ' | tail -1)
printf '%s\n' "$out" | grep -v '^RESULTS '
if [[ -n "$results_line" ]]; then
read -r _ p f_count <<<"$results_line"
total_passed=$((total_passed + p))
total_failed=$((total_failed + f_count))
fi
if [[ $rc -ne 0 ]]; then
exit_code=$rc
fi
done
done
printf '\n'
if [[ $total_failed -gt 0 ]]; then
printf '%s%d passed%s, %s%d failed%s\n' \
"$green" "$total_passed" "$reset" \
"$red" "$total_failed" "$reset"
else
printf '%s%d passed%s\n' "$green" "$total_passed" "$reset"
fi
exit "$exit_code"
+82
View File
@@ -0,0 +1,82 @@
local cfg = vim.fn.stdpath("config")
package.path = package.path
.. (";" .. cfg .. "/lua/?.lua")
.. (";" .. cfg .. "/lua/?/init.lua")
.. (";" .. cfg .. "/?.lua")
.. (";" .. cfg .. "/?/init.lua")
local opt = vim.fn.stdpath("data") .. "/site/pack/core/opt"
for _, lua_dir in ipairs(vim.fn.glob(opt .. "/*/lua", false, true)) do
package.path = package.path
.. (";" .. lua_dir .. "/?.lua")
.. (";" .. lua_dir .. "/?/init.lua")
end
local t = require("test")
local function gather(target)
local abs = vim.fn.fnamemodify(target, ":p")
if vim.fn.isdirectory(abs) == 1 then
return vim.fn.globpath(abs, "**/*_test.lua", false, true)
end
if vim.fn.filereadable(abs) == 1 then
return { abs }
end
end
local args = arg or {}
local targets = #args > 0 and args or { cfg .. "/test" }
local files = {}
local resolve_failed = false
for _, target in ipairs(targets) do
local matched = gather(target)
if matched then
for _, f in ipairs(matched) do
table.insert(files, f)
end
else
io.stderr:write("no such test target: " .. target .. "\n")
resolve_failed = true
end
end
table.sort(files)
local total_passed, total_failed = 0, 0
for _, f in ipairs(files) do
f = f --[[@as string]]
local label = f:sub(1, #cfg + 1) == cfg .. "/" and f:sub(#cfg + 2) or f
t.start_file(label)
local ok, err = pcall(dofile, f)
if not ok then
t.test("(load)", function()
error(err, 0)
end)
end
local p, fl = t.report()
total_passed = total_passed + p
total_failed = total_failed + fl
end
local function color(code, str)
if vim.env.TEST_COLOR == "1" then
return string.format("\27[%sm%s\27[0m", code, str)
end
return str
end
io.stdout:write("\n")
if total_failed > 0 then
io.stdout:write(
string.format(
"%s, %s\n",
color("32", total_passed .. " passed"),
color("31", total_failed .. " failed")
)
)
os.exit(1)
end
io.stdout:write(color("32", total_passed .. " passed") .. "\n")
if resolve_failed then
os.exit(2)
end