initialize with first MVP

This commit is contained in:
2025-12-23 14:10:06 +01:00
commit 4de8921a42
14 changed files with 733 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
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_pattern(str)
local escaped = str:gsub("(%W)", "%%%1")
return "^" .. escaped .. "$"
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 = escape_pattern(spec.test_path)
local cmd = { "go", "test", "./...", "-run", pattern }
return {
cmd = cmd,
cwd = spec.cwd,
}
end
return runner

View File

@@ -0,0 +1,7 @@
local js = require("test-samurai.runners.js")
return js.new({
name = "js-jest",
framework = "jest",
command = { "npx", "jest" },
})

View File

@@ -0,0 +1,7 @@
local js = require("test-samurai.runners.js")
return js.new({
name = "js-mocha",
framework = "mocha",
command = { "npx", "mocha" },
})

View File

@@ -0,0 +1,7 @@
local js = require("test-samurai.runners.js")
return js.new({
name = "js-vitest",
framework = "vitest",
command = { "npx", "vitest" },
})

View File

@@ -0,0 +1,148 @@
local util = require("test-samurai.util")
local M = {}
local default_filetypes = {
javascript = true,
javascriptreact = true,
typescript = true,
typescriptreact = true,
}
local default_patterns = { ".test.", ".spec." }
local function is_js_test_file(bufnr, filetypes, patterns)
local ft = vim.bo[bufnr].filetype
if not filetypes[ft] then
return false
end
local path = util.get_buf_path(bufnr)
if not path or path == "" then
return false
end
for _, pat in ipairs(patterns) do
if path:find(pat, 1, true) then
return true
end
end
return false
end
local function find_nearest_test(bufnr, row)
local lines = util.get_buf_lines(bufnr)
local start = row + 1
if start > #lines then
start = #lines
elseif start < 1 then
start = 1
end
for i = start, 1, -1 do
local line = lines[i]
local call, name = line:match("^%s*(it|test|describe)%s*%(%s*['"`]([^'"`]+)['"`]")
if call and name then
return {
kind = call,
name = name,
line = i - 1,
}
end
end
return nil
end
function M.new(opts)
local cfg = opts or {}
local runner = {}
runner.name = cfg.name or "js"
runner.framework = cfg.framework or "jest"
runner.command = cfg.command or { "npx", runner.framework }
runner.filetypes = {}
if cfg.filetypes then
for _, ft in ipairs(cfg.filetypes) do
runner.filetypes[ft] = true
end
else
runner.filetypes = vim.deepcopy(default_filetypes)
end
runner.patterns = cfg.patterns or default_patterns
function runner.is_test_file(bufnr)
return is_js_test_file(bufnr, runner.filetypes, runner.patterns)
end
function runner.find_nearest(bufnr, row, _col)
if not runner.is_test_file(bufnr) then
return nil, "not a JS/TS test file"
end
local hit = find_nearest_test(bufnr, row)
if not hit then
return nil, "no test call found"
end
local path = util.get_buf_path(bufnr)
local root = util.find_root(path, {
"jest.config.js",
"jest.config.ts",
"vitest.config.ts",
"vitest.config.js",
"package.json",
"node_modules",
})
return {
file = path,
cwd = root,
framework = runner.framework,
test_name = hit.name,
kind = hit.kind,
}
end
local function build_jest(spec)
local cmd = vim.deepcopy(runner.command)
table.insert(cmd, spec.file)
table.insert(cmd, "-t")
table.insert(cmd, spec.test_name)
return cmd
end
local function build_mocha(spec)
local cmd = vim.deepcopy(runner.command)
table.insert(cmd, spec.file)
table.insert(cmd, "--grep")
table.insert(cmd, spec.test_name)
return cmd
end
local function build_vitest(spec)
local cmd = vim.deepcopy(runner.command)
table.insert(cmd, spec.file)
table.insert(cmd, "-t")
table.insert(cmd, spec.test_name)
return cmd
end
function runner.build_command(spec)
local fw = runner.framework
local cmd
if fw == "jest" then
cmd = build_jest(spec)
elseif fw == "mocha" then
cmd = build_mocha(spec)
elseif fw == "vitest" then
cmd = build_vitest(spec)
else
cmd = vim.deepcopy(runner.command)
table.insert(cmd, spec.file)
end
return {
cmd = cmd,
cwd = spec.cwd,
}
end
return runner
end
return M