Files
test-samurai.nvim/lua/test-samurai/runners/go.lua

357 lines
8.1 KiB
Lua

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 = {} }
end
local passes = {}
local failures = {}
local 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)
elseif data.Action == "fail" then
table.insert(failures, data.Test)
elseif data.Action == "skip" then
table.insert(skips, data.Test)
end
end
end
end
return {
passes = collect_unique(passes),
failures = collect_unique(failures),
skips = collect_unique(skips),
}
end
function runner.output_parser()
local seen_pass = {}
local seen_fail = {}
local failures = {}
local passes = {}
local 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
if data.Action == "pass" and not seen_pass[name] then
seen_pass[name] = true
table.insert(passes, name)
return {
passes = { name },
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)
return {
passes = {},
failures = { name },
skips = {},
failures_all = vim.deepcopy(failures),
}
elseif data.Action == "skip" and not seen_pass[name] then
seen_pass[name] = true
table.insert(skips, name)
return {
passes = {},
failures = {},
skips = { name },
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
return runner