diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6b12d35 --- /dev/null +++ b/AGENTS.md @@ -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` → `tn` +- `TSamFile` → `tf` +- `TSamAll` → `ta` +- `TSamLast` → `tl` +- `TSamFailedOnly` → `te` +- `TSamShowOutput` → `to` + +## Output +- Floating Window +- Live Output + Autoscroll +- `` 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 diff --git a/lua/test-samurai/core.lua b/lua/test-samurai/core.lua index 72a52d0..d8b02bc 100644 --- a/lua/test-samurai/core.lua +++ b/lua/test-samurai/core.lua @@ -7,6 +7,7 @@ local state = { runners = {}, last_win = nil, last_buf = nil, + last_command = nil, autocmds_set = false, } @@ -50,6 +51,7 @@ end function M.setup() load_runners() ensure_output_autocmds() + state.last_command = nil end function M.reload_runners() @@ -298,6 +300,12 @@ local function run_cmd(cmd, cwd, handlers) end 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 cwd = command.cwd or vim.loop.cwd() @@ -350,6 +358,19 @@ local function run_command(command) }) 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() local bufnr = vim.api.nvim_get_current_buf() local pos = vim.api.nvim_win_get_cursor(0) diff --git a/lua/test-samurai/init.lua b/lua/test-samurai/init.lua index 91f2a4e..8342807 100644 --- a/lua/test-samurai/init.lua +++ b/lua/test-samurai/init.lua @@ -20,6 +20,10 @@ function M.test_all() core.run_all() end +function M.test_last() + core.run_last() +end + function M.show_output() core.show_output() end diff --git a/lua/test-samurai/runners/go.lua b/lua/test-samurai/runners/go.lua index 06cf2ee..6a31517 100644 --- a/lua/test-samurai/runners/go.lua +++ b/lua/test-samurai/runners/go.lua @@ -67,11 +67,16 @@ local function find_t_runs(lines, func) 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 = name:gsub("(%W)", "%%%1") + local escaped = escape_go_regex(name) if spec.scope == "function" then - return "^" .. escaped .. "$|^" .. escaped .. "/" + return "^" .. escaped .. "($|/)" else return "^" .. escaped .. "$" end diff --git a/plugin/test-samurai.lua b/plugin/test-samurai.lua index fa8ccb1..a7e634f 100644 --- a/plugin/test-samurai.lua +++ b/plugin/test-samurai.lua @@ -19,6 +19,10 @@ vim.api.nvim_create_user_command("TSamAll", function() require("test-samurai").test_all() 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", "tn", function() require("test-samurai").test_nearest() 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", "ta", function() require("test-samurai").test_all() end, { desc = "test-samurai: run all tests in project (per runner)" }) + +vim.keymap.set("n", "tl", function() + require("test-samurai").test_last() +end, { desc = "test-samurai: rerun last test command" }) diff --git a/tests/test_samurai_go_spec.lua b/tests/test_samurai_go_spec.lua index ae98b4e..ef3629c 100644 --- a/tests/test_samurai_go_spec.lua +++ b/tests/test_samurai_go_spec.lua @@ -95,7 +95,7 @@ describe("test-samurai go runner", function() local cmd_spec_sub = go_runner.build_command(spec_sub) assert.are.same( - { "go", "test", "-v", "./pkg", "-run", "^TestFoo%/first$" }, + { "go", "test", "-v", "./pkg", "-run", "^TestFoo/first$" }, cmd_spec_sub.cmd ) @@ -108,7 +108,7 @@ describe("test-samurai go runner", function() local cmd_spec_func = go_runner.build_command(spec_func) assert.are.same( - { "go", "test", "-v", "./", "-run", "^TestFoo$|^TestFoo/" }, + { "go", "test", "-v", "./", "-run", "^TestFoo($|/)" }, cmd_spec_func.cmd ) end) diff --git a/tests/test_samurai_last_spec.lua b/tests/test_samurai_last_spec.lua new file mode 100644 index 0000000..3f5a7f4 --- /dev/null +++ b/tests/test_samurai_last_spec.lua @@ -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) diff --git a/tests/test_samurai_output_spec.lua b/tests/test_samurai_output_spec.lua index 541cf30..d61b48e 100644 --- a/tests/test_samurai_output_spec.lua +++ b/tests/test_samurai_output_spec.lua @@ -61,4 +61,19 @@ describe("test-samurai public API", function() assert.is_true(called) 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)