Compare commits
3 Commits
dcb4320040
...
77a7ebab4d
| Author | SHA1 | Date | |
|---|---|---|---|
|
77a7ebab4d
|
|||
|
6ce8530cf7
|
|||
|
15bc792449
|
33
.gitea/workflows/tests.yml
Normal file
33
.gitea/workflows/tests.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Neovim AppImage
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
arch="$(uname -m)"
|
||||||
|
if [ "${arch}" = "aarch64" ]; then
|
||||||
|
appimage="nvim-linux-arm64.appimage"
|
||||||
|
else
|
||||||
|
appimage="nvim-linux-x86_64.appimage"
|
||||||
|
fi
|
||||||
|
url="https://github.com/neovim/neovim/releases/download/stable/${appimage}"
|
||||||
|
curl -L "${url}" -o "${appimage}"
|
||||||
|
chmod +x "${appimage}"
|
||||||
|
./${appimage} --appimage-extract
|
||||||
|
sudo install -m 0755 ./squashfs-root/usr/bin/nvim /usr/local/bin/nvim
|
||||||
|
|
||||||
|
- name: Neovim Version
|
||||||
|
run: nvim --version
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: bash run_test.sh
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
output
|
output
|
||||||
.nvimlog
|
.nvimlog
|
||||||
|
.idea
|
||||||
|
|||||||
@@ -436,13 +436,6 @@ function M.get_runner_for_buf(bufnr)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
if path:sub(-8) == "_test.go" then
|
|
||||||
local ok, go = pcall(require, "test-samurai.runners.go")
|
|
||||||
if ok and type(go) == "table" then
|
|
||||||
return go
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if path:find(".test.", 1, true) or path:find(".spec.", 1, true) then
|
if path:find(".test.", 1, true) or path:find(".spec.", 1, true) then
|
||||||
local ok, jsjest = pcall(require, "test-samurai.runners.js-jest")
|
local ok, jsjest = pcall(require, "test-samurai.runners.js-jest")
|
||||||
if ok and type(jsjest) == "table" then
|
if ok and type(jsjest) == "table" then
|
||||||
@@ -1231,9 +1224,81 @@ local function pick_display(results, key, scope_kind)
|
|||||||
return results[key]
|
return results[key]
|
||||||
end
|
end
|
||||||
|
|
||||||
local function track_result_lines(start_line, results, scope_kind)
|
local function entry_root(name)
|
||||||
|
if not name or name == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local idx = name:find("/", 1, true)
|
||||||
|
if not idx then
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
return name:sub(1, idx - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parent_of(name)
|
||||||
|
if not name or name == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local last = nil
|
||||||
|
for i = #name, 1, -1 do
|
||||||
|
if name:sub(i, i) == "/" then
|
||||||
|
last = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not last then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return name:sub(1, last - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function group_entries_by_parent(entries)
|
||||||
|
local nodes = {}
|
||||||
|
local ordered = {}
|
||||||
|
local roots = {}
|
||||||
|
|
||||||
|
for _, entry in ipairs(entries) do
|
||||||
|
local full = entry.full
|
||||||
|
if full and full ~= "" then
|
||||||
|
if not nodes[full] then
|
||||||
|
nodes[full] = { entry = entry, children = {} }
|
||||||
|
table.insert(ordered, full)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(roots, { entry = entry, children = {} })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, full in ipairs(ordered) do
|
||||||
|
local node = nodes[full]
|
||||||
|
local parent = parent_of(full)
|
||||||
|
if parent and nodes[parent] then
|
||||||
|
table.insert(nodes[parent].children, node)
|
||||||
|
else
|
||||||
|
table.insert(roots, node)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local out = {}
|
||||||
|
local function emit(node)
|
||||||
|
if node.entry then
|
||||||
|
table.insert(out, node.entry)
|
||||||
|
end
|
||||||
|
for _, child in ipairs(node.children) do
|
||||||
|
emit(child)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, node in ipairs(roots) do
|
||||||
|
emit(node)
|
||||||
|
end
|
||||||
|
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
local function build_listing_entries(results, scope_kind)
|
||||||
if not results then
|
if not results then
|
||||||
return
|
return {}
|
||||||
end
|
end
|
||||||
local entries = {}
|
local entries = {}
|
||||||
local function append_kind(kind)
|
local function append_kind(kind)
|
||||||
@@ -1250,37 +1315,52 @@ local function track_result_lines(start_line, results, scope_kind)
|
|||||||
if not full_name or full_name == "" then
|
if not full_name or full_name == "" then
|
||||||
full_name = name
|
full_name = name
|
||||||
end
|
end
|
||||||
table.insert(entries, full_name)
|
table.insert(entries, {
|
||||||
|
kind = kind,
|
||||||
|
full = full_name,
|
||||||
|
display = name,
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
append_kind("passes")
|
append_kind("passes")
|
||||||
append_kind("skips")
|
append_kind("skips")
|
||||||
append_kind("failures")
|
append_kind("failures")
|
||||||
for i, name in ipairs(entries) do
|
local parent_set = {}
|
||||||
if name and name ~= "" then
|
for _, entry in ipairs(entries) do
|
||||||
state.last_result_line_map[start_line + i] = name
|
if entry.full and entry.full ~= "" then
|
||||||
|
parent_set[entry.full] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, entry in ipairs(entries) do
|
||||||
|
if entry.full and entry.full:find("/", 1, true) then
|
||||||
|
local parent = parent_of(entry.full)
|
||||||
|
if parent and parent_set[parent] then
|
||||||
|
entry.display = entry.full
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return group_entries_by_parent(entries)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function track_result_lines(start_line, results, scope_kind)
|
||||||
|
local entries = build_listing_entries(results, scope_kind)
|
||||||
|
for i, entry in ipairs(entries) do
|
||||||
|
if entry.full and entry.full ~= "" then
|
||||||
|
state.last_result_line_map[start_line + i] = entry.full
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function format_results(results, scope_kind)
|
local function format_results(results, scope_kind)
|
||||||
local lines = {}
|
local lines = {}
|
||||||
local passes = pick_display(results, "passes", scope_kind)
|
local entries = build_listing_entries(results, scope_kind)
|
||||||
if type(passes) == "table" then
|
for _, entry in ipairs(entries) do
|
||||||
for _, title in ipairs(passes) do
|
if entry.kind == "passes" then
|
||||||
table.insert(lines, "[ PASS ] - " .. title)
|
table.insert(lines, "[ PASS ] - " .. entry.display)
|
||||||
end
|
elseif entry.kind == "skips" then
|
||||||
end
|
table.insert(lines, "[ SKIP ] - " .. entry.display)
|
||||||
local skips = pick_display(results, "skips", scope_kind)
|
elseif entry.kind == "failures" then
|
||||||
if type(skips) == "table" then
|
table.insert(lines, "[ FAIL ] - " .. entry.display)
|
||||||
for _, title in ipairs(skips) do
|
|
||||||
table.insert(lines, "[ SKIP ] - " .. title)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local failures = pick_display(results, "failures", scope_kind)
|
|
||||||
if type(failures) == "table" then
|
|
||||||
for _, title in ipairs(failures) do
|
|
||||||
table.insert(lines, "[ FAIL ] - " .. title)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return lines
|
return lines
|
||||||
@@ -1308,6 +1388,79 @@ local function add_unique_items(target, items)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function init_aggregate_results()
|
||||||
|
return {
|
||||||
|
passes = {},
|
||||||
|
failures = {},
|
||||||
|
skips = {},
|
||||||
|
display = { passes = {}, failures = {}, skips = {} },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function merge_results(agg, results, seen)
|
||||||
|
if not agg or not results or not seen then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local function merge_kind(kind)
|
||||||
|
local items = results[kind]
|
||||||
|
if type(items) ~= "table" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local display_items = nil
|
||||||
|
if type(results.display) == "table" and type(results.display[kind]) == "table" then
|
||||||
|
display_items = results.display[kind]
|
||||||
|
end
|
||||||
|
for i, name in ipairs(items) do
|
||||||
|
if name and name ~= "" and not seen[kind][name] then
|
||||||
|
seen[kind][name] = true
|
||||||
|
table.insert(agg[kind], name)
|
||||||
|
if display_items and display_items[i] then
|
||||||
|
table.insert(agg.display[kind], display_items[i])
|
||||||
|
else
|
||||||
|
table.insert(agg.display[kind], name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
merge_kind("passes")
|
||||||
|
merge_kind("failures")
|
||||||
|
merge_kind("skips")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function should_group_results(results)
|
||||||
|
if not results then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local parent_set = {}
|
||||||
|
for _, kind in ipairs({ "passes", "failures", "skips" }) do
|
||||||
|
local list = results[kind]
|
||||||
|
if type(list) == "table" then
|
||||||
|
for _, name in ipairs(list) do
|
||||||
|
if name and name ~= "" and not name:find("/", 1, true) then
|
||||||
|
parent_set[name] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not next(parent_set) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
for _, kind in ipairs({ "passes", "failures", "skips" }) do
|
||||||
|
local list = results[kind]
|
||||||
|
if type(list) == "table" then
|
||||||
|
for _, name in ipairs(list) do
|
||||||
|
if name and name:find("/", 1, true) then
|
||||||
|
local root = entry_root(name)
|
||||||
|
if root and parent_set[root] then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
local function update_summary(summary, results)
|
local function update_summary(summary, results)
|
||||||
if not summary or not results then
|
if not summary or not results then
|
||||||
return
|
return
|
||||||
@@ -1480,6 +1633,14 @@ local function run_command(command, opts)
|
|||||||
local runner = options.runner
|
local runner = options.runner
|
||||||
local parser_state = {}
|
local parser_state = {}
|
||||||
parser_state.scope_kind = options.scope_kind
|
parser_state.scope_kind = options.scope_kind
|
||||||
|
parser_state.aggregate_results = nil
|
||||||
|
parser_state.result_start_line = nil
|
||||||
|
parser_state.result_end_line = nil
|
||||||
|
parser_state.seen = {
|
||||||
|
passes = {},
|
||||||
|
failures = {},
|
||||||
|
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"
|
||||||
local summary = make_summary_tracker(summary_enabled)
|
local summary = make_summary_tracker(summary_enabled)
|
||||||
@@ -1521,6 +1682,10 @@ local function run_command(command, opts)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
had_parsed_output = true
|
had_parsed_output = true
|
||||||
|
if not parser_state.aggregate_results then
|
||||||
|
parser_state.aggregate_results = init_aggregate_results()
|
||||||
|
end
|
||||||
|
merge_results(parser_state.aggregate_results, results, parser_state.seen)
|
||||||
if type(results.failures) == "table" then
|
if type(results.failures) == "table" then
|
||||||
for _, name in ipairs(results.failures) do
|
for _, name in ipairs(results.failures) do
|
||||||
if name and name ~= "" and not failures_seen[name] then
|
if name and name ~= "" and not failures_seen[name] then
|
||||||
@@ -1547,6 +1712,10 @@ local function run_command(command, opts)
|
|||||||
append_lines(buf, lines)
|
append_lines(buf, lines)
|
||||||
apply_result_highlights(buf, start_line, lines)
|
apply_result_highlights(buf, start_line, lines)
|
||||||
track_result_lines(start_line, results, options.scope_kind)
|
track_result_lines(start_line, results, options.scope_kind)
|
||||||
|
if not parser_state.result_start_line then
|
||||||
|
parser_state.result_start_line = start_line
|
||||||
|
end
|
||||||
|
parser_state.result_end_line = vim.api.nvim_buf_line_count(buf)
|
||||||
end
|
end
|
||||||
|
|
||||||
run_cmd(cmd, cwd, {
|
run_cmd(cmd, cwd, {
|
||||||
@@ -1603,6 +1772,18 @@ local function run_command(command, opts)
|
|||||||
handle_parsed(results)
|
handle_parsed(results)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if parser_state.aggregate_results and parser_state.result_start_line and should_group_results(parser_state.aggregate_results) then
|
||||||
|
local start_line = parser_state.result_start_line
|
||||||
|
local end_line = parser_state.result_end_line or start_line
|
||||||
|
local grouped = format_results(parser_state.aggregate_results, options.scope_kind)
|
||||||
|
vim.api.nvim_buf_set_lines(buf, start_line, end_line, false, grouped)
|
||||||
|
vim.api.nvim_buf_clear_namespace(buf, result_ns, start_line, end_line)
|
||||||
|
apply_result_highlights(buf, start_line, grouped)
|
||||||
|
state.last_result_line_map = {}
|
||||||
|
track_result_lines(start_line, parser_state.aggregate_results, options.scope_kind)
|
||||||
|
parser_state.result_end_line = start_line + #grouped
|
||||||
|
end
|
||||||
|
|
||||||
local pass_count, fail_count = count_summary(result_counts)
|
local pass_count, fail_count = count_summary(result_counts)
|
||||||
if fail_count > 0 then
|
if fail_count > 0 then
|
||||||
state.last_border_kind = "fail"
|
state.last_border_kind = "fail"
|
||||||
|
|||||||
181
runner-agents.md
Normal file
181
runner-agents.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# runner-agents.md — test-samurai Runner-API
|
||||||
|
|
||||||
|
Ziel: Diese Datei beschreibt die öffentliche Runner-API, die ein neuer Runner
|
||||||
|
implementieren muss, damit alle Commands vollständig unterstützt werden.
|
||||||
|
|
||||||
|
## Modulform (Pflicht)
|
||||||
|
|
||||||
|
- Der Runner ist ein Lua-Modul, das eine Table mit Funktionen zurückgibt.
|
||||||
|
- Beispiel:
|
||||||
|
- `local runner = {}`
|
||||||
|
- `return runner`
|
||||||
|
- Der Modulpfad in `runner_modules` muss exakt zum Dateipfad unter `lua/` passen.
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
- `is_test_file(bufnr) -> boolean`
|
||||||
|
- Wird für die Runner-Auswahl genutzt.
|
||||||
|
- `find_nearest(bufnr, row, col) -> spec|nil, err?`
|
||||||
|
- Für `TSamNearest`.
|
||||||
|
- `spec.file` muss gesetzt sein.
|
||||||
|
- Bei Fehler/kein Treffer: `nil, "reason"` zurückgeben.
|
||||||
|
- `build_command(spec) -> command_spec`
|
||||||
|
- Für `TSamNearest`.
|
||||||
|
- `build_file_command(bufnr) -> command_spec`
|
||||||
|
- Für `TSamFile`.
|
||||||
|
- `build_all_command(bufnr) -> command_spec`
|
||||||
|
- Für `TSamAll`.
|
||||||
|
- `build_failed_command(last_command, failures, scope_kind) -> command_spec`
|
||||||
|
- Für `TSamFailedOnly`.
|
||||||
|
|
||||||
|
## Output-Parsing (Listing + Summary)
|
||||||
|
|
||||||
|
Genau eine der folgenden Varianten muss vorhanden sein:
|
||||||
|
|
||||||
|
- `parse_results(output) -> results`
|
||||||
|
- `results` muss enthalten:
|
||||||
|
- `passes` (Array von Namen)
|
||||||
|
- `failures` (Array von Namen)
|
||||||
|
- `skips` (Array von Namen)
|
||||||
|
- Optional:
|
||||||
|
- `display = { passes = {}, failures = {}, skips = {} }`
|
||||||
|
- `failures_all` (für Streaming-Parser, um alle bisherigen Failures zu liefern)
|
||||||
|
- Wenn `display` fehlt, werden `passes/failures/skips` direkt im Listing angezeigt.
|
||||||
|
- 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).
|
||||||
|
|
||||||
|
## Listing-Gruppierung fuer Parent/Subtests
|
||||||
|
|
||||||
|
- Wenn Testnamen das Format `Parent/Subtest` oder verschachtelt `Parent/Sub/Subtest` haben
|
||||||
|
und der Parent ebenfalls in den Ergebnissen vorhanden ist, gruppiert das Listing:
|
||||||
|
- Parent kommt vor seinen direkten Kindern.
|
||||||
|
- Mehrstufige Subtests werden hierarchisch gruppiert (Parent -> Kind -> Enkel).
|
||||||
|
- Die Reihenfolge der Kinder folgt der Eingangsreihenfolge des Runners.
|
||||||
|
- Ohne Parent-Eintrag bleibt die normale Reihenfolge erhalten.
|
||||||
|
|
||||||
|
## Detail-Output (TSamShowOutput / <cr> im Listing)
|
||||||
|
|
||||||
|
- `parse_test_output(output) -> table`
|
||||||
|
- Rückgabeform:
|
||||||
|
- `{ [test_name] = { "line1", "line2", ... } }`
|
||||||
|
- `test_name` muss mit `results.*` korrespondieren (gleiches Namensschema).
|
||||||
|
|
||||||
|
## Quickfix-Unterstützung (Failures)
|
||||||
|
|
||||||
|
- `collect_failed_locations(failures, command, scope_kind) -> items`
|
||||||
|
- `items`: Array von Quickfix-Items
|
||||||
|
- `{ filename = "...", lnum = <number>, col = <number>, text = "..." }`
|
||||||
|
|
||||||
|
## Erwartete Datenformen
|
||||||
|
|
||||||
|
- `command_spec`:
|
||||||
|
- `{ cmd = { "binary", "arg1", ... }, cwd = "..." }`
|
||||||
|
- `cmd` darf nicht leer sein.
|
||||||
|
- `cwd` ist optional; wenn nicht gesetzt, nutzt der Core das aktuelle CWD.
|
||||||
|
- `spec` (von `find_nearest`):
|
||||||
|
- Muss mindestens `file` enthalten, z. B.:
|
||||||
|
- `{ file = "...", cwd = "...", test_name = "...", full_name = "...", kind = "..." }`
|
||||||
|
|
||||||
|
## Optional empfohlene Metadaten
|
||||||
|
|
||||||
|
- `name` (String)
|
||||||
|
- Wird in Fehlermeldungen und Logs angezeigt.
|
||||||
|
- `framework` (String)
|
||||||
|
- Wird zur Framework-Auswahl (z. B. JS) genutzt.
|
||||||
|
|
||||||
|
## Prompt-Beispiel
|
||||||
|
|
||||||
|
"Erstelle mir anhand der `runner-agents.md` einen neuen Runner für Rust."
|
||||||
|
|
||||||
|
## Minimaler Runner-Skeleton (Template)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local runner = {
|
||||||
|
name = "my-runner",
|
||||||
|
framework = "my-framework",
|
||||||
|
}
|
||||||
|
|
||||||
|
function runner.is_test_file(bufnr)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.find_nearest(bufnr, row, col)
|
||||||
|
return nil, "no test call found"
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.build_command(spec)
|
||||||
|
return { cmd = { "echo", "not-implemented" }, cwd = spec.cwd }
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.build_file_command(bufnr)
|
||||||
|
return { cmd = { "echo", "not-implemented" } }
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.build_all_command(bufnr)
|
||||||
|
return { cmd = { "echo", "not-implemented" } }
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.build_failed_command(last_command, failures, scope_kind)
|
||||||
|
return { cmd = { "echo", "not-implemented" }, cwd = last_command and last_command.cwd or nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.parse_results(output)
|
||||||
|
return { passes = {}, failures = {}, skips = {}, display = { passes = {}, failures = {}, skips = {} } }
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.parse_test_output(output)
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.collect_failed_locations(failures, command, scope_kind)
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
return runner
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checkliste für neue Runner
|
||||||
|
|
||||||
|
- is_test_file implementiert
|
||||||
|
- find_nearest implementiert (setzt `spec.file`)
|
||||||
|
- build_command implementiert
|
||||||
|
- build_file_command implementiert
|
||||||
|
- build_all_command implementiert
|
||||||
|
- build_failed_command implementiert
|
||||||
|
- parse_results oder output_parser implementiert
|
||||||
|
- 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.
|
||||||
|
- 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 Gitea-Action ist zu erstellen, die bei jedem Push die Tests ausfuehrt.
|
||||||
|
- Beispiel (anpassbarer Workflow):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Run tests
|
||||||
|
run: bash run_test.sh
|
||||||
|
```
|
||||||
@@ -1,2 +1,8 @@
|
|||||||
vim.opt.runtimepath:append(vim.loop.cwd())
|
local cwd = vim.loop.cwd()
|
||||||
|
vim.opt.runtimepath:append(cwd)
|
||||||
|
package.path = table.concat({
|
||||||
|
cwd .. "/lua/?.lua",
|
||||||
|
cwd .. "/lua/?/init.lua",
|
||||||
|
package.path,
|
||||||
|
}, ";")
|
||||||
require("plenary.busted")
|
require("plenary.busted")
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ describe("test-samurai core", function()
|
|||||||
assert.equals("go", runner.name)
|
assert.equals("go", runner.name)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it("does not fallback to Go runner when no runners are configured", function()
|
||||||
|
test_samurai.setup({ runner_modules = {} })
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/no_runner_test.go")
|
||||||
|
local runner = core.get_runner_for_buf(bufnr)
|
||||||
|
assert.is_nil(runner)
|
||||||
|
end)
|
||||||
|
|
||||||
it("selects JS jest runner for *.test.ts files", function()
|
it("selects JS jest runner for *.test.ts files", function()
|
||||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
vim.api.nvim_buf_set_name(bufnr, "/tmp/foo.test.ts")
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/foo.test.ts")
|
||||||
|
|||||||
@@ -1276,16 +1276,18 @@ describe("test-samurai output formatting", function()
|
|||||||
assert.is_true(set_calls.TestSamuraiSummarySkip.fg == 333)
|
assert.is_true(set_calls.TestSamuraiSummarySkip.fg == 333)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("formats go subtests as short names", function()
|
it("groups Go subtests under their parent in listing", function()
|
||||||
local json_line = vim.json.encode({
|
local json_lines = {
|
||||||
Action = "pass",
|
vim.json.encode({ Action = "pass", Test = "TestHandleGet/returns_200" }),
|
||||||
Test = "TestHandleGet/returns_200",
|
vim.json.encode({ Action = "fail", Test = "TestOther/returns_500" }),
|
||||||
})
|
vim.json.encode({ Action = "pass", Test = "TestHandleGet" }),
|
||||||
|
vim.json.encode({ Action = "skip", Test = "TestOther" }),
|
||||||
|
}
|
||||||
|
|
||||||
local orig_jobstart = vim.fn.jobstart
|
local orig_jobstart = vim.fn.jobstart
|
||||||
vim.fn.jobstart = function(_cmd, opts)
|
vim.fn.jobstart = function(_cmd, opts)
|
||||||
if opts and opts.on_stdout then
|
if opts and opts.on_stdout then
|
||||||
opts.on_stdout(1, { json_line }, nil)
|
opts.on_stdout(1, json_lines, nil)
|
||||||
end
|
end
|
||||||
if opts and opts.on_exit then
|
if opts and opts.on_exit then
|
||||||
opts.on_exit(1, 0, nil)
|
opts.on_exit(1, 0, nil)
|
||||||
@@ -1294,21 +1296,23 @@ describe("test-samurai output formatting", function()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
vim.api.nvim_buf_set_name(bufnr, "/tmp/output_go_short_test.go")
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/output_go_grouped_test.go")
|
||||||
vim.bo[bufnr].filetype = "go"
|
vim.bo[bufnr].filetype = "go"
|
||||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
|
||||||
"package main",
|
"package main",
|
||||||
"import \"testing\"",
|
"import \"testing\"",
|
||||||
"",
|
"",
|
||||||
"func TestHandleGet(t *testing.T) {",
|
"func TestHandleGet(t *testing.T) {",
|
||||||
" t.Run(\"returns_200\", func(t *testing.T) {",
|
" t.Run(\"returns_200\", func(t *testing.T) {})",
|
||||||
" -- inside test",
|
"}",
|
||||||
" })",
|
"",
|
||||||
|
"func TestOther(t *testing.T) {",
|
||||||
|
" t.Run(\"returns_500\", func(t *testing.T) {})",
|
||||||
"}",
|
"}",
|
||||||
})
|
})
|
||||||
|
|
||||||
vim.api.nvim_set_current_buf(bufnr)
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
vim.api.nvim_win_set_cursor(0, { 6, 0 })
|
vim.api.nvim_win_set_cursor(0, { 5, 0 })
|
||||||
|
|
||||||
core.run_nearest()
|
core.run_nearest()
|
||||||
|
|
||||||
@@ -1317,17 +1321,97 @@ describe("test-samurai output formatting", function()
|
|||||||
|
|
||||||
vim.fn.jobstart = orig_jobstart
|
vim.fn.jobstart = orig_jobstart
|
||||||
|
|
||||||
local has_pass = false
|
local idx_parent_1 = nil
|
||||||
local has_raw_json = false
|
local idx_sub_1 = nil
|
||||||
for _, line in ipairs(lines) do
|
local idx_parent_2 = nil
|
||||||
if line == "[ PASS ] - returns_200" then
|
local idx_sub_2 = nil
|
||||||
has_pass = true
|
for i, line in ipairs(lines) do
|
||||||
elseif line == json_line then
|
if line == "[ PASS ] - TestHandleGet" then
|
||||||
has_raw_json = true
|
idx_parent_1 = i
|
||||||
|
elseif line == "[ PASS ] - TestHandleGet/returns_200" then
|
||||||
|
idx_sub_1 = i
|
||||||
|
elseif line == "[ SKIP ] - TestOther" then
|
||||||
|
idx_parent_2 = i
|
||||||
|
elseif line == "[ FAIL ] - TestOther/returns_500" then
|
||||||
|
idx_sub_2 = i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
assert.is_true(has_pass)
|
|
||||||
assert.is_false(has_raw_json)
|
assert.is_not_nil(idx_parent_1)
|
||||||
|
assert.is_not_nil(idx_sub_1)
|
||||||
|
assert.is_not_nil(idx_parent_2)
|
||||||
|
assert.is_not_nil(idx_sub_2)
|
||||||
|
assert.is_true(idx_parent_1 < idx_sub_1)
|
||||||
|
assert.is_true(idx_parent_2 < idx_sub_2)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("groups nested Go subtests under subtest parents in listing", function()
|
||||||
|
local json_lines = {
|
||||||
|
vim.json.encode({ Action = "pass", Test = "TestWriteJSON/returns_500_when/data_could_not_be_serialized_and_logs_it" }),
|
||||||
|
vim.json.encode({ Action = "pass", Test = "TestWriteJSON" }),
|
||||||
|
vim.json.encode({ Action = "pass", Test = "TestWriteJSON/returns_500_when" }),
|
||||||
|
vim.json.encode({ Action = "pass", Test = "TestWriteJSON/returns_500_when/error_at_writing_response_occurs_and_logs_it" }),
|
||||||
|
}
|
||||||
|
|
||||||
|
local orig_jobstart = vim.fn.jobstart
|
||||||
|
vim.fn.jobstart = function(_cmd, opts)
|
||||||
|
if opts and opts.on_stdout then
|
||||||
|
opts.on_stdout(1, json_lines, nil)
|
||||||
|
end
|
||||||
|
if opts and opts.on_exit then
|
||||||
|
opts.on_exit(1, 0, nil)
|
||||||
|
end
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/output_go_nested_test.go")
|
||||||
|
vim.bo[bufnr].filetype = "go"
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
|
||||||
|
"package main",
|
||||||
|
"import \"testing\"",
|
||||||
|
"",
|
||||||
|
"func TestWriteJSON(t *testing.T) {",
|
||||||
|
" t.Run(\"returns_500_when\", func(t *testing.T) {",
|
||||||
|
" t.Run(\"data_could_not_be_serialized_and_logs_it\", func(t *testing.T) {})",
|
||||||
|
" t.Run(\"error_at_writing_response_occurs_and_logs_it\", func(t *testing.T) {})",
|
||||||
|
" })",
|
||||||
|
"}",
|
||||||
|
})
|
||||||
|
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 5, 0 })
|
||||||
|
|
||||||
|
core.run_nearest()
|
||||||
|
|
||||||
|
local out_buf = vim.api.nvim_get_current_buf()
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false)
|
||||||
|
|
||||||
|
vim.fn.jobstart = orig_jobstart
|
||||||
|
|
||||||
|
local idx_parent = nil
|
||||||
|
local idx_mid = nil
|
||||||
|
local idx_child_1 = nil
|
||||||
|
local idx_child_2 = nil
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
if line == "[ PASS ] - TestWriteJSON" then
|
||||||
|
idx_parent = i
|
||||||
|
elseif line == "[ PASS ] - TestWriteJSON/returns_500_when" then
|
||||||
|
idx_mid = i
|
||||||
|
elseif line == "[ PASS ] - TestWriteJSON/returns_500_when/data_could_not_be_serialized_and_logs_it" then
|
||||||
|
idx_child_1 = i
|
||||||
|
elseif line == "[ PASS ] - TestWriteJSON/returns_500_when/error_at_writing_response_occurs_and_logs_it" then
|
||||||
|
idx_child_2 = i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert.is_not_nil(idx_parent)
|
||||||
|
assert.is_not_nil(idx_mid)
|
||||||
|
assert.is_not_nil(idx_child_1)
|
||||||
|
assert.is_not_nil(idx_child_2)
|
||||||
|
assert.is_true(idx_parent < idx_mid)
|
||||||
|
assert.is_true(idx_mid < idx_child_1)
|
||||||
|
assert.is_true(idx_mid < idx_child_2)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("does not print raw JSON output for mocha json-stream", function()
|
it("does not print raw JSON output for mocha json-stream", function()
|
||||||
|
|||||||
Reference in New Issue
Block a user