finish first MVP with test-runner-detection for Go, Mocha.js, Jest.js and ViTest.js
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
output
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
local config = require("test-samurai.config")
|
local config = require("test-samurai.config")
|
||||||
|
local util = require("test-samurai.util")
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
@@ -30,19 +31,105 @@ function M.reload_runners()
|
|||||||
load_runners()
|
load_runners()
|
||||||
end
|
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)
|
function M.get_runner_for_buf(bufnr)
|
||||||
|
local path = util.get_buf_path(bufnr)
|
||||||
|
|
||||||
|
local candidates = {}
|
||||||
|
|
||||||
for _, runner in ipairs(state.runners) do
|
for _, runner in ipairs(state.runners) do
|
||||||
if type(runner.is_test_file) == "function" then
|
if type(runner.is_test_file) == "function" then
|
||||||
local ok, is_test = pcall(runner.is_test_file, bufnr)
|
local ok, is_test = pcall(runner.is_test_file, bufnr)
|
||||||
if ok and is_test then
|
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
|
return runner
|
||||||
end
|
end
|
||||||
end
|
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
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function open_float(lines)
|
local function create_output_win(initial_lines)
|
||||||
if state.last_win and vim.api.nvim_win_is_valid(state.last_win) then
|
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)
|
pcall(vim.api.nvim_win_close, state.last_win, true)
|
||||||
end
|
end
|
||||||
@@ -58,7 +145,7 @@ local function open_float(lines)
|
|||||||
local buf = vim.api.nvim_create_buf(false, true)
|
local buf = vim.api.nvim_create_buf(false, true)
|
||||||
vim.api.nvim_buf_set_option(buf, "bufhidden", "wipe")
|
vim.api.nvim_buf_set_option(buf, "bufhidden", "wipe")
|
||||||
vim.api.nvim_buf_set_option(buf, "filetype", "test-samurai-output")
|
vim.api.nvim_buf_set_option(buf, "filetype", "test-samurai-output")
|
||||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, initial_lines or {})
|
||||||
|
|
||||||
local win = vim.api.nvim_open_win(buf, true, {
|
local win = vim.api.nvim_open_win(buf, true, {
|
||||||
relative = "editor",
|
relative = "editor",
|
||||||
@@ -78,42 +165,59 @@ local function open_float(lines)
|
|||||||
|
|
||||||
state.last_win = win
|
state.last_win = win
|
||||||
state.last_buf = buf
|
state.last_buf = buf
|
||||||
|
|
||||||
|
return buf, win
|
||||||
end
|
end
|
||||||
|
|
||||||
local function run_cmd(cmd, cwd, on_exit)
|
local function append_lines(buf, new_lines)
|
||||||
if vim.system then
|
if not (buf and vim.api.nvim_buf_is_valid(buf)) then
|
||||||
vim.system(cmd, { cwd = cwd, text = true }, function(obj)
|
return
|
||||||
local code = obj.code or -1
|
end
|
||||||
local stdout = obj.stdout or ""
|
if not new_lines or #new_lines == 0 then
|
||||||
local stderr = obj.stderr or ""
|
return
|
||||||
vim.schedule(function()
|
end
|
||||||
on_exit(code, stdout, stderr)
|
local existing = vim.api.nvim_buf_line_count(buf)
|
||||||
end)
|
vim.api.nvim_buf_set_lines(buf, existing, existing, false, new_lines)
|
||||||
end)
|
end
|
||||||
else
|
|
||||||
local stdout_chunks = {}
|
local function run_cmd(cmd, cwd, handlers)
|
||||||
local stderr_chunks = {}
|
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, {
|
vim.fn.jobstart(cmd, {
|
||||||
cwd = cwd,
|
cwd = cwd,
|
||||||
stdout_buffered = true,
|
stdout_buffered = false,
|
||||||
stderr_buffered = true,
|
stderr_buffered = false,
|
||||||
on_stdout = function(_, data)
|
on_stdout = function(_, data, _)
|
||||||
if data then
|
handle_chunk(h.on_stdout, data)
|
||||||
table.insert(stdout_chunks, table.concat(data, "\n"))
|
|
||||||
end
|
|
||||||
end,
|
end,
|
||||||
on_stderr = function(_, data)
|
on_stderr = function(_, data, _)
|
||||||
if data then
|
handle_chunk(h.on_stderr, data)
|
||||||
table.insert(stderr_chunks, table.concat(data, "\n"))
|
|
||||||
end
|
|
||||||
end,
|
end,
|
||||||
on_exit = function(_, code)
|
on_exit = function(_, code, _)
|
||||||
local stdout = table.concat(stdout_chunks, "\n")
|
if h.on_exit then
|
||||||
local stderr = table.concat(stderr_chunks, "\n")
|
pcall(h.on_exit, code or 0)
|
||||||
on_exit(code, stdout, stderr)
|
end
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.run_nearest()
|
function M.run_nearest()
|
||||||
@@ -153,23 +257,53 @@ function M.run_nearest()
|
|||||||
local cmd = command.cmd
|
local cmd = command.cmd
|
||||||
local cwd = command.cwd or vim.loop.cwd()
|
local cwd = command.cwd or vim.loop.cwd()
|
||||||
|
|
||||||
run_cmd(cmd, cwd, function(code, stdout, stderr)
|
|
||||||
local header = "$ " .. table.concat(cmd, " ")
|
local header = "$ " .. table.concat(cmd, " ")
|
||||||
local lines = { header, "" }
|
local buf = nil
|
||||||
if stdout ~= "" then
|
local has_output = false
|
||||||
local out_lines = vim.split(stdout, "\n", { plain = true })
|
|
||||||
vim.list_extend(lines, out_lines)
|
run_cmd(cmd, cwd, {
|
||||||
|
on_start = function()
|
||||||
|
buf = select(1, create_output_win({ header, "", "[running...]" }))
|
||||||
|
end,
|
||||||
|
on_stdout = function(lines)
|
||||||
|
if not buf then
|
||||||
|
buf = select(1, create_output_win({ header, "" }))
|
||||||
end
|
end
|
||||||
if stderr ~= "" then
|
if not has_output then
|
||||||
table.insert(lines, "")
|
local cur = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||||
table.insert(lines, "[stderr]")
|
if #cur > 0 and cur[#cur] == "[running...]" then
|
||||||
local err_lines = vim.split(stderr, "\n", { plain = true })
|
vim.api.nvim_buf_set_lines(buf, #cur - 1, #cur, false, {})
|
||||||
vim.list_extend(lines, err_lines)
|
|
||||||
end
|
end
|
||||||
table.insert(lines, "")
|
has_output = true
|
||||||
table.insert(lines, "[exit code] " .. tostring(code))
|
end
|
||||||
open_float(lines)
|
append_lines(buf, lines)
|
||||||
end)
|
end,
|
||||||
|
on_stderr = function(lines)
|
||||||
|
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
|
||||||
|
append_lines(buf, lines)
|
||||||
|
end,
|
||||||
|
on_exit = function(code)
|
||||||
|
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
|
||||||
|
end
|
||||||
|
append_lines(buf, { "", "[exit code] " .. tostring(code) })
|
||||||
|
end,
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -67,9 +67,38 @@ local function find_t_runs(lines, func)
|
|||||||
return subtests
|
return subtests
|
||||||
end
|
end
|
||||||
|
|
||||||
local function escape_pattern(str)
|
local function build_run_pattern(spec)
|
||||||
local escaped = str:gsub("(%W)", "%%%1")
|
local name = spec.test_path or ""
|
||||||
|
local escaped = name:gsub("(%W)", "%%%1")
|
||||||
|
if spec.scope == "function" then
|
||||||
|
return "^" .. escaped .. "$|^" .. escaped .. "/"
|
||||||
|
else
|
||||||
return "^" .. escaped .. "$"
|
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
|
end
|
||||||
|
|
||||||
function runner.is_test_file(bufnr)
|
function runner.is_test_file(bufnr)
|
||||||
@@ -134,8 +163,9 @@ function runner.find_nearest(bufnr, row, _col)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function runner.build_command(spec)
|
function runner.build_command(spec)
|
||||||
local pattern = escape_pattern(spec.test_path)
|
local pattern = build_run_pattern(spec)
|
||||||
local cmd = { "go", "test", "./...", "-run", pattern }
|
local pkg = build_pkg_arg(spec)
|
||||||
|
local cmd = { "go", "test", "-v", pkg, "-run", pattern }
|
||||||
return {
|
return {
|
||||||
cmd = cmd,
|
cmd = cmd,
|
||||||
cwd = spec.cwd,
|
cwd = spec.cwd,
|
||||||
|
|||||||
@@ -28,8 +28,168 @@ local function is_js_test_file(bufnr, filetypes, patterns)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
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 jest_config_files = { "jest.config.js", "jest.config.ts" }
|
||||||
|
|
||||||
|
local function find_jest_root(file)
|
||||||
|
if not file or file == "" then
|
||||||
|
return util.find_root(file, {
|
||||||
|
"jest.config.js",
|
||||||
|
"jest.config.ts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.config.js",
|
||||||
|
"package.json",
|
||||||
|
"node_modules",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local dir = vim.fs.dirname(file)
|
||||||
|
local found = vim.fs.find(jest_config_files, { path = dir, upward = true })
|
||||||
|
if found and #found > 0 then
|
||||||
|
local cfg = found[1]
|
||||||
|
local sep = package.config:sub(1, 1)
|
||||||
|
local marker = sep .. "test" .. sep .. ".bin" .. sep
|
||||||
|
local idx = cfg:find(marker, 1, true)
|
||||||
|
if idx then
|
||||||
|
local root = cfg:sub(1, idx - 1)
|
||||||
|
if root == "" then
|
||||||
|
root = sep
|
||||||
|
end
|
||||||
|
return root
|
||||||
|
end
|
||||||
|
return vim.fs.dirname(cfg)
|
||||||
|
end
|
||||||
|
|
||||||
|
return util.find_root(file, {
|
||||||
|
"jest.config.js",
|
||||||
|
"jest.config.ts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.config.js",
|
||||||
|
"package.json",
|
||||||
|
"node_modules",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function match_test_call(line)
|
||||||
|
local call, name = line:match("^%s*(it)%s*%(%s*['\"`]([^'\"`]+)['\"`]")
|
||||||
|
if call then
|
||||||
|
return call, name
|
||||||
|
end
|
||||||
|
call, name = line:match("^%s*(test)%s*%(%s*['\"`]([^'\"`]+)['\"`]")
|
||||||
|
if call then
|
||||||
|
return call, name
|
||||||
|
end
|
||||||
|
call, name = line:match("^%s*(describe)%s*%(%s*['\"`]([^'\"`]+)['\"`]")
|
||||||
|
if call and name then
|
||||||
|
return call, name
|
||||||
|
end
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function collect_js_structs(lines)
|
||||||
|
local describes = {}
|
||||||
|
local tests = {}
|
||||||
|
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
local dcall, dname = line:match("^%s*(describe)%s*%(%s*['\"`]([^'\"`]+)['\"`]")
|
||||||
|
if dcall and dname then
|
||||||
|
local start0 = i - 1
|
||||||
|
local end0 = find_block_end(lines, i)
|
||||||
|
table.insert(describes, {
|
||||||
|
kind = dcall,
|
||||||
|
name = dname,
|
||||||
|
start = start0,
|
||||||
|
["end"] = end0,
|
||||||
|
})
|
||||||
|
else
|
||||||
|
local tcall, tname = line:match("^%s*(it)%s*%(%s*['\"`]([^'\"`]+)['\"`]")
|
||||||
|
if not tcall then
|
||||||
|
tcall, tname = line:match("^%s*(test)%s*%(%s*['\"`]([^'\"`]+)['\"`]")
|
||||||
|
end
|
||||||
|
if tcall and tname then
|
||||||
|
local start0 = i - 1
|
||||||
|
local end0 = find_block_end(lines, i)
|
||||||
|
table.insert(tests, {
|
||||||
|
kind = tcall,
|
||||||
|
name = tname,
|
||||||
|
start = start0,
|
||||||
|
["end"] = end0,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return describes, tests
|
||||||
|
end
|
||||||
|
|
||||||
|
local function build_full_name(lines, idx, leaf_name)
|
||||||
|
local parts = { leaf_name }
|
||||||
|
for i = idx - 1, 1, -1 do
|
||||||
|
local line = lines[i]
|
||||||
|
local call, name = line:match("^%s*(describe)%s*%(%s*['\"`]([^'\"`]+)['\"`]")
|
||||||
|
if call and name then
|
||||||
|
table.insert(parts, 1, name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return table.concat(parts, " ")
|
||||||
|
end
|
||||||
|
|
||||||
local function find_nearest_test(bufnr, row)
|
local function find_nearest_test(bufnr, row)
|
||||||
local lines = util.get_buf_lines(bufnr)
|
local lines = util.get_buf_lines(bufnr)
|
||||||
|
local describes, tests = collect_js_structs(lines)
|
||||||
|
|
||||||
|
for _, t in ipairs(tests) do
|
||||||
|
if row >= t.start and row <= t["end"] then
|
||||||
|
local full = build_full_name(lines, t.start + 1, t.name)
|
||||||
|
return {
|
||||||
|
kind = t.kind,
|
||||||
|
name = t.name,
|
||||||
|
full_name = full,
|
||||||
|
line = t.start,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local best_describe = nil
|
||||||
|
for _, d in ipairs(describes) do
|
||||||
|
if row >= d.start and row <= d["end"] then
|
||||||
|
if not best_describe or d.start >= best_describe.start then
|
||||||
|
best_describe = d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if best_describe then
|
||||||
|
local full = build_full_name(lines, best_describe.start + 1, best_describe.name)
|
||||||
|
return {
|
||||||
|
kind = "describe",
|
||||||
|
name = best_describe.name,
|
||||||
|
full_name = full,
|
||||||
|
line = best_describe.start,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
local start = row + 1
|
local start = row + 1
|
||||||
if start > #lines then
|
if start > #lines then
|
||||||
start = #lines
|
start = #lines
|
||||||
@@ -38,15 +198,18 @@ local function find_nearest_test(bufnr, row)
|
|||||||
end
|
end
|
||||||
for i = start, 1, -1 do
|
for i = start, 1, -1 do
|
||||||
local line = lines[i]
|
local line = lines[i]
|
||||||
local call, name = line:match("^%s*(it|test|describe)%s*%(%s*['"`]([^'"`]+)['"`]")
|
local call, name = match_test_call(line)
|
||||||
if call and name then
|
if call and name then
|
||||||
|
local full = build_full_name(lines, i, name)
|
||||||
return {
|
return {
|
||||||
kind = call,
|
kind = call,
|
||||||
name = name,
|
name = name,
|
||||||
|
full_name = full,
|
||||||
line = i - 1,
|
line = i - 1,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -82,7 +245,11 @@ function M.new(opts)
|
|||||||
return nil, "no test call found"
|
return nil, "no test call found"
|
||||||
end
|
end
|
||||||
local path = util.get_buf_path(bufnr)
|
local path = util.get_buf_path(bufnr)
|
||||||
local root = util.find_root(path, {
|
local root
|
||||||
|
if runner.framework == "jest" then
|
||||||
|
root = find_jest_root(path)
|
||||||
|
else
|
||||||
|
root = util.find_root(path, {
|
||||||
"jest.config.js",
|
"jest.config.js",
|
||||||
"jest.config.ts",
|
"jest.config.ts",
|
||||||
"vitest.config.ts",
|
"vitest.config.ts",
|
||||||
@@ -90,11 +257,13 @@ function M.new(opts)
|
|||||||
"package.json",
|
"package.json",
|
||||||
"node_modules",
|
"node_modules",
|
||||||
})
|
})
|
||||||
|
end
|
||||||
return {
|
return {
|
||||||
file = path,
|
file = path,
|
||||||
cwd = root,
|
cwd = root,
|
||||||
framework = runner.framework,
|
framework = runner.framework,
|
||||||
test_name = hit.name,
|
test_name = hit.name,
|
||||||
|
full_name = hit.full_name,
|
||||||
kind = hit.kind,
|
kind = hit.kind,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -109,9 +278,10 @@ function M.new(opts)
|
|||||||
|
|
||||||
local function build_mocha(spec)
|
local function build_mocha(spec)
|
||||||
local cmd = vim.deepcopy(runner.command)
|
local cmd = vim.deepcopy(runner.command)
|
||||||
|
local target = spec.full_name or spec.test_name
|
||||||
|
table.insert(cmd, "--fgrep")
|
||||||
|
table.insert(cmd, target)
|
||||||
table.insert(cmd, spec.file)
|
table.insert(cmd, spec.file)
|
||||||
table.insert(cmd, "--grep")
|
|
||||||
table.insert(cmd, spec.test_name)
|
|
||||||
return cmd
|
return cmd
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
3
run_test.sh
Normal file
3
run_test.sh
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
nvim --headless -u tests/minimal_init.lua -c "PlenaryBustedDirectory tests" -c qa
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
local test_samurai = require("test-samurai")
|
local test_samurai = require("test-samurai")
|
||||||
local core = require("test-samurai.core")
|
local core = require("test-samurai.core")
|
||||||
|
local util = require("test-samurai.util")
|
||||||
|
|
||||||
|
local orig_find_root = util.find_root
|
||||||
|
local orig_fs_stat = vim.loop.fs_stat
|
||||||
|
local orig_readfile = vim.fn.readfile
|
||||||
|
|
||||||
describe("test-samurai core", function()
|
describe("test-samurai core", function()
|
||||||
before_each(function()
|
before_each(function()
|
||||||
test_samurai.setup()
|
test_samurai.setup()
|
||||||
|
util.find_root = orig_find_root
|
||||||
|
vim.loop.fs_stat = orig_fs_stat
|
||||||
|
vim.fn.readfile = orig_readfile
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("selects Go runner for _test.go files", function()
|
it("selects Go runner for _test.go files", function()
|
||||||
@@ -23,4 +31,43 @@ describe("test-samurai core", function()
|
|||||||
assert.is_not_nil(runner)
|
assert.is_not_nil(runner)
|
||||||
assert.equals("js-jest", runner.name)
|
assert.equals("js-jest", runner.name)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it("prefers mocha runner when mocha is in package.json", function()
|
||||||
|
util.find_root = function(_, _)
|
||||||
|
return "/tmp/mocha_proj"
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.loop.fs_stat = function(path)
|
||||||
|
if path == "/tmp/mocha_proj/package.json" then
|
||||||
|
return { type = "file" }
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.fn.readfile = function(path)
|
||||||
|
if path == "/tmp/mocha_proj/package.json" then
|
||||||
|
return {
|
||||||
|
"{",
|
||||||
|
' "devDependencies": { "mocha": "^10.0.0" }',
|
||||||
|
"}",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
test_samurai.setup()
|
||||||
|
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/mocha_proj/foo.test.ts")
|
||||||
|
vim.bo[bufnr].filetype = "typescript"
|
||||||
|
|
||||||
|
local runner = core.get_runner_for_buf(bufnr)
|
||||||
|
|
||||||
|
util.find_root = orig_find_root
|
||||||
|
vim.loop.fs_stat = orig_fs_stat
|
||||||
|
vim.fn.readfile = orig_readfile
|
||||||
|
|
||||||
|
assert.is_not_nil(runner)
|
||||||
|
assert.equals("js-mocha", runner.name)
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ local go_runner = require("test-samurai.runners.go")
|
|||||||
describe("test-samurai go runner", function()
|
describe("test-samurai go runner", function()
|
||||||
it("detects Go test files by suffix", function()
|
it("detects Go test files by suffix", function()
|
||||||
local bufnr1 = vim.api.nvim_create_buf(false, true)
|
local bufnr1 = vim.api.nvim_create_buf(false, true)
|
||||||
vim.api.nvim_buf_set_name(bufnr1, "/tmp/foo_test.go")
|
vim.api.nvim_buf_set_name(bufnr1, "/tmp/go_suffix_test.go")
|
||||||
assert.is_true(go_runner.is_test_file(bufnr1))
|
assert.is_true(go_runner.is_test_file(bufnr1))
|
||||||
|
|
||||||
local bufnr2 = vim.api.nvim_create_buf(false, true)
|
local bufnr2 = vim.api.nvim_create_buf(false, true)
|
||||||
vim.api.nvim_buf_set_name(bufnr2, "/tmp/foo.go")
|
vim.api.nvim_buf_set_name(bufnr2, "/tmp/go_main.go")
|
||||||
assert.is_false(go_runner.is_test_file(bufnr2))
|
assert.is_false(go_runner.is_test_file(bufnr2))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("finds subtest when cursor is inside t.Run block", function()
|
it("finds subtest when cursor is inside t.Run block", function()
|
||||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
vim.api.nvim_buf_set_name(bufnr, "/tmp/foo_test.go")
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/go_subtest_test.go")
|
||||||
local lines = {
|
local lines = {
|
||||||
"package main",
|
"package main",
|
||||||
"import \"testing\"",
|
"import \"testing\"",
|
||||||
@@ -44,13 +44,13 @@ describe("test-samurai go runner", function()
|
|||||||
assert.is_not_nil(spec)
|
assert.is_not_nil(spec)
|
||||||
assert.equals("TestFoo/first", spec.test_path)
|
assert.equals("TestFoo/first", spec.test_path)
|
||||||
assert.equals("subtest", spec.scope)
|
assert.equals("subtest", spec.scope)
|
||||||
assert.equals("/tmp/foo_test.go", spec.file)
|
assert.is_true(spec.file:match("go_subtest_test%.go$") ~= nil)
|
||||||
assert.equals("/tmp", spec.cwd)
|
assert.is_true(spec.cwd:match("tmp$") ~= nil)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("falls back to whole test function when between subtests", function()
|
it("falls back to whole test function when between subtests", function()
|
||||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
vim.api.nvim_buf_set_name(bufnr, "/tmp/foo_test.go")
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/go_between_test.go")
|
||||||
local lines = {
|
local lines = {
|
||||||
"package main",
|
"package main",
|
||||||
"import \"testing\"",
|
"import \"testing\"",
|
||||||
@@ -81,5 +81,35 @@ describe("test-samurai go runner", function()
|
|||||||
assert.is_not_nil(spec)
|
assert.is_not_nil(spec)
|
||||||
assert.equals("TestFoo", spec.test_path)
|
assert.equals("TestFoo", spec.test_path)
|
||||||
assert.equals("function", spec.scope)
|
assert.equals("function", spec.scope)
|
||||||
|
assert.is_true(spec.file:match("go_between_test%.go$") ~= nil)
|
||||||
|
assert.is_true(spec.cwd:match("tmp$") ~= nil)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("build_command uses current package and correct run pattern", function()
|
||||||
|
local spec_sub = {
|
||||||
|
file = "/tmp/project/pkg/foo_test.go",
|
||||||
|
cwd = "/tmp/project",
|
||||||
|
test_path = "TestFoo/first",
|
||||||
|
scope = "subtest",
|
||||||
|
}
|
||||||
|
|
||||||
|
local cmd_spec_sub = go_runner.build_command(spec_sub)
|
||||||
|
assert.are.same(
|
||||||
|
{ "go", "test", "-v", "./pkg", "-run", "^TestFoo%/first$" },
|
||||||
|
cmd_spec_sub.cmd
|
||||||
|
)
|
||||||
|
|
||||||
|
local spec_func = {
|
||||||
|
file = "/tmp/project/foo_test.go",
|
||||||
|
cwd = "/tmp/project",
|
||||||
|
test_path = "TestFoo",
|
||||||
|
scope = "function",
|
||||||
|
}
|
||||||
|
|
||||||
|
local cmd_spec_func = go_runner.build_command(spec_func)
|
||||||
|
assert.are.same(
|
||||||
|
{ "go", "test", "-v", "./", "-run", "^TestFoo$|^TestFoo/" },
|
||||||
|
cmd_spec_func.cmd
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
local jest = require("test-samurai.runners.js-jest")
|
local jest = require("test-samurai.runners.js-jest")
|
||||||
|
local mocha = require("test-samurai.runners.js-mocha")
|
||||||
|
|
||||||
describe("test-samurai js runner (jest)", function()
|
describe("test-samurai js runner (jest)", function()
|
||||||
it("detects JS/TS test files by name and filetype", function()
|
it("detects JS/TS test files by name and filetype", function()
|
||||||
local bufnr1 = vim.api.nvim_create_buf(false, true)
|
local bufnr1 = vim.api.nvim_create_buf(false, true)
|
||||||
vim.api.nvim_buf_set_name(bufnr1, "/tmp/foo.test.ts")
|
vim.api.nvim_buf_set_name(bufnr1, "/tmp/foo_detect.test.ts")
|
||||||
vim.bo[bufnr1].filetype = "typescript"
|
vim.bo[bufnr1].filetype = "typescript"
|
||||||
assert.is_true(jest.is_test_file(bufnr1))
|
assert.is_true(jest.is_test_file(bufnr1))
|
||||||
|
|
||||||
local bufnr2 = vim.api.nvim_create_buf(false, true)
|
local bufnr2 = vim.api.nvim_create_buf(false, true)
|
||||||
vim.api.nvim_buf_set_name(bufnr2, "/tmp/foo.ts")
|
vim.api.nvim_buf_set_name(bufnr2, "/tmp/foo_detect.ts")
|
||||||
vim.bo[bufnr2].filetype = "typescript"
|
vim.bo[bufnr2].filetype = "typescript"
|
||||||
assert.is_false(jest.is_test_file(bufnr2))
|
assert.is_false(jest.is_test_file(bufnr2))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("finds nearest it() call as test name", function()
|
it("finds nearest it() call as test name and builds full_name when cursor is inside the test", function()
|
||||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
vim.api.nvim_buf_set_name(bufnr, "/tmp/foo.test.ts")
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/foo_nearest.test.ts")
|
||||||
vim.bo[bufnr].filetype = "typescript"
|
vim.bo[bufnr].filetype = "typescript"
|
||||||
local lines = {
|
local lines = {
|
||||||
"describe(\"outer\", function() {",
|
'describe("outer", function() {',
|
||||||
" it(\"inner 1\", function() {",
|
' it("inner 1", function() {',
|
||||||
" -- inside 1",
|
" -- inside 1",
|
||||||
" })",
|
" })",
|
||||||
"",
|
"",
|
||||||
" it(\"inner 2\", function() {",
|
' it("inner 2", function() {',
|
||||||
" -- inside 2",
|
" -- inside 2",
|
||||||
" })",
|
" })",
|
||||||
"})",
|
"})",
|
||||||
@@ -31,10 +32,85 @@ describe("test-samurai js runner (jest)", function()
|
|||||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||||
|
|
||||||
local orig_fs_find = vim.fs.find
|
local orig_fs_find = vim.fs.find
|
||||||
vim.fs.find = function(markers, opts)
|
vim.fs.find = function(names, opts)
|
||||||
return { "/tmp/package.json" }
|
return { "/tmp/package.json" }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Cursor in der zweiten it()-Body
|
||||||
|
local row_inside_second = 6 -- 0-basiert -> Zeile mit "-- inside 2"
|
||||||
|
local spec, err = jest.find_nearest(bufnr, row_inside_second, 0)
|
||||||
|
|
||||||
|
vim.fs.find = orig_fs_find
|
||||||
|
|
||||||
|
assert.is_nil(err)
|
||||||
|
assert.is_not_nil(spec)
|
||||||
|
assert.equals("inner 2", spec.test_name)
|
||||||
|
assert.equals("outer inner 2", spec.full_name)
|
||||||
|
assert.equals("jest", spec.framework)
|
||||||
|
assert.is_true(spec.file:match("foo_nearest%.test%.ts$") ~= nil)
|
||||||
|
assert.is_true(spec.cwd:match("tmp$") ~= nil)
|
||||||
|
|
||||||
|
local cmd_spec = jest.build_command(spec)
|
||||||
|
assert.are.same({ "npx", "jest", spec.file, "-t", "inner 2" }, cmd_spec.cmd)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("returns describe block when cursor is between it() calls", function()
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/foo_between.test.ts")
|
||||||
|
vim.bo[bufnr].filetype = "typescript"
|
||||||
|
local lines = {
|
||||||
|
'describe("outer", function() {',
|
||||||
|
' it("inner 1", function() {',
|
||||||
|
" -- inside 1",
|
||||||
|
" })",
|
||||||
|
"",
|
||||||
|
' it("inner 2", function() {',
|
||||||
|
" -- inside 2",
|
||||||
|
" })",
|
||||||
|
"})",
|
||||||
|
}
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||||
|
|
||||||
|
local orig_fs_find = vim.fs.find
|
||||||
|
vim.fs.find = function(names, opts)
|
||||||
|
return { "/tmp/package.json" }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Cursor auf der Leerzeile zwischen den beiden it()-Blöcken
|
||||||
|
local row_between = 4 -- 0-basiert -> leere Zeile zwischen den Tests
|
||||||
|
local spec, err = jest.find_nearest(bufnr, row_between, 0)
|
||||||
|
|
||||||
|
vim.fs.find = orig_fs_find
|
||||||
|
|
||||||
|
assert.is_nil(err)
|
||||||
|
assert.is_not_nil(spec)
|
||||||
|
assert.equals("outer", spec.test_name)
|
||||||
|
assert.equals("outer", spec.full_name)
|
||||||
|
assert.equals("jest", spec.framework)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("treats jest.config in test/.bin as project root parent", function()
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/foo_binroot.test.ts")
|
||||||
|
vim.bo[bufnr].filetype = "typescript"
|
||||||
|
local lines = {
|
||||||
|
'describe("outer", function() {',
|
||||||
|
' it("inner 1", function() {',
|
||||||
|
" -- inside 1",
|
||||||
|
" })",
|
||||||
|
"",
|
||||||
|
' it("inner 2", function() {',
|
||||||
|
" -- inside 2",
|
||||||
|
" })",
|
||||||
|
"})",
|
||||||
|
}
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||||
|
|
||||||
|
local orig_fs_find = vim.fs.find
|
||||||
|
vim.fs.find = function(names, opts)
|
||||||
|
return { "/tmp/test/.bin/jest.config.js" }
|
||||||
|
end
|
||||||
|
|
||||||
local row_inside_second = 6
|
local row_inside_second = 6
|
||||||
local spec, err = jest.find_nearest(bufnr, row_inside_second, 0)
|
local spec, err = jest.find_nearest(bufnr, row_inside_second, 0)
|
||||||
|
|
||||||
@@ -42,12 +118,25 @@ describe("test-samurai js runner (jest)", function()
|
|||||||
|
|
||||||
assert.is_nil(err)
|
assert.is_nil(err)
|
||||||
assert.is_not_nil(spec)
|
assert.is_not_nil(spec)
|
||||||
assert.equals("inner 2", spec.test_name)
|
|
||||||
assert.equals("jest", spec.framework)
|
|
||||||
assert.equals("/tmp/foo.test.ts", spec.file)
|
|
||||||
assert.equals("/tmp", spec.cwd)
|
assert.equals("/tmp", spec.cwd)
|
||||||
|
end)
|
||||||
local cmd_spec = jest.build_command(spec)
|
end)
|
||||||
assert.are.same({ "npx", "jest", "/tmp/foo.test.ts", "-t", "inner 2" }, cmd_spec.cmd)
|
|
||||||
|
describe("test-samurai js runner (mocha)", function()
|
||||||
|
it("builds mocha command with fgrep and full test title", function()
|
||||||
|
local spec = {
|
||||||
|
file = "/tmp/project/test/foo_nearest.test.ts",
|
||||||
|
cwd = "/tmp/project",
|
||||||
|
test_name = "inner 2",
|
||||||
|
full_name = "outer inner 2",
|
||||||
|
}
|
||||||
|
|
||||||
|
local cmd_spec = mocha.build_command(spec)
|
||||||
|
|
||||||
|
assert.are.same(
|
||||||
|
{ "npx", "mocha", "--fgrep", "outer inner 2", spec.file },
|
||||||
|
cmd_spec.cmd
|
||||||
|
)
|
||||||
|
assert.equals("/tmp/project", cmd_spec.cwd)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|||||||
Reference in New Issue
Block a user