write all failed test into the quickfix list

This commit is contained in:
2025-12-29 21:13:17 +01:00
parent b916a11481
commit 3615ac0e2f
17 changed files with 1065 additions and 2 deletions

View File

@@ -35,6 +35,7 @@ local apply_border_kind
local close_container
local restore_listing_full
local close_detail_float
local jump_to_first_quickfix
local function disable_container_maps(buf)
local opts = { buffer = buf, nowait = true, silent = true }
@@ -356,6 +357,14 @@ close_container = function()
end
end
jump_to_first_quickfix = function()
close_container()
local info = vim.fn.getqflist({ size = 0 })
if type(info) == "table" and (info.size or 0) > 0 then
vim.cmd("cfirst")
end
end
close_detail_float = function()
if state.detail_win and vim.api.nvim_win_is_valid(state.detail_win) then
pcall(vim.api.nvim_win_close, state.detail_win, true)
@@ -481,6 +490,9 @@ local function create_output_win(initial_lines)
vim.keymap.set("n", "<leader>z", function()
M.toggle_detail_full()
end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>qn", function()
jump_to_first_quickfix()
end, { buffer = buf, nowait = true, silent = true })
disable_container_maps(buf)
state.last_win = listing
@@ -533,6 +545,9 @@ local function reopen_output_win()
vim.keymap.set("n", "<leader>z", function()
M.toggle_detail_full()
end, { buffer = state.last_buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>qn", function()
jump_to_first_quickfix()
end, { buffer = state.last_buf, nowait = true, silent = true })
disable_container_maps(state.last_buf)
state.last_win = win
@@ -845,6 +860,9 @@ local function ensure_detail_buf(lines)
vim.keymap.set("n", "<C-c>", function()
close_detail_float()
end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>qn", function()
jump_to_first_quickfix()
end, { buffer = buf, nowait = true, silent = true })
disable_container_maps(buf)
end
local clean_lines, highlights = parse_ansi_lines(normalize_output_lines(lines))
@@ -1243,16 +1261,41 @@ local function apply_result_highlights(buf, start_line, lines)
end
end
local function collect_failure_names_from_listing()
if not (state.last_buf and vim.api.nvim_buf_is_valid(state.last_buf)) then
return {}
end
local lines = vim.api.nvim_buf_get_lines(state.last_buf, 0, -1, false)
local out = {}
local seen = {}
for idx, line in ipairs(lines) do
if line:match("^%[ FAIL %]") then
local name = state.last_result_line_map[idx]
if not name then
name = line:match("^%[ FAIL %] %-%s*(.+)$")
end
if name and name ~= "" and not seen[name] then
seen[name] = true
table.insert(out, name)
end
end
end
return out
end
local function run_command(command, opts)
local options = opts or {}
state.last_test_outputs = {}
state.last_result_line_map = {}
state.last_raw_output = nil
local failures = {}
local failures_seen = {}
if command and type(command.cmd) == "table" and #command.cmd > 0 then
if options.save_last ~= false then
state.last_command = {
cmd = vim.deepcopy(command.cmd),
cwd = command.cwd,
file = command.file,
}
state.last_runner = options.runner
end
@@ -1260,6 +1303,7 @@ local function run_command(command, opts)
state.last_scope_command = {
cmd = vim.deepcopy(command.cmd),
cwd = command.cwd,
file = command.file,
}
state.last_scope_runner = options.runner
state.last_scope_kind = options.scope_kind
@@ -1321,6 +1365,14 @@ local function run_command(command, opts)
return
end
had_parsed_output = true
if type(results.failures) == "table" then
for _, name in ipairs(results.failures) do
if name and name ~= "" and not failures_seen[name] then
failures_seen[name] = true
table.insert(failures, name)
end
end
end
update_summary(summary, results)
update_summary(result_counts, results)
if options.track_scope then
@@ -1446,6 +1498,50 @@ local function run_command(command, opts)
if options.track_scope then
state.last_scope_exit_code = code
end
local items = {}
local failures_for_qf = failures
if options.track_scope and type(state.last_scope_failures) == "table" then
local merged = {}
local seen = {}
for _, name in ipairs(failures or {}) do
if name and not seen[name] then
seen[name] = true
table.insert(merged, name)
end
end
for _, name in ipairs(state.last_scope_failures or {}) do
if name and not seen[name] then
seen[name] = true
table.insert(merged, name)
end
end
failures_for_qf = merged
end
local listing_failures = collect_failure_names_from_listing()
if #listing_failures > 0 then
local merged = {}
local seen = {}
for _, name in ipairs(failures_for_qf or {}) do
if name and not seen[name] then
seen[name] = true
table.insert(merged, name)
end
end
for _, name in ipairs(listing_failures) do
if name and not seen[name] then
seen[name] = true
table.insert(merged, name)
end
end
failures_for_qf = merged
end
if #failures_for_qf > 0 and runner and type(runner.collect_failed_locations) == "function" then
local ok_collect, collected = pcall(runner.collect_failed_locations, failures_for_qf, command, options.scope_kind)
if ok_collect and type(collected) == "table" then
items = collected
end
end
vim.fn.setqflist({}, "r", { title = "Test-Samurai Failures", items = items })
end,
})
end
@@ -1507,6 +1603,7 @@ function M.run_nearest()
vim.notify("[test-samurai] Runner failed to build command", vim.log.levels.ERROR)
return
end
command.file = spec.file
local parser = runner.output_parser
if type(parser) == "function" then
@@ -1539,6 +1636,7 @@ function M.run_file()
vim.notify("[test-samurai] Runner failed to build file command", vim.log.levels.ERROR)
return
end
command.file = util.get_buf_path(bufnr)
local parser = runner.output_parser
if type(parser) == "function" then
@@ -1571,6 +1669,7 @@ function M.run_all()
vim.notify("[test-samurai] Runner failed to build all-tests command", vim.log.levels.ERROR)
return
end
command.file = util.get_buf_path(bufnr)
local parser = runner.output_parser
if type(parser) == "function" then
@@ -1625,6 +1724,9 @@ function M.run_failed_only()
end
local runner = state.last_scope_runner
if state.last_scope_command and state.last_scope_command.file then
command.file = state.last_scope_command.file
end
local parser = nil
if runner then
parser = runner.output_parser

View File

@@ -286,6 +286,63 @@ local function split_output_lines(text)
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
function runner.parse_test_output(output)
local out = {}
if not output or output == "" then
@@ -399,4 +456,47 @@ function runner.build_failed_command(last_command, failures, _scope_kind)
}
end
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 seen = {}
local function add_locations(name, locs)
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 "")
if not seen[key] then
seen[key] = true
table.insert(items, loc)
end
end
end
for _, name in ipairs(failures) do
local direct = locations[name]
if direct then
add_locations(name, direct)
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
return items
end
return runner

View File

@@ -186,7 +186,69 @@ local function collect_js_structs(lines)
return describes, tests
end
local function build_full_name(lines, idx, leaf_name)
local build_full_name
local shorten_js_name
local normalize_js_name
local function add_location(target, name, file, line)
if not name or name == "" or not file or file == "" or not line then
return
end
if not target[name] then
target[name] = {}
end
table.insert(target[name], {
filename = file,
lnum = line,
col = 1,
text = name,
})
end
local function collect_js_locations(lines, file, target)
local describes, tests = collect_js_structs(lines)
for _, t in ipairs(tests) do
local full = build_full_name(lines, t.start + 1, t.name)
add_location(target, full, file, t.start + 1)
add_location(target, t.name, file, t.start + 1)
local short = shorten_js_name and shorten_js_name(full) or nil
if short and short ~= full then
add_location(target, short, file, t.start + 1)
end
end
for _, d in ipairs(describes) do
local full = build_full_name(lines, d.start + 1, d.name)
add_location(target, full, file, d.start + 1)
add_location(target, d.name, file, d.start + 1)
local short = shorten_js_name and shorten_js_name(full) or nil
if short and short ~= full then
add_location(target, short, file, d.start + 1)
end
end
end
local function collect_js_test_files(root, patterns)
if not root or root == "" then
root = vim.loop.cwd()
end
local files = {}
local seen = {}
for _, pat in ipairs(patterns or {}) do
local glob = "**/*" .. pat .. "*"
local hits = vim.fn.globpath(root, glob, false, true)
if type(hits) == "table" then
for _, file in ipairs(hits) do
if not seen[file] then
seen[file] = true
table.insert(files, file)
end
end
end
end
return files
end
build_full_name = function(lines, idx, leaf_name)
local parts = { leaf_name }
for i = idx - 1, 1, -1 do
local line = lines[i]
@@ -597,7 +659,17 @@ function M.new(opts)
skip = string.char(0xE2, 0x97, 0x8B),
}
local function shorten_js_name(name)
normalize_js_name = function(name)
if not name or name == "" then
return nil
end
local out = name
out = out:gsub("%s*[%>›»]%s*", " ")
out = out:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
return out
end
shorten_js_name = function(name)
if not name or name == "" then
return nil
end
@@ -1108,6 +1180,61 @@ function M.new(opts)
}
end
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_js_test_files(command and command.cwd or nil, runner.patterns)
else
local file = command and command.file or nil
if not file and command and type(command.cmd) == "table" then
file = find_test_file_arg(command.cmd)
end
if file then
files = { file }
end
end
if #files == 0 then
return {}
end
local locations = {}
for _, file in ipairs(files) do
local ok, lines = pcall(vim.fn.readfile, file)
if ok and type(lines) == "table" then
collect_js_locations(lines, file, locations)
end
end
local items = {}
local seen = {}
for _, name in ipairs(failures) do
local keys = { name }
if normalize_js_name then
local normalized = normalize_js_name(name)
if normalized and normalized ~= name then
table.insert(keys, normalized)
end
end
if shorten_js_name then
local short = shorten_js_name(name)
if short and short ~= name then
table.insert(keys, short)
end
end
for _, key_name in ipairs(keys) do
for _, loc in ipairs(locations[key_name] or {}) do
local key = string.format("%s:%d", loc.filename or "", loc.lnum or 0)
if not seen[key] then
seen[key] = true
table.insert(items, loc)
end
end
end
end
return items
end
return runner
end