Compare commits
14 Commits
6505a91cce
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
456a157549
|
|||
|
8c598002e4
|
|||
|
bd6930adc0
|
|||
|
24bd89a7be
|
|||
|
fd3952bedf
|
|||
|
238d5f9634
|
|||
|
118f84c31e
|
|||
|
924584d8b3
|
|||
|
5d0b4e9dd6
|
|||
|
c538a32307
|
|||
|
e9a7d2029b
|
|||
|
4c2d585f2d
|
|||
|
e315a8e8f2
|
|||
|
1d9b682a58
|
@@ -11,6 +11,9 @@
|
||||
- **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.
|
||||
- **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
|
||||
- Neovim Plugin: **test-samurai**
|
||||
|
||||
30
README.md
30
README.md
@@ -59,12 +59,27 @@ If no runner matches the current test file, test-samurai will show:
|
||||
- `TSamLast` -> `<leader>tl`
|
||||
- `TSamFailedOnly` -> `<leader>te`
|
||||
- `TSamShowOutput` -> `<leader>to`
|
||||
- Help: `:help test-samurai`
|
||||
|
||||
Additional keymaps:
|
||||
|
||||
- Listing navigation:
|
||||
- `<leader>fn` -> [F]ind [N]ext failed test in listing (wraps to the first, opens Detail-Float, works in Detail-Float)
|
||||
- `<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
|
||||
- `<leader>nf` -> jump to the next `[ FAIL ]` entry in the Test-Listing-Float (wraps to the first)
|
||||
- `<leader>pf` -> jump to the previous `[ FAIL ]` entry in the Test-Listing-Float (wraps to the last)
|
||||
- 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
|
||||
|
||||
@@ -74,11 +89,15 @@ Additional keymaps:
|
||||
- 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).
|
||||
- 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
|
||||
|
||||
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:
|
||||
|
||||
- `is_test_file`
|
||||
@@ -87,6 +106,9 @@ Required functions:
|
||||
- `build_file_command`
|
||||
- `build_all_command`
|
||||
- `build_failed_command`
|
||||
- `parse_results`
|
||||
- `output_parser` (must stream listing output via `on_line`)
|
||||
- `parse_test_output`
|
||||
- `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.
|
||||
@@ -100,6 +122,8 @@ No runner for your environment exists? No problem: use `runners-agents.md` to gu
|
||||
|
||||
## 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.
|
||||
|
||||
Run tests:
|
||||
|
||||
9
doc/tags
Normal file
9
doc/tags
Normal 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
102
doc/test-samurai.txt
Normal 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
|
||||
@@ -23,6 +23,12 @@ local state = {
|
||||
detail_win = nil,
|
||||
detail_opening = 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_was_enabled = false,
|
||||
autocmds_set = false,
|
||||
@@ -31,11 +37,16 @@ local state = {
|
||||
local summary_ns = vim.api.nvim_create_namespace("TestSamuraiSummary")
|
||||
local result_ns = vim.api.nvim_create_namespace("TestSamuraiResult")
|
||||
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 close_container
|
||||
local restore_listing_full
|
||||
local close_detail_float
|
||||
local jump_to_first_quickfix
|
||||
local apply_summary_highlights
|
||||
local apply_result_highlights
|
||||
local run_command
|
||||
|
||||
local function disable_container_maps(buf)
|
||||
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)
|
||||
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 ok, hardtime = pcall(require, "hardtime")
|
||||
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 buf = vim.api.nvim_get_current_buf()
|
||||
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
|
||||
return
|
||||
return nil
|
||||
end
|
||||
local total = vim.api.nvim_buf_line_count(buf)
|
||||
if total == 0 then
|
||||
return
|
||||
return nil
|
||||
end
|
||||
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||
local cursor = vim.api.nvim_win_get_cursor(win)
|
||||
@@ -163,6 +338,76 @@ local function jump_listing_fail(direction)
|
||||
if target then
|
||||
vim.api.nvim_win_set_cursor(win, { target, 0 })
|
||||
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
|
||||
|
||||
local function find_normal_window()
|
||||
@@ -175,6 +420,54 @@ local function find_normal_window()
|
||||
return nil
|
||||
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 cursor = vim.api.nvim_win_get_cursor(0)
|
||||
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, "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, "TestSamuraiListingActive", { link = "Visual" })
|
||||
end
|
||||
|
||||
local function load_runners()
|
||||
@@ -302,6 +596,7 @@ local function ensure_output_autocmds()
|
||||
if state.detail_win and closed == state.detail_win then
|
||||
state.detail_win = nil
|
||||
restore_listing_full()
|
||||
clear_listing_selection()
|
||||
hardtime_restore()
|
||||
return
|
||||
end
|
||||
@@ -312,6 +607,7 @@ local function ensure_output_autocmds()
|
||||
pcall(vim.api.nvim_win_close, state.detail_win, true)
|
||||
state.detail_win = nil
|
||||
end
|
||||
clear_listing_selection()
|
||||
return
|
||||
end
|
||||
end,
|
||||
@@ -479,6 +775,12 @@ close_container = function()
|
||||
pcall(vim.api.nvim_win_close, state.last_win, true)
|
||||
state.last_win = nil
|
||||
end
|
||||
clear_listing_selection()
|
||||
end
|
||||
|
||||
local function close_container_and_restore()
|
||||
close_container()
|
||||
restore_trigger_location()
|
||||
end
|
||||
|
||||
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
|
||||
pcall(vim.api.nvim_win_close, state.detail_win, true)
|
||||
end
|
||||
clear_listing_selection()
|
||||
end
|
||||
|
||||
restore_listing_full = function()
|
||||
@@ -566,6 +869,7 @@ local function apply_split_layout(left_ratio)
|
||||
end
|
||||
|
||||
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
|
||||
pcall(vim.api.nvim_win_close, state.detail_win, true)
|
||||
state.detail_win = nil
|
||||
@@ -600,7 +904,7 @@ local function create_output_win(initial_lines)
|
||||
hardtime_disable()
|
||||
|
||||
vim.keymap.set("n", "<esc><esc>", function()
|
||||
close_container()
|
||||
close_container_and_restore()
|
||||
end, { buffer = buf, nowait = true, silent = true })
|
||||
vim.keymap.set("n", "<cr>", function()
|
||||
M.open_test_output_at_cursor()
|
||||
@@ -614,11 +918,20 @@ 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>nf", function()
|
||||
jump_listing_fail("next")
|
||||
vim.keymap.set("n", "<leader>fn", function()
|
||||
jump_listing_and_open("next")
|
||||
end, { buffer = buf, nowait = true, silent = true })
|
||||
vim.keymap.set("n", "<leader>pf", function()
|
||||
jump_listing_fail("prev")
|
||||
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", "<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 })
|
||||
vim.keymap.set("n", "<leader>o", function()
|
||||
jump_to_listing_test()
|
||||
@@ -626,6 +939,21 @@ local function create_output_win(initial_lines)
|
||||
vim.keymap.set("n", "<leader>qn", function()
|
||||
jump_to_first_quickfix()
|
||||
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)
|
||||
|
||||
state.last_win = listing
|
||||
@@ -649,6 +977,7 @@ local function reopen_output_win()
|
||||
state.detail_win = nil
|
||||
end
|
||||
|
||||
capture_trigger_location()
|
||||
local width, height, row, col = float_geometry()
|
||||
state.last_float = { width = width, height = height, row = row, col = col }
|
||||
|
||||
@@ -664,7 +993,7 @@ local function reopen_output_win()
|
||||
hardtime_disable()
|
||||
|
||||
vim.keymap.set("n", "<esc><esc>", function()
|
||||
close_container()
|
||||
close_container_and_restore()
|
||||
end, { buffer = state.last_buf, nowait = true, silent = true })
|
||||
vim.keymap.set("n", "<cr>", function()
|
||||
M.open_test_output_at_cursor()
|
||||
@@ -678,11 +1007,20 @@ 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>nf", function()
|
||||
jump_listing_fail("next")
|
||||
vim.keymap.set("n", "<leader>fn", function()
|
||||
jump_listing_and_open("next")
|
||||
end, { buffer = state.last_buf, nowait = true, silent = true })
|
||||
vim.keymap.set("n", "<leader>pf", function()
|
||||
jump_listing_fail("prev")
|
||||
vim.keymap.set("n", "<leader>fp", function()
|
||||
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 })
|
||||
vim.keymap.set("n", "<leader>o", function()
|
||||
jump_to_listing_test()
|
||||
@@ -690,6 +1028,21 @@ local function reopen_output_win()
|
||||
vim.keymap.set("n", "<leader>qn", function()
|
||||
jump_to_first_quickfix()
|
||||
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)
|
||||
|
||||
state.last_win = win
|
||||
@@ -952,6 +1305,18 @@ local function apply_detail_highlights(buf, highlights)
|
||||
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 out = {}
|
||||
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")
|
||||
state.detail_buf = buf
|
||||
vim.keymap.set("n", "<esc><esc>", function()
|
||||
close_container()
|
||||
close_container_and_restore()
|
||||
end, { buffer = buf, nowait = true, silent = true })
|
||||
vim.keymap.set("n", "<C-w>h", function()
|
||||
M.focus_listing()
|
||||
@@ -999,16 +1364,29 @@ local function ensure_detail_buf(lines)
|
||||
vim.keymap.set("n", "<leader>z", function()
|
||||
M.toggle_detail_full()
|
||||
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()
|
||||
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 })
|
||||
vim.keymap.set("n", "?", function()
|
||||
M.show_help()
|
||||
end, { buffer = buf, nowait = true, silent = true })
|
||||
disable_container_maps(buf)
|
||||
end
|
||||
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_clear_namespace(buf, help_ns, 0, -1)
|
||||
apply_detail_highlights(buf, highlights)
|
||||
return buf
|
||||
end
|
||||
@@ -1126,11 +1504,109 @@ function M.open_test_output_at_cursor()
|
||||
end
|
||||
end
|
||||
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
|
||||
end
|
||||
local border_kind = status and status:lower() or nil
|
||||
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
|
||||
|
||||
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)
|
||||
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
|
||||
return
|
||||
end
|
||||
@@ -1547,7 +2023,7 @@ local function apply_summary_highlights(buf, start_line, lines)
|
||||
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
|
||||
return
|
||||
end
|
||||
@@ -1585,11 +2061,14 @@ local function collect_failure_names_from_listing()
|
||||
return out
|
||||
end
|
||||
|
||||
local function run_command(command, opts)
|
||||
run_command = function(command, opts)
|
||||
local options = opts or {}
|
||||
vim.cmd("wall")
|
||||
state.last_test_outputs = {}
|
||||
state.last_result_line_map = {}
|
||||
state.last_raw_output = nil
|
||||
state.listing_unfiltered_lines = nil
|
||||
state.listing_filtered_kind = nil
|
||||
local failures = {}
|
||||
local failures_seen = {}
|
||||
if command and type(command.cmd) == "table" and #command.cmd > 0 then
|
||||
@@ -1635,7 +2114,10 @@ local function run_command(command, opts)
|
||||
skips = {},
|
||||
}
|
||||
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 result_counts = make_summary_tracker(true)
|
||||
state.last_border_kind = "default"
|
||||
@@ -1804,7 +2286,7 @@ local function run_command(command, opts)
|
||||
append_lines(buf, summary_lines)
|
||||
apply_summary_highlights(buf, start_line, summary_lines)
|
||||
end
|
||||
append_lines(buf, { "", "[exit code] " .. tostring(code) })
|
||||
append_lines(buf, { "" })
|
||||
else
|
||||
if not has_output then
|
||||
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)
|
||||
apply_summary_highlights(buf, start_line, summary_lines)
|
||||
end
|
||||
append_lines(buf, { "", "[exit code] " .. tostring(code) })
|
||||
append_lines(buf, { "" })
|
||||
end
|
||||
if options.track_scope then
|
||||
state.last_scope_exit_code = code
|
||||
end
|
||||
local items = {}
|
||||
local failures_for_qf = failures
|
||||
local fallback_failures = options.qf_fallback_failures
|
||||
if options.track_scope and type(state.last_scope_failures) == "table" then
|
||||
local merged = {}
|
||||
local seen = {}
|
||||
@@ -1865,8 +2348,20 @@ local function run_command(command, opts)
|
||||
end
|
||||
failures_for_qf = merged
|
||||
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
|
||||
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
|
||||
items = collected
|
||||
end
|
||||
@@ -1896,6 +2391,7 @@ function M.run_last()
|
||||
end
|
||||
run_command(command, {
|
||||
runner = runner,
|
||||
scope_kind = state.last_scope_kind or "last",
|
||||
output_parser = parser or (runner and runner.parse_results),
|
||||
})
|
||||
end
|
||||
@@ -2066,10 +2562,17 @@ function M.run_failed_only()
|
||||
end
|
||||
run_command(command, {
|
||||
save_last = false,
|
||||
runner = runner,
|
||||
output_parser = parser or (runner and runner.parse_results),
|
||||
qf_fallback_failures = state.last_scope_failures,
|
||||
qf_scope_kind = state.last_scope_kind,
|
||||
})
|
||||
end
|
||||
|
||||
function M.close_output_and_restore()
|
||||
close_container_and_restore()
|
||||
end
|
||||
|
||||
function M.show_output()
|
||||
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)
|
||||
|
||||
@@ -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")`.
|
||||
- `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`
|
||||
- Wird für die Runner-Auswahl genutzt.
|
||||
@@ -29,10 +29,18 @@ implementieren muss, damit alle Commands vollständig unterstützt werden.
|
||||
- Für `TSamAll`.
|
||||
- `build_failed_command(last_command, failures, scope_kind) -> command_spec`
|
||||
- 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)
|
||||
|
||||
Genau eine der folgenden Varianten muss vorhanden sein:
|
||||
Beide Varianten müssen vorhanden sein:
|
||||
|
||||
- `parse_results(output) -> results`
|
||||
- `results` muss enthalten:
|
||||
@@ -46,6 +54,9 @@ Genau eine der folgenden Varianten muss vorhanden sein:
|
||||
- oder `output_parser() -> { on_line, on_complete }`
|
||||
- `on_line(line, 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
|
||||
|
||||
@@ -78,6 +89,25 @@ Genau eine der folgenden Varianten muss vorhanden sein:
|
||||
- `spec` (von `find_nearest`):
|
||||
- Muss mindestens `file` enthalten, z. B.:
|
||||
- `{ 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
|
||||
|
||||
@@ -126,6 +156,17 @@ function runner.parse_results(output)
|
||||
return { passes = {}, failures = {}, skips = {}, display = { passes = {}, failures = {}, skips = {} } }
|
||||
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)
|
||||
return {}
|
||||
end
|
||||
@@ -145,21 +186,33 @@ return runner
|
||||
- build_file_command implementiert
|
||||
- build_all_command implementiert
|
||||
- build_failed_command implementiert
|
||||
- parse_results oder output_parser implementiert
|
||||
- parse_results implementiert
|
||||
- output_parser implementiert (Streaming)
|
||||
- parse_test_output implementiert
|
||||
- collect_failed_locations implementiert
|
||||
- command_spec `{ cmd, cwd }` korrekt zurückgegeben
|
||||
|
||||
## Projekt- und Prozessanforderungen
|
||||
|
||||
- Eine englischsprachige `README.md` ist zu erstellen.
|
||||
- Sie muss bei jedem weiteren Prompt aktualisiert werden, wenn die Aenderungen es erfordern.
|
||||
- Rolle: **TDD-first Entwickler**.
|
||||
- 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:
|
||||
- Neue Funktionen/Commands/Verhaltensaenderungen muessen getestet werden.
|
||||
- Tests nach jeder Code-Aenderung ausfuehren.
|
||||
- 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.
|
||||
- Neovim wird per AppImage installiert (kein `apt`).
|
||||
- Runner laeuft auf `gitea-act-runner` mit Raspberry Pi 5 (ARM).
|
||||
- Beispiel (anpassbarer Workflow):
|
||||
|
||||
```yaml
|
||||
@@ -176,6 +229,19 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
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
|
||||
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
Reference in New Issue
Block a user