test: add headless test framework
This commit is contained in:
@@ -1,24 +1,78 @@
|
||||
local h = require("helpers")
|
||||
local t = require("test")
|
||||
|
||||
require("git").init()
|
||||
|
||||
---Run the cursor-restore autocmd that was responsible for the original
|
||||
---cursor-jump bug. Replicating it lets the regression test exercise the
|
||||
---same interaction the user had in their config.
|
||||
---@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
|
||||
|
||||
---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 group =
|
||||
vim.api.nvim_create_augroup("test.cursor_restore", { clear = true })
|
||||
vim.api.nvim_create_autocmd("BufReadPost", {
|
||||
group = group,
|
||||
pattern = "*",
|
||||
command = 'silent! normal! g`"zv',
|
||||
})
|
||||
t.defer(function()
|
||||
pcall(vim.api.nvim_del_augroup_by_name, "test.cursor_restore")
|
||||
end)
|
||||
end
|
||||
|
||||
---@param sidebar_buf integer
|
||||
---@param needle string
|
||||
---@return integer?
|
||||
local function find_line(sidebar_buf, needle)
|
||||
for i, l in
|
||||
ipairs(vim.api.nvim_buf_get_lines(sidebar_buf, 0, -1, false))
|
||||
do
|
||||
for i, l in ipairs(vim.api.nvim_buf_get_lines(sidebar_buf, 0, -1, false)) do
|
||||
if l:match(needle) then
|
||||
return i
|
||||
end
|
||||
@@ -51,8 +105,6 @@ local function find_diff_win(role)
|
||||
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 committed_content string
|
||||
---@param worktree_content string
|
||||
@@ -63,8 +115,8 @@ local function setup_sidebar_with_unstaged_file(
|
||||
committed_content,
|
||||
worktree_content
|
||||
)
|
||||
local repo = h.make_repo({ [file_path] = committed_content })
|
||||
h.write(repo, file_path, worktree_content)
|
||||
local repo = make_repo({ [file_path] = committed_content })
|
||||
t.write(repo, file_path, worktree_content)
|
||||
vim.cmd("cd " .. repo)
|
||||
|
||||
require("git.status_view").open({ placement = "sidebar" })
|
||||
@@ -96,43 +148,40 @@ end
|
||||
---@param cond fun(): boolean
|
||||
---@param msg string
|
||||
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
|
||||
|
||||
h.test(
|
||||
"stage with diff open: sidebar cursor stays put",
|
||||
function()
|
||||
install_cursor_restore_autocmd()
|
||||
local sidebar_win, line = setup_sidebar_with_unstaged_file(
|
||||
"zsh/rc",
|
||||
"ZSH=true\n",
|
||||
"ZSH=true\nmodified\n"
|
||||
)
|
||||
t.test("stage with diff open: sidebar cursor stays put", function()
|
||||
install_cursor_restore_autocmd()
|
||||
local sidebar_win, line = setup_sidebar_with_unstaged_file(
|
||||
"zsh/rc",
|
||||
"ZSH=true\n",
|
||||
"ZSH=true\nmodified\n"
|
||||
)
|
||||
|
||||
vim.api.nvim_set_current_win(sidebar_win)
|
||||
vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 })
|
||||
vim.api.nvim_set_current_win(sidebar_win)
|
||||
vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 })
|
||||
|
||||
press("<Tab>")
|
||||
wait_for(function()
|
||||
return find_diff_win("left") ~= nil
|
||||
end, "diff windows to appear")
|
||||
press("<Tab>")
|
||||
wait_for(function()
|
||||
return find_diff_win("left") ~= nil
|
||||
end, "diff windows to appear")
|
||||
|
||||
local r = assert(require("git.repo").find(vim.fn.getcwd()))
|
||||
vim.api.nvim_set_current_win(sidebar_win)
|
||||
press("s")
|
||||
wait_for(function()
|
||||
return #r.status:by_kind("staged") > 0
|
||||
end, "stage to propagate to repo state")
|
||||
local r = assert(require("git.repo").find(vim.fn.getcwd()))
|
||||
vim.api.nvim_set_current_win(sidebar_win)
|
||||
press("s")
|
||||
wait_for(function()
|
||||
return #r.status:by_kind("staged") > 0
|
||||
end, "stage to propagate to repo state")
|
||||
|
||||
h.eq(
|
||||
vim.api.nvim_win_get_cursor(sidebar_win),
|
||||
{ line, 0 },
|
||||
"sidebar cursor should remain at the entry's original line"
|
||||
)
|
||||
end
|
||||
)
|
||||
t.eq(
|
||||
vim.api.nvim_win_get_cursor(sidebar_win),
|
||||
{ line, 0 },
|
||||
"sidebar cursor should remain at the entry's original line"
|
||||
)
|
||||
end)
|
||||
|
||||
h.test(
|
||||
t.test(
|
||||
"stage with diff open: diff foldmethod is preserved on refresh",
|
||||
function()
|
||||
local sidebar_win, line = setup_sidebar_with_unstaged_file(
|
||||
@@ -149,7 +198,7 @@ h.test(
|
||||
return find_diff_win("left") ~= nil
|
||||
end, "diff windows to appear")
|
||||
local left_win = assert(find_diff_win("left"))
|
||||
h.eq(
|
||||
t.eq(
|
||||
vim.wo[left_win].foldmethod,
|
||||
"diff",
|
||||
"left diff foldmethod should be 'diff' after Tab"
|
||||
@@ -162,7 +211,7 @@ h.test(
|
||||
return #r.status:by_kind("staged") > 0
|
||||
end, "stage to propagate to repo state")
|
||||
|
||||
h.eq(
|
||||
t.eq(
|
||||
vim.wo[left_win].foldmethod,
|
||||
"diff",
|
||||
"left diff foldmethod should still be 'diff' after stage refresh"
|
||||
@@ -170,12 +219,9 @@ h.test(
|
||||
end
|
||||
)
|
||||
|
||||
h.test("refresh on stage updates the index URI buffer's content", function()
|
||||
local sidebar_win, line = setup_sidebar_with_unstaged_file(
|
||||
"foo.txt",
|
||||
"v1\n",
|
||||
"v2\n"
|
||||
)
|
||||
t.test("refresh on stage updates the index URI buffer's content", function()
|
||||
local sidebar_win, line =
|
||||
setup_sidebar_with_unstaged_file("foo.txt", "v1\n", "v2\n")
|
||||
|
||||
vim.api.nvim_set_current_win(sidebar_win)
|
||||
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 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),
|
||||
{ "v1" },
|
||||
"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)
|
||||
press("s")
|
||||
wait_for(function()
|
||||
local first =
|
||||
vim.api.nvim_buf_get_lines(index_buf, 0, -1, false)[1]
|
||||
local first = vim.api.nvim_buf_get_lines(index_buf, 0, -1, false)[1]
|
||||
return first == "v2"
|
||||
end, "index pane to refresh to staged content")
|
||||
|
||||
h.eq(
|
||||
t.eq(
|
||||
vim.api.nvim_buf_get_lines(index_buf, 0, -1, false),
|
||||
{ "v2" },
|
||||
"index pane should reflect staged content after refresh"
|
||||
)
|
||||
end)
|
||||
|
||||
h.report()
|
||||
|
||||
+23
-21
@@ -1,47 +1,49 @@
|
||||
local h = require("helpers")
|
||||
local t = require("test")
|
||||
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)
|
||||
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
|
||||
util.set_buf_lines(buf, 0, -1, { "a", "b", "c" })
|
||||
h.eq(
|
||||
t.eq(
|
||||
vim.api.nvim_buf_get_lines(buf, 0, -1, false),
|
||||
{ "a", "b", "c" },
|
||||
"lines should be replaced"
|
||||
)
|
||||
h.falsy(vim.bo[buf].modifiable, "modifiable should stay false")
|
||||
h.falsy(vim.bo[buf].modified, "modified should be cleared")
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
t.falsy(vim.bo[buf].modifiable, "modifiable should stay false")
|
||||
t.falsy(vim.bo[buf].modified, "modified should be cleared")
|
||||
end)
|
||||
|
||||
h.test("set_buf_lines preserves modifiable=true", function()
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
t.test("set_buf_lines preserves modifiable=true", function()
|
||||
local buf = fresh_buf()
|
||||
vim.bo[buf].modifiable = true
|
||||
util.set_buf_lines(buf, 0, -1, { "a", "b" })
|
||||
h.truthy(vim.bo[buf].modifiable, "modifiable should stay true")
|
||||
h.falsy(vim.bo[buf].modified, "modified should be cleared")
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
t.truthy(vim.bo[buf].modifiable, "modifiable should stay true")
|
||||
t.falsy(vim.bo[buf].modified, "modified should be cleared")
|
||||
end)
|
||||
|
||||
h.test("set_buf_lines partial range update", function()
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
t.test("set_buf_lines partial range update", function()
|
||||
local buf = fresh_buf()
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "a", "b", "c", "d" })
|
||||
util.set_buf_lines(buf, 1, 3, { "X", "Y", "Z" })
|
||||
h.eq(
|
||||
t.eq(
|
||||
vim.api.nvim_buf_get_lines(buf, 0, -1, false),
|
||||
{ "a", "X", "Y", "Z", "d" },
|
||||
"lines [1, 3) should be replaced"
|
||||
)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
h.test("set_buf_lines errors on out-of-bounds (strict_indexing)", function()
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
t.test("set_buf_lines errors on out-of-bounds (strict_indexing)", function()
|
||||
local buf = fresh_buf()
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "a", "b" })
|
||||
local ok = pcall(util.set_buf_lines, buf, 100, 200, { "x" })
|
||||
h.falsy(ok, "out-of-bounds index should error")
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
t.falsy(ok, "out-of-bounds index should error")
|
||||
end)
|
||||
|
||||
h.report()
|
||||
|
||||
Reference in New Issue
Block a user