test(git): add regression tests with a minimal headless harness
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
local M = {}
|
||||
|
||||
local stats = { passed = 0, failed = 0, errors = {} }
|
||||
local pending_cleanup = {}
|
||||
local started = false
|
||||
|
||||
local color_on = vim.env.TEST_COLOR == "1"
|
||||
|
||||
local function color(code, str)
|
||||
if color_on then
|
||||
return string.format("\27[%sm%s\27[0m", code, str)
|
||||
end
|
||||
return str
|
||||
end
|
||||
|
||||
local function red(s)
|
||||
return color("31", s)
|
||||
end
|
||||
|
||||
local function green(s)
|
||||
return color("32", s)
|
||||
end
|
||||
|
||||
local function ensure_started()
|
||||
if started then
|
||||
return
|
||||
end
|
||||
started = true
|
||||
io.stdout:write(
|
||||
string.format("-> %s ", vim.env.TEST_FILE_LABEL or "?")
|
||||
)
|
||||
io.stdout:flush()
|
||||
end
|
||||
|
||||
---Tear down repos created during the current test. Stops fs watchers first,
|
||||
---drains any scheduled callbacks, wipes buffers tied to the repo, then nukes
|
||||
---the directory.
|
||||
---@param dir string
|
||||
function M.cleanup(dir)
|
||||
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
|
||||
|
||||
---@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
|
||||
|
||||
function M.report()
|
||||
ensure_started()
|
||||
if stats.failed > 0 then
|
||||
io.stdout:write(" " .. red("FAIL") .. "\n")
|
||||
for _, e in ipairs(stats.errors) do
|
||||
io.stdout:write(
|
||||
string.format(
|
||||
" %s %s\n %s\n",
|
||||
red("FAIL"),
|
||||
e.name,
|
||||
e.err
|
||||
)
|
||||
)
|
||||
end
|
||||
else
|
||||
io.stdout:write(" " .. green("OK") .. "\n")
|
||||
end
|
||||
io.stdout:write(
|
||||
string.format("RESULTS %d %d\n", stats.passed, stats.failed)
|
||||
)
|
||||
if stats.failed > 0 then
|
||||
vim.cmd("cquit 1")
|
||||
end
|
||||
end
|
||||
|
||||
local function fmt_value(v)
|
||||
if type(v) == "string" then
|
||||
return string.format("%q", v)
|
||||
end
|
||||
return vim.inspect(v)
|
||||
end
|
||||
|
||||
---@param actual any
|
||||
---@param expected any
|
||||
---@param msg string?
|
||||
function M.eq(actual, expected, msg)
|
||||
if not vim.deep_equal(actual, expected) then
|
||||
error(
|
||||
string.format(
|
||||
"%s\n expected: %s\n actual: %s",
|
||||
msg or "values differ",
|
||||
fmt_value(expected),
|
||||
fmt_value(actual)
|
||||
),
|
||||
2
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
---@param val any
|
||||
---@param msg string?
|
||||
function M.truthy(val, msg)
|
||||
if not val then
|
||||
error(msg or ("expected truthy, got " .. tostring(val)), 2)
|
||||
end
|
||||
end
|
||||
|
||||
---@param val any
|
||||
---@param msg string?
|
||||
function M.falsy(val, msg)
|
||||
if val then
|
||||
error(msg or ("expected falsy, got " .. tostring(val)), 2)
|
||||
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 path string
|
||||
---@param content string
|
||||
function M.write(dir, path, content)
|
||||
local full = vim.fs.joinpath(dir, path)
|
||||
vim.fn.mkdir(vim.fs.dirname(full), "p")
|
||||
local f = assert(io.open(full, "w"))
|
||||
f:write(content)
|
||||
f:close()
|
||||
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
|
||||
Reference in New Issue
Block a user