diff --git a/lua/test-samurai/core.lua b/lua/test-samurai/core.lua index 4dec1a6..9bbf156 100644 --- a/lua/test-samurai/core.lua +++ b/lua/test-samurai/core.lua @@ -8,6 +8,7 @@ local state = { last_win = nil, last_buf = nil, last_command = nil, + last_runner = nil, last_scope_command = nil, last_scope_runner = nil, last_scope_kind = nil, @@ -57,6 +58,7 @@ function M.setup() load_runners() ensure_output_autocmds() state.last_command = nil + state.last_runner = nil state.last_scope_command = nil state.last_scope_runner = nil state.last_scope_kind = nil @@ -337,6 +339,7 @@ local function run_command(command, opts) cmd = vim.deepcopy(command.cmd), cwd = command.cwd, } + state.last_runner = options.runner end if options.track_scope then state.last_scope_command = { @@ -489,7 +492,18 @@ function M.run_last() cmd = vim.deepcopy(state.last_command.cmd), cwd = state.last_command.cwd, } - run_command(command) + local runner = state.last_runner + local parser = nil + if runner then + parser = runner.output_parser + if type(parser) == "function" then + parser = parser() + end + end + run_command(command, { + runner = runner, + output_parser = parser or (runner and runner.parse_results), + }) end function M.run_nearest() @@ -642,7 +656,18 @@ function M.run_failed_only() return end - run_command(command, { save_last = false }) + local runner = state.last_scope_runner + local parser = nil + if runner then + parser = runner.output_parser + if type(parser) == "function" then + parser = parser() + end + end + run_command(command, { + save_last = false, + output_parser = parser or (runner and runner.parse_results), + }) end function M.show_output() diff --git a/lua/test-samurai/runners/go.lua b/lua/test-samurai/runners/go.lua index 0896616..42d4981 100644 --- a/lua/test-samurai/runners/go.lua +++ b/lua/test-samurai/runners/go.lua @@ -201,6 +201,22 @@ function runner.build_file_command(bufnr) 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, diff --git a/tests/test_samurai_failed_only_spec.lua b/tests/test_samurai_failed_only_spec.lua index 9d3fdf1..b5c4b00 100644 --- a/tests/test_samurai_failed_only_spec.lua +++ b/tests/test_samurai_failed_only_spec.lua @@ -178,6 +178,49 @@ describe("TSamFailedOnly", function() 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("does not affect TSamLast history", function() local json = vim.json.encode({ testResults = { diff --git a/tests/test_samurai_go_spec.lua b/tests/test_samurai_go_spec.lua index 8e1048a..38cb86f 100644 --- a/tests/test_samurai_go_spec.lua +++ b/tests/test_samurai_go_spec.lua @@ -1,4 +1,5 @@ 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() @@ -112,4 +113,35 @@ describe("test-samurai go runner", function() 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) diff --git a/tests/test_samurai_last_spec.lua b/tests/test_samurai_last_spec.lua index 334a0d9..3d7afc5 100644 --- a/tests/test_samurai_last_spec.lua +++ b/tests/test_samurai_last_spec.lua @@ -54,6 +54,57 @@ describe("TSamLast", function() 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() diff --git a/tests/test_samurai_output_spec.lua b/tests/test_samurai_output_spec.lua index abf5e5c..fa5945a 100644 --- a/tests/test_samurai_output_spec.lua +++ b/tests/test_samurai_output_spec.lua @@ -164,6 +164,280 @@ describe("test-samurai output formatting", function() assert.is_true(has_fail) end) + it("does not print raw JSON output for jest runs", function() + local json = vim.json.encode({ + testResults = { + { + assertionResults = { + { status = "passed", title = "inner 1", fullName = "outer inner 1" }, + }, + }, + }, + }) + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts and opts.on_stdout then + opts.on_stdout(1, { json }, nil) + end + if opts and opts.on_exit then + opts.on_exit(1, 0, nil) + end + return 1 + end + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/output_raw_json.test.ts") + vim.bo[bufnr].filetype = "typescript" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'describe("outer", function() {', + ' it("inner 1", function() {', + " -- inside 1", + " })", + "})", + }) + + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 3, 0 }) + + core.run_nearest() + + local out_buf = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false) + + vim.fn.jobstart = orig_jobstart + + local has_raw_json = false + for _, line in ipairs(lines) do + if line == json then + has_raw_json = true + break + end + end + + assert.is_false(has_raw_json) + end) + + it("does not print raw JSON output for mocha json-stream", function() + test_samurai.setup({ + runner_modules = { + "test-samurai.runners.js-mocha", + }, + }) + + local json_line = vim.json.encode({ + event = "pass", + fullTitle = "outer inner 1", + }) + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, 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, 0, nil) + end + return 1 + end + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/output_raw_json.test.js") + vim.bo[bufnr].filetype = "javascript" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'describe("outer", function() {', + ' it("inner 1", function() {', + " -- inside 1", + " })", + "})", + }) + + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 3, 0 }) + + core.run_nearest() + + local out_buf = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false) + + vim.fn.jobstart = orig_jobstart + + local has_raw_json = false + for _, line in ipairs(lines) do + if line == json_line then + has_raw_json = true + break + end + end + + assert.is_false(has_raw_json) + end) + + it("does not print raw JSON when JSON arrives on stdout and stderr", function() + test_samurai.setup({ + runner_modules = { + "test-samurai.runners.js-jest", + }, + }) + + local json1 = vim.json.encode({ + testResults = { + { + assertionResults = { + { status = "passed", title = "inner 1", fullName = "outer inner 1" }, + }, + }, + }, + }) + local json2 = vim.json.encode({ + testResults = { + { + assertionResults = { + { status = "failed", title = "inner 2", fullName = "outer inner 2" }, + }, + }, + }, + }) + + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(_cmd, opts) + if opts and opts.on_stdout then + opts.on_stdout(1, { json1 }, nil) + end + if opts and opts.on_stderr then + opts.on_stderr(1, { json2 }, nil) + end + if opts and opts.on_exit then + opts.on_exit(1, 1, nil) + end + return 1 + end + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/output_raw_json_both.test.ts") + vim.bo[bufnr].filetype = "typescript" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + '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() + + local out_buf = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false) + + vim.fn.jobstart = orig_jobstart + + local has_raw_json = false + for _, line in ipairs(lines) do + if line == json1 or line == json2 then + has_raw_json = true + break + end + end + + assert.is_false(has_raw_json) + end) + + it("handles mixed mocha json-stream events and tracks failures for failed-only", function() + test_samurai.setup({ + runner_modules = { + "test-samurai.runners.js-mocha", + }, + }) + + local pass_line = vim.json.encode({ + event = "pass", + fullTitle = "outer inner 1", + }) + local fail_line = vim.json.encode({ + event = "fail", + fullTitle = "outer inner 2", + }) + + local job_calls = {} + local orig_jobstart = vim.fn.jobstart + vim.fn.jobstart = function(cmd, opts) + table.insert(job_calls, { cmd = cmd, opts = opts }) + if #job_calls == 1 then + if opts and opts.on_stdout then + opts.on_stdout(1, { pass_line, fail_line }, nil) + end + if opts and opts.on_exit then + opts.on_exit(1, 1, nil) + end + else + if opts and opts.on_exit then + opts.on_exit(1, 1, nil) + end + end + return 1 + end + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(bufnr, "/tmp/output_mixed_json.test.js") + vim.bo[bufnr].filetype = "javascript" + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + '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() + + local out_buf = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(out_buf, 0, -1, false) + + core.run_failed_only() + + vim.fn.jobstart = orig_jobstart + + local has_pass = false + local has_fail = false + for _, line in ipairs(lines) do + if line == "[ PASS ] - outer inner 1" then + has_pass = true + elseif line == "[ FAIL ] - outer inner 2" then + has_fail = true + end + end + assert.is_true(has_pass) + assert.is_true(has_fail) + + assert.is_true(#job_calls >= 2) + local failed_cmd = job_calls[2].cmd or {} + local saw_grep = false + local saw_title = false + for _, arg in ipairs(failed_cmd) do + if arg == "--grep" then + saw_grep = true + elseif arg == "outer inner 2" then + saw_title = true + end + end + assert.is_true(saw_grep) + assert.is_true(saw_title) + end) + it("formats TAP output as PASS/FAIL lines", function() test_samurai.setup({ runner_modules = {