add TSamLast command for reexecuting last running tests
This commit is contained in:
77
AGENTS.md
Normal file
77
AGENTS.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# agent.md — test-samurai
|
||||||
|
|
||||||
|
## Rolle & Arbeitsweise
|
||||||
|
- 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 so lange korrigieren, bis alle Tests grün sind.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
## Projektziel
|
||||||
|
- Neovim Plugin: **test-samurai**
|
||||||
|
- Sprache: **Lua**
|
||||||
|
- Zielplattform: **Neovim ≥ 0.11.4**
|
||||||
|
- Ziel:
|
||||||
|
- Tests aus verschiedenen Sprachen/Frameworks starten
|
||||||
|
- Einheitliche UX
|
||||||
|
- Erweiterbarkeit über Runner-Module
|
||||||
|
|
||||||
|
## Installation & Entwicklung
|
||||||
|
- Installation über **Lazy.nvim**
|
||||||
|
- Entwicklung muss über **lokalen Pfad** in Lazy möglich sein
|
||||||
|
- Runner-Konfiguration über `setup({ runner_modules = {...} })`
|
||||||
|
|
||||||
|
## Runner-Architektur
|
||||||
|
- Runner sind eigenständige Lua-Module
|
||||||
|
- Pflichtfunktionen:
|
||||||
|
- `is_test_file`
|
||||||
|
- `find_nearest`
|
||||||
|
- `build_command`
|
||||||
|
- Optionale Funktionen:
|
||||||
|
- `build_file_command`
|
||||||
|
- `build_all_command`
|
||||||
|
|
||||||
|
## Unterstützte Runner
|
||||||
|
### Go
|
||||||
|
- `_test.go`
|
||||||
|
- Subtests via `t.Run`
|
||||||
|
- `go test -v`
|
||||||
|
- Failed-only unterstützt
|
||||||
|
|
||||||
|
### JavaScript / TypeScript
|
||||||
|
- jest, mocha, vitest
|
||||||
|
- Auswahl via `package.json`
|
||||||
|
- Nearest: `it()` oder umschließendes `describe`
|
||||||
|
|
||||||
|
### Lua
|
||||||
|
- Eingeschränkt
|
||||||
|
- `TSamAll` / `TSamFile` ok
|
||||||
|
- `TSamNearest` instabil
|
||||||
|
|
||||||
|
## Commands & Keymaps
|
||||||
|
- `TSamNearest` → `<leader>tn`
|
||||||
|
- `TSamFile` → `<leader>tf`
|
||||||
|
- `TSamAll` → `<leader>ta`
|
||||||
|
- `TSamLast` → `<leader>tl`
|
||||||
|
- `TSamFailedOnly` → `<leader>te`
|
||||||
|
- `TSamShowOutput` → `<leader>to`
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Floating Window
|
||||||
|
- Live Output + Autoscroll
|
||||||
|
- `<esc><esc>` versteckt Window
|
||||||
|
- Reopen via `TSamShowOutput`
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
- plenary.nvim / busted
|
||||||
|
- Mocks & Stubs erlaubt
|
||||||
|
- Neue Features benötigen Tests
|
||||||
|
|
||||||
|
## Einschränkungen
|
||||||
|
- Failed-only: nur Go
|
||||||
|
- Lua Nearest pausiert
|
||||||
|
- Farbiger Output später
|
||||||
@@ -7,6 +7,7 @@ local state = {
|
|||||||
runners = {},
|
runners = {},
|
||||||
last_win = nil,
|
last_win = nil,
|
||||||
last_buf = nil,
|
last_buf = nil,
|
||||||
|
last_command = nil,
|
||||||
autocmds_set = false,
|
autocmds_set = false,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ end
|
|||||||
function M.setup()
|
function M.setup()
|
||||||
load_runners()
|
load_runners()
|
||||||
ensure_output_autocmds()
|
ensure_output_autocmds()
|
||||||
|
state.last_command = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.reload_runners()
|
function M.reload_runners()
|
||||||
@@ -298,6 +300,12 @@ local function run_cmd(cmd, cwd, handlers)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function run_command(command)
|
local function run_command(command)
|
||||||
|
if command and type(command.cmd) == "table" and #command.cmd > 0 then
|
||||||
|
state.last_command = {
|
||||||
|
cmd = vim.deepcopy(command.cmd),
|
||||||
|
cwd = command.cwd,
|
||||||
|
}
|
||||||
|
end
|
||||||
local cmd = command.cmd
|
local cmd = command.cmd
|
||||||
local cwd = command.cwd or vim.loop.cwd()
|
local cwd = command.cwd or vim.loop.cwd()
|
||||||
|
|
||||||
@@ -350,6 +358,19 @@ local function run_command(command)
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function M.run_last()
|
||||||
|
if not (state.last_command and type(state.last_command.cmd) == "table") then
|
||||||
|
vim.notify("[test-samurai] No previous test command", vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local command = {
|
||||||
|
cmd = vim.deepcopy(state.last_command.cmd),
|
||||||
|
cwd = state.last_command.cwd,
|
||||||
|
}
|
||||||
|
run_command(command)
|
||||||
|
end
|
||||||
|
|
||||||
function M.run_nearest()
|
function M.run_nearest()
|
||||||
local bufnr = vim.api.nvim_get_current_buf()
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
local pos = vim.api.nvim_win_get_cursor(0)
|
local pos = vim.api.nvim_win_get_cursor(0)
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ function M.test_all()
|
|||||||
core.run_all()
|
core.run_all()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function M.test_last()
|
||||||
|
core.run_last()
|
||||||
|
end
|
||||||
|
|
||||||
function M.show_output()
|
function M.show_output()
|
||||||
core.show_output()
|
core.show_output()
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -67,11 +67,16 @@ local function find_t_runs(lines, func)
|
|||||||
return subtests
|
return subtests
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function escape_go_regex(s)
|
||||||
|
s = s or ""
|
||||||
|
return (s:gsub("([\\.^$|()%%[%]{}*+?%-])", "\\\\%1"))
|
||||||
|
end
|
||||||
|
|
||||||
local function build_run_pattern(spec)
|
local function build_run_pattern(spec)
|
||||||
local name = spec.test_path or ""
|
local name = spec.test_path or ""
|
||||||
local escaped = name:gsub("(%W)", "%%%1")
|
local escaped = escape_go_regex(name)
|
||||||
if spec.scope == "function" then
|
if spec.scope == "function" then
|
||||||
return "^" .. escaped .. "$|^" .. escaped .. "/"
|
return "^" .. escaped .. "($|/)"
|
||||||
else
|
else
|
||||||
return "^" .. escaped .. "$"
|
return "^" .. escaped .. "$"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ vim.api.nvim_create_user_command("TSamAll", function()
|
|||||||
require("test-samurai").test_all()
|
require("test-samurai").test_all()
|
||||||
end, { desc = "test-samurai: run all tests in project (per runner)" })
|
end, { desc = "test-samurai: run all tests in project (per runner)" })
|
||||||
|
|
||||||
|
vim.api.nvim_create_user_command("TSamLast", function()
|
||||||
|
require("test-samurai").test_last()
|
||||||
|
end, { desc = "test-samurai: rerun last test command" })
|
||||||
|
|
||||||
vim.keymap.set("n", "<leader>tn", function()
|
vim.keymap.set("n", "<leader>tn", function()
|
||||||
require("test-samurai").test_nearest()
|
require("test-samurai").test_nearest()
|
||||||
end, { desc = "test-samurai: run nearest test" })
|
end, { desc = "test-samurai: run nearest test" })
|
||||||
@@ -34,3 +38,7 @@ end, { desc = "test-samurai: run all tests in current file" })
|
|||||||
vim.keymap.set("n", "<leader>ta", function()
|
vim.keymap.set("n", "<leader>ta", function()
|
||||||
require("test-samurai").test_all()
|
require("test-samurai").test_all()
|
||||||
end, { desc = "test-samurai: run all tests in project (per runner)" })
|
end, { desc = "test-samurai: run all tests in project (per runner)" })
|
||||||
|
|
||||||
|
vim.keymap.set("n", "<leader>tl", function()
|
||||||
|
require("test-samurai").test_last()
|
||||||
|
end, { desc = "test-samurai: rerun last test command" })
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ describe("test-samurai go runner", function()
|
|||||||
|
|
||||||
local cmd_spec_sub = go_runner.build_command(spec_sub)
|
local cmd_spec_sub = go_runner.build_command(spec_sub)
|
||||||
assert.are.same(
|
assert.are.same(
|
||||||
{ "go", "test", "-v", "./pkg", "-run", "^TestFoo%/first$" },
|
{ "go", "test", "-v", "./pkg", "-run", "^TestFoo/first$" },
|
||||||
cmd_spec_sub.cmd
|
cmd_spec_sub.cmd
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ describe("test-samurai go runner", function()
|
|||||||
|
|
||||||
local cmd_spec_func = go_runner.build_command(spec_func)
|
local cmd_spec_func = go_runner.build_command(spec_func)
|
||||||
assert.are.same(
|
assert.are.same(
|
||||||
{ "go", "test", "-v", "./", "-run", "^TestFoo$|^TestFoo/" },
|
{ "go", "test", "-v", "./", "-run", "^TestFoo($|/)" },
|
||||||
cmd_spec_func.cmd
|
cmd_spec_func.cmd
|
||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
|
|||||||
126
tests/test_samurai_last_spec.lua
Normal file
126
tests/test_samurai_last_spec.lua
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
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", "-v", "./", "-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("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", "/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)
|
||||||
|
|
||||||
|
it("reruns last Lua command", function()
|
||||||
|
local calls, orig_jobstart = capture_jobstart()
|
||||||
|
|
||||||
|
local bufnr = mkbuf("/tmp/project/foo_last_spec.lua", "lua", {
|
||||||
|
"describe('outer', function()",
|
||||||
|
" it('inner 1', function()",
|
||||||
|
" local x = 1",
|
||||||
|
" end)",
|
||||||
|
"",
|
||||||
|
" it('inner 2', function()",
|
||||||
|
" local y = 2",
|
||||||
|
" end)",
|
||||||
|
"end)",
|
||||||
|
})
|
||||||
|
|
||||||
|
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({
|
||||||
|
"nvim",
|
||||||
|
"--headless",
|
||||||
|
"-u",
|
||||||
|
"/tmp/project/tests/minimal_init.lua",
|
||||||
|
"-c",
|
||||||
|
'PlenaryBustedFile /tmp/project/foo_last_spec.lua { busted_args = { "--filter", "inner 2" } }',
|
||||||
|
"-c",
|
||||||
|
"qa",
|
||||||
|
}, calls[1].cmd)
|
||||||
|
assert.are.same(calls[1].cmd, calls[2].cmd)
|
||||||
|
assert.equals(calls[1].opts.cwd, calls[2].opts.cwd)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
@@ -61,4 +61,19 @@ describe("test-samurai public API", function()
|
|||||||
|
|
||||||
assert.is_true(called)
|
assert.is_true(called)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it("delegates test_last to core.run_last", function()
|
||||||
|
local called = false
|
||||||
|
local orig = core.run_last
|
||||||
|
|
||||||
|
core.run_last = function()
|
||||||
|
called = true
|
||||||
|
end
|
||||||
|
|
||||||
|
test_samurai.test_last()
|
||||||
|
|
||||||
|
core.run_last = orig
|
||||||
|
|
||||||
|
assert.is_true(called)
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|||||||
Reference in New Issue
Block a user