This commit is contained in:
28
.gitea/workflows/tests.yml
Normal file
28
.gitea/workflows/tests.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Neovim
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y snapd
|
||||||
|
sudo snap install nvim --classic
|
||||||
|
|
||||||
|
- name: Install plenary.nvim
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/.local/share/nvim/site/pack/packer/start"
|
||||||
|
git clone --depth 1 https://github.com/nvim-lua/plenary.nvim \
|
||||||
|
"$HOME/.local/share/nvim/site/pack/packer/start/plenary.nvim"
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
nvim --version
|
||||||
|
bash run_test.sh
|
||||||
@@ -137,3 +137,7 @@ return runner
|
|||||||
- parse_test_output implementiert
|
- parse_test_output implementiert
|
||||||
- collect_failed_locations implementiert
|
- collect_failed_locations implementiert
|
||||||
- command_spec `{ cmd, cwd }` korrekt zurückgegeben
|
- command_spec `{ cmd, cwd }` korrekt zurückgegeben
|
||||||
|
|
||||||
|
## Repository-Hinweis
|
||||||
|
|
||||||
|
- `README.md` bei Änderungen am Runner immer mit aktualisieren.
|
||||||
|
|||||||
@@ -53,3 +53,10 @@ Main plugin: https://gitea.mschirmer.com/m13r/test-samurai.nvim
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Use the standard `test-samurai.nvim` commands (e.g. `TSamNearest`, `TSamFile`, `TSamAll`, `TSamFailedOnly`).
|
Use the standard `test-samurai.nvim` commands (e.g. `TSamNearest`, `TSamFile`, `TSamAll`, `TSamFailedOnly`).
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Subtests (`t.Run`) are reported as separate entries via `go test -json -v`.
|
||||||
|
- Subtest selection uses anchored patterns (`^Parent$/^Sub$`) to avoid matching the same subtest name in other tests.
|
||||||
|
- `go test` cannot scope execution to a single file; if two tests in the same package share the same test and subtest names, both will run.
|
||||||
|
- Result lists are ordered so that parent tests appear before their subtests within each status.
|
||||||
|
|||||||
@@ -3,65 +3,330 @@ local runner = {
|
|||||||
framework = "go",
|
framework = "go",
|
||||||
}
|
}
|
||||||
|
|
||||||
local function is_test_name(name)
|
local function get_buf_path(bufnr)
|
||||||
return name:match("^Test%w+") or name:match("^Example%w+") or name:match("^Benchmark%w+")
|
return vim.api.nvim_buf_get_name(bufnr)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function escape_regex(text)
|
local function get_buf_lines(bufnr)
|
||||||
if vim and vim.pesc then
|
return vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
return vim.pesc(text)
|
|
||||||
end
|
|
||||||
return text:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function find_go_mod_root(start_dir)
|
local function find_root(path, markers)
|
||||||
if not start_dir or start_dir == "" then
|
if not path or path == "" then
|
||||||
start_dir = vim.fn.getcwd()
|
|
||||||
end
|
|
||||||
local start_path = vim.fn.fnamemodify(start_dir, ":p")
|
|
||||||
local mod_path = vim.fn.findfile("go.mod", start_path .. ";")
|
|
||||||
if mod_path == "" then
|
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
return vim.fn.fnamemodify(mod_path, ":p:h")
|
local dir = vim.fs.dirname(path)
|
||||||
|
if not dir or dir == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local found = vim.fs.find(markers, { path = dir, upward = true })
|
||||||
|
if not found or not found[1] then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return vim.fs.dirname(found[1])
|
||||||
end
|
end
|
||||||
|
|
||||||
local function collect_results(output)
|
local function find_block_end(lines, start_idx)
|
||||||
local passes = {}
|
local depth = 0
|
||||||
local failures = {}
|
local started = false
|
||||||
local skips = {}
|
for i = start_idx, #lines do
|
||||||
local seen = { passes = {}, failures = {}, skips = {} }
|
local line = lines[i]
|
||||||
|
for j = 1, #line do
|
||||||
for line in output:gmatch("[^\n]+") do
|
local ch = line:sub(j, j)
|
||||||
local name = line:match("^%-%-%- PASS:%s+(%S+)")
|
if ch == "{" then
|
||||||
if name and not seen.passes[name] then
|
depth = depth + 1
|
||||||
seen.passes[name] = true
|
started = true
|
||||||
passes[#passes + 1] = name
|
elseif ch == "}" then
|
||||||
|
if started then
|
||||||
|
depth = depth - 1
|
||||||
|
if depth == 0 then
|
||||||
|
return i - 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
return #lines - 1
|
||||||
|
end
|
||||||
|
|
||||||
name = line:match("^%-%-%- FAIL:%s+(%S+)")
|
local function find_test_functions(lines)
|
||||||
if name and not seen.failures[name] then
|
local funcs = {}
|
||||||
seen.failures[name] = true
|
for i, line in ipairs(lines) do
|
||||||
failures[#failures + 1] = name
|
local name = line:match("^%s*func%s+([%w_]+)%s*%(")
|
||||||
|
if not name then
|
||||||
|
name = line:match("^%s*func%s+%([^)]-%)%s+([%w_]+)%s*%(")
|
||||||
end
|
end
|
||||||
|
if name and line:find("%*testing%.T") then
|
||||||
|
local start_0 = i - 1
|
||||||
|
local end_0 = find_block_end(lines, i)
|
||||||
|
table.insert(funcs, {
|
||||||
|
name = name,
|
||||||
|
start = start_0,
|
||||||
|
["end"] = end_0,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return funcs
|
||||||
|
end
|
||||||
|
|
||||||
name = line:match("^%-%-%- SKIP:%s+(%S+)")
|
local function find_t_runs(lines, func)
|
||||||
if name and not seen.skips[name] then
|
local subtests = {}
|
||||||
seen.skips[name] = true
|
for i = func.start + 1, func["end"] do
|
||||||
skips[#skips + 1] = name
|
local line = lines[i + 1]
|
||||||
|
if line then
|
||||||
|
local name = line:match("t%.Run%(%s*['\"]([^'\"]+)['\"]")
|
||||||
|
if name then
|
||||||
|
local start_idx = i + 1
|
||||||
|
local end_0 = find_block_end(lines, start_idx)
|
||||||
|
table.insert(subtests, {
|
||||||
|
name = name,
|
||||||
|
start = start_idx - 1,
|
||||||
|
["end"] = end_0,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return subtests
|
||||||
|
end
|
||||||
|
|
||||||
|
local function escape_go_regex(text)
|
||||||
|
text = text or ""
|
||||||
|
return (text:gsub("([\\.^$|()%%[%]{}*+?%-])", "\\\\%1"))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function build_run_pattern(spec)
|
||||||
|
local name = spec.test_path or spec.test_name or ""
|
||||||
|
if spec.scope == "function" then
|
||||||
|
local escaped = escape_go_regex(name)
|
||||||
|
return "^" .. escaped .. "$"
|
||||||
|
end
|
||||||
|
|
||||||
|
local parts = vim.split(name, "/", { plain = true })
|
||||||
|
local anchored = {}
|
||||||
|
for _, part in ipairs(parts) do
|
||||||
|
anchored[#anchored + 1] = "^" .. escape_go_regex(part) .. "$"
|
||||||
|
end
|
||||||
|
return table.concat(anchored, "/")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function build_pkg_arg(spec)
|
||||||
|
local file = spec.file
|
||||||
|
local cwd = spec.cwd
|
||||||
|
if not file or not cwd or file == "" or cwd == "" then
|
||||||
|
return "./..."
|
||||||
|
end
|
||||||
|
|
||||||
|
local dir = vim.fs.dirname(file)
|
||||||
|
if dir == cwd then
|
||||||
|
return "./"
|
||||||
|
end
|
||||||
|
|
||||||
|
if file:sub(1, #cwd) ~= cwd then
|
||||||
|
return "./..."
|
||||||
|
end
|
||||||
|
|
||||||
|
local rel = dir:sub(#cwd + 2)
|
||||||
|
if not rel or rel == "" then
|
||||||
|
return "./"
|
||||||
|
end
|
||||||
|
|
||||||
|
return "./" .. rel
|
||||||
|
end
|
||||||
|
|
||||||
|
local function collect_unique(list)
|
||||||
|
local out = {}
|
||||||
|
local seen = {}
|
||||||
|
for _, item in ipairs(list) do
|
||||||
|
if item and item ~= "" and not seen[item] then
|
||||||
|
seen[item] = true
|
||||||
|
table.insert(out, item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
local function order_by_root(names)
|
||||||
|
local roots = {}
|
||||||
|
local seen_root = {}
|
||||||
|
local buckets = {}
|
||||||
|
|
||||||
|
for _, name in ipairs(names) do
|
||||||
|
local root = name:match("^[^/]+") or name
|
||||||
|
if not seen_root[root] then
|
||||||
|
seen_root[root] = true
|
||||||
|
table.insert(roots, root)
|
||||||
|
end
|
||||||
|
buckets[root] = buckets[root] or { main = nil, subs = {} }
|
||||||
|
if name == root then
|
||||||
|
buckets[root].main = name
|
||||||
|
else
|
||||||
|
table.insert(buckets[root].subs, name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return passes, failures, skips
|
local ordered = {}
|
||||||
|
for _, root in ipairs(roots) do
|
||||||
|
local bucket = buckets[root]
|
||||||
|
if bucket.main then
|
||||||
|
table.insert(ordered, bucket.main)
|
||||||
|
end
|
||||||
|
for _, sub in ipairs(bucket.subs) do
|
||||||
|
table.insert(ordered, sub)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return ordered
|
||||||
|
end
|
||||||
|
|
||||||
|
local function order_with_display(names, display_map)
|
||||||
|
local ordered = order_by_root(names)
|
||||||
|
local display = {}
|
||||||
|
for _, name in ipairs(ordered) do
|
||||||
|
display[#display + 1] = display_map[name] or name
|
||||||
|
end
|
||||||
|
return ordered, display
|
||||||
|
end
|
||||||
|
|
||||||
|
local function display_name(name)
|
||||||
|
if not name or name == "" then
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
if name:find("/", 1, true) then
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
|
||||||
|
local function split_output_lines(text)
|
||||||
|
if not text or text == "" then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local lines = vim.split(text, "\n", { plain = true })
|
||||||
|
if #lines > 0 and lines[#lines] == "" then
|
||||||
|
table.remove(lines, #lines)
|
||||||
|
end
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
local function normalize_go_name(name)
|
||||||
|
if not name or name == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return (name:gsub("%s+", "_"))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function add_location(target, key, file, line, label)
|
||||||
|
if not key or key == "" or not file or file == "" or not line then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local text = label or key
|
||||||
|
if not target[key] then
|
||||||
|
target[key] = {}
|
||||||
|
end
|
||||||
|
table.insert(target[key], {
|
||||||
|
filename = file,
|
||||||
|
lnum = line,
|
||||||
|
col = 1,
|
||||||
|
text = text,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function collect_file_locations(file, target)
|
||||||
|
local ok, lines = pcall(vim.fn.readfile, file)
|
||||||
|
if not ok or type(lines) ~= "table" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local funcs = find_test_functions(lines)
|
||||||
|
for _, fn in ipairs(funcs) do
|
||||||
|
add_location(target, fn.name, file, fn.start + 1, fn.name)
|
||||||
|
local normalized = normalize_go_name(fn.name)
|
||||||
|
if normalized and normalized ~= fn.name then
|
||||||
|
add_location(target, normalized, file, fn.start + 1, fn.name)
|
||||||
|
end
|
||||||
|
for _, sub in ipairs(find_t_runs(lines, fn)) do
|
||||||
|
local full = fn.name .. "/" .. sub.name
|
||||||
|
add_location(target, full, file, sub.start + 1, full)
|
||||||
|
local normalized_full = normalize_go_name(full)
|
||||||
|
if normalized_full and normalized_full ~= full then
|
||||||
|
add_location(target, normalized_full, file, sub.start + 1, full)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function collect_go_test_files(root)
|
||||||
|
if not root or root == "" then
|
||||||
|
root = vim.loop.cwd()
|
||||||
|
end
|
||||||
|
local files = vim.fn.globpath(root, "**/*_test.go", false, true)
|
||||||
|
if type(files) ~= "table" then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
return files
|
||||||
end
|
end
|
||||||
|
|
||||||
function runner.is_test_file(bufnr)
|
function runner.is_test_file(bufnr)
|
||||||
local name = vim.api.nvim_buf_get_name(bufnr)
|
local path = get_buf_path(bufnr)
|
||||||
return name:sub(-8) == "_test.go"
|
if not path or path == "" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return path:sub(-8) == "_test.go"
|
||||||
end
|
end
|
||||||
|
|
||||||
function runner.find_nearest(bufnr, row, _col)
|
function runner.find_nearest(bufnr, row, _col)
|
||||||
local line_count = vim.api.nvim_buf_line_count(bufnr)
|
if not runner.is_test_file(bufnr) then
|
||||||
|
return nil, "not a Go test file"
|
||||||
|
end
|
||||||
|
|
||||||
|
local lines = get_buf_lines(bufnr)
|
||||||
|
local funcs = find_test_functions(lines)
|
||||||
|
|
||||||
|
local current
|
||||||
|
for _, f in ipairs(funcs) do
|
||||||
|
if row >= f.start and row <= f["end"] then
|
||||||
|
current = f
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local path = get_buf_path(bufnr)
|
||||||
|
local root = find_root(path, { "go.mod", ".git" })
|
||||||
|
local cwd = root or vim.fs.dirname(path)
|
||||||
|
|
||||||
|
if current then
|
||||||
|
local subtests = find_t_runs(lines, current)
|
||||||
|
local inside_sub
|
||||||
|
for _, sub in ipairs(subtests) do
|
||||||
|
if row >= sub.start and row <= sub["end"] then
|
||||||
|
inside_sub = sub
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if inside_sub then
|
||||||
|
local full = current.name .. "/" .. inside_sub.name
|
||||||
|
return {
|
||||||
|
file = path,
|
||||||
|
cwd = cwd,
|
||||||
|
test_path = full,
|
||||||
|
test_name = full,
|
||||||
|
scope = "subtest",
|
||||||
|
func = current.name,
|
||||||
|
subtest = inside_sub.name,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
file = path,
|
||||||
|
cwd = cwd,
|
||||||
|
test_path = current.name,
|
||||||
|
test_name = current.name,
|
||||||
|
scope = "function",
|
||||||
|
func = current.name,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local line_count = #lines
|
||||||
local idx = row or (line_count - 1)
|
local idx = row or (line_count - 1)
|
||||||
if idx < 0 then
|
if idx < 0 then
|
||||||
idx = 0
|
idx = 0
|
||||||
@@ -69,7 +334,6 @@ function runner.find_nearest(bufnr, row, _col)
|
|||||||
idx = line_count - 1
|
idx = line_count - 1
|
||||||
end
|
end
|
||||||
|
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, line_count, false)
|
|
||||||
for i = idx + 1, 1, -1 do
|
for i = idx + 1, 1, -1 do
|
||||||
local line = lines[i]
|
local line = lines[i]
|
||||||
local name = line:match("^%s*func%s+(Test%w+)%s*%(")
|
local name = line:match("^%s*func%s+(Test%w+)%s*%(")
|
||||||
@@ -82,14 +346,13 @@ function runner.find_nearest(bufnr, row, _col)
|
|||||||
name = line:match("^%s*func%s+(Benchmark%w+)%s*%(")
|
name = line:match("^%s*func%s+(Benchmark%w+)%s*%(")
|
||||||
kind = "benchmark"
|
kind = "benchmark"
|
||||||
end
|
end
|
||||||
if name and is_test_name(name) then
|
if name then
|
||||||
local file = vim.api.nvim_buf_get_name(bufnr)
|
|
||||||
local cwd = vim.fn.fnamemodify(file, ":p:h")
|
|
||||||
return {
|
return {
|
||||||
file = file,
|
file = path,
|
||||||
cwd = cwd,
|
cwd = cwd,
|
||||||
|
test_path = name,
|
||||||
test_name = name,
|
test_name = name,
|
||||||
full_name = name,
|
scope = "function",
|
||||||
kind = kind,
|
kind = kind,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -99,31 +362,69 @@ function runner.find_nearest(bufnr, row, _col)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function runner.build_command(spec)
|
function runner.build_command(spec)
|
||||||
local name = spec.full_name or spec.test_name
|
|
||||||
if spec.kind == "benchmark" then
|
if spec.kind == "benchmark" then
|
||||||
return {
|
return {
|
||||||
cmd = { "go", "test", "-run", "^$", "-bench", "^" .. escape_regex(name) .. "$" },
|
cmd = { "go", "test", "-json", "-v", "-run", "^$", "-bench", "^" .. escape_go_regex(spec.test_path) .. "$" },
|
||||||
cwd = spec.cwd,
|
cwd = spec.cwd,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local pattern = build_run_pattern(spec)
|
||||||
|
local target = build_pkg_arg(spec)
|
||||||
return {
|
return {
|
||||||
cmd = { "go", "test", "-run", "^" .. escape_regex(name) .. "$" },
|
cmd = { "go", "test", "-json", "-v", target, "-run", pattern },
|
||||||
cwd = spec.cwd,
|
cwd = spec.cwd,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
function runner.build_file_command(bufnr)
|
function runner.build_file_command(bufnr)
|
||||||
local file = vim.api.nvim_buf_get_name(bufnr)
|
local path = get_buf_path(bufnr)
|
||||||
local cwd = vim.fn.fnamemodify(file, ":p:h")
|
if not path or path == "" then
|
||||||
return { cmd = { "go", "test" }, cwd = cwd }
|
return nil
|
||||||
|
end
|
||||||
|
local root = find_root(path, { "go.mod", ".git" })
|
||||||
|
if not root or root == "" then
|
||||||
|
root = vim.loop.cwd()
|
||||||
|
end
|
||||||
|
local spec = { file = path, cwd = root }
|
||||||
|
local pkg = build_pkg_arg(spec)
|
||||||
|
local cmd = { "go", "test", "-json", "-v", pkg }
|
||||||
|
local lines = get_buf_lines(bufnr)
|
||||||
|
local funcs = find_test_functions(lines)
|
||||||
|
local names = {}
|
||||||
|
for _, fn in ipairs(funcs) do
|
||||||
|
table.insert(names, fn.name)
|
||||||
|
end
|
||||||
|
names = collect_unique(names)
|
||||||
|
if #names > 0 then
|
||||||
|
local pattern_parts = {}
|
||||||
|
for _, name in ipairs(names) do
|
||||||
|
table.insert(pattern_parts, escape_go_regex(name))
|
||||||
|
end
|
||||||
|
local pattern = "^(" .. table.concat(pattern_parts, "|") .. ")$"
|
||||||
|
table.insert(cmd, "-run")
|
||||||
|
table.insert(cmd, pattern)
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
cmd = cmd,
|
||||||
|
cwd = root,
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
function runner.build_all_command(bufnr)
|
function runner.build_all_command(bufnr)
|
||||||
local file = vim.api.nvim_buf_get_name(bufnr)
|
local path = get_buf_path(bufnr)
|
||||||
local cwd = vim.fn.fnamemodify(file, ":p:h")
|
local root
|
||||||
local root = find_go_mod_root(cwd) or cwd
|
if path and path ~= "" then
|
||||||
return { cmd = { "go", "test", "./..." }, cwd = root }
|
root = find_root(path, { "go.mod", ".git" })
|
||||||
|
end
|
||||||
|
if not root or root == "" then
|
||||||
|
root = vim.loop.cwd()
|
||||||
|
end
|
||||||
|
local cmd = { "go", "test", "-json", "-v", "./..." }
|
||||||
|
return {
|
||||||
|
cmd = cmd,
|
||||||
|
cwd = root,
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
function runner.build_failed_command(last_command, failures, _scope_kind)
|
function runner.build_failed_command(last_command, failures, _scope_kind)
|
||||||
@@ -131,67 +432,151 @@ function runner.build_failed_command(last_command, failures, _scope_kind)
|
|||||||
if last_command and last_command.cmd then
|
if last_command and last_command.cmd then
|
||||||
return { cmd = last_command.cmd, cwd = last_command.cwd }
|
return { cmd = last_command.cmd, cwd = last_command.cwd }
|
||||||
end
|
end
|
||||||
return { cmd = { "go", "test" } }
|
return { cmd = { "go", "test", "-json", "-v" } }
|
||||||
end
|
end
|
||||||
|
|
||||||
local escaped = {}
|
local pattern_parts = {}
|
||||||
for _, name in ipairs(failures) do
|
for _, name in ipairs(failures or {}) do
|
||||||
escaped[#escaped + 1] = escape_regex(name)
|
if name and name ~= "" then
|
||||||
|
local spec = { test_path = name, scope = name:find("/", 1, true) and "subtest" or "function" }
|
||||||
|
table.insert(pattern_parts, build_run_pattern(spec))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
local pattern = "(" .. table.concat(pattern_parts, "|") .. ")"
|
||||||
|
|
||||||
|
local cmd = {}
|
||||||
|
local skip_next = false
|
||||||
|
for _, arg in ipairs(last_command and last_command.cmd or {}) do
|
||||||
|
if skip_next then
|
||||||
|
skip_next = false
|
||||||
|
elseif arg == "-run" then
|
||||||
|
skip_next = true
|
||||||
|
else
|
||||||
|
table.insert(cmd, arg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #cmd == 0 then
|
||||||
|
cmd = { "go", "test", "-json", "-v" }
|
||||||
|
end
|
||||||
|
table.insert(cmd, "-run")
|
||||||
|
table.insert(cmd, pattern)
|
||||||
|
|
||||||
local pattern = "^(" .. table.concat(escaped, "|") .. ")$"
|
|
||||||
return {
|
return {
|
||||||
cmd = { "go", "test", "-run", pattern },
|
cmd = cmd,
|
||||||
cwd = last_command and last_command.cwd or nil,
|
cwd = last_command and last_command.cwd or nil,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
function runner.parse_results(output)
|
function runner.parse_results(output)
|
||||||
local passes, failures, skips = collect_results(output)
|
if not output or output == "" then
|
||||||
return { passes = passes, failures = failures, skips = skips }
|
return { passes = {}, failures = {}, skips = {}, display = { passes = {}, failures = {}, skips = {} } }
|
||||||
|
end
|
||||||
|
local passes = {}
|
||||||
|
local failures = {}
|
||||||
|
local skips = {}
|
||||||
|
local display = { passes = {}, failures = {}, skips = {} }
|
||||||
|
local pass_display = {}
|
||||||
|
local fail_display = {}
|
||||||
|
local skip_display = {}
|
||||||
|
local seen_pass = {}
|
||||||
|
local seen_fail = {}
|
||||||
|
local seen_skip = {}
|
||||||
|
for line in output:gmatch("[^\n]+") do
|
||||||
|
local ok, data = pcall(vim.json.decode, line)
|
||||||
|
if ok and type(data) == "table" then
|
||||||
|
if data.Test and data.Test ~= "" then
|
||||||
|
if data.Action == "pass" then
|
||||||
|
if not seen_pass[data.Test] then
|
||||||
|
seen_pass[data.Test] = true
|
||||||
|
table.insert(passes, data.Test)
|
||||||
|
pass_display[data.Test] = display_name(data.Test)
|
||||||
|
end
|
||||||
|
elseif data.Action == "fail" then
|
||||||
|
if not seen_fail[data.Test] then
|
||||||
|
seen_fail[data.Test] = true
|
||||||
|
table.insert(failures, data.Test)
|
||||||
|
fail_display[data.Test] = display_name(data.Test)
|
||||||
|
end
|
||||||
|
elseif data.Action == "skip" then
|
||||||
|
if not seen_skip[data.Test] then
|
||||||
|
seen_skip[data.Test] = true
|
||||||
|
table.insert(skips, data.Test)
|
||||||
|
skip_display[data.Test] = display_name(data.Test)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
passes = collect_unique(passes)
|
||||||
|
failures = collect_unique(failures)
|
||||||
|
skips = collect_unique(skips)
|
||||||
|
|
||||||
|
passes, display.passes = order_with_display(passes, pass_display)
|
||||||
|
failures, display.failures = order_with_display(failures, fail_display)
|
||||||
|
skips, display.skips = order_with_display(skips, skip_display)
|
||||||
|
|
||||||
|
return { passes = passes, failures = failures, skips = skips, display = display }
|
||||||
end
|
end
|
||||||
|
|
||||||
function runner.parse_test_output(output)
|
function runner.parse_test_output(output)
|
||||||
local results = {}
|
local out = {}
|
||||||
local current = nil
|
if not output or output == "" then
|
||||||
|
return out
|
||||||
|
end
|
||||||
for line in output:gmatch("[^\n]+") do
|
for line in output:gmatch("[^\n]+") do
|
||||||
local name = line:match("^=== RUN%s+(%S+)")
|
local ok, data = pcall(vim.json.decode, line)
|
||||||
if name then
|
if ok and type(data) == "table" and data.Action == "output" and data.Test and data.Output then
|
||||||
current = name
|
if not out[data.Test] then
|
||||||
results[current] = results[current] or {}
|
out[data.Test] = {}
|
||||||
elseif line:match("^%-%-%- %u+:%s+%S+") then
|
end
|
||||||
current = nil
|
for _, item in ipairs(split_output_lines(data.Output)) do
|
||||||
elseif current then
|
table.insert(out[data.Test], item)
|
||||||
results[current] = results[current] or {}
|
end
|
||||||
results[current][#results[current] + 1] = line
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
return out
|
||||||
return results
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function runner.collect_failed_locations(failures, _command, _scope_kind)
|
function runner.collect_failed_locations(failures, command, scope_kind)
|
||||||
|
if type(failures) ~= "table" or #failures == 0 then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local files = {}
|
||||||
|
if scope_kind == "all" then
|
||||||
|
files = collect_go_test_files(command and command.cwd or nil)
|
||||||
|
elseif command and command.file then
|
||||||
|
files = { command.file }
|
||||||
|
end
|
||||||
|
if #files == 0 then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local locations = {}
|
||||||
|
for _, file in ipairs(files) do
|
||||||
|
collect_file_locations(file, locations)
|
||||||
|
end
|
||||||
local items = {}
|
local items = {}
|
||||||
if not failures then
|
local seen = {}
|
||||||
return items
|
local function add_locations(name, locs)
|
||||||
end
|
for _, loc in ipairs(locs or {}) do
|
||||||
|
local key = string.format("%s:%d:%s", loc.filename or "", loc.lnum or 0, loc.text or name or "")
|
||||||
for _, failure in ipairs(failures) do
|
if not seen[key] then
|
||||||
local filename, lnum, col = failure:match("([^:%s]+%.go):(%d+):(%d+)")
|
seen[key] = true
|
||||||
if not filename then
|
table.insert(items, loc)
|
||||||
filename, lnum = failure:match("([^:%s]+%.go):(%d+)")
|
end
|
||||||
end
|
end
|
||||||
if filename and lnum then
|
end
|
||||||
items[#items + 1] = {
|
for _, name in ipairs(failures) do
|
||||||
filename = filename,
|
local direct = locations[name]
|
||||||
lnum = tonumber(lnum),
|
if direct then
|
||||||
col = tonumber(col) or 1,
|
add_locations(name, direct)
|
||||||
text = failure,
|
elseif not name:find("/", 1, true) then
|
||||||
}
|
for full, locs in pairs(locations) do
|
||||||
|
if full:sub(-#name - 1) == "/" .. name then
|
||||||
|
add_locations(full, locs)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return items
|
return items
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
3
run_test.sh
Executable file
3
run_test.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
nvim --headless -u tests/minimal_init.lua -c "PlenaryBustedDirectory tests" -c qa
|
||||||
15
tests/minimal_init.lua
Normal file
15
tests/minimal_init.lua
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
vim.cmd("set rtp^=.")
|
||||||
|
|
||||||
|
local data_path = vim.fn.stdpath("data")
|
||||||
|
local plenary_paths = {
|
||||||
|
data_path .. "/site/pack/packer/start/plenary.nvim",
|
||||||
|
data_path .. "/lazy/plenary.nvim",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path in ipairs(plenary_paths) do
|
||||||
|
if vim.fn.isdirectory(path) == 1 then
|
||||||
|
vim.cmd("set rtp^=" .. path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.cmd("runtime! plugin/plenary.vim")
|
||||||
226
tests/test_go_runner_spec.lua
Normal file
226
tests/test_go_runner_spec.lua
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
local runner = require("test-samurai-go-runner")
|
||||||
|
|
||||||
|
describe("test-samurai-go-runner", function()
|
||||||
|
it("detects Go test files by suffix", function()
|
||||||
|
local bufnr1 = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr1, "/tmp/go_suffix_test.go")
|
||||||
|
assert.is_true(runner.is_test_file(bufnr1))
|
||||||
|
|
||||||
|
local bufnr2 = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr2, "/tmp/go_main.go")
|
||||||
|
assert.is_false(runner.is_test_file(bufnr2))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("finds subtest when cursor is inside t.Run block", function()
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/go_subtest_test.go")
|
||||||
|
local lines = {
|
||||||
|
"package main",
|
||||||
|
"import \"testing\"",
|
||||||
|
"",
|
||||||
|
"func TestFoo(t *testing.T) {",
|
||||||
|
" t.Run(\"first\", func(t *testing.T) {",
|
||||||
|
" -- inside first",
|
||||||
|
" })",
|
||||||
|
"",
|
||||||
|
" t.Run(\"second\", func(t *testing.T) {",
|
||||||
|
" -- inside second",
|
||||||
|
" })",
|
||||||
|
"}",
|
||||||
|
}
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||||
|
|
||||||
|
local orig_fs_find = vim.fs.find
|
||||||
|
vim.fs.find = function(_, _)
|
||||||
|
return { "/tmp/go.mod" }
|
||||||
|
end
|
||||||
|
|
||||||
|
local row_inside_first = 5
|
||||||
|
local spec, err = runner.find_nearest(bufnr, row_inside_first, 0)
|
||||||
|
|
||||||
|
vim.fs.find = orig_fs_find
|
||||||
|
|
||||||
|
assert.is_nil(err)
|
||||||
|
assert.is_not_nil(spec)
|
||||||
|
assert.equals("TestFoo/first", spec.test_path)
|
||||||
|
assert.equals("subtest", spec.scope)
|
||||||
|
assert.is_true(spec.file:match("go_subtest_test%.go$") ~= nil)
|
||||||
|
assert.is_true(spec.cwd:match("/tmp$") ~= nil)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("falls back to whole test function when between subtests", function()
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/go_between_test.go")
|
||||||
|
local lines = {
|
||||||
|
"package main",
|
||||||
|
"import \"testing\"",
|
||||||
|
"",
|
||||||
|
"func TestFoo(t *testing.T) {",
|
||||||
|
" t.Run(\"first\", func(t *testing.T) {",
|
||||||
|
" -- inside first",
|
||||||
|
" })",
|
||||||
|
"",
|
||||||
|
" t.Run(\"second\", func(t *testing.T) {",
|
||||||
|
" -- inside second",
|
||||||
|
" })",
|
||||||
|
"}",
|
||||||
|
}
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||||
|
|
||||||
|
local orig_fs_find = vim.fs.find
|
||||||
|
vim.fs.find = function(_, _)
|
||||||
|
return { "/tmp/go.mod" }
|
||||||
|
end
|
||||||
|
|
||||||
|
local row_between = 7
|
||||||
|
local spec, err = runner.find_nearest(bufnr, row_between, 0)
|
||||||
|
|
||||||
|
vim.fs.find = orig_fs_find
|
||||||
|
|
||||||
|
assert.is_nil(err)
|
||||||
|
assert.is_not_nil(spec)
|
||||||
|
assert.equals("TestFoo", spec.test_path)
|
||||||
|
assert.equals("function", spec.scope)
|
||||||
|
assert.is_true(spec.file:match("go_between_test%.go$") ~= nil)
|
||||||
|
assert.is_true(spec.cwd:match("/tmp$") ~= nil)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("build_command uses current package and correct run pattern", function()
|
||||||
|
local spec_sub = {
|
||||||
|
file = "/tmp/project/pkg/foo_test.go",
|
||||||
|
cwd = "/tmp/project",
|
||||||
|
test_path = "TestFoo/first",
|
||||||
|
scope = "subtest",
|
||||||
|
}
|
||||||
|
|
||||||
|
local cmd_spec_sub = runner.build_command(spec_sub)
|
||||||
|
assert.are.same(
|
||||||
|
{ "go", "test", "-json", "-v", "./pkg", "-run", "^TestFoo$/^first$" },
|
||||||
|
cmd_spec_sub.cmd
|
||||||
|
)
|
||||||
|
|
||||||
|
local spec_func = {
|
||||||
|
file = "/tmp/project/foo_test.go",
|
||||||
|
cwd = "/tmp/project",
|
||||||
|
test_path = "TestFoo",
|
||||||
|
scope = "function",
|
||||||
|
}
|
||||||
|
|
||||||
|
local cmd_spec_func = runner.build_command(spec_func)
|
||||||
|
assert.are.same(
|
||||||
|
{ "go", "test", "-json", "-v", "./", "-run", "^TestFoo$" },
|
||||||
|
cmd_spec_func.cmd
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("build_file_command uses exact test names from current file", function()
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/project/get_test.go")
|
||||||
|
local lines = {
|
||||||
|
"package main",
|
||||||
|
"import \"testing\"",
|
||||||
|
"",
|
||||||
|
"func TestHandleGet(t *testing.T) {",
|
||||||
|
" t.Run(\"returns_200\", func(t *testing.T) {",
|
||||||
|
" -- inside test",
|
||||||
|
" })",
|
||||||
|
"}",
|
||||||
|
}
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||||
|
|
||||||
|
local orig_fs_find = vim.fs.find
|
||||||
|
vim.fs.find = function(_, _)
|
||||||
|
return { "/tmp/project/go.mod" }
|
||||||
|
end
|
||||||
|
|
||||||
|
local cmd_spec = runner.build_file_command(bufnr)
|
||||||
|
|
||||||
|
vim.fs.find = orig_fs_find
|
||||||
|
|
||||||
|
assert.are.same(
|
||||||
|
{ "go", "test", "-json", "-v", "./", "-run", "^(TestHandleGet)$" },
|
||||||
|
cmd_spec.cmd
|
||||||
|
)
|
||||||
|
assert.equals("/tmp/project", cmd_spec.cwd)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("parse_results reports subtests and display names", function()
|
||||||
|
local output = table.concat({
|
||||||
|
vim.json.encode({ Action = "run", Test = "TestFoo" }),
|
||||||
|
vim.json.encode({ Action = "pass", Test = "TestFoo/first" }),
|
||||||
|
vim.json.encode({ Action = "fail", Test = "TestFoo/second" }),
|
||||||
|
vim.json.encode({ Action = "skip", Test = "TestFoo/third" }),
|
||||||
|
}, "\n")
|
||||||
|
|
||||||
|
local results = runner.parse_results(output)
|
||||||
|
assert.are.same({ "TestFoo/first" }, results.passes)
|
||||||
|
assert.are.same({ "TestFoo/second" }, results.failures)
|
||||||
|
assert.are.same({ "TestFoo/third" }, results.skips)
|
||||||
|
assert.are.same({ "TestFoo/first" }, results.display.passes)
|
||||||
|
assert.are.same({ "TestFoo/second" }, results.display.failures)
|
||||||
|
assert.are.same({ "TestFoo/third" }, results.display.skips)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("orders parent tests before subtests within each status", function()
|
||||||
|
local output = table.concat({
|
||||||
|
vim.json.encode({ Action = "fail", Test = "TestHandleGet/returns_200" }),
|
||||||
|
vim.json.encode({ Action = "fail", Test = "TestHandleGet" }),
|
||||||
|
vim.json.encode({ Action = "pass", Test = "TestOther/alpha" }),
|
||||||
|
vim.json.encode({ Action = "pass", Test = "TestOther" }),
|
||||||
|
}, "\n")
|
||||||
|
|
||||||
|
local results = runner.parse_results(output)
|
||||||
|
assert.are.same({ "TestHandleGet", "TestHandleGet/returns_200" }, results.failures)
|
||||||
|
assert.are.same({ "TestOther", "TestOther/alpha" }, results.passes)
|
||||||
|
assert.are.same({ "TestHandleGet", "TestHandleGet/returns_200" }, results.display.failures)
|
||||||
|
assert.are.same({ "TestOther", "TestOther/alpha" }, results.display.passes)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("parse_test_output groups output per test", function()
|
||||||
|
local output = table.concat({
|
||||||
|
vim.json.encode({ Action = "output", Test = "TestFoo", Output = "line1\n" }),
|
||||||
|
vim.json.encode({ Action = "output", Test = "TestFoo", Output = "line2\n" }),
|
||||||
|
vim.json.encode({ Action = "output", Test = "TestFoo/first", Output = "sub1\n" }),
|
||||||
|
}, "\n")
|
||||||
|
|
||||||
|
local results = runner.parse_test_output(output)
|
||||||
|
assert.are.same({ "line1", "line2" }, results["TestFoo"])
|
||||||
|
assert.are.same({ "sub1" }, results["TestFoo/first"])
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("build_failed_command narrows to failed tests", function()
|
||||||
|
local last_command = {
|
||||||
|
cmd = { "go", "test", "-json", "-v", "./", "-run", "^TestFoo($|/)" },
|
||||||
|
cwd = "/tmp/project",
|
||||||
|
}
|
||||||
|
local failures = { "TestFoo/first", "TestBar" }
|
||||||
|
|
||||||
|
local cmd_spec = runner.build_failed_command(last_command, failures, "file")
|
||||||
|
assert.are.same(
|
||||||
|
{ "go", "test", "-json", "-v", "./", "-run", "(^TestFoo$/^first$|^TestBar$)" },
|
||||||
|
cmd_spec.cmd
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("collect_failed_locations finds subtest positions", function()
|
||||||
|
local temp_dir = vim.fn.tempname()
|
||||||
|
vim.fn.mkdir(temp_dir, "p")
|
||||||
|
local file = temp_dir .. "/sample_test.go"
|
||||||
|
local lines = {
|
||||||
|
"package main",
|
||||||
|
"import \"testing\"",
|
||||||
|
"",
|
||||||
|
"func TestFoo(t *testing.T) {",
|
||||||
|
" t.Run(\"first\", func(t *testing.T) {",
|
||||||
|
" -- inside test",
|
||||||
|
" })",
|
||||||
|
"}",
|
||||||
|
}
|
||||||
|
vim.fn.writefile(lines, file)
|
||||||
|
|
||||||
|
local items = runner.collect_failed_locations({ "TestFoo/first" }, { cwd = temp_dir }, "all")
|
||||||
|
assert.is_true(#items > 0)
|
||||||
|
assert.equals(file, items[1].filename)
|
||||||
|
assert.equals(5, items[1].lnum)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
Reference in New Issue
Block a user