825 lines
20 KiB
Lua
825 lines
20 KiB
Lua
local runner = {
|
|
name = "test-samurai-mocha-runner",
|
|
framework = "javascript",
|
|
}
|
|
|
|
local default_config = {
|
|
all_test_glob = "test/**/*.test.js",
|
|
debug_log_path = nil,
|
|
}
|
|
|
|
local config = vim.deepcopy(default_config)
|
|
|
|
function runner.setup(opts)
|
|
if not opts then
|
|
config = vim.deepcopy(default_config)
|
|
return config
|
|
end
|
|
config = vim.tbl_deep_extend("force", vim.deepcopy(default_config), opts)
|
|
return config
|
|
end
|
|
|
|
local function debug_log_line(label, line)
|
|
if not config or not config.debug_log_path then
|
|
return
|
|
end
|
|
local ok, handle = pcall(io.open, config.debug_log_path, "a")
|
|
if not ok or not handle then
|
|
return
|
|
end
|
|
handle:write(string.format("%s %s\n", label, line or ""))
|
|
handle:close()
|
|
end
|
|
|
|
local function runner_root()
|
|
local source = debug.getinfo(1, "S").source
|
|
if source:sub(1, 1) == "@" then
|
|
source = source:sub(2)
|
|
end
|
|
return vim.fn.fnamemodify(source, ":p:h:h:h")
|
|
end
|
|
|
|
local function runner_paths()
|
|
local root = runner_root()
|
|
return {
|
|
ui = root .. "/scripts/bdd-with-location.cjs",
|
|
reporter = root .. "/scripts/mocha-ndjson-reporter.cjs",
|
|
}
|
|
end
|
|
|
|
local RUNNER_PATHS = runner_paths()
|
|
|
|
local function read_file(path)
|
|
local ok, lines = pcall(vim.fn.readfile, path)
|
|
if not ok then
|
|
return nil
|
|
end
|
|
return table.concat(lines, "\n")
|
|
end
|
|
|
|
local function find_project_root(start_path)
|
|
if not start_path or start_path == "" then
|
|
return nil
|
|
end
|
|
local dir = vim.fn.fnamemodify(start_path, ":h")
|
|
local prev = nil
|
|
while dir and dir ~= prev do
|
|
local candidate = dir .. "/package.json"
|
|
if vim.fn.filereadable(candidate) == 1 then
|
|
return dir
|
|
end
|
|
prev = dir
|
|
dir = vim.fn.fnamemodify(dir, ":h")
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function load_package_json(root)
|
|
if not root then
|
|
return nil
|
|
end
|
|
local content = read_file(root .. "/package.json")
|
|
if not content then
|
|
return nil
|
|
end
|
|
local ok, data = pcall(vim.json.decode, content)
|
|
if not ok then
|
|
return nil
|
|
end
|
|
return data
|
|
end
|
|
|
|
local function has_mocha_dependency(pkg)
|
|
if not pkg then
|
|
return false
|
|
end
|
|
local deps = pkg.dependencies or {}
|
|
local dev_deps = pkg.devDependencies or {}
|
|
return deps.mocha ~= nil or dev_deps.mocha ~= nil
|
|
end
|
|
|
|
local function count_char(line, char)
|
|
local _, count = line:gsub(char, "")
|
|
return count
|
|
end
|
|
|
|
local function extract_title(line, names)
|
|
for _, name in ipairs(names) do
|
|
local pattern = name .. "%s*%(%s*(['\"])(.-)%1"
|
|
local _, title = line:match(pattern)
|
|
if title and title ~= "" then
|
|
return name, title
|
|
end
|
|
end
|
|
return nil, nil
|
|
end
|
|
|
|
local function build_full_name(suites, title)
|
|
if #suites == 0 then
|
|
return title
|
|
end
|
|
return table.concat(suites, "/") .. "/" .. title
|
|
end
|
|
|
|
local function build_mocha_title(suites, title)
|
|
if #suites == 0 then
|
|
return title
|
|
end
|
|
return table.concat(suites, " ") .. " " .. title
|
|
end
|
|
|
|
local function suite_titles_from_stack(stack)
|
|
local suites = {}
|
|
for _, entry in ipairs(stack) do
|
|
if entry.kind == "suite" then
|
|
table.insert(suites, entry.title)
|
|
end
|
|
end
|
|
return suites
|
|
end
|
|
|
|
local function last_suite_from_stack(stack)
|
|
for i = #stack, 1, -1 do
|
|
if stack[i].kind == "suite" then
|
|
return stack[i]
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function parse_buffer_blocks(lines)
|
|
local depth = 0
|
|
local stack = {}
|
|
local suites = {}
|
|
local tests = {}
|
|
local suite_names = { "describe", "context" }
|
|
local test_names = { "it", "test" }
|
|
|
|
for i = 1, #lines do
|
|
local line = lines[i] or ""
|
|
local opens = count_char(line, "{")
|
|
local closes = count_char(line, "}")
|
|
local kind, title = extract_title(line, suite_names)
|
|
local new_depth = depth + opens - closes
|
|
|
|
if title then
|
|
local parent_titles = suite_titles_from_stack(stack)
|
|
local node = {
|
|
kind = "suite",
|
|
title = title,
|
|
start_line = i,
|
|
end_line = i,
|
|
depth = new_depth,
|
|
parent = last_suite_from_stack(stack),
|
|
}
|
|
node.full_name = build_full_name(parent_titles, title)
|
|
node.mocha_full_title = build_mocha_title(parent_titles, title)
|
|
table.insert(suites, node)
|
|
if opens > closes then
|
|
table.insert(stack, node)
|
|
else
|
|
node.end_line = i
|
|
end
|
|
depth = new_depth
|
|
else
|
|
kind, title = extract_title(line, test_names)
|
|
if title then
|
|
local parent_titles = suite_titles_from_stack(stack)
|
|
local node = {
|
|
kind = "test",
|
|
title = title,
|
|
start_line = i,
|
|
end_line = i,
|
|
depth = new_depth,
|
|
parent = last_suite_from_stack(stack),
|
|
}
|
|
node.full_name = build_full_name(parent_titles, title)
|
|
node.mocha_full_title = build_mocha_title(parent_titles, title)
|
|
table.insert(tests, node)
|
|
if opens > closes then
|
|
table.insert(stack, node)
|
|
else
|
|
node.end_line = i
|
|
end
|
|
end
|
|
depth = new_depth
|
|
end
|
|
|
|
while #stack > 0 and stack[#stack].depth > depth do
|
|
local node = table.remove(stack)
|
|
node.end_line = i
|
|
end
|
|
end
|
|
|
|
for _, node in ipairs(stack) do
|
|
if not node.end_line or node.end_line < node.start_line then
|
|
node.end_line = #lines
|
|
end
|
|
end
|
|
|
|
return suites, tests
|
|
end
|
|
|
|
local function find_deepest_block_at_row(blocks, row)
|
|
local best = nil
|
|
for _, block in ipairs(blocks) do
|
|
if row >= block.start_line and row <= block.end_line then
|
|
if not best then
|
|
best = block
|
|
else
|
|
local best_span = best.end_line - best.start_line
|
|
local block_span = block.end_line - block.start_line
|
|
if block_span <= best_span then
|
|
best = block
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return best
|
|
end
|
|
|
|
local function build_grep_pattern(title)
|
|
local pattern = title or ""
|
|
pattern = pattern:gsub("%s+", ".*")
|
|
return pattern
|
|
end
|
|
|
|
local function ensure_state(state)
|
|
state.results = state.results or { passes = {}, failures = {}, skips = {} }
|
|
state.seen = state.seen or { passes = {}, failures = {}, skips = {} }
|
|
state.outputs = state.outputs or {}
|
|
state.locations = state.locations or {}
|
|
state.mocha_titles = state.mocha_titles or {}
|
|
return state
|
|
end
|
|
|
|
local function build_error_lines(error_obj)
|
|
local lines = {}
|
|
if not error_obj then
|
|
return lines
|
|
end
|
|
if type(error_obj) == "string" then
|
|
table.insert(lines, error_obj)
|
|
return lines
|
|
end
|
|
if type(error_obj) ~= "table" then
|
|
return lines
|
|
end
|
|
if error_obj.message and error_obj.message ~= "" then
|
|
table.insert(lines, error_obj.message)
|
|
end
|
|
if error_obj.stack and error_obj.stack ~= "" then
|
|
for _, line in ipairs(vim.split(error_obj.stack, "\n")) do
|
|
table.insert(lines, line)
|
|
end
|
|
end
|
|
return lines
|
|
end
|
|
|
|
local function output_lines_from_payload(payload)
|
|
local output = payload and payload.output
|
|
if type(output) == "string" then
|
|
return vim.split(output, "\n", { plain = true, trimempty = false })
|
|
end
|
|
if type(output) == "table" then
|
|
local lines = {}
|
|
for _, line in ipairs(output) do
|
|
if type(line) == "string" then
|
|
if line:find("\n", 1, true) then
|
|
for _, split_line in ipairs(vim.split(line, "\n", { plain = true, trimempty = false })) do
|
|
table.insert(lines, split_line)
|
|
end
|
|
else
|
|
table.insert(lines, line)
|
|
end
|
|
end
|
|
end
|
|
return lines
|
|
end
|
|
return {}
|
|
end
|
|
|
|
local function append_lines(target, lines)
|
|
for _, line in ipairs(lines) do
|
|
table.insert(target, line)
|
|
end
|
|
end
|
|
|
|
local function extract_location(stack)
|
|
if not stack then
|
|
return nil
|
|
end
|
|
local filename, lnum, col = stack:match("%(([^%s%(%):]+):(%d+):(%d+)%)")
|
|
if not filename then
|
|
filename, lnum, col = stack:match("at%s+[^%s]+%s+([^%s%(%):]+):(%d+):(%d+)")
|
|
end
|
|
if not filename then
|
|
filename, lnum, col = stack:match("([^%s%(%):]+):(%d+):(%d+)")
|
|
end
|
|
if not filename then
|
|
return nil
|
|
end
|
|
return {
|
|
filename = filename,
|
|
lnum = tonumber(lnum),
|
|
col = tonumber(col),
|
|
}
|
|
end
|
|
|
|
local function normalize_title_path(payload)
|
|
if type(payload.titlePath) ~= "table" then
|
|
return nil
|
|
end
|
|
local path = {}
|
|
for _, part in ipairs(payload.titlePath) do
|
|
if type(part) == "string" and part ~= "" then
|
|
table.insert(path, part)
|
|
end
|
|
end
|
|
if #path == 0 then
|
|
return nil
|
|
end
|
|
return path
|
|
end
|
|
|
|
local function build_full_name_from_payload(payload)
|
|
local path = normalize_title_path(payload)
|
|
if path then
|
|
local full = table.concat(path, "/")
|
|
local mocha_full = payload.fullTitle or table.concat(path, " ")
|
|
return full, mocha_full
|
|
end
|
|
|
|
local title = payload.title or payload.fullTitle
|
|
if not title or title == "" then
|
|
return nil, nil
|
|
end
|
|
|
|
if payload.fullTitle and payload.title then
|
|
local suffix = " " .. payload.title
|
|
if payload.fullTitle:sub(-#suffix) == suffix then
|
|
local prefix = payload.fullTitle:sub(1, #payload.fullTitle - #suffix)
|
|
if prefix ~= "" then
|
|
local parts = vim.split(prefix, " ", { plain = true })
|
|
table.insert(parts, payload.title)
|
|
return table.concat(parts, "/"), payload.fullTitle
|
|
end
|
|
end
|
|
end
|
|
|
|
return title, payload.fullTitle or title
|
|
end
|
|
|
|
local function build_hook_name_from_payload(payload)
|
|
local hook_type = payload.hookType or payload.hook_type or payload.title or "hook"
|
|
local hook_label = tostring(hook_type)
|
|
local path = normalize_title_path(payload)
|
|
if path then
|
|
return hook_label .. ": " .. table.concat(path, "/"), hook_label .. " " .. table.concat(path, " ")
|
|
end
|
|
if payload.fullTitle and payload.fullTitle ~= "" then
|
|
return hook_label .. ": " .. payload.fullTitle, hook_label .. " " .. payload.fullTitle
|
|
end
|
|
if payload.title and payload.title ~= "" then
|
|
return hook_label .. ": " .. payload.title, hook_label .. " " .. payload.title
|
|
end
|
|
return hook_label, hook_label
|
|
end
|
|
|
|
local function normalize_location(location)
|
|
if not location or type(location) ~= "table" then
|
|
return nil
|
|
end
|
|
local filename = location.file or location.filename
|
|
local lnum = location.line or location.lnum
|
|
local col = location.column or location.col
|
|
if not filename or not lnum or not col then
|
|
return nil
|
|
end
|
|
return {
|
|
filename = filename,
|
|
lnum = tonumber(lnum),
|
|
col = tonumber(col),
|
|
}
|
|
end
|
|
|
|
local function record_ndjson_result(state, status, payload)
|
|
local full_name, mocha_title = build_full_name_from_payload(payload)
|
|
if not full_name then
|
|
return nil
|
|
end
|
|
state.mocha_titles[full_name] = mocha_title
|
|
|
|
local bucket
|
|
if status == "passed" then
|
|
bucket = "passes"
|
|
elseif status == "failed" then
|
|
bucket = "failures"
|
|
elseif status == "pending" or status == "skipped" then
|
|
bucket = "skips"
|
|
else
|
|
return nil
|
|
end
|
|
|
|
local added = false
|
|
if not state.seen[bucket][full_name] then
|
|
state.seen[bucket][full_name] = true
|
|
table.insert(state.results[bucket], full_name)
|
|
added = true
|
|
end
|
|
|
|
local location = normalize_location(payload.location)
|
|
local output_lines = output_lines_from_payload(payload)
|
|
if status == "failed" then
|
|
local lines = {}
|
|
append_lines(lines, output_lines)
|
|
append_lines(lines, build_error_lines(payload.error))
|
|
if #lines > 0 then
|
|
state.outputs[full_name] = lines
|
|
end
|
|
|
|
if not location and payload.error and payload.error.stack then
|
|
location = extract_location(payload.error.stack)
|
|
end
|
|
if location then
|
|
location.text = (payload.error and payload.error.message) or "test failure"
|
|
state.locations[full_name] = location
|
|
end
|
|
else
|
|
if not state.outputs[full_name] then
|
|
state.outputs[full_name] = { status }
|
|
end
|
|
if location then
|
|
location.text = status
|
|
state.locations[full_name] = location
|
|
end
|
|
if #output_lines > 0 then
|
|
state.outputs[full_name] = output_lines
|
|
end
|
|
end
|
|
|
|
if (not state.outputs[full_name] or #state.outputs[full_name] == 0) and status ~= "failed" then
|
|
state.outputs[full_name] = { status }
|
|
end
|
|
|
|
if added then
|
|
return full_name
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function record_hook_failure(state, payload)
|
|
local full_name, mocha_title = build_hook_name_from_payload(payload or {})
|
|
if not full_name then
|
|
return nil
|
|
end
|
|
state.mocha_titles[full_name] = mocha_title
|
|
|
|
if not state.seen.failures[full_name] then
|
|
state.seen.failures[full_name] = true
|
|
table.insert(state.results.failures, full_name)
|
|
end
|
|
|
|
local location = normalize_location(payload.location)
|
|
local output_lines = output_lines_from_payload(payload)
|
|
local lines = {}
|
|
append_lines(lines, output_lines)
|
|
append_lines(lines, build_error_lines(payload.error))
|
|
if #lines > 0 then
|
|
state.outputs[full_name] = lines
|
|
end
|
|
|
|
if not location and payload.error and payload.error.stack then
|
|
location = extract_location(payload.error.stack)
|
|
end
|
|
if location then
|
|
location.text = (payload.error and payload.error.message) or "hook failure"
|
|
state.locations[full_name] = location
|
|
end
|
|
|
|
if not state.outputs[full_name] or #state.outputs[full_name] == 0 then
|
|
state.outputs[full_name] = { "failed" }
|
|
end
|
|
|
|
return full_name
|
|
end
|
|
|
|
local function decode_ndjson_payload(line)
|
|
if not line or line == "" then
|
|
return nil
|
|
end
|
|
local trimmed = vim.trim(line)
|
|
if trimmed == "" then
|
|
return nil
|
|
end
|
|
local ok, payload = pcall(vim.json.decode, trimmed)
|
|
if ok and type(payload) == "table" then
|
|
return payload
|
|
end
|
|
local start = trimmed:find("{", 1, true)
|
|
if not start then
|
|
return nil
|
|
end
|
|
local last = nil
|
|
for i = #trimmed, 1, -1 do
|
|
if trimmed:sub(i, i) == "}" then
|
|
last = i
|
|
break
|
|
end
|
|
end
|
|
if not last or last <= start then
|
|
return nil
|
|
end
|
|
local candidate = trimmed:sub(start, last)
|
|
ok, payload = pcall(vim.json.decode, candidate)
|
|
if ok and type(payload) == "table" then
|
|
return payload
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function parse_ndjson_line(line, state)
|
|
debug_log_line("on_line_raw", line)
|
|
local payload = decode_ndjson_payload(line)
|
|
if not payload then
|
|
return nil
|
|
end
|
|
if payload.event == "test" then
|
|
local name = record_ndjson_result(state, payload.status, payload)
|
|
if name then
|
|
return { event = payload.status, name = name }
|
|
end
|
|
elseif payload.event == "hook" and payload.status == "failed" then
|
|
local name = record_hook_failure(state, payload)
|
|
if name then
|
|
return { event = "failed", name = name }
|
|
end
|
|
elseif payload.event == "suite" then
|
|
if payload.status == "skipped" or payload.status == "pending" then
|
|
local name = record_ndjson_result(state, payload.status, payload)
|
|
if name then
|
|
return { event = payload.status, name = name }
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function parse_ndjson_output(output)
|
|
local state = ensure_state({})
|
|
for line in output:gmatch("[^\n]+") do
|
|
parse_ndjson_line(line, state)
|
|
end
|
|
return state
|
|
end
|
|
|
|
local function project_root_for_buf(bufnr)
|
|
local file = vim.api.nvim_buf_get_name(bufnr)
|
|
if file == "" then
|
|
return nil
|
|
end
|
|
return find_project_root(file)
|
|
end
|
|
|
|
function runner.is_test_file(bufnr)
|
|
local file = vim.api.nvim_buf_get_name(bufnr)
|
|
if file == "" then
|
|
return false
|
|
end
|
|
local root = find_project_root(file)
|
|
if not root then
|
|
return false
|
|
end
|
|
local pkg = load_package_json(root)
|
|
return has_mocha_dependency(pkg)
|
|
end
|
|
|
|
function runner.find_nearest(bufnr, row, _col)
|
|
local file = vim.api.nvim_buf_get_name(bufnr)
|
|
if file == "" then
|
|
return nil, "buffer has no name"
|
|
end
|
|
local root = find_project_root(file)
|
|
if not root then
|
|
return nil, "no package.json found"
|
|
end
|
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
local suites, tests = parse_buffer_blocks(lines)
|
|
local test_block = find_deepest_block_at_row(tests, row)
|
|
if test_block then
|
|
return {
|
|
file = file,
|
|
cwd = root,
|
|
test_name = test_block.title,
|
|
full_name = test_block.full_name,
|
|
mocha_full_title = test_block.mocha_full_title,
|
|
kind = "test",
|
|
}
|
|
end
|
|
|
|
local suite_block = find_deepest_block_at_row(suites, row)
|
|
if suite_block then
|
|
return {
|
|
file = file,
|
|
cwd = root,
|
|
test_name = suite_block.title,
|
|
full_name = suite_block.full_name,
|
|
mocha_full_title = suite_block.mocha_full_title,
|
|
kind = "suite",
|
|
}
|
|
end
|
|
|
|
return {
|
|
file = file,
|
|
cwd = root,
|
|
kind = "file",
|
|
}
|
|
end
|
|
|
|
function runner.build_command(spec)
|
|
if not spec.cwd or spec.cwd == "" then
|
|
return { cmd = { "echo", "no package.json found" } }
|
|
end
|
|
if spec.kind == "file" then
|
|
return runner.build_file_command(spec.file)
|
|
end
|
|
|
|
local grep = spec.mocha_full_title or spec.full_name or spec.test_name
|
|
return {
|
|
cmd = {
|
|
"npx",
|
|
"mocha",
|
|
"--ui",
|
|
"bdd-with-location",
|
|
"--require",
|
|
RUNNER_PATHS.ui,
|
|
"--reporter",
|
|
RUNNER_PATHS.reporter,
|
|
"--grep",
|
|
build_grep_pattern(grep),
|
|
spec.file,
|
|
},
|
|
cwd = spec.cwd,
|
|
}
|
|
end
|
|
|
|
function runner.build_file_command(bufnr_or_file)
|
|
local file = bufnr_or_file
|
|
if type(bufnr_or_file) == "number" then
|
|
file = vim.api.nvim_buf_get_name(bufnr_or_file)
|
|
end
|
|
local cwd = file and find_project_root(file) or nil
|
|
if not cwd then
|
|
return { cmd = { "echo", "no package.json found" } }
|
|
end
|
|
return {
|
|
cmd = {
|
|
"npx",
|
|
"mocha",
|
|
"--ui",
|
|
"bdd-with-location",
|
|
"--require",
|
|
RUNNER_PATHS.ui,
|
|
"--reporter",
|
|
RUNNER_PATHS.reporter,
|
|
file,
|
|
},
|
|
cwd = cwd,
|
|
}
|
|
end
|
|
|
|
function runner.build_all_command(bufnr)
|
|
local cwd = project_root_for_buf(bufnr)
|
|
if not cwd then
|
|
return { cmd = { "echo", "no package.json found" } }
|
|
end
|
|
local glob = config and config.all_test_glob or default_config.all_test_glob
|
|
return {
|
|
cmd = {
|
|
"npx",
|
|
"mocha",
|
|
"--ui",
|
|
"bdd-with-location",
|
|
"--require",
|
|
RUNNER_PATHS.ui,
|
|
"--reporter",
|
|
RUNNER_PATHS.reporter,
|
|
glob,
|
|
},
|
|
cwd = cwd,
|
|
}
|
|
end
|
|
|
|
function runner.build_failed_command(last_command, failures, _scope_kind)
|
|
local cwd = last_command and last_command.cwd or nil
|
|
if not failures or #failures == 0 then
|
|
return {
|
|
cmd = {
|
|
"npx",
|
|
"mocha",
|
|
"--ui",
|
|
"bdd-with-location",
|
|
"--require",
|
|
RUNNER_PATHS.ui,
|
|
"--reporter",
|
|
RUNNER_PATHS.reporter,
|
|
},
|
|
cwd = cwd,
|
|
}
|
|
end
|
|
|
|
local patterns = {}
|
|
for _, failure in ipairs(failures) do
|
|
local mocha_title = runner._last_mocha_titles and runner._last_mocha_titles[failure]
|
|
if not mocha_title then
|
|
mocha_title = failure:gsub("/", " ")
|
|
end
|
|
table.insert(patterns, build_grep_pattern(mocha_title))
|
|
end
|
|
|
|
return {
|
|
cmd = {
|
|
"npx",
|
|
"mocha",
|
|
"--ui",
|
|
"bdd-with-location",
|
|
"--require",
|
|
RUNNER_PATHS.ui,
|
|
"--reporter",
|
|
RUNNER_PATHS.reporter,
|
|
"--grep",
|
|
table.concat(patterns, "|"),
|
|
},
|
|
cwd = cwd,
|
|
}
|
|
end
|
|
|
|
function runner.parse_results(output)
|
|
local state = parse_ndjson_output(output)
|
|
runner._last_locations = state.locations
|
|
runner._last_mocha_titles = state.mocha_titles
|
|
return state.results
|
|
end
|
|
|
|
function runner.output_parser()
|
|
return {
|
|
on_line = function(line, state)
|
|
state = ensure_state(state or {})
|
|
local info = parse_ndjson_line(line, state)
|
|
runner._last_locations = state.locations
|
|
runner._last_mocha_titles = state.mocha_titles
|
|
if not info or not info.event or not info.name then
|
|
return nil
|
|
end
|
|
local results = { passes = {}, failures = {}, skips = {} }
|
|
if info.event == "passed" then
|
|
results.passes = { info.name }
|
|
elseif info.event == "failed" then
|
|
results.failures = { info.name }
|
|
results.failures_all = vim.deepcopy(state.results.failures)
|
|
elseif info.event == "pending" or info.event == "skipped" then
|
|
results.skips = { info.name }
|
|
end
|
|
return results
|
|
end,
|
|
on_complete = function(output, state)
|
|
debug_log_line("on_complete_raw", output)
|
|
state = ensure_state(state or {})
|
|
local parsed = parse_ndjson_output(output)
|
|
runner._last_locations = parsed.locations
|
|
runner._last_mocha_titles = parsed.mocha_titles
|
|
if #state.results.passes > 0 or #state.results.failures > 0 or #state.results.skips > 0 then
|
|
return nil
|
|
end
|
|
return parsed.results
|
|
end,
|
|
}
|
|
end
|
|
|
|
function runner.parse_test_output(output)
|
|
local state = parse_ndjson_output(output)
|
|
return state.outputs
|
|
end
|
|
|
|
function runner.collect_failed_locations(failures, _command, _scope_kind)
|
|
local items = {}
|
|
if not failures or not runner._last_locations then
|
|
return items
|
|
end
|
|
|
|
for _, name in ipairs(failures) do
|
|
local location = runner._last_locations[name]
|
|
if location then
|
|
table.insert(items, {
|
|
filename = location.filename,
|
|
lnum = location.lnum,
|
|
col = location.col,
|
|
text = location.text or name,
|
|
})
|
|
end
|
|
end
|
|
return items
|
|
end
|
|
|
|
return runner
|