Files
test-samurai.nvim/lua/test-samurai/core.lua

932 lines
27 KiB
Lua

local config = require("test-samurai.config")
local util = require("test-samurai.util")
local M = {}
local state = {
runners = {},
last_win = nil,
last_buf = nil,
last_command = nil,
last_runner = nil,
last_scope_command = nil,
last_scope_runner = nil,
last_scope_kind = nil,
last_scope_exit_code = nil,
last_scope_failures = nil,
last_border_kind = "default",
autocmds_set = false,
}
local summary_ns = vim.api.nvim_create_namespace("TestSamuraiSummary")
local result_ns = vim.api.nvim_create_namespace("TestSamuraiResult")
local function get_hl_fg(name)
local ok, hl = pcall(vim.api.nvim_get_hl, 0, { name = name, link = true })
if ok and type(hl) == "table" and hl.fg then
return hl.fg
end
return nil
end
local function resolve_hl_fg(names, fallback)
for _, name in ipairs(names or {}) do
local fg = get_hl_fg(name)
if fg then
return fg
end
end
return fallback
end
local function setup_summary_highlights()
local normal_fg = get_hl_fg("Normal")
local pass_fg = resolve_hl_fg({ "DiffAdd", "DiagnosticOk" }, normal_fg)
local fail_fg = resolve_hl_fg({ "DiffDelete", "DiagnosticError" }, normal_fg)
local skip_fg = resolve_hl_fg({ "DiagnosticInfo", "DiagnosticHint" }, normal_fg)
pcall(vim.api.nvim_set_hl, 0, "TestSamuraiSummaryBold", { fg = normal_fg, bold = true })
pcall(vim.api.nvim_set_hl, 0, "TestSamuraiSummaryPass", { fg = pass_fg, bold = true })
pcall(vim.api.nvim_set_hl, 0, "TestSamuraiSummaryFail", { fg = fail_fg, bold = true })
pcall(vim.api.nvim_set_hl, 0, "TestSamuraiSummarySkip", { fg = skip_fg, bold = true })
pcall(vim.api.nvim_set_hl, 0, "TestSamuraiResultPass", { fg = pass_fg })
pcall(vim.api.nvim_set_hl, 0, "TestSamuraiResultFail", { fg = fail_fg })
pcall(vim.api.nvim_set_hl, 0, "TestSamuraiResultSkip", { fg = skip_fg })
pcall(vim.api.nvim_set_hl, 0, "TestSamuraiBorderPass", { fg = pass_fg, bold = true })
pcall(vim.api.nvim_set_hl, 0, "TestSamuraiBorderFail", { fg = fail_fg, bold = true })
end
local function load_runners()
state.runners = {}
local opts = config.get()
local mods = opts.runner_modules or {}
for _, mod in ipairs(mods) do
local ok, runner = pcall(require, mod)
if ok and type(runner) == "table" then
table.insert(state.runners, runner)
else
vim.notify("[test-samurai] Failed to load runner " .. mod, vim.log.levels.WARN)
end
end
end
local function ensure_output_autocmds()
if state.autocmds_set then
return
end
local group = vim.api.nvim_create_augroup("TestSamuraiOutput", { clear = true })
vim.api.nvim_create_autocmd({ "BufEnter", "WinEnter" }, {
group = group,
callback = function()
if state.last_win and vim.api.nvim_win_is_valid(state.last_win) then
local cur = vim.api.nvim_get_current_win()
if cur ~= state.last_win then
pcall(vim.api.nvim_win_close, state.last_win, true)
state.last_win = nil
end
end
end,
})
vim.api.nvim_create_autocmd("ColorScheme", {
group = group,
callback = function()
setup_summary_highlights()
end,
})
state.autocmds_set = true
end
function M.setup()
load_runners()
ensure_output_autocmds()
setup_summary_highlights()
state.last_command = nil
state.last_runner = nil
state.last_scope_command = nil
state.last_scope_runner = nil
state.last_scope_kind = nil
state.last_scope_exit_code = nil
state.last_scope_failures = nil
state.last_border_kind = "default"
end
function M.reload_runners()
load_runners()
end
local function detect_js_framework(file)
local root = util.find_root(file, { "package.json", "node_modules" })
if not root or root == "" then
return nil
end
local pkg_path = vim.fs.joinpath(root, "package.json")
local stat = vim.loop.fs_stat(pkg_path)
if not stat or stat.type ~= "file" then
return nil
end
local ok_read, lines = pcall(vim.fn.readfile, pkg_path)
if not ok_read or type(lines) ~= "table" then
return nil
end
local json = table.concat(lines, "\n")
local ok_json, pkg = pcall(vim.json.decode, json)
if not ok_json or type(pkg) ~= "table" then
return nil
end
local present = {}
local function scan_section(section)
if type(section) ~= "table" then
return
end
for name, _ in pairs(section) do
if name == "mocha" or name == "jest" or name == "vitest" then
present[name] = true
end
end
end
scan_section(pkg.dependencies)
scan_section(pkg.devDependencies)
if next(present) == nil then
return nil
end
return present
end
function M.get_runner_for_buf(bufnr)
local path = util.get_buf_path(bufnr)
local candidates = {}
for _, runner in ipairs(state.runners) do
if type(runner.is_test_file) == "function" then
local ok, is_test = pcall(runner.is_test_file, bufnr)
if ok and is_test then
table.insert(candidates, runner)
end
end
end
if #candidates == 1 then
return candidates[1]
elseif #candidates > 1 then
local frameworks = nil
if path and path ~= "" then
frameworks = detect_js_framework(path)
end
if frameworks then
for _, runner in ipairs(candidates) do
if runner.framework and frameworks[runner.framework] then
return runner
end
end
end
return candidates[1]
end
if not path or path == "" then
return nil
end
if path:sub(-8) == "_test.go" then
local ok, go = pcall(require, "test-samurai.runners.go")
if ok and type(go) == "table" then
return go
end
end
if path:find(".test.", 1, true) or path:find(".spec.", 1, true) then
local ok, jsjest = pcall(require, "test-samurai.runners.js-jest")
if ok and type(jsjest) == "table" then
return jsjest
end
end
return nil
end
local function float_geometry()
local width = math.floor(vim.o.columns * 0.8)
local height = math.floor(vim.o.lines * 0.8)
local row = math.floor((vim.o.lines - height) / 2)
local col = math.floor((vim.o.columns - width) / 2)
return width, height, row, col
end
local function apply_border_kind(win, kind)
if not (win and vim.api.nvim_win_is_valid(win)) then
return
end
if kind == "pass" then
vim.api.nvim_win_set_option(win, "winhighlight", "FloatBorder:TestSamuraiBorderPass")
elseif kind == "fail" then
vim.api.nvim_win_set_option(win, "winhighlight", "FloatBorder:TestSamuraiBorderFail")
else
vim.api.nvim_win_set_option(win, "winhighlight", "")
end
end
local function create_output_win(initial_lines)
if state.last_win and vim.api.nvim_win_is_valid(state.last_win) then
pcall(vim.api.nvim_win_close, state.last_win, true)
state.last_win = nil
end
local buf = state.last_buf
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_option(buf, "bufhidden", "hide")
vim.api.nvim_buf_set_option(buf, "filetype", "test-samurai-output")
state.last_buf = buf
end
vim.api.nvim_buf_set_lines(buf, 0, -1, false, initial_lines or {})
local width, height, row, col = float_geometry()
local win = vim.api.nvim_open_win(buf, true, {
relative = "editor",
width = width,
height = height,
row = row,
col = col,
style = "minimal",
border = "rounded",
})
vim.keymap.set("n", "<esc><esc>", function()
if vim.api.nvim_win_is_valid(win) then
vim.api.nvim_win_close(win, true)
if state.last_win == win then
state.last_win = nil
end
end
end, { buffer = buf, nowait = true, silent = true })
state.last_win = win
state.last_buf = buf
apply_border_kind(win, state.last_border_kind)
return buf, win
end
local function reopen_output_win()
if not (state.last_buf and vim.api.nvim_buf_is_valid(state.last_buf)) then
return nil, nil
end
if state.last_win and vim.api.nvim_win_is_valid(state.last_win) then
vim.api.nvim_set_current_win(state.last_win)
return state.last_buf, state.last_win
end
local width, height, row, col = float_geometry()
local win = vim.api.nvim_open_win(state.last_buf, true, {
relative = "editor",
width = width,
height = height,
row = row,
col = col,
style = "minimal",
border = "rounded",
})
vim.keymap.set("n", "<esc><esc>", function()
if vim.api.nvim_win_is_valid(win) then
vim.api.nvim_win_close(win, true)
if state.last_win == win then
state.last_win = nil
end
end
end, { buffer = state.last_buf, nowait = true, silent = true })
state.last_win = win
apply_border_kind(win, state.last_border_kind)
return state.last_buf, win
end
local function append_lines(buf, new_lines)
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
return
end
if not new_lines or #new_lines == 0 then
return
end
local existing = vim.api.nvim_buf_line_count(buf)
vim.api.nvim_buf_set_lines(buf, existing, existing, false, new_lines)
if state.last_win and vim.api.nvim_win_is_valid(state.last_win) then
local total = vim.api.nvim_buf_line_count(buf)
vim.api.nvim_win_set_cursor(state.last_win, { total, 0 })
end
end
local function run_cmd(cmd, cwd, handlers)
local h = handlers or {}
if h.on_start then
pcall(h.on_start)
end
local function handle_chunk(fn, data)
if not fn or not data then
return
end
local lines = {}
for _, line in ipairs(data) do
if line ~= nil and line ~= "" then
table.insert(lines, line)
end
end
if #lines > 0 then
fn(lines)
end
end
vim.fn.jobstart(cmd, {
cwd = cwd,
stdout_buffered = false,
stderr_buffered = false,
on_stdout = function(_, data, _)
handle_chunk(h.on_stdout, data)
end,
on_stderr = function(_, data, _)
handle_chunk(h.on_stderr, data)
end,
on_exit = function(_, code, _)
if h.on_exit then
pcall(h.on_exit, code or 0)
end
end,
})
end
local function pick_display(results, key, scope_kind)
if scope_kind == "all" then
return results[key]
end
if type(results.display) == "table" and type(results.display[key]) == "table" then
return results.display[key]
end
return results[key]
end
local function format_results(results, scope_kind)
local lines = {}
local passes = pick_display(results, "passes", scope_kind)
if type(passes) == "table" then
for _, title in ipairs(passes) do
table.insert(lines, "[ PASS ] - " .. title)
end
end
local skips = pick_display(results, "skips", scope_kind)
if type(skips) == "table" then
for _, title in ipairs(skips) do
table.insert(lines, "[ SKIP ] - " .. title)
end
end
local failures = pick_display(results, "failures", scope_kind)
if type(failures) == "table" then
for _, title in ipairs(failures) do
table.insert(lines, "[ FAIL ] - " .. title)
end
end
return lines
end
local function make_summary_tracker(enabled)
if not enabled then
return nil
end
return {
passes = {},
failures = {},
skips = {},
}
end
local function add_unique_items(target, items)
if type(items) ~= "table" then
return
end
for _, item in ipairs(items) do
if item and item ~= "" then
target[item] = true
end
end
end
local function update_summary(summary, results)
if not summary or not results then
return
end
add_unique_items(summary.passes, results.passes)
add_unique_items(summary.failures, results.failures)
add_unique_items(summary.skips, results.skips)
end
local function count_summary(summary)
if not summary then
return 0, 0, 0, 0
end
local function count(set)
local total = 0
for _ in pairs(set) do
total = total + 1
end
return total
end
local pass_count = count(summary.passes)
local fail_count = count(summary.failures)
local skip_count = count(summary.skips)
return pass_count, fail_count, skip_count, pass_count + fail_count + skip_count
end
local function format_summary_lines(summary, elapsed_seconds)
local pass_count, fail_count, skip_count, total = count_summary(summary)
local minutes = 0
local seconds = 0
if type(elapsed_seconds) == "number" and elapsed_seconds >= 0 then
local total_seconds = math.floor(elapsed_seconds + 0.0000001)
minutes = math.floor(total_seconds / 60)
seconds = total_seconds % 60
end
local duration = string.format("%02dm %02ds", minutes, seconds)
return {
"",
string.format("TOTAL %d", total),
string.format("DURATION %s", duration),
"",
string.format(" PASS %d - SKIPPED %d - FAILED %d", pass_count, skip_count, fail_count),
}
end
local function highlight_label_number(buf, ns, lnum, line, label, hl_group)
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
return
end
local label_start = line:find(label, 1, true)
if not label_start then
return
end
local num_start, num_end = line:find("%d+", label_start + #label)
if not num_start then
return
end
vim.api.nvim_buf_add_highlight(buf, ns, hl_group, lnum, num_start - 1, num_end)
end
local function highlight_label_word(buf, ns, lnum, line, label, hl_group)
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
return
end
local label_start = line:find(label, 1, true)
if not label_start then
return
end
vim.api.nvim_buf_add_highlight(buf, ns, hl_group, lnum, label_start - 1, label_start - 1 + #label)
end
local function apply_summary_highlights(buf, start_line, lines)
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
return
end
for i, line in ipairs(lines or {}) do
local lnum = start_line + i - 1
if line:match("^%s*PASS%s+%d+") then
highlight_label_word(buf, summary_ns, lnum, line, "PASS", "TestSamuraiSummaryPass")
highlight_label_number(buf, summary_ns, lnum, line, "PASS", "TestSamuraiSummaryPass")
highlight_label_word(buf, summary_ns, lnum, line, "SKIPPED", "TestSamuraiSummarySkip")
highlight_label_number(buf, summary_ns, lnum, line, "SKIPPED", "TestSamuraiSummarySkip")
highlight_label_word(buf, summary_ns, lnum, line, "FAILED", "TestSamuraiSummaryFail")
highlight_label_number(buf, summary_ns, lnum, line, "FAILED", "TestSamuraiSummaryFail")
elseif line:match("^TOTAL%s+%d+") then
highlight_label_word(buf, summary_ns, lnum, line, "TOTAL", "TestSamuraiSummaryBold")
highlight_label_number(buf, summary_ns, lnum, line, "TOTAL", "TestSamuraiSummaryBold")
elseif line:match("^DURATION%s+") then
highlight_label_word(buf, summary_ns, lnum, line, "DURATION", "TestSamuraiSummaryBold")
end
end
end
local function apply_result_highlights(buf, start_line, lines)
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
return
end
for i, line in ipairs(lines or {}) do
local lnum = start_line + i - 1
if line:match("^%[ PASS %]") then
highlight_label_word(buf, result_ns, lnum, line, "PASS", "TestSamuraiResultPass")
elseif line:match("^%[ FAIL %]") then
highlight_label_word(buf, result_ns, lnum, line, "FAIL", "TestSamuraiResultFail")
elseif line:match("^%[ SKIP %]") then
highlight_label_word(buf, result_ns, lnum, line, "SKIP", "TestSamuraiResultSkip")
end
end
end
local function run_command(command, opts)
local options = opts or {}
if command and type(command.cmd) == "table" and #command.cmd > 0 then
if options.save_last ~= false then
state.last_command = {
cmd = vim.deepcopy(command.cmd),
cwd = command.cwd,
}
state.last_runner = options.runner
end
if options.track_scope then
state.last_scope_command = {
cmd = vim.deepcopy(command.cmd),
cwd = command.cwd,
}
state.last_scope_runner = options.runner
state.last_scope_kind = options.scope_kind
state.last_scope_exit_code = nil
state.last_scope_failures = nil
end
end
local cmd = command.cmd
local cwd = command.cwd or vim.loop.cwd()
local header = "$ " .. table.concat(cmd, " ")
local buf = nil
local has_output = false
local parser = options.output_parser
if type(parser) == "function" then
parser = { on_complete = parser }
end
local parser_state = {}
parser_state.scope_kind = options.scope_kind
local had_parsed_output = false
local summary_enabled = options.scope_kind == "file" or options.scope_kind == "all" or options.scope_kind == "nearest"
local summary = make_summary_tracker(summary_enabled)
local result_counts = make_summary_tracker(true)
state.last_border_kind = "default"
local start_time = nil
if vim.loop and vim.loop.hrtime then
start_time = vim.loop.hrtime()
end
local output_lines = {}
local function collect_output(lines)
if not lines or #lines == 0 then
return
end
for _, line in ipairs(lines) do
if line ~= nil then
table.insert(output_lines, line)
end
end
end
local function ensure_output_started()
if not buf then
buf = select(1, create_output_win({ header, "" }))
end
if not has_output then
local cur = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
if #cur > 0 and cur[#cur] == "[running...]" then
vim.api.nvim_buf_set_lines(buf, #cur - 1, #cur, false, {})
end
has_output = true
end
end
local function handle_parsed(results)
if not results then
return
end
had_parsed_output = true
update_summary(summary, results)
update_summary(result_counts, results)
if options.track_scope then
if results.failures_all ~= nil then
state.last_scope_failures = results.failures_all
elseif results.failures ~= nil then
state.last_scope_failures = results.failures
end
end
local lines = format_results(results, options.scope_kind)
if #lines == 0 then
return
end
ensure_output_started()
local start_line = vim.api.nvim_buf_line_count(buf)
append_lines(buf, lines)
apply_result_highlights(buf, start_line, lines)
end
run_cmd(cmd, cwd, {
on_start = function()
buf = select(1, create_output_win({ header, "", "[running...]" }))
end,
on_stdout = function(lines)
if parser then
collect_output(lines)
if parser.on_line then
for _, line in ipairs(lines or {}) do
local ok_parse, results = pcall(parser.on_line, line, parser_state)
if ok_parse then
handle_parsed(results)
end
end
end
return
end
ensure_output_started()
append_lines(buf, lines)
end,
on_stderr = function(lines)
if parser then
collect_output(lines)
if parser.on_line then
for _, line in ipairs(lines or {}) do
local ok_parse, results = pcall(parser.on_line, line, parser_state)
if ok_parse then
handle_parsed(results)
end
end
end
return
end
ensure_output_started()
append_lines(buf, lines)
end,
on_exit = function(code)
if not buf then
buf = select(1, create_output_win({ header }))
end
if parser and parser.on_complete then
local output = table.concat(output_lines, "\n")
local ok_parse, results = pcall(parser.on_complete, output, parser_state)
if ok_parse then
handle_parsed(results)
end
end
local pass_count, fail_count = count_summary(result_counts)
if fail_count > 0 then
state.last_border_kind = "fail"
elseif pass_count > 0 then
state.last_border_kind = "pass"
else
state.last_border_kind = "default"
end
apply_border_kind(state.last_win, state.last_border_kind)
if parser then
if not had_parsed_output and #output_lines > 0 then
ensure_output_started()
append_lines(buf, output_lines)
elseif not has_output then
ensure_output_started()
end
if summary_enabled then
local elapsed = nil
if start_time and vim.loop and vim.loop.hrtime then
elapsed = (vim.loop.hrtime() - start_time) / 1000000000
end
ensure_output_started()
local summary_lines = format_summary_lines(summary, elapsed)
local start_line = vim.api.nvim_buf_line_count(buf)
append_lines(buf, summary_lines)
apply_summary_highlights(buf, start_line, summary_lines)
end
append_lines(buf, { "", "[exit code] " .. tostring(code) })
else
if not has_output then
local cur = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
if #cur > 0 and cur[#cur] == "[running...]" then
vim.api.nvim_buf_set_lines(buf, #cur - 1, #cur, false, {})
end
end
if summary_enabled then
local elapsed = nil
if start_time and vim.loop and vim.loop.hrtime then
elapsed = (vim.loop.hrtime() - start_time) / 1000000000
end
ensure_output_started()
local summary_lines = format_summary_lines(summary, elapsed)
local start_line = vim.api.nvim_buf_line_count(buf)
append_lines(buf, summary_lines)
apply_summary_highlights(buf, start_line, summary_lines)
end
append_lines(buf, { "", "[exit code] " .. tostring(code) })
end
if options.track_scope then
state.last_scope_exit_code = code
end
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,
}
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()
local bufnr = vim.api.nvim_get_current_buf()
local pos = vim.api.nvim_win_get_cursor(0)
local row = pos[1] - 1
local col = pos[2]
local runner = M.get_runner_for_buf(bufnr)
if not runner then
vim.notify("[test-samurai] No runner for this file", vim.log.levels.WARN)
return
end
if type(runner.find_nearest) ~= "function" or type(runner.build_command) ~= "function" then
vim.notify("[test-samurai] Runner missing methods", vim.log.levels.ERROR)
return
end
local ok, spec_or_err = pcall(runner.find_nearest, bufnr, row, col)
if not ok or not spec_or_err then
local msg = "[test-samurai] No test found"
if type(spec_or_err) == "string" then
msg = "[test-samurai] " .. spec_or_err
end
vim.notify(msg, vim.log.levels.WARN)
return
end
local spec = spec_or_err
local ok_cmd, command = pcall(runner.build_command, spec)
if not ok_cmd or not command or type(command.cmd) ~= "table" or #command.cmd == 0 then
vim.notify("[test-samurai] Runner failed to build command", vim.log.levels.ERROR)
return
end
local parser = runner.output_parser
if type(parser) == "function" then
parser = parser()
end
run_command(command, {
track_scope = true,
runner = runner,
scope_kind = "nearest",
output_parser = parser or runner.parse_results,
})
end
function M.run_file()
local bufnr = vim.api.nvim_get_current_buf()
local runner = M.get_runner_for_buf(bufnr)
if not runner then
vim.notify("[test-samurai] No runner for this file", vim.log.levels.WARN)
return
end
if type(runner.build_file_command) ~= "function" then
vim.notify("[test-samurai] Runner does not support file-level execution", vim.log.levels.WARN)
return
end
local ok_cmd, command = pcall(runner.build_file_command, bufnr)
if not ok_cmd or not command or type(command.cmd) ~= "table" or #command.cmd == 0 then
vim.notify("[test-samurai] Runner failed to build file command", vim.log.levels.ERROR)
return
end
local parser = runner.output_parser
if type(parser) == "function" then
parser = parser()
end
run_command(command, {
track_scope = true,
runner = runner,
scope_kind = "file",
output_parser = parser or runner.parse_results,
})
end
function M.run_all()
local bufnr = vim.api.nvim_get_current_buf()
local runner = M.get_runner_for_buf(bufnr)
if not runner then
vim.notify("[test-samurai] No runner for this file", vim.log.levels.WARN)
return
end
if type(runner.build_all_command) ~= "function" then
vim.notify("[test-samurai] Runner does not support project-level execution", vim.log.levels.WARN)
return
end
local ok_cmd, command = pcall(runner.build_all_command, bufnr)
if not ok_cmd or not command or type(command.cmd) ~= "table" or #command.cmd == 0 then
vim.notify("[test-samurai] Runner failed to build all-tests command", vim.log.levels.ERROR)
return
end
local parser = runner.output_parser
if type(parser) == "function" then
parser = parser()
end
run_command(command, {
track_scope = true,
runner = runner,
scope_kind = "all",
output_parser = parser or runner.parse_results,
})
end
local function build_failed_only_command()
if not (state.last_scope_command and type(state.last_scope_command.cmd) == "table") then
return nil, "[test-samurai] No previous scoped test command"
end
local failures = state.last_scope_failures or {}
if #failures == 0 then
return nil, nil
end
local runner = state.last_scope_runner
if not runner or type(runner.build_failed_command) ~= "function" then
local name = runner and runner.name or "unknown"
return nil, "[test-samurai] Runner does not support failed-only: " .. name
end
local ok_build, command = pcall(runner.build_failed_command, state.last_scope_command, failures, state.last_scope_kind)
if not ok_build or not command or type(command.cmd) ~= "table" or #command.cmd == 0 then
return nil, "[test-samurai] Runner failed to build failed-only command"
end
return command, nil
end
function M.run_failed_only()
if not (state.last_scope_command and type(state.last_scope_command.cmd) == "table") then
vim.notify("[test-samurai] No previous scoped test command", vim.log.levels.WARN)
return
end
local command, err = build_failed_only_command()
if not command then
if not err then
M.run_last()
return
end
vim.notify(err, vim.log.levels.WARN)
return
end
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()
if not (state.last_buf and vim.api.nvim_buf_is_valid(state.last_buf)) then
vim.notify("[test-samurai] No previous output", vim.log.levels.WARN)
return
end
reopen_output_win()
end
return M