Compare commits

..

14 Commits

Author SHA1 Message Date
456a157549 mark listing entry when details are visible
All checks were successful
tests / test (push) Successful in 10s
2026-01-20 09:59:21 +01:00
8c598002e4 show automatically the details after keymap navigating to a failed test entry 2026-01-17 16:04:42 +01:00
bd6930adc0 add new keypmap for quick selecting first entry 2026-01-17 15:44:47 +01:00
24bd89a7be save and restore current cursor position 2026-01-17 15:01:33 +01:00
fd3952bedf fix missing summary after TSamLast call
All checks were successful
tests / test (push) Successful in 9s
2026-01-09 16:24:55 +01:00
238d5f9634 show always detail-float on <cr>
All checks were successful
tests / test (push) Successful in 8s
2026-01-08 13:47:45 +01:00
118f84c31e add convenient keymaps for test-command inspection 2026-01-08 13:21:01 +01:00
924584d8b3 add rerun function from within the listing-float
All checks were successful
tests / test (push) Successful in 8s
2026-01-07 20:11:53 +01:00
5d0b4e9dd6 add keymaps for quick filtering the test-listings
All checks were successful
tests / test (push) Successful in 10s
2026-01-07 17:58:53 +01:00
c538a32307 add documentation for common neovim help 2026-01-07 17:34:02 +01:00
e9a7d2029b add quick help within detail-float 2026-01-07 17:22:50 +01:00
4c2d585f2d update AI helper for runner creations 2026-01-07 17:04:01 +01:00
e315a8e8f2 update ai prompt for external runner creation
All checks were successful
tests / test (push) Successful in 8s
2026-01-03 15:20:01 +01:00
1d9b682a58 fix quickfix-list filling for TSamFailedOnly 2026-01-03 15:19:35 +01:00
7 changed files with 2064 additions and 31 deletions

View File

@@ -11,6 +11,9 @@
- **Keine stillen Änderungen**: - **Keine stillen Änderungen**:
- Bestehende Features dürfen nicht unbemerkt geändert oder ersetzt werden. - Bestehende Features dürfen nicht unbemerkt geändert oder ersetzt werden.
- Notwendige Anpassungen zur Koexistenz mehrerer Features müssen klar erkennbar sein. - Notwendige Anpassungen zur Koexistenz mehrerer Features müssen klar erkennbar sein.
- **Hilfe-Ansicht aktuell halten**:
- Änderungen und/oder Erweiterungen müssen **immer** die `:help`-Dokumentation, die README.md und die `quick-help`-Ansicht (via `?`) automatisch aktualisieren.
- Sprache der Hilfe ist wie die README.md immer **englisch**.
## Projektziel ## Projektziel
- Neovim Plugin: **test-samurai** - Neovim Plugin: **test-samurai**

View File

@@ -59,12 +59,27 @@ If no runner matches the current test file, test-samurai will show:
- `TSamLast` -> `<leader>tl` - `TSamLast` -> `<leader>tl`
- `TSamFailedOnly` -> `<leader>te` - `TSamFailedOnly` -> `<leader>te`
- `TSamShowOutput` -> `<leader>to` - `TSamShowOutput` -> `<leader>to`
- Help: `:help test-samurai`
Additional keymaps: Additional keymaps:
- `<leader>qn` -> close the testing floats and jump to the first quickfix entry - Listing navigation:
- `<leader>nf` -> jump to the next `[ FAIL ]` entry in the Test-Listing-Float (wraps to the first) - `<leader>fn` -> [F]ind [N]ext failed test in listing (wraps to the first, opens Detail-Float, works in Detail-Float)
- `<leader>pf` -> jump to the previous `[ FAIL ]` entry in the Test-Listing-Float (wraps to the last) - `<leader>fp` -> [F]ind [P]revious failed test in listing (wraps to the last, opens Detail-Float, works in Detail-Float)
- `<leader>ff` -> [F]ind [F]irst list entry (opens Detail-Float, works in Detail-Float)
- `<leader>o` -> jump to the test location
- `<leader>qn` -> close the testing floats and jump to the first quickfix entry
- Listing filters:
- `<leader>sf` -> filter the listing to `[ FAIL ] - ...` entries
- `<leader>ss` -> filter the listing to `[ SKIP ] - ...` entries
- `<leader>sa` -> clear the listing filter and show all entries
- Listing actions:
- `<leader>tt` -> run the test under the cursor in the listing
- `<leader>cb` -> breaks test-command onto multiple lines (clears search highlight)
- `<leader>cj` -> joins test-command onto single line
- `?` -> show help with TSam commands and standard keymaps in the Detail-Float
Before running any test command, test-samurai runs `:wall` to save all buffers.
## Output UI ## Output UI
@@ -74,11 +89,15 @@ Additional keymaps:
- After `TSamNearest`, `TSamFile`, `TSamAll`, `TSamFailedOnly`, etc., the UI opens in listing mode (only **Test-Listing-Float** visible). - After `TSamNearest`, `TSamFile`, `TSamAll`, `TSamFailedOnly`, etc., the UI opens in listing mode (only **Test-Listing-Float** visible).
- Press `<cr>` on a `[ FAIL ] ...` line in the listing to open/update the **Detail-Float** as a 20/80 split (left 20% listing, right 80% detail). - Press `<cr>` on a `[ FAIL ] ...` line in the listing to open/update the **Detail-Float** as a 20/80 split (left 20% listing, right 80% detail).
- ANSI color translation is only applied in the **Detail-Float**; the **Test-Listing-Float** shows raw text without ANSI translation. - ANSI color translation is only applied in the **Detail-Float**; the **Test-Listing-Float** shows raw text without ANSI translation.
- `<esc><esc>` hides the floating window; `TSamShowOutput` reopens it. - `<esc><esc>` hides the floating window and restores the cursor position; `TSamShowOutput` reopens it.
- If no output is captured for a test, the **Detail-Float** shows `No output captured`.
- The active listing entry is highlighted while the **Detail-Float** is visible.
- Summary lines (`TOTAL`/`DURATION`) are appended in the listing output, including `TSamLast`.
## Runner architecture ## Runner architecture
Runners are standalone Lua modules. All runner modules are expected to implement the full interface so every command and keymap works. Runners are standalone Lua modules. All runner modules are expected to implement the full interface so every command and keymap works.
All functions are required (including the previously optional ones) and listing output must be streamed.
Required functions: Required functions:
- `is_test_file` - `is_test_file`
@@ -87,6 +106,9 @@ Required functions:
- `build_file_command` - `build_file_command`
- `build_all_command` - `build_all_command`
- `build_failed_command` - `build_failed_command`
- `parse_results`
- `output_parser` (must stream listing output via `on_line`)
- `parse_test_output`
- `collect_failed_locations` - `collect_failed_locations`
No runner for your environment exists? No problem: use `runners-agents.md` to guide an AI-assisted runner implementation tailored to your stack. No runner for your environment exists? No problem: use `runners-agents.md` to guide an AI-assisted runner implementation tailored to your stack.
@@ -100,6 +122,8 @@ No runner for your environment exists? No problem: use `runners-agents.md` to gu
## Development ## Development
Runner development guidelines, including required data formats for keymaps, tests (`run_test.sh`), Gitea CI (Neovim AppImage on ARM runners), and framework-agnostic best practices (naming conventions, TSamNearest priority, reporter payloads, failed-only behavior), are documented in `runner-agents.md`.
Tests are written with `plenary.nvim` / `busted`. Mocks and stubs are allowed. Tests are written with `plenary.nvim` / `busted`. Mocks and stubs are allowed.
Run tests: Run tests:

9
doc/tags Normal file
View File

@@ -0,0 +1,9 @@
test-samurai test-samurai.txt /*test-samurai*
test-samurai-commands test-samurai.txt /*test-samurai-commands*
test-samurai-default-keys test-samurai.txt /*test-samurai-default-keys*
test-samurai-quickhelp test-samurai.txt /*test-samurai-quickhelp*
test-samurai-req test-samurai.txt /*test-samurai-req*
test-samurai-runners test-samurai.txt /*test-samurai-runners*
test-samurai-setup test-samurai.txt /*test-samurai-setup*
test-samurai-ui test-samurai.txt /*test-samurai-ui*
test-samurai.txt test-samurai.txt /*test-samurai.txt*

102
doc/test-samurai.txt Normal file
View File

@@ -0,0 +1,102 @@
*test-samurai.txt* Run tests with a unified UX
INTRODUCTION *test-samurai*
Test Samurai provides a unified UX to run tests across languages
and frameworks, backed by an extensible runner architecture.
REQUIREMENTS *test-samurai-req*
Neovim 0.11.4 or later is required.
SETUP *test-samurai-setup*
This plugin ships without built-in runners. Configure runner modules
in your setup so test commands and keymaps work properly:
>
require("test-samurai").setup({
runner_modules = {
"my-runners.go",
"my-runners.js",
},
})
<
COMMANDS *test-samurai-commands*
:TSamNearest Run the nearest test.
:TSamFile Run all tests in the current file.
:TSamAll Run all tests in the project.
:TSamLast Re-run the last test command.
:TSamFailedOnly Re-run only previously failed tests.
:TSamShowOutput Reopen the Testing-Float output window.
DEFAULT KEYMAPS *test-samurai-default-keys*
TSamNearest <leader>tn
TSamFile <leader>tf
TSamAll <leader>ta
TSamLast <leader>tl
TSamFailedOnly <leader>te
TSamShowOutput <leader>to
QUICK-HELP & FLOATS *test-samurai-quickhelp*
In the Testing-Float, press ? to open the quick-help in the Detail-Float.
Additional keymaps:
Listing navigation:
<leader>fn [F]ind [N]ext failed test in listing (opens Detail-Float; works in Detail-Float)
<leader>fp [F]ind [P]revious failed test in listing (opens Detail-Float; works in Detail-Float)
<leader>ff [F]ind [F]irst list entry (opens Detail-Float; works in Detail-Float)
<leader>o Jump to test location
<leader>qn Close floats + jump to the first quickfix entry
Listing filters:
<leader>sf Filter listing to [ FAIL ] only
<leader>ss Filter listing to [ SKIP ] only
<leader>sa Show all listing entries (clear filter)
Listing actions:
<leader>tt Run the test under the cursor in the listing
<leader>cb breaks test-command onto multiple lines (clears search highlight)
<leader>cj joins test-command onto single line
Testing-Float:
<leader>z Toggle Detail-Float full width
<C-l> Focus Detail-Float (press l again for full)
<C-h> Focus Test-Listing-Float
<esc><esc> Close Testing-Float and restore cursor
<C-c> Close Detail-Float (when focused)
Notes:
Active listing entry is highlighted while the Detail-Float is visible.
Buffers are saved via :wall before every test run.
OUTPUT UI *test-samurai-ui*
Testing-Float: container floating window for output.
Test-Listing-Float: left subwindow listing test results.
Detail-Float: right subwindow showing detailed output for a test.
After running a test command, the UI opens in listing mode (only the
Test-Listing-Float is visible). Press <cr> on a [ FAIL ] entry to open
the Detail-Float with a 20/80 split. ANSI colors are translated only
inside the Detail-Float. If no output is captured for a test, the
Detail-Float shows "No output captured".
Summary lines (TOTAL/DURATION) are rendered in the listing output,
including after :TSamLast.
RUNNER ARCHITECTURE *test-samurai-runners*
Runners are standalone Lua modules that implement the full interface.
See README.md for the required functions and runner guidelines.
==============================================================================
vim:ft=help

View File

@@ -23,6 +23,12 @@ local state = {
detail_win = nil, detail_win = nil,
detail_opening = false, detail_opening = false,
detail_full = false, detail_full = false,
trigger_win = nil,
trigger_buf = nil,
trigger_cursor = nil,
listing_unfiltered_lines = nil,
listing_filtered_kind = nil,
detail_line = nil,
hardtime_refcount = 0, hardtime_refcount = 0,
hardtime_was_enabled = false, hardtime_was_enabled = false,
autocmds_set = false, autocmds_set = false,
@@ -31,11 +37,16 @@ local state = {
local summary_ns = vim.api.nvim_create_namespace("TestSamuraiSummary") local summary_ns = vim.api.nvim_create_namespace("TestSamuraiSummary")
local result_ns = vim.api.nvim_create_namespace("TestSamuraiResult") local result_ns = vim.api.nvim_create_namespace("TestSamuraiResult")
local detail_ns = vim.api.nvim_create_namespace("TestSamuraiDetailAnsi") local detail_ns = vim.api.nvim_create_namespace("TestSamuraiDetailAnsi")
local help_ns = vim.api.nvim_create_namespace("TestSamuraiHelp")
local listing_sel_ns = vim.api.nvim_create_namespace("TestSamuraiListingSelection")
local apply_border_kind local apply_border_kind
local close_container local close_container
local restore_listing_full local restore_listing_full
local close_detail_float local close_detail_float
local jump_to_first_quickfix local jump_to_first_quickfix
local apply_summary_highlights
local apply_result_highlights
local run_command
local function disable_container_maps(buf) local function disable_container_maps(buf)
local opts = { buffer = buf, nowait = true, silent = true } local opts = { buffer = buf, nowait = true, silent = true }
@@ -43,6 +54,170 @@ local function disable_container_maps(buf)
vim.keymap.set("n", "<C-k>", "<Nop>", opts) vim.keymap.set("n", "<C-k>", "<Nop>", opts)
end end
local function help_lines()
return {
"Test-Samurai Help",
"",
"TSam commands:",
" TSamNearest <leader>tn",
" TSamFile <leader>tf",
" TSamAll <leader>ta",
" TSamLast <leader>tl",
" TSamFailedOnly <leader>te",
" TSamShowOutput <leader>to",
"",
"Listing navigation:",
" <leader>fn [F]ind [N]ext failed test in listing (opens Detail-Float; works in Detail-Float)",
" <leader>fp [F]ind [P]revious failed test in listing (opens Detail-Float; works in Detail-Float)",
" <leader>ff [F]ind [F]irst list entry (opens Detail-Float; works in Detail-Float)",
" <leader>o Jump to test location",
" <leader>qn Close floats + jump to first quickfix entry",
"",
"Listing filters:",
" <leader>sf Filter listing to [ FAIL ] only",
" <leader>ss Filter listing to [ SKIP ] only",
" <leader>sa Show all listing entries (clear filter)",
"",
"Listing actions:",
" <leader>tt Run the test under the cursor",
" <leader>cb breaks test-command onto multiple lines (clears search highlight)",
" <leader>cj joins test-command onto single line",
"",
"Testing-Float (Listing):",
" <cr> Open Detail-Float for selected test",
" <esc><esc> Close Testing-Float and restore cursor",
" <C-l> Focus Detail-Float (press l again for full)",
" <C-h> Focus Test-Listing-Float",
" <leader>z Toggle Detail-Float full width",
" ? Show this help",
"",
"Testing-Float (Detail):",
" <esc><esc> Close Testing-Float and restore cursor",
" <C-h> Focus Test-Listing-Float",
" <C-w>h Focus Test-Listing-Float",
" <C-l> Focus Detail-Float",
" <leader>z Toggle Detail-Float full width",
" <C-c> Close Detail-Float",
" ? Show this help",
"",
"Notes:",
" No output captured -> shows placeholder text",
" Active listing entry is highlighted while Detail-Float is visible",
" Buffers are saved via :wall before every test run",
}
end
local function split_listing_sections(lines)
local summary_start = nil
for i, line in ipairs(lines or {}) do
if line:match("^TOTAL%s+%d+") then
summary_start = i - 1
if summary_start < 2 then
summary_start = 2
end
break
end
end
local header = {}
local body = {}
local summary = {}
if lines and #lines > 0 then
header = { lines[1] }
end
for i, line in ipairs(lines or {}) do
if i == 1 then
elseif summary_start and i >= summary_start then
table.insert(summary, line)
else
table.insert(body, line)
end
end
return header, body, summary
end
local function rebuild_result_line_map(lines)
state.last_result_line_map = {}
for idx, line in ipairs(lines or {}) do
local status = line:match("^%[%s*(%u+)%s*%]%s*%-")
if status == "PASS" or status == "FAIL" or status == "SKIP" then
local name = line:match("^%[%s*[%u]+%s*%]%s*%-%s*(.+)$")
if name and name ~= "" then
state.last_result_line_map[idx] = name
end
end
end
end
local function apply_listing_lines(buf, lines)
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
vim.api.nvim_buf_clear_namespace(buf, result_ns, 0, -1)
vim.api.nvim_buf_clear_namespace(buf, summary_ns, 0, -1)
vim.api.nvim_buf_clear_namespace(buf, listing_sel_ns, 0, -1)
apply_result_highlights(buf, 0, lines)
apply_summary_highlights(buf, 0, lines)
rebuild_result_line_map(lines)
end
local function apply_listing_substitution(command)
local buf = vim.api.nvim_get_current_buf()
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
return
end
vim.api.nvim_buf_call(buf, function()
vim.cmd(command)
end)
end
local function apply_listing_filter(kind)
if not (state.last_buf and vim.api.nvim_buf_is_valid(state.last_buf)) then
return
end
if kind == "all" then
if not state.listing_filtered_kind or not state.listing_unfiltered_lines then
return
end
apply_listing_lines(state.last_buf, state.listing_unfiltered_lines)
state.listing_filtered_kind = nil
return
end
if state.listing_filtered_kind == kind then
return
end
local base = state.listing_unfiltered_lines
if not base then
base = vim.api.nvim_buf_get_lines(state.last_buf, 0, -1, false)
state.listing_unfiltered_lines = vim.deepcopy(base)
end
local header, body, summary = split_listing_sections(base)
local filtered = {}
for _, line in ipairs(body) do
if kind == "fail" and line:match("^%[ FAIL %] %-") then
table.insert(filtered, line)
elseif kind == "skip" and line:match("^%[ SKIP %] %-") then
table.insert(filtered, line)
end
end
if #filtered == 0 then
return
end
local combined = {}
if #header > 0 then
table.insert(combined, header[1])
table.insert(combined, "")
end
for _, line in ipairs(filtered) do
table.insert(combined, line)
end
for _, line in ipairs(summary) do
table.insert(combined, line)
end
apply_listing_lines(state.last_buf, combined)
state.listing_filtered_kind = kind
end
local function get_hardtime() local function get_hardtime()
local ok, hardtime = pcall(require, "hardtime") local ok, hardtime = pcall(require, "hardtime")
if not ok or type(hardtime) ~= "table" then if not ok or type(hardtime) ~= "table" then
@@ -107,11 +282,11 @@ local function jump_listing_fail(direction)
local win = vim.api.nvim_get_current_win() local win = vim.api.nvim_get_current_win()
local buf = vim.api.nvim_get_current_buf() local buf = vim.api.nvim_get_current_buf()
if not (buf and vim.api.nvim_buf_is_valid(buf)) then if not (buf and vim.api.nvim_buf_is_valid(buf)) then
return return nil
end end
local total = vim.api.nvim_buf_line_count(buf) local total = vim.api.nvim_buf_line_count(buf)
if total == 0 then if total == 0 then
return return nil
end end
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
local cursor = vim.api.nvim_win_get_cursor(win) local cursor = vim.api.nvim_win_get_cursor(win)
@@ -163,6 +338,76 @@ local function jump_listing_fail(direction)
if target then if target then
vim.api.nvim_win_set_cursor(win, { target, 0 }) vim.api.nvim_win_set_cursor(win, { target, 0 })
end end
return target
end
local function jump_to_first_listing_entry()
local win = vim.api.nvim_get_current_win()
local buf = vim.api.nvim_get_current_buf()
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
return nil
end
local total = vim.api.nvim_buf_line_count(buf)
if total == 0 then
return nil
end
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
for i = 1, total do
if lines[i] and lines[i]:match("^%[ %u+ %] %- ") then
vim.api.nvim_win_set_cursor(win, { i, 0 })
return i
end
end
return nil
end
local function jump_listing_and_open(kind)
local current_win = vim.api.nvim_get_current_win()
local listing_win = state.last_win
if listing_win and vim.api.nvim_win_is_valid(listing_win) then
vim.api.nvim_set_current_win(listing_win)
else
listing_win = current_win
end
local target = nil
if kind == "next" then
target = jump_listing_fail("next")
elseif kind == "prev" then
target = jump_listing_fail("prev")
elseif kind == "first" then
target = jump_to_first_listing_entry()
end
if target then
M.open_test_output_at_cursor()
return
end
if current_win and vim.api.nvim_win_is_valid(current_win) then
vim.api.nvim_set_current_win(current_win)
end
end
local function clear_listing_selection()
if state.last_buf and vim.api.nvim_buf_is_valid(state.last_buf) then
vim.api.nvim_buf_clear_namespace(state.last_buf, listing_sel_ns, 0, -1)
end
state.detail_line = nil
end
local function apply_listing_selection(line)
if not (state.last_buf and vim.api.nvim_buf_is_valid(state.last_buf)) then
return
end
vim.api.nvim_buf_clear_namespace(state.last_buf, listing_sel_ns, 0, -1)
if not line then
state.detail_line = nil
return
end
local total = vim.api.nvim_buf_line_count(state.last_buf)
if line < 1 or line > total then
return
end
vim.api.nvim_buf_add_highlight(state.last_buf, listing_sel_ns, "TestSamuraiListingActive", line - 1, 0, -1)
state.detail_line = line
end end
local function find_normal_window() local function find_normal_window()
@@ -175,6 +420,54 @@ local function find_normal_window()
return nil return nil
end end
local function capture_trigger_location()
local win = vim.api.nvim_get_current_win()
local cfg = vim.api.nvim_win_get_config(win)
if cfg.relative ~= "" then
win = find_normal_window()
end
if not (win and vim.api.nvim_win_is_valid(win)) then
return
end
local buf = vim.api.nvim_win_get_buf(win)
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
return
end
local cursor = vim.api.nvim_win_get_cursor(win)
state.trigger_win = win
state.trigger_buf = buf
state.trigger_cursor = { cursor[1], cursor[2] }
end
local function restore_trigger_location()
local buf = state.trigger_buf
local cursor = state.trigger_cursor
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
return
end
local target_win = nil
if state.trigger_win and vim.api.nvim_win_is_valid(state.trigger_win) then
local cfg = vim.api.nvim_win_get_config(state.trigger_win)
if cfg.relative == "" then
target_win = state.trigger_win
end
end
if not target_win then
target_win = find_normal_window()
end
if not (target_win and vim.api.nvim_win_is_valid(target_win)) then
return
end
vim.api.nvim_set_current_win(target_win)
vim.api.nvim_win_set_buf(target_win, buf)
if cursor then
pcall(vim.api.nvim_win_set_cursor, target_win, cursor)
end
state.trigger_win = nil
state.trigger_buf = nil
state.trigger_cursor = nil
end
local function jump_to_listing_test() local function jump_to_listing_test()
local cursor = vim.api.nvim_win_get_cursor(0) local cursor = vim.api.nvim_win_get_cursor(0)
local line = cursor[1] local line = cursor[1]
@@ -269,6 +562,7 @@ local function setup_summary_highlights()
pcall(vim.api.nvim_set_hl, 0, "TestSamuraiResultSkip", { fg = skip_fg }) pcall(vim.api.nvim_set_hl, 0, "TestSamuraiResultSkip", { fg = skip_fg })
pcall(vim.api.nvim_set_hl, 0, "TestSamuraiBorderPass", { fg = pass_fg, bold = true }) pcall(vim.api.nvim_set_hl, 0, "TestSamuraiBorderPass", { fg = pass_fg, bold = true })
pcall(vim.api.nvim_set_hl, 0, "TestSamuraiBorderFail", { fg = fail_fg, bold = true }) pcall(vim.api.nvim_set_hl, 0, "TestSamuraiBorderFail", { fg = fail_fg, bold = true })
pcall(vim.api.nvim_set_hl, 0, "TestSamuraiListingActive", { link = "Visual" })
end end
local function load_runners() local function load_runners()
@@ -302,6 +596,7 @@ local function ensure_output_autocmds()
if state.detail_win and closed == state.detail_win then if state.detail_win and closed == state.detail_win then
state.detail_win = nil state.detail_win = nil
restore_listing_full() restore_listing_full()
clear_listing_selection()
hardtime_restore() hardtime_restore()
return return
end end
@@ -312,6 +607,7 @@ local function ensure_output_autocmds()
pcall(vim.api.nvim_win_close, state.detail_win, true) pcall(vim.api.nvim_win_close, state.detail_win, true)
state.detail_win = nil state.detail_win = nil
end end
clear_listing_selection()
return return
end end
end, end,
@@ -479,6 +775,12 @@ close_container = function()
pcall(vim.api.nvim_win_close, state.last_win, true) pcall(vim.api.nvim_win_close, state.last_win, true)
state.last_win = nil state.last_win = nil
end end
clear_listing_selection()
end
local function close_container_and_restore()
close_container()
restore_trigger_location()
end end
jump_to_first_quickfix = function() jump_to_first_quickfix = function()
@@ -493,6 +795,7 @@ close_detail_float = function()
if state.detail_win and vim.api.nvim_win_is_valid(state.detail_win) then 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) pcall(vim.api.nvim_win_close, state.detail_win, true)
end end
clear_listing_selection()
end end
restore_listing_full = function() restore_listing_full = function()
@@ -566,6 +869,7 @@ local function apply_split_layout(left_ratio)
end end
local function create_output_win(initial_lines) local function create_output_win(initial_lines)
capture_trigger_location()
if state.detail_win and vim.api.nvim_win_is_valid(state.detail_win) then 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) pcall(vim.api.nvim_win_close, state.detail_win, true)
state.detail_win = nil state.detail_win = nil
@@ -600,7 +904,7 @@ local function create_output_win(initial_lines)
hardtime_disable() hardtime_disable()
vim.keymap.set("n", "<esc><esc>", function() vim.keymap.set("n", "<esc><esc>", function()
close_container() close_container_and_restore()
end, { buffer = buf, nowait = true, silent = true }) end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<cr>", function() vim.keymap.set("n", "<cr>", function()
M.open_test_output_at_cursor() M.open_test_output_at_cursor()
@@ -614,11 +918,20 @@ local function create_output_win(initial_lines)
vim.keymap.set("n", "<leader>z", function() vim.keymap.set("n", "<leader>z", function()
M.toggle_detail_full() M.toggle_detail_full()
end, { buffer = buf, nowait = true, silent = true }) end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>nf", function() vim.keymap.set("n", "<leader>fn", function()
jump_listing_fail("next") jump_listing_and_open("next")
end, { buffer = buf, nowait = true, silent = true }) end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>pf", function() vim.keymap.set("n", "<leader>fp", function()
jump_listing_fail("prev") jump_listing_and_open("prev")
end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>ff", function()
jump_listing_and_open("first")
end, { buffer = buf, nowait = true, silent = true, desc = "[F]ind [F]irst list entry" })
vim.keymap.set("n", "<leader>cb", function()
M.listing_break_on_dashes()
end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>cj", function()
M.listing_join_backslashes()
end, { buffer = buf, nowait = true, silent = true }) end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>o", function() vim.keymap.set("n", "<leader>o", function()
jump_to_listing_test() jump_to_listing_test()
@@ -626,6 +939,21 @@ local function create_output_win(initial_lines)
vim.keymap.set("n", "<leader>qn", function() vim.keymap.set("n", "<leader>qn", function()
jump_to_first_quickfix() jump_to_first_quickfix()
end, { buffer = buf, nowait = true, silent = true }) end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>sf", function()
M.filter_listing_failures()
end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>ss", function()
M.filter_listing_skips()
end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>sa", function()
M.filter_listing_all()
end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>tt", function()
M.run_test_at_cursor()
end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "?", function()
M.show_help()
end, { buffer = buf, nowait = true, silent = true })
disable_container_maps(buf) disable_container_maps(buf)
state.last_win = listing state.last_win = listing
@@ -649,6 +977,7 @@ local function reopen_output_win()
state.detail_win = nil state.detail_win = nil
end end
capture_trigger_location()
local width, height, row, col = float_geometry() local width, height, row, col = float_geometry()
state.last_float = { width = width, height = height, row = row, col = col } state.last_float = { width = width, height = height, row = row, col = col }
@@ -664,7 +993,7 @@ local function reopen_output_win()
hardtime_disable() hardtime_disable()
vim.keymap.set("n", "<esc><esc>", function() vim.keymap.set("n", "<esc><esc>", function()
close_container() close_container_and_restore()
end, { buffer = state.last_buf, nowait = true, silent = true }) end, { buffer = state.last_buf, nowait = true, silent = true })
vim.keymap.set("n", "<cr>", function() vim.keymap.set("n", "<cr>", function()
M.open_test_output_at_cursor() M.open_test_output_at_cursor()
@@ -678,11 +1007,20 @@ local function reopen_output_win()
vim.keymap.set("n", "<leader>z", function() vim.keymap.set("n", "<leader>z", function()
M.toggle_detail_full() M.toggle_detail_full()
end, { buffer = state.last_buf, nowait = true, silent = true }) end, { buffer = state.last_buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>nf", function() vim.keymap.set("n", "<leader>fn", function()
jump_listing_fail("next") jump_listing_and_open("next")
end, { buffer = state.last_buf, nowait = true, silent = true }) end, { buffer = state.last_buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>pf", function() vim.keymap.set("n", "<leader>fp", function()
jump_listing_fail("prev") jump_listing_and_open("prev")
end, { buffer = state.last_buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>ff", function()
jump_listing_and_open("first")
end, { buffer = state.last_buf, nowait = true, silent = true, desc = "[F]ind [F]irst list entry" })
vim.keymap.set("n", "<leader>cb", function()
M.listing_break_on_dashes()
end, { buffer = state.last_buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>cj", function()
M.listing_join_backslashes()
end, { buffer = state.last_buf, nowait = true, silent = true }) end, { buffer = state.last_buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>o", function() vim.keymap.set("n", "<leader>o", function()
jump_to_listing_test() jump_to_listing_test()
@@ -690,6 +1028,21 @@ local function reopen_output_win()
vim.keymap.set("n", "<leader>qn", function() vim.keymap.set("n", "<leader>qn", function()
jump_to_first_quickfix() jump_to_first_quickfix()
end, { buffer = state.last_buf, nowait = true, silent = true }) end, { buffer = state.last_buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>sf", function()
M.filter_listing_failures()
end, { buffer = state.last_buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>ss", function()
M.filter_listing_skips()
end, { buffer = state.last_buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>sa", function()
M.filter_listing_all()
end, { buffer = state.last_buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>tt", function()
M.run_test_at_cursor()
end, { buffer = state.last_buf, nowait = true, silent = true })
vim.keymap.set("n", "?", function()
M.show_help()
end, { buffer = state.last_buf, nowait = true, silent = true })
disable_container_maps(state.last_buf) disable_container_maps(state.last_buf)
state.last_win = win state.last_win = win
@@ -952,6 +1305,18 @@ local function apply_detail_highlights(buf, highlights)
end end
end end
local function apply_help_highlights(buf, lines)
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
return
end
vim.api.nvim_buf_clear_namespace(buf, help_ns, 0, -1)
for lnum, line in ipairs(lines or {}) do
if line:match(":%s*$") then
vim.api.nvim_buf_add_highlight(buf, help_ns, "TestSamuraiSummaryPass", lnum - 1, 0, -1)
end
end
end
local function parse_go_output_from_raw(output) local function parse_go_output_from_raw(output)
local out = {} local out = {}
if not output or output == "" then if not output or output == "" then
@@ -985,7 +1350,7 @@ local function ensure_detail_buf(lines)
vim.api.nvim_buf_set_option(buf, "filetype", "test-samurai-output") vim.api.nvim_buf_set_option(buf, "filetype", "test-samurai-output")
state.detail_buf = buf state.detail_buf = buf
vim.keymap.set("n", "<esc><esc>", function() vim.keymap.set("n", "<esc><esc>", function()
close_container() close_container_and_restore()
end, { buffer = buf, nowait = true, silent = true }) end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<C-w>h", function() vim.keymap.set("n", "<C-w>h", function()
M.focus_listing() M.focus_listing()
@@ -999,16 +1364,29 @@ local function ensure_detail_buf(lines)
vim.keymap.set("n", "<leader>z", function() vim.keymap.set("n", "<leader>z", function()
M.toggle_detail_full() M.toggle_detail_full()
end, { buffer = buf, nowait = true, silent = true }) end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>fn", function()
jump_listing_and_open("next")
end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>fp", function()
jump_listing_and_open("prev")
end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>ff", function()
jump_listing_and_open("first")
end, { buffer = buf, nowait = true, silent = true, desc = "[F]ind [F]irst list entry" })
vim.keymap.set("n", "<C-c>", function() vim.keymap.set("n", "<C-c>", function()
close_detail_float() close_detail_float()
end, { buffer = buf, nowait = true, silent = true }) end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "<leader>qn", function() vim.keymap.set("n", "<leader>qn", function()
jump_to_first_quickfix() jump_to_first_quickfix()
end, { buffer = buf, nowait = true, silent = true }) end, { buffer = buf, nowait = true, silent = true })
vim.keymap.set("n", "?", function()
M.show_help()
end, { buffer = buf, nowait = true, silent = true })
disable_container_maps(buf) disable_container_maps(buf)
end end
local clean_lines, highlights = parse_ansi_lines(normalize_output_lines(lines)) local clean_lines, highlights = parse_ansi_lines(normalize_output_lines(lines))
vim.api.nvim_buf_set_lines(buf, 0, -1, false, clean_lines) vim.api.nvim_buf_set_lines(buf, 0, -1, false, clean_lines)
vim.api.nvim_buf_clear_namespace(buf, help_ns, 0, -1)
apply_detail_highlights(buf, highlights) apply_detail_highlights(buf, highlights)
return buf return buf
end end
@@ -1126,11 +1504,109 @@ function M.open_test_output_at_cursor()
end end
end end
if type(output) ~= "table" or #output == 0 then if type(output) ~= "table" or #output == 0 then
vim.notify("[test-samurai] No output captured for " .. test_name, vim.log.levels.WARN) open_detail_split({ "", "No output captured" }, "default")
apply_listing_selection(line)
return return
end end
local border_kind = status and status:lower() or nil local border_kind = status and status:lower() or nil
open_detail_split(output, border_kind) open_detail_split(output, border_kind)
apply_listing_selection(line)
end
function M.show_help()
if not (state.last_win and vim.api.nvim_win_is_valid(state.last_win)) then
vim.notify("[test-samurai] No test output window", vim.log.levels.WARN)
return
end
local lines = help_lines()
open_detail_split(lines, "default")
clear_listing_selection()
apply_help_highlights(state.detail_buf, lines)
end
function M.filter_listing_failures()
apply_listing_filter("fail")
end
function M.filter_listing_skips()
apply_listing_filter("skip")
end
function M.filter_listing_all()
apply_listing_filter("all")
end
function M.listing_break_on_dashes()
apply_listing_substitution([[%s/--/\\\r\t--/g]])
vim.cmd("noh")
end
function M.listing_join_backslashes()
apply_listing_substitution([[%s/\\\n\t//g]])
end
function M.run_test_at_cursor()
local cursor = vim.api.nvim_win_get_cursor(0)
local line = cursor[1]
local text = vim.api.nvim_get_current_line()
local status = text:match("^%[%s*(%u+)%s*%]%s*%-")
if status ~= "PASS" and status ~= "FAIL" and status ~= "SKIP" then
return
end
local test_name = state.last_result_line_map[line]
if not test_name then
test_name = text:match("^%[%s*[%u]+%s*%]%s*%-%s*(.+)$")
end
if not test_name or test_name == "" then
return
end
local runner = state.last_scope_runner or state.last_runner
if not runner or type(runner.build_command) ~= "function" then
vim.notify("[test-samurai] Runner missing methods", vim.log.levels.ERROR)
return
end
local command_src = state.last_scope_command or state.last_command
if not command_src then
vim.notify("[test-samurai] No previous test command", vim.log.levels.WARN)
return
end
local spec = {
file = command_src.file,
cwd = command_src.cwd,
test_name = test_name,
full_name = test_name,
}
if runner._last_mocha_titles and type(runner._last_mocha_titles) == "table" then
spec.mocha_full_title = runner._last_mocha_titles[test_name]
end
if not spec.mocha_full_title and test_name:find("/", 1, true) then
spec.mocha_full_title = test_name:gsub("/", " ")
end
if not spec.file or spec.file == "" then
vim.notify("[test-samurai] Missing test file for rerun", vim.log.levels.WARN)
return
end
local ok_cmd, command = pcall(runner.build_command, spec)
if not ok_cmd or not command or type(command.cmd) ~= "table" or #command.cmd == 0 then
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
parser = parser()
end
run_command(command, {
track_scope = true,
runner = runner,
scope_kind = "nearest",
output_parser = parser or runner.parse_results,
})
end end
function M.focus_listing() function M.focus_listing()
@@ -1525,7 +2001,7 @@ local function highlight_label_word(buf, ns, lnum, line, label, hl_group)
vim.api.nvim_buf_add_highlight(buf, ns, hl_group, lnum, label_start - 1, label_start - 1 + #label) vim.api.nvim_buf_add_highlight(buf, ns, hl_group, lnum, label_start - 1, label_start - 1 + #label)
end end
local function apply_summary_highlights(buf, start_line, lines) apply_summary_highlights = function(buf, start_line, lines)
if not (buf and vim.api.nvim_buf_is_valid(buf)) then if not (buf and vim.api.nvim_buf_is_valid(buf)) then
return return
end end
@@ -1547,7 +2023,7 @@ local function apply_summary_highlights(buf, start_line, lines)
end end
end end
local function apply_result_highlights(buf, start_line, lines) apply_result_highlights = function(buf, start_line, lines)
if not (buf and vim.api.nvim_buf_is_valid(buf)) then if not (buf and vim.api.nvim_buf_is_valid(buf)) then
return return
end end
@@ -1585,11 +2061,14 @@ local function collect_failure_names_from_listing()
return out return out
end end
local function run_command(command, opts) run_command = function(command, opts)
local options = opts or {} local options = opts or {}
vim.cmd("wall")
state.last_test_outputs = {} state.last_test_outputs = {}
state.last_result_line_map = {} state.last_result_line_map = {}
state.last_raw_output = nil state.last_raw_output = nil
state.listing_unfiltered_lines = nil
state.listing_filtered_kind = nil
local failures = {} local failures = {}
local failures_seen = {} local failures_seen = {}
if command and type(command.cmd) == "table" and #command.cmd > 0 then if command and type(command.cmd) == "table" and #command.cmd > 0 then
@@ -1635,7 +2114,10 @@ local function run_command(command, opts)
skips = {}, skips = {},
} }
local had_parsed_output = false local had_parsed_output = false
local summary_enabled = options.scope_kind == "file" or options.scope_kind == "all" or options.scope_kind == "nearest" local summary_enabled = options.scope_kind == "file"
or options.scope_kind == "all"
or options.scope_kind == "nearest"
or options.scope_kind == "last"
local summary = make_summary_tracker(summary_enabled) local summary = make_summary_tracker(summary_enabled)
local result_counts = make_summary_tracker(true) local result_counts = make_summary_tracker(true)
state.last_border_kind = "default" state.last_border_kind = "default"
@@ -1804,7 +2286,7 @@ local function run_command(command, opts)
append_lines(buf, summary_lines) append_lines(buf, summary_lines)
apply_summary_highlights(buf, start_line, summary_lines) apply_summary_highlights(buf, start_line, summary_lines)
end end
append_lines(buf, { "", "[exit code] " .. tostring(code) }) append_lines(buf, { "" })
else else
if not has_output then if not has_output then
local cur = vim.api.nvim_buf_get_lines(buf, 0, -1, false) local cur = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
@@ -1823,13 +2305,14 @@ local function run_command(command, opts)
append_lines(buf, summary_lines) append_lines(buf, summary_lines)
apply_summary_highlights(buf, start_line, summary_lines) apply_summary_highlights(buf, start_line, summary_lines)
end end
append_lines(buf, { "", "[exit code] " .. tostring(code) }) append_lines(buf, { "" })
end end
if options.track_scope then if options.track_scope then
state.last_scope_exit_code = code state.last_scope_exit_code = code
end end
local items = {} local items = {}
local failures_for_qf = failures local failures_for_qf = failures
local fallback_failures = options.qf_fallback_failures
if options.track_scope and type(state.last_scope_failures) == "table" then if options.track_scope and type(state.last_scope_failures) == "table" then
local merged = {} local merged = {}
local seen = {} local seen = {}
@@ -1865,8 +2348,20 @@ local function run_command(command, opts)
end end
failures_for_qf = merged failures_for_qf = merged
end end
if (not failures_for_qf or #failures_for_qf == 0) and type(fallback_failures) == "table" then
local merged = {}
local seen = {}
for _, name in ipairs(fallback_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 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) local scope_kind = options.qf_scope_kind or options.scope_kind
local ok_collect, collected = pcall(runner.collect_failed_locations, failures_for_qf, command, scope_kind)
if ok_collect and type(collected) == "table" then if ok_collect and type(collected) == "table" then
items = collected items = collected
end end
@@ -1896,6 +2391,7 @@ function M.run_last()
end end
run_command(command, { run_command(command, {
runner = runner, runner = runner,
scope_kind = state.last_scope_kind or "last",
output_parser = parser or (runner and runner.parse_results), output_parser = parser or (runner and runner.parse_results),
}) })
end end
@@ -2066,10 +2562,17 @@ function M.run_failed_only()
end end
run_command(command, { run_command(command, {
save_last = false, save_last = false,
runner = runner,
output_parser = parser or (runner and runner.parse_results), output_parser = parser or (runner and runner.parse_results),
qf_fallback_failures = state.last_scope_failures,
qf_scope_kind = state.last_scope_kind,
}) })
end end
function M.close_output_and_restore()
close_container_and_restore()
end
function M.show_output() function M.show_output()
if not (state.last_buf and vim.api.nvim_buf_is_valid(state.last_buf)) then if not (state.last_buf and vim.api.nvim_buf_is_valid(state.last_buf)) then
vim.notify("[test-samurai] No previous output", vim.log.levels.WARN) vim.notify("[test-samurai] No previous output", vim.log.levels.WARN)

View File

@@ -13,7 +13,7 @@ implementieren muss, damit alle Commands vollständig unterstützt werden.
- Beispiel: `lua/test-samurai-go-runner/init.lua` -> `require("test-samurai-go-runner")`. - Beispiel: `lua/test-samurai-go-runner/init.lua` -> `require("test-samurai-go-runner")`.
- `lua/init.lua` wäre `require("init")` und ist kollisionsanfällig; vermeiden. - `lua/init.lua` wäre `require("init")` und ist kollisionsanfällig; vermeiden.
## Pflichtfunktionen (volle Command-Unterstützung) ## Pflichtfunktionen (volle Command- und Keymap-Unterstützung)
- `is_test_file(bufnr) -> boolean` - `is_test_file(bufnr) -> boolean`
- Wird für die Runner-Auswahl genutzt. - Wird für die Runner-Auswahl genutzt.
@@ -29,10 +29,18 @@ implementieren muss, damit alle Commands vollständig unterstützt werden.
- Für `TSamAll`. - Für `TSamAll`.
- `build_failed_command(last_command, failures, scope_kind) -> command_spec` - `build_failed_command(last_command, failures, scope_kind) -> command_spec`
- Für `TSamFailedOnly`. - Für `TSamFailedOnly`.
- `parse_results(output) -> results`
- Fallback-Parser für vollständigen Output.
- `output_parser() -> { on_line, on_complete }`
- Muss Streaming unterstützen (Listing wird live befüllt).
- `parse_test_output(output) -> table`
- Detail-Ausgabe pro Test (Detail-Float und `<cr>` im Listing).
- `collect_failed_locations(failures, command, scope_kind) -> items`
- Quickfix-Unterstützung (Keymap `<leader>qn`).
## Output-Parsing (Listing + Summary) ## Output-Parsing (Listing + Summary)
Genau eine der folgenden Varianten muss vorhanden sein: Beide Varianten müssen vorhanden sein:
- `parse_results(output) -> results` - `parse_results(output) -> results`
- `results` muss enthalten: - `results` muss enthalten:
@@ -46,6 +54,9 @@ Genau eine der folgenden Varianten muss vorhanden sein:
- oder `output_parser() -> { on_line, on_complete }` - oder `output_parser() -> { on_line, on_complete }`
- `on_line(line, state)` kann `results` liefern (siehe oben). - `on_line(line, state)` kann `results` liefern (siehe oben).
- `on_complete(output, state)` kann `results` liefern (siehe oben). - `on_complete(output, state)` kann `results` liefern (siehe oben).
- `on_line` muss Ergebnisse liefern, damit das Listing-Float immer gestreamt wird.
Hinweis: `parse_results` darf intern `output_parser().on_complete` nutzen, aber beide Funktionen müssen existieren.
## Listing-Gruppierung fuer Parent/Subtests ## Listing-Gruppierung fuer Parent/Subtests
@@ -78,6 +89,25 @@ Genau eine der folgenden Varianten muss vorhanden sein:
- `spec` (von `find_nearest`): - `spec` (von `find_nearest`):
- Muss mindestens `file` enthalten, z. B.: - Muss mindestens `file` enthalten, z. B.:
- `{ file = "...", cwd = "...", test_name = "...", full_name = "...", kind = "..." }` - `{ file = "...", cwd = "...", test_name = "...", full_name = "...", kind = "..." }`
- `results` (für Listing-Float + Keymaps):
- `{ passes = { "Name1", ... }, failures = { "Name2", ... }, skips = { "Name3", ... } }`
- Optional: `display = { passes = { "DisplayName1", ... }, failures = { "DisplayName2", ... }, skips = { "DisplayName3", ... } }`
- `failures` steuert `[ FAIL ]`-Zeilen im Listing und wird von `<leader>nf`/`<leader>pf` genutzt.
- `items` (für Quickfix):
- `{ { filename = "...", lnum = 1, col = 1, text = "..." }, ... }`
- Wird von `<leader>qn` verwendet.
## Keymaps (Datenlieferung)
- `<leader>nf` / `<leader>pf`
- benötigt `[ FAIL ]`-Einträge im Listing.
- Runner muss `results.failures` (und optional `display.failures`) liefern.
- `<leader>qn`
- springt in die Quickfix-Liste.
- Runner muss `collect_failed_locations` implementieren und gültige `items` liefern.
- `<cr>` im Listing
- öffnet Detail-Float.
- Runner muss `parse_test_output` liefern und Testnamen konsistent zu `results.*` halten.
## Optional empfohlene Metadaten ## Optional empfohlene Metadaten
@@ -126,6 +156,17 @@ function runner.parse_results(output)
return { passes = {}, failures = {}, skips = {}, display = { passes = {}, failures = {}, skips = {} } } return { passes = {}, failures = {}, skips = {}, display = { passes = {}, failures = {}, skips = {} } }
end end
function runner.output_parser()
return {
on_line = function(_line, _state)
return nil
end,
on_complete = function(_output, _state)
return runner.parse_results(_output)
end,
}
end
function runner.parse_test_output(output) function runner.parse_test_output(output)
return {} return {}
end end
@@ -145,21 +186,33 @@ return runner
- build_file_command implementiert - build_file_command implementiert
- build_all_command implementiert - build_all_command implementiert
- build_failed_command implementiert - build_failed_command implementiert
- parse_results oder output_parser implementiert - parse_results implementiert
- output_parser implementiert (Streaming)
- 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
## Projekt- und Prozessanforderungen ## Projekt- und Prozessanforderungen
- Eine englischsprachige `README.md` ist zu erstellen. - Rolle: **TDD-first Entwickler**.
- Sie muss bei jedem weiteren Prompt aktualisiert werden, wenn die Aenderungen es erfordern. - Jede neue Funktion, jedes neue Kommando und jede Verhaltensänderung muss durch Tests abgesichert sein.
- Nach jeder Code-Änderung Tests via `bash run_test.sh` ausführen und bei Fehlern korrigieren, bis alles grün ist.
- **Nicht raten**:
- Bei unklaren oder mehrdeutigen Anforderungen Arbeit stoppen und Klarstellung verlangen.
- TODO/NOTE im Code ist zulässig, stilles Raten nicht.
- **Keine stillen Änderungen**:
- Bestehende Features dürfen nicht unbemerkt geändert oder ersetzt werden.
- Notwendige Anpassungen zur Koexistenz mehrerer Features müssen klar erkennbar sein.
- Antworten immer auf Deutsch.
- Eine englischsprachige `README.md` ist zu erstellen und wird bei Änderungen automatisch aktualisiert.
- TDD-Vorgaben (aus `AGENTS.md`) uebernehmen: - TDD-Vorgaben (aus `AGENTS.md`) uebernehmen:
- Neue Funktionen/Commands/Verhaltensaenderungen muessen getestet werden. - Neue Funktionen/Commands/Verhaltensaenderungen muessen getestet werden.
- Tests nach jeder Code-Aenderung ausfuehren. - Tests nach jeder Code-Aenderung ausfuehren.
- Im Runner erstellter Quellcode ist ebenfalls zu testen. - Im Runner erstellter Quellcode ist ebenfalls zu testen.
- Eine eigene `run_test.sh` darf im Runner-Repo angelegt werden. - Eine eigene `run_test.sh` wird im Runner-Repo angelegt.
- Eine Gitea-Action ist zu erstellen, die bei jedem Push die Tests ausfuehrt. - Eine Gitea-Action ist zu erstellen, die bei jedem Push die Tests ausfuehrt.
- Neovim wird per AppImage installiert (kein `apt`).
- Runner laeuft auf `gitea-act-runner` mit Raspberry Pi 5 (ARM).
- Beispiel (anpassbarer Workflow): - Beispiel (anpassbarer Workflow):
```yaml ```yaml
@@ -176,6 +229,19 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Neovim (AppImage)
run: |
curl -L -o nvim.appimage https://github.com/neovim/neovim/releases/download/v0.11.4/nvim.appimage
chmod +x nvim.appimage
sudo mv nvim.appimage /usr/local/bin/nvim
- name: Run tests - name: Run tests
run: bash run_test.sh run: bash run_test.sh
``` ```
## Zusaetzliche Runner-Guidelines (framework-agnostisch)
- **Testnamen-Konvention:** Runner sollen eine konsistente, dokumentierte Full-Name-Bildung verwenden (z. B. `Parent/Subtest`), inklusive Mehrfach-Nesting. Diese Konvention muss in `results.*`, `parse_test_output` und `collect_failed_locations` uebereinstimmen.
- **TSamNearest-Prioritaet:** Falls moeglich, gelten folgende Regeln: Test-Block > Describe/Context-Block > File-Command. Das Verhalten muss getestet werden (Cursor im Test, zwischen Tests, ausserhalb von Describe/Context).
- **Reporter-Payload-Schema:** Wenn ein Custom-Reporter verwendet wird, soll dessen JSON-Payload dokumentiert und stabil sein (z. B. `{ name, status, file, location, output }`), damit Parser/Quickfix/Detail-Output konsistent bleiben.
- **Failed-Only-Logik:** Failed-Only muss auf den letzten Fehlermeldungen basieren und nur die fehlerhaften Tests erneut ausfuehren. Die Pattern-Strategie (z. B. Titel-only vs. Full-Name) muss getestet werden.
- **CI-Installations-Snippet:** Die Neovim-Installation in CI soll als „authoritative snippet“ behandelt werden und in Runner-Repos 1:1 uebernommen werden.

File diff suppressed because it is too large Load Diff