Compare commits

...

20 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
6505a91cce add README.md
All checks were successful
tests / test (push) Successful in 9s
2026-01-02 15:22:11 +01:00
f5fc9822ce remove built-in runner 2026-01-02 15:21:42 +01:00
58f0edc14b add plenary install stage to ci test runner
All checks were successful
tests / test (push) Successful in 8s
2026-01-01 12:53:28 +01:00
77a7ebab4d add ci action runner
All checks were successful
tests / test (push) Successful in 6s
2026-01-01 12:49:26 +01:00
6ce8530cf7 add grouping for test listing entries 2025-12-31 21:19:29 +01:00
15bc792449 add ready2use prompting text for runner creations with codex or other ai agents 2025-12-31 21:18:53 +01:00
39 changed files with 2618 additions and 6285 deletions

View File

@@ -0,0 +1,41 @@
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: Install plenary.nvim
shell: bash
run: |
set -euo pipefail
target_dir="${HOME}/.local/share/nvim/site/pack/vendor/start/plenary.nvim"
mkdir -p "$(dirname "${target_dir}")"
git clone --depth 1 https://github.com/nvim-lua/plenary.nvim "${target_dir}"
- name: Run Tests
run: bash run_test.sh

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
output output
.nvimlog .nvimlog
.idea

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**

133
README.md Normal file
View File

@@ -0,0 +1,133 @@
# test-samurai.nvim
A Neovim plugin to run tests across multiple languages and frameworks with a unified UX and an extensible runner architecture.
## Requirements
- Neovim >= 0.11.4
- Lua
- Test runners are provided as separate Lua modules
## Installation (Lazy.nvim)
Use the GitHub repository. The example below shows how to add the Go runner as a dependency and configure it in `setup()`:
```lua
{
"m13r/test-samurai.nvim",
dependencies = {
"m13r/test-samurai-go-runner",
-- furthur samurai runners
},
config = function()
require("test-samurai").setup({
runner_modules = {
"test-samurai-go-runner",
-- furthur samurai runners
},
})
end,
}
```
## Configuration
### Runner modules (required)
test-samurai does not ship with any built-in runners. You must explicitly configure the runners you want to use:
```lua
require("test-samurai").setup({
runner_modules = {
"my-runners.go",
"my-runners.js",
},
})
```
If no runner matches the current test file, test-samurai will show:
```
[test-samurai] no runner installed for this kind of test
```
## Commands and keymaps
- `TSamNearest` -> `<leader>tn`
- `TSamFile` -> `<leader>tf`
- `TSamAll` -> `<leader>ta`
- `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
- 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 is shown in a floating container called **Testing-Float**.
- The **Test-Listing-Float** is the left subwindow and shows the test result list.
- The **Detail-Float** is the right subwindow and shows detailed output for a selected test.
- 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 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`
- `find_nearest`
- `build_command`
- `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.
## Known runners
- [`m13r/test-samurai-go-runner`](https://gitea.mschirmer.com/m13r/test-samurai-go-runner)
- [`m13r/test-samurai-jest-runner`](https://gitea.mschirmer.com/m13r/test-samurai-jest-runner)
- [`m13r/test-samurai-mocha-runner`](https://gitea.mschirmer.com/m13r/test-samurai-mocha-runner)
- [`m13r/test-samurai-vitest-runner`](https://gitea.mschirmer.com/m13r/test-samurai-vitest-runner)
## 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:
```sh
bash run_test.sh
```

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

@@ -1,12 +1,7 @@
local M = {} local M = {}
local defaults = { local defaults = {
runner_modules = { runner_modules = {},
"test-samurai.runners.go",
"test-samurai.runners.js-jest",
"test-samurai.runners.js-mocha",
"test-samurai.runners.js-vitest",
},
} }
local current = vim.deepcopy(defaults) local current = vim.deepcopy(defaults)

File diff suppressed because it is too large Load Diff

View File

@@ -1,502 +0,0 @@
local util = require("test-samurai.util")
local runner = {
name = "go",
}
local function find_block_end(lines, start_idx)
local depth = 0
local started = false
for i = start_idx, #lines do
local line = lines[i]
for j = 1, #line do
local ch = line:sub(j, j)
if ch == "{" then
depth = depth + 1
started = true
elseif ch == "}" then
if started then
depth = depth - 1
if depth == 0 then
return i - 1
end
end
end
end
end
return #lines - 1
end
local function find_test_functions(lines)
local funcs = {}
for i, line in ipairs(lines) do
local name = line:match("^%s*func%s+([%w_]+)%s*%(")
if not name then
name = line:match("^%s*func%s+%([^)]-%)%s+([%w_]+)%s*%(")
end
if name and line:find("%*testing%.T") then
local start_0 = i - 1
local end_0 = find_block_end(lines, i)
table.insert(funcs, {
name = name,
start = start_0,
["end"] = end_0,
})
end
end
return funcs
end
local function find_t_runs(lines, func)
local subtests = {}
for i = func.start + 1, func["end"] do
local line = lines[i + 1]
if line then
local name = line:match("t%.Run%(%s*['\"]([^'\"]+)['\"]")
if name then
local start_idx = i + 1
local end_0 = find_block_end(lines, start_idx)
table.insert(subtests, {
name = name,
start = start_idx - 1,
["end"] = end_0,
})
end
end
end
return subtests
end
local function escape_go_regex(s)
s = s or ""
return (s:gsub("([\\.^$|()%%[%]{}*+?%-])", "\\\\%1"))
end
local function build_run_pattern(spec)
local name = spec.test_path or ""
local escaped = escape_go_regex(name)
if spec.scope == "function" then
return "^" .. escaped .. "($|/)"
else
return "^" .. escaped .. "$"
end
end
local function build_pkg_arg(spec)
local file = spec.file
local cwd = spec.cwd
if not file or not cwd or file == "" or cwd == "" then
return "./..."
end
local dir = vim.fs.dirname(file)
if dir == cwd then
return "./"
end
if file:sub(1, #cwd) ~= cwd then
return "./..."
end
local rel = dir:sub(#cwd + 2)
if not rel or rel == "" then
return "./"
end
return "./" .. rel
end
local function collect_unique(list)
local out = {}
local seen = {}
for _, item in ipairs(list) do
if item and item ~= "" and not seen[item] then
seen[item] = true
table.insert(out, item)
end
end
return out
end
function runner.is_test_file(bufnr)
local path = util.get_buf_path(bufnr)
if not path or path == "" then
return false
end
return path:sub(-8) == "_test.go"
end
function runner.find_nearest(bufnr, row, _col)
if not runner.is_test_file(bufnr) then
return nil, "not a Go test file"
end
local lines = util.get_buf_lines(bufnr)
local funcs = find_test_functions(lines)
local current
for _, f in ipairs(funcs) do
if row >= f.start and row <= f["end"] then
current = f
break
end
end
if not current then
return nil, "cursor not inside a test function"
end
local subtests = find_t_runs(lines, current)
local inside_sub
for _, sub in ipairs(subtests) do
if row >= sub.start and row <= sub["end"] then
inside_sub = sub
break
end
end
local path = util.get_buf_path(bufnr)
local root = util.find_root(path, { "go.mod", ".git" })
if inside_sub then
local full = current.name .. "/" .. inside_sub.name
return {
file = path,
cwd = root,
test_path = full,
scope = "subtest",
func = current.name,
subtest = inside_sub.name,
}
else
return {
file = path,
cwd = root,
test_path = current.name,
scope = "function",
func = current.name,
}
end
end
function runner.build_command(spec)
local pattern = build_run_pattern(spec)
local pkg = build_pkg_arg(spec)
local cmd = { "go", "test", "-json", pkg, "-run", pattern }
return {
cmd = cmd,
cwd = spec.cwd,
}
end
function runner.build_file_command(bufnr)
local path = util.get_buf_path(bufnr)
if not path or path == "" then
return nil
end
local root = util.find_root(path, { "go.mod", ".git" })
if not root or root == "" then
root = vim.loop.cwd()
end
local spec = { file = path, cwd = root }
local pkg = build_pkg_arg(spec)
local cmd = { "go", "test", "-json", pkg }
local lines = util.get_buf_lines(bufnr)
local funcs = find_test_functions(lines)
local names = {}
for _, fn in ipairs(funcs) do
table.insert(names, fn.name)
end
names = collect_unique(names)
if #names > 0 then
local pattern_parts = {}
for _, name in ipairs(names) do
table.insert(pattern_parts, escape_go_regex(name))
end
local pattern = "^(" .. table.concat(pattern_parts, "|") .. ")$"
table.insert(cmd, "-run")
table.insert(cmd, pattern)
end
return {
cmd = cmd,
cwd = root,
}
end
function runner.build_all_command(bufnr)
local path = util.get_buf_path(bufnr)
local root
if path and path ~= "" then
root = util.find_root(path, { "go.mod", ".git" })
end
if not root or root == "" then
root = vim.loop.cwd()
end
local cmd = { "go", "test", "-json", "./..." }
return {
cmd = cmd,
cwd = root,
}
end
function runner.parse_results(output)
if not output or output == "" then
return { passes = {}, failures = {}, skips = {}, display = { passes = {}, failures = {}, skips = {} } }
end
local passes = {}
local failures = {}
local skips = {}
local display = { passes = {}, failures = {}, skips = {} }
for line in output:gmatch("[^\n]+") do
local ok, data = pcall(vim.json.decode, line)
if ok and type(data) == "table" then
if data.Test and data.Test ~= "" then
if data.Action == "pass" then
table.insert(passes, data.Test)
local short = data.Test:match("([^/]+)$") or data.Test
table.insert(display.passes, short)
elseif data.Action == "fail" then
table.insert(failures, data.Test)
local short = data.Test:match("([^/]+)$") or data.Test
table.insert(display.failures, short)
elseif data.Action == "skip" then
table.insert(skips, data.Test)
local short = data.Test:match("([^/]+)$") or data.Test
table.insert(display.skips, short)
end
end
end
end
return {
passes = collect_unique(passes),
failures = collect_unique(failures),
skips = collect_unique(skips),
display = display,
}
end
local function split_output_lines(text)
if not text or text == "" then
return {}
end
local lines = vim.split(text, "\n", { plain = true })
if #lines > 0 and lines[#lines] == "" then
table.remove(lines, #lines)
end
return lines
end
local function normalize_go_name(name)
if not name or name == "" then
return nil
end
return (name:gsub("%s+", "_"))
end
local function add_location(target, key, file, line, label)
if not key or key == "" or not file or file == "" or not line then
return
end
local text = label or key
if not target[key] then
target[key] = {}
end
table.insert(target[key], {
filename = file,
lnum = line,
col = 1,
text = text,
})
end
local function collect_file_locations(file, target)
local ok, lines = pcall(vim.fn.readfile, file)
if not ok or type(lines) ~= "table" then
return
end
local funcs = find_test_functions(lines)
for _, fn in ipairs(funcs) do
add_location(target, fn.name, file, fn.start + 1, fn.name)
local normalized = normalize_go_name(fn.name)
if normalized and normalized ~= fn.name then
add_location(target, normalized, file, fn.start + 1, fn.name)
end
for _, sub in ipairs(find_t_runs(lines, fn)) do
local full = fn.name .. "/" .. sub.name
add_location(target, full, file, sub.start + 1, full)
local normalized_full = normalize_go_name(full)
if normalized_full and normalized_full ~= full then
add_location(target, normalized_full, file, sub.start + 1, full)
end
end
end
end
local function collect_go_test_files(root)
if not root or root == "" then
root = vim.loop.cwd()
end
local files = vim.fn.globpath(root, "**/*_test.go", false, true)
if type(files) ~= "table" then
return {}
end
return files
end
function runner.parse_test_output(output)
local out = {}
if not output or output == "" then
return out
end
for line in output:gmatch("[^\n]+") do
local ok, data = pcall(vim.json.decode, line)
if ok and type(data) == "table" and data.Action == "output" and data.Test and data.Output then
if not out[data.Test] then
out[data.Test] = {}
end
for _, item in ipairs(split_output_lines(data.Output)) do
table.insert(out[data.Test], item)
end
end
end
return out
end
function runner.output_parser()
local seen_pass = {}
local seen_fail = {}
local failures = {}
local passes = {}
local skips = {}
local display = { passes = {}, failures = {}, skips = {} }
return {
on_line = function(line, _state)
local ok, data = pcall(vim.json.decode, line)
if not ok or type(data) ~= "table" then
return nil
end
local name = data.Test
if not name or name == "" then
return nil
end
local short = name:match("([^/]+)$") or name
if data.Action == "pass" and not seen_pass[name] then
seen_pass[name] = true
table.insert(passes, name)
table.insert(display.passes, short)
return {
passes = { name },
failures = {},
skips = {},
display = { passes = { short }, failures = {}, skips = {} },
failures_all = vim.deepcopy(failures),
}
elseif data.Action == "fail" and not seen_fail[name] then
seen_fail[name] = true
table.insert(failures, name)
table.insert(display.failures, short)
return {
passes = {},
failures = { name },
skips = {},
display = { passes = {}, failures = { short }, skips = {} },
failures_all = vim.deepcopy(failures),
}
elseif data.Action == "skip" and not seen_pass[name] then
seen_pass[name] = true
table.insert(skips, name)
table.insert(display.skips, short)
return {
passes = {},
failures = {},
skips = { name },
display = { passes = {}, failures = {}, skips = { short } },
failures_all = vim.deepcopy(failures),
}
end
return nil
end,
on_complete = function(_output, _state)
return nil
end,
}
end
function runner.build_failed_command(last_command, failures, _scope_kind)
if not last_command or type(last_command.cmd) ~= "table" then
return nil
end
local pattern_parts = {}
for _, name in ipairs(failures or {}) do
table.insert(pattern_parts, escape_go_regex(name))
end
if #pattern_parts == 0 then
return nil
end
local pattern = "^(" .. table.concat(pattern_parts, "|") .. ")$"
local cmd = {}
local skip_next = false
for _, arg in ipairs(last_command.cmd) do
if skip_next then
skip_next = false
elseif arg == "-run" then
skip_next = true
else
table.insert(cmd, arg)
end
end
table.insert(cmd, "-run")
table.insert(cmd, pattern)
return {
cmd = cmd,
cwd = last_command.cwd,
}
end
function runner.collect_failed_locations(failures, command, scope_kind)
if type(failures) ~= "table" or #failures == 0 then
return {}
end
local files = {}
if scope_kind == "all" then
files = collect_go_test_files(command and command.cwd or nil)
elseif command and command.file then
files = { command.file }
end
if #files == 0 then
return {}
end
local locations = {}
for _, file in ipairs(files) do
collect_file_locations(file, locations)
end
local items = {}
local seen = {}
local function add_locations(name, locs)
for _, loc in ipairs(locs or {}) do
local key = string.format("%s:%d:%s", loc.filename or "", loc.lnum or 0, loc.text or name or "")
if not seen[key] then
seen[key] = true
table.insert(items, loc)
end
end
end
for _, name in ipairs(failures) do
local direct = locations[name]
if direct then
add_locations(name, direct)
elseif not name:find("/", 1, true) then
for full, locs in pairs(locations) do
if full:sub(-#name - 1) == "/" .. name then
add_locations(full, locs)
end
end
end
end
return items
end
return runner

View File

@@ -1,8 +0,0 @@
local js = require("test-samurai.runners.js")
return js.new({
name = "js-jest",
framework = "jest",
command = { "npx", "jest" },
json_args = { "--json", "--verbose" },
})

View File

@@ -1,9 +0,0 @@
local js = require("test-samurai.runners.js")
return js.new({
name = "js-mocha",
framework = "mocha",
command = { "npx", "mocha" },
all_glob = "test/**/*.test.js",
json_args = { "--reporter", "json-stream" },
})

View File

@@ -1,8 +0,0 @@
local js = require("test-samurai.runners.js")
return js.new({
name = "js-vitest",
framework = "vitest",
command = { "npx", "vitest" },
json_args = { "--reporter", "tap-flat" },
})

File diff suppressed because it is too large Load Diff

247
runner-agents.md Normal file
View File

@@ -0,0 +1,247 @@
# 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- und Keymap-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`.
- `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)
Beide Varianten müssen 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).
- `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
- 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 = "..." }`
- `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
- `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.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
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 implementiert
- output_parser implementiert (Streaming)
- parse_test_output implementiert
- collect_failed_locations implementiert
- command_spec `{ cmd, cwd }` korrekt zurückgegeben
## Projekt- und Prozessanforderungen
- 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` 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
name: tests
on:
push:
branches:
- "**"
jobs:
test:
runs-on: ubuntu-latest
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.

View File

@@ -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")

File diff suppressed because it is too large Load Diff

View File

@@ -1,339 +0,0 @@
local test_samurai = require("test-samurai")
local core = require("test-samurai.core")
local function mkbuf(path, ft, lines)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, path)
vim.bo[bufnr].filetype = ft
if lines then
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
end
return bufnr
end
local function stub_jobstart(opts_config)
local calls = {}
local orig = vim.fn.jobstart
local idx = 0
local config = opts_config or {}
vim.fn.jobstart = function(cmd, opts)
idx = idx + 1
table.insert(calls, { cmd = cmd, opts = opts })
local code = 0
if type(config.exit_codes) == "table" then
code = config.exit_codes[idx] or 0
elseif type(config.exit_codes) == "number" then
code = config.exit_codes
end
local out = config.stdout and config.stdout[idx] or nil
if out and opts and opts.on_stdout then
if type(out) == "string" then
out = { out }
end
opts.on_stdout(1, out, nil)
end
local err = config.stderr and config.stderr[idx] or nil
if err and opts and opts.on_stderr then
if type(err) == "string" then
err = { err }
end
opts.on_stderr(1, err, nil)
end
if opts and opts.on_exit then
opts.on_exit(1, code, nil)
end
return 1
end
return calls, orig
end
describe("TSamFailedOnly", function()
before_each(function()
test_samurai.setup()
end)
it("reruns failed jest tests with --onlyFailures", function()
local json = vim.json.encode({
testResults = {
{
assertionResults = {
{ status = "passed", title = "inner 1", fullName = "outer inner 1" },
{ status = "failed", title = "inner 2", fullName = "outer inner 2" },
},
},
},
})
local calls, orig_jobstart = stub_jobstart({
exit_codes = { 1, 0 },
stdout = { { json } },
})
local bufnr = mkbuf("/tmp/project/foo_failed_only.test.ts", "typescript", {
'describe("outer", function() {',
' it("inner 1", function() {',
" -- inside 1",
" })",
"",
' it("inner 2", function() {',
" -- inside 2",
" })",
"})",
})
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 7, 0 })
core.run_nearest()
core.run_failed_only()
vim.fn.jobstart = orig_jobstart
assert.equals(2, #calls)
assert.are.same(
{ "npx", "jest", "--json", "--verbose", "/tmp/project/foo_failed_only.test.ts", "-t", "inner 2" },
calls[1].cmd
)
assert.are.same(
{ "npx", "jest", "--json", "--verbose", "-t", "outer inner 2", "/tmp/project/foo_failed_only.test.ts" },
calls[2].cmd
)
end)
it("falls back to TSamLast when last run had no failures", function()
local json = vim.json.encode({
testResults = {
{
assertionResults = {
{ status = "passed", title = "inner 1", fullName = "outer inner 1" },
},
},
},
})
local calls, orig_jobstart = stub_jobstart({
exit_codes = { 0, 0 },
stdout = { { json } },
})
local bufnr = mkbuf("/tmp/project/foo_failed_only_pass.test.ts", "typescript", {
'describe("outer", function() {',
' it("inner 1", function() {',
" -- inside 1",
" })",
"",
' it("inner 2", function() {',
" -- inside 2",
" })",
"})",
})
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 7, 0 })
core.run_nearest()
core.run_failed_only()
vim.fn.jobstart = orig_jobstart
assert.equals(2, #calls)
assert.are.same(calls[1].cmd, calls[2].cmd)
end)
it("reruns failed go tests with -run regex", function()
local json_lines = {
vim.json.encode({ Action = "fail", Test = "TestFoo/first" }),
vim.json.encode({ Action = "fail", Test = "TestBar" }),
}
local calls, orig_jobstart = stub_jobstart({
exit_codes = { 1, 0 },
stdout = { json_lines },
})
local bufnr = mkbuf("/tmp/project/foo_failed_only_test.go", "go", {
"package main",
"import \"testing\"",
"",
"func TestFoo(t *testing.T) {",
" t.Run(\"first\", func(t *testing.T) {",
" -- inside first",
" })",
"}",
"",
"func TestBar(t *testing.T) {",
" -- inside bar",
"}",
})
vim.api.nvim_set_current_buf(bufnr)
core.run_all()
core.run_failed_only()
vim.fn.jobstart = orig_jobstart
assert.equals(2, #calls)
assert.are.same({ "go", "test", "-json", "./..." }, calls[1].cmd)
assert.are.same({ "go", "test", "-json", "./...", "-run", "^(TestFoo/first|TestBar)$" }, calls[2].cmd)
end)
it("uses go parser for failed-only output (no raw JSON)", function()
local json_line = vim.json.encode({
Action = "fail",
Test = "TestHandleGet/returns_200",
})
local calls, orig_jobstart = stub_jobstart({
exit_codes = { 1, 1 },
stdout = { { json_line }, { json_line } },
})
local bufnr = mkbuf("/tmp/project/foo_failed_only_output_test.go", "go", {
"package main",
"import \"testing\"",
"",
"func TestHandleGet(t *testing.T) {",
" t.Run(\"returns_200\", func(t *testing.T) {",
" -- inside test",
" })",
"}",
})
vim.api.nvim_set_current_buf(bufnr)
core.run_all()
core.run_failed_only()
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
assert.equals(2, #calls)
local has_raw = false
for _, line in ipairs(lines) do
if line == json_line then
has_raw = true
break
end
end
assert.is_false(has_raw)
end)
it("reruns failed mocha tests from json-stream array output without raw JSON", function()
test_samurai.setup({
runner_modules = {
"test-samurai.runners.js-mocha",
},
})
local fail_line = vim.json.encode({
event = "fail",
fullTitle = "API :: /brands... GET: /",
})
local start_line = vim.json.encode({ "start", { total = 1 } })
local end_line = vim.json.encode({ "end", { tests = 0 } })
local calls, orig_jobstart = stub_jobstart({
exit_codes = { 1, 1 },
stdout = { { fail_line }, { start_line, end_line } },
})
local bufnr = mkbuf("/tmp/project/brands.test.js", "javascript", {
'describe("API :: /brands...", function() {',
' it("GET: /", function() {',
" -- inside test",
" })",
"})",
})
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 3, 0 })
core.run_file()
core.run_failed_only()
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
assert.equals(2, #calls)
assert.are.same(
{ "npx", "mocha", "--reporter", "json-stream", "/tmp/project/brands.test.js" },
calls[1].cmd
)
local failed_cmd = calls[2].cmd or {}
local saw_grep = false
local saw_fgrep = false
local saw_title = false
local plain_title = "API :: /brands... GET: /"
for _, arg in ipairs(failed_cmd) do
if arg == "--grep" then
saw_grep = true
elseif arg == "--fgrep" then
saw_fgrep = true
elseif arg == plain_title then
saw_title = true
end
end
assert.is_false(saw_grep)
assert.is_true(saw_fgrep)
assert.is_true(saw_title)
local has_raw = false
for _, line in ipairs(lines) do
if line == start_line or line == end_line then
has_raw = true
break
end
end
assert.is_false(has_raw)
end)
it("does not affect TSamLast history", function()
local json = vim.json.encode({
testResults = {
{
assertionResults = {
{ status = "failed", title = "inner 2", fullName = "outer inner 2" },
},
},
},
})
local calls, orig_jobstart = stub_jobstart({
exit_codes = { 1, 1, 1 },
stdout = { { json } },
})
local bufnr = mkbuf("/tmp/project/foo_failed_only_last.test.ts", "typescript", {
'describe("outer", function() {',
' it("inner 1", function() {',
" -- inside 1",
" })",
"",
' it("inner 2", function() {',
" -- inside 2",
" })",
"})",
})
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 7, 0 })
core.run_nearest()
core.run_failed_only()
core.run_last()
vim.fn.jobstart = orig_jobstart
assert.equals(3, #calls)
assert.are.same(calls[1].cmd, calls[3].cmd)
assert.are.same(
{ "npx", "jest", "--json", "--verbose", "-t", "outer inner 2", "/tmp/project/foo_failed_only_last.test.ts" },
calls[2].cmd
)
end)
end)

View File

@@ -1,147 +0,0 @@
local go_runner = require("test-samurai.runners.go")
local util = require("test-samurai.util")
describe("test-samurai go runner", function()
it("detects Go test files by suffix", function()
local bufnr1 = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr1, "/tmp/go_suffix_test.go")
assert.is_true(go_runner.is_test_file(bufnr1))
local bufnr2 = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr2, "/tmp/go_main.go")
assert.is_false(go_runner.is_test_file(bufnr2))
end)
it("finds subtest when cursor is inside t.Run block", function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, "/tmp/go_subtest_test.go")
local lines = {
"package main",
"import \"testing\"",
"",
"func TestFoo(t *testing.T) {",
" t.Run(\"first\", func(t *testing.T) {",
" -- inside first",
" })",
"",
" t.Run(\"second\", func(t *testing.T) {",
" -- inside second",
" })",
"}",
}
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
local orig_fs_find = vim.fs.find
vim.fs.find = function(markers, opts)
return { "/tmp/go.mod" }
end
local row_inside_first = 5
local spec, err = go_runner.find_nearest(bufnr, row_inside_first, 0)
vim.fs.find = orig_fs_find
assert.is_nil(err)
assert.is_not_nil(spec)
assert.equals("TestFoo/first", spec.test_path)
assert.equals("subtest", spec.scope)
assert.is_true(spec.file:match("go_subtest_test%.go$") ~= nil)
assert.is_true(spec.cwd:match("tmp$") ~= nil)
end)
it("falls back to whole test function when between subtests", function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, "/tmp/go_between_test.go")
local lines = {
"package main",
"import \"testing\"",
"",
"func TestFoo(t *testing.T) {",
" t.Run(\"first\", func(t *testing.T) {",
" -- inside first",
" })",
"",
" t.Run(\"second\", func(t *testing.T) {",
" -- inside second",
" })",
"}",
}
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
local orig_fs_find = vim.fs.find
vim.fs.find = function(markers, opts)
return { "/tmp/go.mod" }
end
local row_between = 7
local spec, err = go_runner.find_nearest(bufnr, row_between, 0)
vim.fs.find = orig_fs_find
assert.is_nil(err)
assert.is_not_nil(spec)
assert.equals("TestFoo", spec.test_path)
assert.equals("function", spec.scope)
assert.is_true(spec.file:match("go_between_test%.go$") ~= nil)
assert.is_true(spec.cwd:match("tmp$") ~= nil)
end)
it("build_command uses current package and correct run pattern", function()
local spec_sub = {
file = "/tmp/project/pkg/foo_test.go",
cwd = "/tmp/project",
test_path = "TestFoo/first",
scope = "subtest",
}
local cmd_spec_sub = go_runner.build_command(spec_sub)
assert.are.same(
{ "go", "test", "-json", "./pkg", "-run", "^TestFoo/first$" },
cmd_spec_sub.cmd
)
local spec_func = {
file = "/tmp/project/foo_test.go",
cwd = "/tmp/project",
test_path = "TestFoo",
scope = "function",
}
local cmd_spec_func = go_runner.build_command(spec_func)
assert.are.same(
{ "go", "test", "-json", "./", "-run", "^TestFoo($|/)" },
cmd_spec_func.cmd
)
end)
it("build_file_command uses exact test names from current file", function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, "/tmp/project/get_test.go")
local lines = {
"package main",
"import \"testing\"",
"",
"func TestHandleGet(t *testing.T) {",
" t.Run(\"returns_200\", func(t *testing.T) {",
" -- inside test",
" })",
"}",
}
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
local orig_find_root = util.find_root
util.find_root = function(path, markers)
return "/tmp/project"
end
local cmd_spec = go_runner.build_file_command(bufnr)
util.find_root = orig_find_root
assert.are.same(
{ "go", "test", "-json", "./", "-run", "^(TestHandleGet)$" },
cmd_spec.cmd
)
assert.equals("/tmp/project", cmd_spec.cwd)
end)
end)

View File

@@ -1,203 +0,0 @@
local jest = require("test-samurai.runners.js-jest")
local mocha = require("test-samurai.runners.js-mocha")
local vitest = require("test-samurai.runners.js-vitest")
local util = require("test-samurai.util")
describe("test-samurai js runner (jest)", function()
it("detects JS/TS test files by name and filetype", function()
local bufnr1 = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr1, "/tmp/foo_detect.test.ts")
vim.bo[bufnr1].filetype = "typescript"
assert.is_true(jest.is_test_file(bufnr1))
local bufnr2 = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr2, "/tmp/foo_detect.ts")
vim.bo[bufnr2].filetype = "typescript"
assert.is_false(jest.is_test_file(bufnr2))
end)
it("finds nearest it() call as test name and builds full_name when cursor is inside the test", function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, "/tmp/foo_nearest.test.ts")
vim.bo[bufnr].filetype = "typescript"
local lines = {
'describe("outer", function() {',
' it("inner 1", function() {',
" -- inside 1",
" })",
"",
' it("inner 2", function() {',
" -- inside 2",
" })",
"})",
}
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
local orig_fs_find = vim.fs.find
vim.fs.find = function(names, opts)
return { "/tmp/package.json" }
end
local row_inside_second = 6
local spec, err = jest.find_nearest(bufnr, row_inside_second, 0)
vim.fs.find = orig_fs_find
assert.is_nil(err)
assert.is_not_nil(spec)
assert.equals("inner 2", spec.test_name)
assert.equals("outer inner 2", spec.full_name)
assert.equals("jest", spec.framework)
assert.is_true(spec.file:match("foo_nearest%.test%.ts$") ~= nil)
assert.is_true(spec.cwd:match("tmp$") ~= nil)
local cmd_spec = jest.build_command(spec)
assert.are.same({ "npx", "jest", "--json", "--verbose", spec.file, "-t", "inner 2" }, cmd_spec.cmd)
end)
it("returns describe block when cursor is between it() calls", function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, "/tmp/foo_between.test.ts")
vim.bo[bufnr].filetype = "typescript"
local lines = {
'describe("outer", function() {',
' it("inner 1", function() {',
" -- inside 1",
" })",
"",
' it("inner 2", function() {',
" -- inside 2",
" })",
"})",
}
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
local orig_fs_find = vim.fs.find
vim.fs.find = function(names, opts)
return { "/tmp/package.json" }
end
local row_between = 4
local spec, err = jest.find_nearest(bufnr, row_between, 0)
vim.fs.find = orig_fs_find
assert.is_nil(err)
assert.is_not_nil(spec)
assert.equals("outer", spec.test_name)
assert.equals("outer", spec.full_name)
assert.equals("jest", spec.framework)
end)
it("treats jest.config in test/.bin as project root parent", function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, "/tmp/foo_binroot.test.ts")
vim.bo[bufnr].filetype = "typescript"
local lines = {
'describe("outer", function() {',
' it("inner 1", function() {',
" -- inside 1",
" })",
"",
' it("inner 2", function() {',
" -- inside 2",
" })",
"})",
}
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
local orig_fs_find = vim.fs.find
vim.fs.find = function(names, opts)
return { "/tmp/test/.bin/jest.config.js" }
end
local row_inside_second = 6
local spec, err = jest.find_nearest(bufnr, row_inside_second, 0)
vim.fs.find = orig_fs_find
assert.is_nil(err)
assert.is_not_nil(spec)
assert.equals("/tmp", spec.cwd)
end)
end)
describe("test-samurai js runner (mocha)", function()
it("builds mocha command with fgrep and full test title", function()
local spec = {
file = "/tmp/project/test/foo_nearest.test.ts",
cwd = "/tmp/project",
test_name = "inner 2",
full_name = "outer inner 2",
}
local cmd_spec = mocha.build_command(spec)
assert.are.same(
{ "npx", "mocha", "--reporter", "json-stream", "--fgrep", "outer inner 2", spec.file },
cmd_spec.cmd
)
assert.equals("/tmp/project", cmd_spec.cwd)
end)
it("builds mocha all command with default glob", function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, "/tmp/project/test/foo_all.test.js")
vim.bo[bufnr].filetype = "javascript"
local orig_find_root = util.find_root
util.find_root = function(path, markers)
return "/tmp/project"
end
local cmd_spec = mocha.build_all_command(bufnr)
util.find_root = orig_find_root
assert.are.same(
{ "npx", "mocha", "--reporter", "json-stream", "test/**/*.test.js" },
cmd_spec.cmd
)
assert.equals("/tmp/project", cmd_spec.cwd)
end)
end)
describe("test-samurai js runner (vitest)", function()
it("builds vitest command with tap-flat reporter", function()
local spec = {
file = "/tmp/project/test/foo_nearest.test.ts",
cwd = "/tmp/project",
test_name = "inner 2",
full_name = "outer inner 2",
}
local cmd_spec = vitest.build_command(spec)
assert.are.same(
{ "npx", "vitest", "--reporter", "tap-flat", spec.file, "-t", "inner 2" },
cmd_spec.cmd
)
assert.equals("/tmp/project", cmd_spec.cwd)
end)
it("builds vitest all command with tap-flat reporter", function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, "/tmp/project/test/foo_all.test.ts")
vim.bo[bufnr].filetype = "typescript"
local orig_find_root = util.find_root
util.find_root = function(path, markers)
return "/tmp/project"
end
local cmd_spec = vitest.build_all_command(bufnr)
util.find_root = orig_find_root
assert.are.same(
{ "npx", "vitest", "--reporter", "tap-flat" },
cmd_spec.cmd
)
assert.equals("/tmp/project", cmd_spec.cwd)
end)
end)

View File

@@ -1,139 +0,0 @@
local test_samurai = require("test-samurai")
local core = require("test-samurai.core")
local function mkbuf(path, ft, lines)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, path)
vim.bo[bufnr].filetype = ft
if lines then
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
end
return bufnr
end
local function capture_jobstart()
local calls = {}
local orig = vim.fn.jobstart
vim.fn.jobstart = function(cmd, opts)
table.insert(calls, { cmd = cmd, opts = opts })
return 1
end
return calls, orig
end
describe("TSamLast", function()
before_each(function()
test_samurai.setup()
end)
it("reruns last Go command", function()
local calls, orig_jobstart = capture_jobstart()
local bufnr = mkbuf("/tmp/project/foo_test.go", "go", {
"package main",
"import \"testing\"",
"",
"func TestFoo(t *testing.T) {",
" t.Run(\"first\", func(t *testing.T) {",
" -- inside first",
" })",
"}",
})
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 6, 0 })
core.run_nearest()
core.run_last()
vim.fn.jobstart = orig_jobstart
assert.equals(2, #calls)
assert.are.same({ "go", "test", "-json", "./", "-run", "^TestFoo/first$" }, calls[1].cmd)
assert.are.same(calls[1].cmd, calls[2].cmd)
assert.equals(calls[1].opts.cwd, calls[2].opts.cwd)
end)
it("uses go parser for TSamLast output (no raw JSON)", function()
local json_line = vim.json.encode({
Action = "fail",
Test = "TestHandleGet/returns_200",
})
local calls = {}
local orig_jobstart = vim.fn.jobstart
vim.fn.jobstart = function(cmd, opts)
table.insert(calls, { cmd = cmd, opts = opts })
if opts and opts.on_stdout then
opts.on_stdout(1, { json_line }, nil)
end
if opts and opts.on_exit then
opts.on_exit(1, 1, nil)
end
return 1
end
local bufnr = mkbuf("/tmp/project/foo_last_output_test.go", "go", {
"package main",
"import \"testing\"",
"",
"func TestHandleGet(t *testing.T) {",
" t.Run(\"returns_200\", func(t *testing.T) {",
" -- inside test",
" })",
"}",
})
vim.api.nvim_set_current_buf(bufnr)
core.run_all()
core.run_last()
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
assert.equals(2, #calls)
local has_raw = false
for _, line in ipairs(lines) do
if line == json_line then
has_raw = true
break
end
end
assert.is_false(has_raw)
end)
it("reruns last JS command", function()
local calls, orig_jobstart = capture_jobstart()
local bufnr = mkbuf("/tmp/project/foo_last.test.ts", "typescript", {
'describe("outer", function() {',
' it("inner 1", function() {',
" -- inside 1",
" })",
"",
' it("inner 2", function() {',
" -- inside 2",
" })",
"})",
})
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 7, 0 })
core.run_nearest()
core.run_last()
vim.fn.jobstart = orig_jobstart
assert.equals(2, #calls)
assert.are.same(
{ "npx", "jest", "--json", "--verbose", "/tmp/project/foo_last.test.ts", "-t", "inner 2" },
calls[1].cmd
)
assert.are.same(calls[1].cmd, calls[2].cmd)
assert.equals(calls[1].opts.cwd, calls[2].opts.cwd)
end)
end)

File diff suppressed because it is too large Load Diff

View File

@@ -1,97 +0,0 @@
local test_samurai = require("test-samurai")
local core = require("test-samurai.core")
local function close_output_container()
local keys = vim.api.nvim_replace_termcodes("<esc><esc>", true, false, true)
local attempts = 5
while attempts > 0 do
local float_win = nil
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
local cfg = vim.api.nvim_win_get_config(win)
if cfg.relative ~= "" then
float_win = win
break
end
end
if not float_win then
break
end
vim.api.nvim_set_current_win(float_win)
vim.api.nvim_feedkeys(keys, "x", false)
vim.wait(20, function()
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
local cfg = vim.api.nvim_win_get_config(win)
if cfg.relative ~= "" then
return false
end
end
return true
end)
attempts = attempts - 1
end
end
local function stub_jobstart(opts_config)
local orig = vim.fn.jobstart
local config = opts_config or {}
vim.fn.jobstart = function(_cmd, opts)
local out = config.stdout or nil
if out and opts and opts.on_stdout then
if type(out) == "string" then
out = { out }
end
opts.on_stdout(1, out, nil)
end
if opts and opts.on_exit then
opts.on_exit(1, config.exit_code or 0, nil)
end
return 1
end
return orig
end
describe("test-samurai quickfix (js)", function()
before_each(function()
test_samurai.setup()
end)
after_each(function()
close_output_container()
vim.fn.setqflist({}, "r")
end)
it("mappt jest-verbose Failures auf die Zeile des Tests", function()
local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf_js")
vim.fn.mkdir(root, "p")
local path = root .. "/foo_qf.test.ts"
vim.fn.writefile({
'describe("outer", function() {',
' it("inner 1", function() {',
" })",
"",
' it("inner 2", function() {',
" })",
"})",
}, path)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, path)
vim.bo[bufnr].filetype = "typescript"
vim.api.nvim_set_current_buf(bufnr)
local fail_symbol = string.char(0xE2, 0x9C, 0x95)
local orig_jobstart = stub_jobstart({
exit_code = 1,
stdout = { " " .. fail_symbol .. " inner 2" },
})
core.run_file()
local qf = vim.fn.getqflist()
assert.equals(1, #qf)
assert.equals(path, vim.fn.bufname(qf[1].bufnr))
assert.equals(5, qf[1].lnum)
vim.fn.jobstart = orig_jobstart
end)
end)

View File

@@ -1,409 +0,0 @@
local test_samurai = require("test-samurai")
local core = require("test-samurai.core")
local function close_output_container()
local keys = vim.api.nvim_replace_termcodes("<esc><esc>", true, false, true)
local attempts = 5
while attempts > 0 do
local float_win = nil
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
local cfg = vim.api.nvim_win_get_config(win)
if cfg.relative ~= "" then
float_win = win
break
end
end
if not float_win then
break
end
vim.api.nvim_set_current_win(float_win)
vim.api.nvim_feedkeys(keys, "x", false)
vim.wait(20, function()
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
local cfg = vim.api.nvim_win_get_config(win)
if cfg.relative ~= "" then
return false
end
end
return true
end)
attempts = attempts - 1
end
end
local function stub_jobstart(opts_config)
local orig = vim.fn.jobstart
local idx = 0
local config = opts_config or {}
vim.fn.jobstart = function(cmd, opts)
idx = idx + 1
local code = 0
if type(config.exit_codes) == "table" then
code = config.exit_codes[idx] or 0
elseif type(config.exit_codes) == "number" then
code = config.exit_codes
end
local out = config.stdout and config.stdout[idx] or nil
if out and opts and opts.on_stdout then
if type(out) == "string" then
out = { out }
end
opts.on_stdout(1, out, nil)
end
if opts and opts.on_exit then
opts.on_exit(1, code, nil)
end
return 1
end
return orig
end
describe("test-samurai quickfix", function()
before_each(function()
test_samurai.setup()
end)
after_each(function()
close_output_container()
vim.fn.setqflist({}, "r")
end)
it("fuellt die Quickfix-Liste mit Fehltests und leert sie bei Erfolg", function()
local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf")
vim.fn.mkdir(root, "p")
local path = root .. "/foo_test.go"
vim.fn.writefile({
"package foo",
"",
"func TestFoo(t *testing.T) {",
' t.Run("bar", func(t *testing.T) {',
" })",
"}",
"",
"func TestBaz(t *testing.T) {",
"}",
}, path)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, path)
vim.bo[bufnr].filetype = "go"
vim.api.nvim_set_current_buf(bufnr)
local orig_jobstart = stub_jobstart({
exit_codes = { 1, 0 },
stdout = {
{
vim.json.encode({ Action = "fail", Test = "TestFoo/bar" }),
vim.json.encode({ Action = "fail", Test = "TestBaz" }),
},
{ vim.json.encode({ Action = "pass", Test = "TestFoo" }) },
},
})
core.run_file()
local first = vim.fn.getqflist()
assert.equals(2, #first)
assert.equals(path, vim.fn.bufname(first[1].bufnr))
assert.equals(4, first[1].lnum)
assert.equals(path, vim.fn.bufname(first[2].bufnr))
assert.equals(8, first[2].lnum)
close_output_container()
vim.api.nvim_set_current_buf(bufnr)
core.run_file()
local second = vim.fn.getqflist()
assert.equals(0, #second)
vim.fn.jobstart = orig_jobstart
end)
it("enthaelt bei Go auch Eltern- und Subtest-Failures im Quickfix", function()
local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf_go_sub")
vim.fn.mkdir(root, "p")
local path = root .. "/foo_sub_test.go"
vim.fn.writefile({
"package foo",
"",
"func TestAwesomeThing(t *testing.T) {",
' t.Run("evergreen", func(t *testing.T) {',
" })",
"",
' t.Run("everred", func(t *testing.T) {',
" })",
"}",
}, path)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, path)
vim.bo[bufnr].filetype = "go"
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
"package foo",
"",
"func TestAwesomeThing(t *testing.T) {",
' t.Run("evergreen", func(t *testing.T) {',
" })",
"",
' t.Run("everred", func(t *testing.T) {',
" })",
"}",
})
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 6, 0 })
local orig_jobstart = stub_jobstart({
exit_codes = { 1 },
stdout = {
{
vim.json.encode({ Action = "fail", Test = "TestAwesomeThing/everred" }),
vim.json.encode({ Action = "fail", Test = "TestAwesomeThing" }),
},
},
})
core.run_nearest()
local qf = vim.fn.getqflist()
assert.equals(2, #qf)
assert.equals(path, vim.fn.bufname(qf[1].bufnr))
assert.equals(path, vim.fn.bufname(qf[2].bufnr))
local lines = { qf[1].lnum, qf[2].lnum }
table.sort(lines)
assert.are.same({ 3, 7 }, lines)
vim.fn.jobstart = orig_jobstart
end)
it("vereinigt Failures aus Parser und Scope fuer Go", function()
local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf_go_union")
vim.fn.mkdir(root, "p")
local path = root .. "/foo_union_test.go"
vim.fn.writefile({
"package foo",
"",
"func TestAwesomeThing(t *testing.T) {",
' t.Run(\"evergreen\", func(t *testing.T) {',
" })",
"",
' t.Run(\"everred\", func(t *testing.T) {',
" })",
"}",
}, path)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, path)
vim.bo[bufnr].filetype = "go"
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
"package foo",
"",
"func TestAwesomeThing(t *testing.T) {",
' t.Run(\"evergreen\", func(t *testing.T) {',
" })",
"",
' t.Run(\"everred\", func(t *testing.T) {',
" })",
"}",
})
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 6, 0 })
local go = require("test-samurai.runners.go")
local orig_parser = go.output_parser
go.output_parser = function()
local seen = 0
return {
on_line = function()
seen = seen + 1
if seen == 1 then
return {
passes = {},
failures = { "TestAwesomeThing/everred" },
skips = {},
display = { passes = {}, failures = { "everred" }, skips = {} },
failures_all = { "TestAwesomeThing" },
}
end
return {
passes = {},
failures = { "TestAwesomeThing" },
skips = {},
display = { passes = {}, failures = { "TestAwesomeThing" }, skips = {} },
failures_all = { "TestAwesomeThing" },
}
end,
on_complete = function()
return nil
end,
}
end
local orig_jobstart = stub_jobstart({
exit_codes = { 1 },
stdout = {
{
vim.json.encode({ Action = "fail", Test = "TestAwesomeThing/everred" }),
vim.json.encode({ Action = "fail", Test = "TestAwesomeThing" }),
},
},
})
core.run_nearest()
local qf = vim.fn.getqflist()
assert.equals(2, #qf)
assert.equals(path, vim.fn.bufname(qf[1].bufnr))
assert.equals(path, vim.fn.bufname(qf[2].bufnr))
local lines = { qf[1].lnum, qf[2].lnum }
table.sort(lines)
assert.are.same({ 3, 7 }, lines)
vim.fn.jobstart = orig_jobstart
go.output_parser = orig_parser
end)
it("nutzt Listing-Namen wenn Parser keine Failure-Liste liefert", function()
local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf_go_listing")
vim.fn.mkdir(root, "p")
local path = root .. "/foo_listing_test.go"
vim.fn.writefile({
"package foo",
"",
"func TestAwesomeThing(t *testing.T) {",
' t.Run(\"evergreen\", func(t *testing.T) {',
" })",
"",
' t.Run(\"everred\", func(t *testing.T) {',
" })",
"}",
}, path)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, path)
vim.bo[bufnr].filetype = "go"
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
"package foo",
"",
"func TestAwesomeThing(t *testing.T) {",
' t.Run(\"evergreen\", func(t *testing.T) {',
" })",
"",
' t.Run(\"everred\", func(t *testing.T) {',
" })",
"}",
})
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 6, 0 })
local go = require("test-samurai.runners.go")
local orig_parser = go.output_parser
go.output_parser = function()
local step = 0
return {
on_line = function()
step = step + 1
if step == 1 then
return {
passes = {},
failures = {},
skips = {},
display = { passes = {}, failures = { "TestAwesomeThing" }, skips = {} },
}
end
return {
passes = {},
failures = {},
skips = {},
display = { passes = {}, failures = { "everred" }, skips = {} },
}
end,
on_complete = function()
return nil
end,
}
end
local orig_jobstart = stub_jobstart({
exit_codes = { 1 },
stdout = {
{
vim.json.encode({ Action = "fail", Test = "TestAwesomeThing/everred" }),
vim.json.encode({ Action = "fail", Test = "TestAwesomeThing" }),
},
},
})
core.run_nearest()
local qf = vim.fn.getqflist()
assert.equals(2, #qf)
assert.equals(path, vim.fn.bufname(qf[1].bufnr))
assert.equals(path, vim.fn.bufname(qf[2].bufnr))
local lines = { qf[1].lnum, qf[2].lnum }
table.sort(lines)
assert.are.same({ 3, 7 }, lines)
vim.fn.jobstart = orig_jobstart
go.output_parser = orig_parser
end)
it("mappt Go-Subtests mit durch Unterstriche normalisierten Namen", function()
local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf_go_norm")
vim.fn.mkdir(root, "p")
local path = root .. "/foo_norm_test.go"
vim.fn.writefile({
"package foo",
"",
"func TestHandleGet(t *testing.T) {",
' t.Run(\"returns 200 with an list of all badges\", func(t *testing.T) {',
" })",
"",
' t.Run(\"returns 500 on any db error\", func(t *testing.T) {',
" })",
"}",
}, path)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, path)
vim.bo[bufnr].filetype = "go"
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
"package foo",
"",
"func TestHandleGet(t *testing.T) {",
' t.Run(\"returns 200 with an list of all badges\", func(t *testing.T) {',
" })",
"",
' t.Run(\"returns 500 on any db error\", func(t *testing.T) {',
" })",
"}",
})
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 6, 0 })
local orig_jobstart = stub_jobstart({
exit_codes = { 1 },
stdout = {
{
vim.json.encode({
Action = "fail",
Test = "TestHandleGet/returns_500_on_any_db_error",
}),
vim.json.encode({ Action = "fail", Test = "TestHandleGet" }),
},
},
})
core.run_nearest()
local qf = vim.fn.getqflist()
assert.equals(2, #qf)
assert.equals(path, vim.fn.bufname(qf[1].bufnr))
assert.equals(path, vim.fn.bufname(qf[2].bufnr))
local lines = { qf[1].lnum, qf[2].lnum }
table.sort(lines)
assert.are.same({ 3, 7 }, lines)
vim.fn.jobstart = orig_jobstart
end)
end)

View File

@@ -1,102 +0,0 @@
local test_samurai = require("test-samurai")
local core = require("test-samurai.core")
local function close_output_container()
local keys = vim.api.nvim_replace_termcodes("<esc><esc>", true, false, true)
local attempts = 5
while attempts > 0 do
local float_win = nil
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
local cfg = vim.api.nvim_win_get_config(win)
if cfg.relative ~= "" then
float_win = win
break
end
end
if not float_win then
break
end
vim.api.nvim_set_current_win(float_win)
vim.api.nvim_feedkeys(keys, "x", false)
vim.wait(20, function()
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
local cfg = vim.api.nvim_win_get_config(win)
if cfg.relative ~= "" then
return false
end
end
return true
end)
attempts = attempts - 1
end
end
local function stub_jobstart(opts_config)
local orig = vim.fn.jobstart
local config = opts_config or {}
vim.fn.jobstart = function(_cmd, opts)
local out = config.stdout or nil
if out and opts and opts.on_stdout then
if type(out) == "string" then
out = { out }
end
opts.on_stdout(1, out, nil)
end
if opts and opts.on_exit then
opts.on_exit(1, config.exit_code or 0, nil)
end
return 1
end
return orig
end
describe("test-samurai quickfix (vitest)", function()
before_each(function()
test_samurai.setup()
end)
after_each(function()
close_output_container()
vim.fn.setqflist({}, "r")
end)
it("mappt tap-flat Failures mit >-Trenner auf die Testzeile", function()
local root = vim.fs.joinpath(vim.loop.cwd(), "tests", "tmp_qf_vitest")
vim.fn.mkdir(root, "p")
local path = root .. "/foo_qf.test.ts"
local pkg = root .. "/package.json"
vim.fn.writefile({
"{",
' "devDependencies": { "vitest": "^1.0.0" }',
"}",
}, pkg)
vim.fn.writefile({
'describe("outer", function() {',
' it("inner 1", function() {',
" })",
"",
' it("inner 2", function() {',
" })",
"})",
}, path)
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, path)
vim.bo[bufnr].filetype = "typescript"
vim.api.nvim_set_current_buf(bufnr)
local orig_jobstart = stub_jobstart({
exit_code = 1,
stdout = { "not ok 1 - outer > inner 2 # time=12.3ms" },
})
core.run_file()
local qf = vim.fn.getqflist()
assert.equals(1, #qf)
assert.equals(path, vim.fn.bufname(qf[1].bufnr))
assert.equals(5, qf[1].lnum)
vim.fn.jobstart = orig_jobstart
end)
end)

View File

@@ -1 +0,0 @@
{ "name": "mocha-jump" }

View File

@@ -1,4 +0,0 @@
describe("suite one", function() {
it("test one", function() {
})
})

View File

@@ -1,4 +0,0 @@
describe("suite two", function() {
it("test two", function() {
})
})

View File

@@ -1,9 +0,0 @@
package foo
func TestFoo(t *testing.T) {
t.Run("bar", func(t *testing.T) {
})
}
func TestBaz(t *testing.T) {
}

View File

@@ -1,9 +0,0 @@
package foo
func TestAwesomeThing(t *testing.T) {
t.Run("evergreen", func(t *testing.T) {
})
t.Run("everred", func(t *testing.T) {
})
}

View File

@@ -1,9 +0,0 @@
package foo
func TestHandleGet(t *testing.T) {
t.Run("returns 200 with an list of all badges", func(t *testing.T) {
})
t.Run("returns 500 on any db error", func(t *testing.T) {
})
}

View File

@@ -1,9 +0,0 @@
package foo
func TestAwesomeThing(t *testing.T) {
t.Run("evergreen", func(t *testing.T) {
})
t.Run("everred", func(t *testing.T) {
})
}

View File

@@ -1,9 +0,0 @@
package foo
func TestAwesomeThing(t *testing.T) {
t.Run("evergreen", func(t *testing.T) {
})
t.Run("everred", func(t *testing.T) {
})
}

View File

@@ -1,7 +0,0 @@
describe("outer", function() {
it("inner 1", function() {
})
it("inner 2", function() {
})
})

View File

@@ -1,3 +0,0 @@
package foo
func TestFoo(t *testing.T) {}

View File

@@ -1,4 +0,0 @@
package foo
func TestFoo(t *testing.T) {
}

View File

@@ -1,4 +0,0 @@
package foo
func TestFoo(t *testing.T) {
}

View File

@@ -1,4 +0,0 @@
package foo
func TestFoo(t *testing.T) {
}

View File

@@ -1,7 +0,0 @@
describe("outer", function() {
it("inner 1", function() {
})
it("inner 2", function() {
})
})

View File

@@ -1,3 +0,0 @@
{
"devDependencies": { "vitest": "^1.0.0" }
}