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 build_run_pattern(spec) local name = spec.test_path or "" local escaped = name:gsub("(%W)", "%%%1") if spec.scope == "function" then return "^" .. escaped .. "$|^" .. 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 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", "-v", pkg, "-run", pattern } return { cmd = cmd, cwd = spec.cwd, } end return runner