test: add headless test framework
This commit is contained in:
+16
-2
@@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.PHONY: all check lint test
|
||||||
|
|
||||||
|
all: check
|
||||||
|
|
||||||
|
check: lint test
|
||||||
|
|
||||||
|
test:
|
||||||
|
@scripts/test
|
||||||
|
|
||||||
|
lint:
|
||||||
|
@scripts/lint
|
||||||
Executable
+17
@@ -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
@@ -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 "$@"
|
||||||
@@ -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,12 +148,10 @@ 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",
|
|
||||||
function()
|
|
||||||
install_cursor_restore_autocmd()
|
install_cursor_restore_autocmd()
|
||||||
local sidebar_win, line = setup_sidebar_with_unstaged_file(
|
local sidebar_win, line = setup_sidebar_with_unstaged_file(
|
||||||
"zsh/rc",
|
"zsh/rc",
|
||||||
@@ -124,15 +174,14 @@ 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.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
@@ -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()
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -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"
|
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user