From 3615ac0e2f252c615110a4474cef6c92f5aa784c Mon Sep 17 00:00:00 2001 From: "M.Schirmer" Date: Mon, 29 Dec 2025 21:13:17 +0100 Subject: [PATCH] write all failed test into the quickfix list --- lua/test-samurai/core.lua | 102 +++++ lua/test-samurai/runners/go.lua | 100 +++++ lua/test-samurai/runners/js.lua | 131 ++++++- tests/test_samurai_output_spec.lua | 57 +++ tests/test_samurai_quickfix_js_spec.lua | 97 +++++ tests/test_samurai_quickfix_spec.lua | 409 ++++++++++++++++++++ tests/test_samurai_quickfix_vitest_spec.lua | 102 +++++ tests/tmp_qf/foo_test.go | 9 + tests/tmp_qf_go_listing/foo_listing_test.go | 9 + tests/tmp_qf_go_norm/foo_norm_test.go | 9 + tests/tmp_qf_go_sub/foo_sub_test.go | 9 + tests/tmp_qf_go_union/foo_union_test.go | 9 + tests/tmp_qf_js/foo_qf.test.ts | 7 + tests/tmp_qf_jump/jump_test.go | 3 + tests/tmp_qf_jump/output_detail_qn_test.go | 4 + tests/tmp_qf_vitest/foo_qf.test.ts | 7 + tests/tmp_qf_vitest/package.json | 3 + 17 files changed, 1065 insertions(+), 2 deletions(-) create mode 100644 tests/test_samurai_quickfix_js_spec.lua create mode 100644 tests/test_samurai_quickfix_spec.lua create mode 100644 tests/test_samurai_quickfix_vitest_spec.lua create mode 100644 tests/tmp_qf/foo_test.go create mode 100644 tests/tmp_qf_go_listing/foo_listing_test.go create mode 100644 tests/tmp_qf_go_norm/foo_norm_test.go create mode 100644 tests/tmp_qf_go_sub/foo_sub_test.go create mode 100644 tests/tmp_qf_go_union/foo_union_test.go create mode 100644 tests/tmp_qf_js/foo_qf.test.ts create mode 100644 tests/tmp_qf_jump/jump_test.go create mode 100644 tests/tmp_qf_jump/output_detail_qn_test.go create mode 100644 tests/tmp_qf_vitest/foo_qf.test.ts create mode 100644 tests/tmp_qf_vitest/package.json diff --git a/lua/test-samurai/core.lua b/lua/test-samurai/core.lua index 487a7a1..5836d4e 100644 --- a/lua/test-samurai/core.lua +++ b/lua/test-samurai/core.lua @@ -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", "z", function() M.toggle_detail_full() end, { buffer = buf, nowait = true, silent = true }) + vim.keymap.set("n", "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", "z", function() M.toggle_detail_full() end, { buffer = state.last_buf, nowait = true, silent = true }) + vim.keymap.set("n", "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", "", function() close_detail_float() end, { buffer = buf, nowait = true, silent = true }) + vim.keymap.set("n", "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 diff --git a/lua/test-samurai/runners/go.lua b/lua/test-samurai/runners/go.lua index eb1d735..0f29e03 100644 --- a/lua/test-samurai/runners/go.lua +++ b/lua/test-samurai/runners/go.lua @@ -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 diff --git a/lua/test-samurai/runners/js.lua b/lua/test-samurai/runners/js.lua index 00d0c3a..7f1cfc7 100644 --- a/lua/test-samurai/runners/js.lua +++ b/lua/test-samurai/runners/js.lua @@ -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 diff --git a/tests/test_samurai_output_spec.lua b/tests/test_samurai_output_spec.lua index ee9bc40..f388aba 100644 --- a/tests/test_samurai_output_spec.lua +++ b/tests/test_samurai_output_spec.lua @@ -2201,6 +2201,63 @@ describe("test-samurai output detail view", function() vim.fn.jobstart = orig_jobstart end) + it("schliesst den Test-Container und springt mit qn zum ersten Quickfix-Eintrag", function() + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts and opts.on_stdout then + opts.on_stdout(1, { + vim.json.encode({ Action = "fail", Test = "TestFoo" }), + }, nil) + end + if opts and opts.on_exit then + opts.on_exit(1, 1, nil) + end + return 1 + end + + local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf_jump") + vim.fn.mkdir(root, "p") + local target = root .. "/output_detail_qn_test.go" + vim.fn.writefile({ + "package foo", + "", + "func TestFoo(t *testing.T) {", + "}", + }, target) + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, target) + vim.bo[bufnr].filetype = "go" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + "package foo", + "", + "func TestFoo(t *testing.T) {", + "}", + }) + vim.api.nvim_set_current_buf(bufnr) + + test_samurai.test_file() + + local wins = find_float_wins() + assert.equals(1, #wins) + + vim.wait(20, function() + return #vim.fn.getqflist() > 0 + end) + + local keys = vim.api.nvim_replace_termcodes("qn", true, false, true) + vim.api.nvim_feedkeys(keys, "x", false) + vim.wait(20, function() + return #find_float_wins() == 0 + end) + + assert.equals(target, vim.api.nvim_buf_get_name(0)) + local cursor = vim.api.nvim_win_get_cursor(0) + assert.equals(3, cursor[1]) + + vim.fn.jobstart = orig_jobstart + end) + it("disables hardtime in listing/detail and restores on close", function() local orig_hardtime = package.loaded["hardtime"] local disable_calls = 0 diff --git a/tests/test_samurai_quickfix_js_spec.lua b/tests/test_samurai_quickfix_js_spec.lua new file mode 100644 index 0000000..2a72a48 --- /dev/null +++ b/tests/test_samurai_quickfix_js_spec.lua @@ -0,0 +1,97 @@ +local test_samurai = require("test-samurai") +local core = require("test-samurai.core") + +local function close_output_container() + local keys = vim.api.nvim_replace_termcodes("", true, false, true) + local attempts = 5 + while attempts > 0 do + local float_win = nil + for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + local cfg = vim.api.nvim_win_get_config(win) + if cfg.relative ~= "" then + float_win = win + break + end + end + if not float_win then + break + end + vim.api.nvim_set_current_win(float_win) + vim.api.nvim_feedkeys(keys, "x", false) + vim.wait(20, function() + for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + local cfg = vim.api.nvim_win_get_config(win) + if cfg.relative ~= "" then + return false + end + end + return true + end) + attempts = attempts - 1 + end +end + +local function stub_jobstart(opts_config) + local orig = vim.fn.jobstart + local config = opts_config or {} + vim.fn.jobstart = function(_cmd, opts) + local out = config.stdout or nil + if out and opts and opts.on_stdout then + if type(out) == "string" then + out = { out } + end + opts.on_stdout(1, out, nil) + end + if opts and opts.on_exit then + opts.on_exit(1, config.exit_code or 0, nil) + end + return 1 + end + return orig +end + +describe("test-samurai quickfix (js)", function() + before_each(function() + test_samurai.setup() + end) + + after_each(function() + close_output_container() + vim.fn.setqflist({}, "r") + end) + + it("mappt jest-verbose Failures auf die Zeile des Tests", function() + local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf_js") + vim.fn.mkdir(root, "p") + local path = root .. "/foo_qf.test.ts" + vim.fn.writefile({ + 'describe("outer", function() {', + ' it("inner 1", function() {', + " })", + "", + ' it("inner 2", function() {', + " })", + "})", + }, path) + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, path) + vim.bo[bufnr].filetype = "typescript" + vim.api.nvim_set_current_buf(bufnr) + + local fail_symbol = string.char(0xE2, 0x9C, 0x95) + local orig_jobstart = stub_jobstart({ + exit_code = 1, + stdout = { " " .. fail_symbol .. " inner 2" }, + }) + + core.run_file() + + local qf = vim.fn.getqflist() + assert.equals(1, #qf) + assert.equals(path, vim.fn.bufname(qf[1].bufnr)) + assert.equals(5, qf[1].lnum) + + vim.fn.jobstart = orig_jobstart + end) +end) diff --git a/tests/test_samurai_quickfix_spec.lua b/tests/test_samurai_quickfix_spec.lua new file mode 100644 index 0000000..ea6009f --- /dev/null +++ b/tests/test_samurai_quickfix_spec.lua @@ -0,0 +1,409 @@ +local test_samurai = require("test-samurai") +local core = require("test-samurai.core") + +local function close_output_container() + local keys = vim.api.nvim_replace_termcodes("", true, false, true) + local attempts = 5 + while attempts > 0 do + local float_win = nil + for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + local cfg = vim.api.nvim_win_get_config(win) + if cfg.relative ~= "" then + float_win = win + break + end + end + if not float_win then + break + end + vim.api.nvim_set_current_win(float_win) + vim.api.nvim_feedkeys(keys, "x", false) + vim.wait(20, function() + for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + local cfg = vim.api.nvim_win_get_config(win) + if cfg.relative ~= "" then + return false + end + end + return true + end) + attempts = attempts - 1 + end +end + +local function stub_jobstart(opts_config) + local orig = vim.fn.jobstart + local idx = 0 + local config = opts_config or {} + vim.fn.jobstart = function(cmd, opts) + idx = idx + 1 + local code = 0 + if type(config.exit_codes) == "table" then + code = config.exit_codes[idx] or 0 + elseif type(config.exit_codes) == "number" then + code = config.exit_codes + end + local out = config.stdout and config.stdout[idx] or nil + if out and opts and opts.on_stdout then + if type(out) == "string" then + out = { out } + end + opts.on_stdout(1, out, nil) + end + if opts and opts.on_exit then + opts.on_exit(1, code, nil) + end + return 1 + end + return orig +end + +describe("test-samurai quickfix", function() + before_each(function() + test_samurai.setup() + end) + + after_each(function() + close_output_container() + vim.fn.setqflist({}, "r") + end) + + it("fuellt die Quickfix-Liste mit Fehltests und leert sie bei Erfolg", function() + local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf") + vim.fn.mkdir(root, "p") + local path = root .. "/foo_test.go" + vim.fn.writefile({ + "package foo", + "", + "func TestFoo(t *testing.T) {", + ' t.Run("bar", func(t *testing.T) {', + " })", + "}", + "", + "func TestBaz(t *testing.T) {", + "}", + }, path) + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, path) + vim.bo[bufnr].filetype = "go" + vim.api.nvim_set_current_buf(bufnr) + + local orig_jobstart = stub_jobstart({ + exit_codes = { 1, 0 }, + stdout = { + { + vim.json.encode({ Action = "fail", Test = "TestFoo/bar" }), + vim.json.encode({ Action = "fail", Test = "TestBaz" }), + }, + { vim.json.encode({ Action = "pass", Test = "TestFoo" }) }, + }, + }) + + core.run_file() + + local first = vim.fn.getqflist() + assert.equals(2, #first) + assert.equals(path, vim.fn.bufname(first[1].bufnr)) + assert.equals(4, first[1].lnum) + assert.equals(path, vim.fn.bufname(first[2].bufnr)) + assert.equals(8, first[2].lnum) + + close_output_container() + vim.api.nvim_set_current_buf(bufnr) + + core.run_file() + + local second = vim.fn.getqflist() + assert.equals(0, #second) + + vim.fn.jobstart = orig_jobstart + end) + + it("enthaelt bei Go auch Eltern- und Subtest-Failures im Quickfix", function() + local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf_go_sub") + vim.fn.mkdir(root, "p") + local path = root .. "/foo_sub_test.go" + vim.fn.writefile({ + "package foo", + "", + "func TestAwesomeThing(t *testing.T) {", + ' t.Run("evergreen", func(t *testing.T) {', + " })", + "", + ' t.Run("everred", func(t *testing.T) {', + " })", + "}", + }, path) + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, path) + vim.bo[bufnr].filetype = "go" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + "package foo", + "", + "func TestAwesomeThing(t *testing.T) {", + ' t.Run("evergreen", func(t *testing.T) {', + " })", + "", + ' t.Run("everred", func(t *testing.T) {', + " })", + "}", + }) + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 6, 0 }) + + local orig_jobstart = stub_jobstart({ + exit_codes = { 1 }, + stdout = { + { + vim.json.encode({ Action = "fail", Test = "TestAwesomeThing/everred" }), + vim.json.encode({ Action = "fail", Test = "TestAwesomeThing" }), + }, + }, + }) + + core.run_nearest() + + local qf = vim.fn.getqflist() + assert.equals(2, #qf) + assert.equals(path, vim.fn.bufname(qf[1].bufnr)) + assert.equals(path, vim.fn.bufname(qf[2].bufnr)) + local lines = { qf[1].lnum, qf[2].lnum } + table.sort(lines) + assert.are.same({ 3, 7 }, lines) + + vim.fn.jobstart = orig_jobstart + end) + + it("vereinigt Failures aus Parser und Scope fuer Go", function() + local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf_go_union") + vim.fn.mkdir(root, "p") + local path = root .. "/foo_union_test.go" + vim.fn.writefile({ + "package foo", + "", + "func TestAwesomeThing(t *testing.T) {", + ' t.Run(\"evergreen\", func(t *testing.T) {', + " })", + "", + ' t.Run(\"everred\", func(t *testing.T) {', + " })", + "}", + }, path) + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, path) + vim.bo[bufnr].filetype = "go" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + "package foo", + "", + "func TestAwesomeThing(t *testing.T) {", + ' t.Run(\"evergreen\", func(t *testing.T) {', + " })", + "", + ' t.Run(\"everred\", func(t *testing.T) {', + " })", + "}", + }) + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 6, 0 }) + + local go = require("test-samurai.runners.go") + local orig_parser = go.output_parser + go.output_parser = function() + local seen = 0 + return { + on_line = function() + seen = seen + 1 + if seen == 1 then + return { + passes = {}, + failures = { "TestAwesomeThing/everred" }, + skips = {}, + display = { passes = {}, failures = { "everred" }, skips = {} }, + failures_all = { "TestAwesomeThing" }, + } + end + return { + passes = {}, + failures = { "TestAwesomeThing" }, + skips = {}, + display = { passes = {}, failures = { "TestAwesomeThing" }, skips = {} }, + failures_all = { "TestAwesomeThing" }, + } + end, + on_complete = function() + return nil + end, + } + end + + local orig_jobstart = stub_jobstart({ + exit_codes = { 1 }, + stdout = { + { + vim.json.encode({ Action = "fail", Test = "TestAwesomeThing/everred" }), + vim.json.encode({ Action = "fail", Test = "TestAwesomeThing" }), + }, + }, + }) + + core.run_nearest() + + local qf = vim.fn.getqflist() + assert.equals(2, #qf) + assert.equals(path, vim.fn.bufname(qf[1].bufnr)) + assert.equals(path, vim.fn.bufname(qf[2].bufnr)) + local lines = { qf[1].lnum, qf[2].lnum } + table.sort(lines) + assert.are.same({ 3, 7 }, lines) + + vim.fn.jobstart = orig_jobstart + go.output_parser = orig_parser + end) + + it("nutzt Listing-Namen wenn Parser keine Failure-Liste liefert", function() + local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf_go_listing") + vim.fn.mkdir(root, "p") + local path = root .. "/foo_listing_test.go" + vim.fn.writefile({ + "package foo", + "", + "func TestAwesomeThing(t *testing.T) {", + ' t.Run(\"evergreen\", func(t *testing.T) {', + " })", + "", + ' t.Run(\"everred\", func(t *testing.T) {', + " })", + "}", + }, path) + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, path) + vim.bo[bufnr].filetype = "go" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + "package foo", + "", + "func TestAwesomeThing(t *testing.T) {", + ' t.Run(\"evergreen\", func(t *testing.T) {', + " })", + "", + ' t.Run(\"everred\", func(t *testing.T) {', + " })", + "}", + }) + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 6, 0 }) + + local go = require("test-samurai.runners.go") + local orig_parser = go.output_parser + go.output_parser = function() + local step = 0 + return { + on_line = function() + step = step + 1 + if step == 1 then + return { + passes = {}, + failures = {}, + skips = {}, + display = { passes = {}, failures = { "TestAwesomeThing" }, skips = {} }, + } + end + return { + passes = {}, + failures = {}, + skips = {}, + display = { passes = {}, failures = { "everred" }, skips = {} }, + } + end, + on_complete = function() + return nil + end, + } + end + + local orig_jobstart = stub_jobstart({ + exit_codes = { 1 }, + stdout = { + { + vim.json.encode({ Action = "fail", Test = "TestAwesomeThing/everred" }), + vim.json.encode({ Action = "fail", Test = "TestAwesomeThing" }), + }, + }, + }) + + core.run_nearest() + + local qf = vim.fn.getqflist() + assert.equals(2, #qf) + assert.equals(path, vim.fn.bufname(qf[1].bufnr)) + assert.equals(path, vim.fn.bufname(qf[2].bufnr)) + local lines = { qf[1].lnum, qf[2].lnum } + table.sort(lines) + assert.are.same({ 3, 7 }, lines) + + vim.fn.jobstart = orig_jobstart + go.output_parser = orig_parser + end) + + it("mappt Go-Subtests mit durch Unterstriche normalisierten Namen", function() + local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf_go_norm") + vim.fn.mkdir(root, "p") + local path = root .. "/foo_norm_test.go" + vim.fn.writefile({ + "package foo", + "", + "func TestHandleGet(t *testing.T) {", + ' t.Run(\"returns 200 with an list of all badges\", func(t *testing.T) {', + " })", + "", + ' t.Run(\"returns 500 on any db error\", func(t *testing.T) {', + " })", + "}", + }, path) + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, path) + vim.bo[bufnr].filetype = "go" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + "package foo", + "", + "func TestHandleGet(t *testing.T) {", + ' t.Run(\"returns 200 with an list of all badges\", func(t *testing.T) {', + " })", + "", + ' t.Run(\"returns 500 on any db error\", func(t *testing.T) {', + " })", + "}", + }) + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 6, 0 }) + + local orig_jobstart = stub_jobstart({ + exit_codes = { 1 }, + stdout = { + { + vim.json.encode({ + Action = "fail", + Test = "TestHandleGet/returns_500_on_any_db_error", + }), + vim.json.encode({ Action = "fail", Test = "TestHandleGet" }), + }, + }, + }) + + core.run_nearest() + + local qf = vim.fn.getqflist() + assert.equals(2, #qf) + assert.equals(path, vim.fn.bufname(qf[1].bufnr)) + assert.equals(path, vim.fn.bufname(qf[2].bufnr)) + local lines = { qf[1].lnum, qf[2].lnum } + table.sort(lines) + assert.are.same({ 3, 7 }, lines) + + vim.fn.jobstart = orig_jobstart + end) +end) diff --git a/tests/test_samurai_quickfix_vitest_spec.lua b/tests/test_samurai_quickfix_vitest_spec.lua new file mode 100644 index 0000000..6377a28 --- /dev/null +++ b/tests/test_samurai_quickfix_vitest_spec.lua @@ -0,0 +1,102 @@ +local test_samurai = require("test-samurai") +local core = require("test-samurai.core") + +local function close_output_container() + local keys = vim.api.nvim_replace_termcodes("", true, false, true) + local attempts = 5 + while attempts > 0 do + local float_win = nil + for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + local cfg = vim.api.nvim_win_get_config(win) + if cfg.relative ~= "" then + float_win = win + break + end + end + if not float_win then + break + end + vim.api.nvim_set_current_win(float_win) + vim.api.nvim_feedkeys(keys, "x", false) + vim.wait(20, function() + for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + local cfg = vim.api.nvim_win_get_config(win) + if cfg.relative ~= "" then + return false + end + end + return true + end) + attempts = attempts - 1 + end +end + +local function stub_jobstart(opts_config) + local orig = vim.fn.jobstart + local config = opts_config or {} + vim.fn.jobstart = function(_cmd, opts) + local out = config.stdout or nil + if out and opts and opts.on_stdout then + if type(out) == "string" then + out = { out } + end + opts.on_stdout(1, out, nil) + end + if opts and opts.on_exit then + opts.on_exit(1, config.exit_code or 0, nil) + end + return 1 + end + return orig +end + +describe("test-samurai quickfix (vitest)", function() + before_each(function() + test_samurai.setup() + end) + + after_each(function() + close_output_container() + vim.fn.setqflist({}, "r") + end) + + it("mappt tap-flat Failures mit >-Trenner auf die Testzeile", function() + local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf_vitest") + vim.fn.mkdir(root, "p") + local path = root .. "/foo_qf.test.ts" + local pkg = root .. "/package.json" + vim.fn.writefile({ + "{", + ' "devDependencies": { "vitest": "^1.0.0" }', + "}", + }, pkg) + vim.fn.writefile({ + 'describe("outer", function() {', + ' it("inner 1", function() {', + " })", + "", + ' it("inner 2", function() {', + " })", + "})", + }, path) + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, path) + vim.bo[bufnr].filetype = "typescript" + vim.api.nvim_set_current_buf(bufnr) + + local orig_jobstart = stub_jobstart({ + exit_code = 1, + stdout = { "not ok 1 - outer > inner 2 # time=12.3ms" }, + }) + + core.run_file() + + local qf = vim.fn.getqflist() + assert.equals(1, #qf) + assert.equals(path, vim.fn.bufname(qf[1].bufnr)) + assert.equals(5, qf[1].lnum) + + vim.fn.jobstart = orig_jobstart + end) +end) diff --git a/tests/tmp_qf/foo_test.go b/tests/tmp_qf/foo_test.go new file mode 100644 index 0000000..ef7bc0d --- /dev/null +++ b/tests/tmp_qf/foo_test.go @@ -0,0 +1,9 @@ +package foo + +func TestFoo(t *testing.T) { + t.Run("bar", func(t *testing.T) { + }) +} + +func TestBaz(t *testing.T) { +} diff --git a/tests/tmp_qf_go_listing/foo_listing_test.go b/tests/tmp_qf_go_listing/foo_listing_test.go new file mode 100644 index 0000000..9d9a9b1 --- /dev/null +++ b/tests/tmp_qf_go_listing/foo_listing_test.go @@ -0,0 +1,9 @@ +package foo + +func TestAwesomeThing(t *testing.T) { + t.Run("evergreen", func(t *testing.T) { + }) + + t.Run("everred", func(t *testing.T) { + }) +} diff --git a/tests/tmp_qf_go_norm/foo_norm_test.go b/tests/tmp_qf_go_norm/foo_norm_test.go new file mode 100644 index 0000000..a11d9e6 --- /dev/null +++ b/tests/tmp_qf_go_norm/foo_norm_test.go @@ -0,0 +1,9 @@ +package foo + +func TestHandleGet(t *testing.T) { + t.Run("returns 200 with an list of all badges", func(t *testing.T) { + }) + + t.Run("returns 500 on any db error", func(t *testing.T) { + }) +} diff --git a/tests/tmp_qf_go_sub/foo_sub_test.go b/tests/tmp_qf_go_sub/foo_sub_test.go new file mode 100644 index 0000000..9d9a9b1 --- /dev/null +++ b/tests/tmp_qf_go_sub/foo_sub_test.go @@ -0,0 +1,9 @@ +package foo + +func TestAwesomeThing(t *testing.T) { + t.Run("evergreen", func(t *testing.T) { + }) + + t.Run("everred", func(t *testing.T) { + }) +} diff --git a/tests/tmp_qf_go_union/foo_union_test.go b/tests/tmp_qf_go_union/foo_union_test.go new file mode 100644 index 0000000..9d9a9b1 --- /dev/null +++ b/tests/tmp_qf_go_union/foo_union_test.go @@ -0,0 +1,9 @@ +package foo + +func TestAwesomeThing(t *testing.T) { + t.Run("evergreen", func(t *testing.T) { + }) + + t.Run("everred", func(t *testing.T) { + }) +} diff --git a/tests/tmp_qf_js/foo_qf.test.ts b/tests/tmp_qf_js/foo_qf.test.ts new file mode 100644 index 0000000..c40a8ea --- /dev/null +++ b/tests/tmp_qf_js/foo_qf.test.ts @@ -0,0 +1,7 @@ +describe("outer", function() { + it("inner 1", function() { + }) + + it("inner 2", function() { + }) +}) diff --git a/tests/tmp_qf_jump/jump_test.go b/tests/tmp_qf_jump/jump_test.go new file mode 100644 index 0000000..48fbd08 --- /dev/null +++ b/tests/tmp_qf_jump/jump_test.go @@ -0,0 +1,3 @@ +package foo + +func TestFoo(t *testing.T) {} diff --git a/tests/tmp_qf_jump/output_detail_qn_test.go b/tests/tmp_qf_jump/output_detail_qn_test.go new file mode 100644 index 0000000..cddd917 --- /dev/null +++ b/tests/tmp_qf_jump/output_detail_qn_test.go @@ -0,0 +1,4 @@ +package foo + +func TestFoo(t *testing.T) { +} diff --git a/tests/tmp_qf_vitest/foo_qf.test.ts b/tests/tmp_qf_vitest/foo_qf.test.ts new file mode 100644 index 0000000..c40a8ea --- /dev/null +++ b/tests/tmp_qf_vitest/foo_qf.test.ts @@ -0,0 +1,7 @@ +describe("outer", function() { + it("inner 1", function() { + }) + + it("inner 2", function() { + }) +}) diff --git a/tests/tmp_qf_vitest/package.json b/tests/tmp_qf_vitest/package.json new file mode 100644 index 0000000..d9e67ee --- /dev/null +++ b/tests/tmp_qf_vitest/package.json @@ -0,0 +1,3 @@ +{ + "devDependencies": { "vitest": "^1.0.0" } +}