fix TSamFailedOnly and the output formatting for the jest-runner

This commit is contained in:
2025-12-25 18:53:31 +01:00
parent 20b8cf8009
commit a6e51f280f
10 changed files with 617 additions and 106 deletions

View File

@@ -241,21 +241,28 @@ end
function runner.parse_results(output)
if not output or output == "" then
return { passes = {}, failures = {}, skips = {} }
return { passes = {}, failures = {}, skips = {}, display = { passes = {}, failures = {}, skips = {} } }
end
local passes = {}
local failures = {}
local skips = {}
local display = { passes = {}, failures = {}, 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)
local short = data.Test:match("([^/]+)$") or data.Test
table.insert(display.passes, short)
elseif data.Action == "fail" then
table.insert(failures, data.Test)
local short = data.Test:match("([^/]+)$") or data.Test
table.insert(display.failures, short)
elseif data.Action == "skip" then
table.insert(skips, data.Test)
local short = data.Test:match("([^/]+)$") or data.Test
table.insert(display.skips, short)
end
end
end
@@ -264,56 +271,65 @@ function runner.parse_results(output)
passes = collect_unique(passes),
failures = collect_unique(failures),
skips = collect_unique(skips),
display = display,
}
end
function runner.output_parser()
local seen_pass = {}
local seen_fail = {}
local failures = {}
local passes = {}
local skips = {}
function runner.output_parser()
local seen_pass = {}
local seen_fail = {}
local failures = {}
local passes = {}
local skips = {}
local display = { passes = {}, failures = {}, skips = {} }
return {
on_line = function(line, _state)
local ok, data = pcall(vim.json.decode, line)
if not ok or type(data) ~= "table" then
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
local short = name:match("([^/]+)$") or name
if data.Action == "pass" and not seen_pass[name] then
seen_pass[name] = true
table.insert(passes, name)
table.insert(display.passes, short)
return {
passes = { name },
failures = {},
skips = {},
display = { passes = { short }, 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)
table.insert(display.failures, short)
return {
passes = {},
failures = { name },
skips = {},
display = { passes = {}, failures = { short }, skips = {} },
failures_all = vim.deepcopy(failures),
}
elseif data.Action == "skip" and not seen_pass[name] then
seen_pass[name] = true
table.insert(skips, name)
table.insert(display.skips, short)
return {
passes = {},
failures = {},
skips = { name },
display = { passes = {}, failures = {}, skips = { short } },
failures_all = vim.deepcopy(failures),
}
end
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,
end,
on_complete = function(_output, _state)
return nil
end,

View File

@@ -4,5 +4,5 @@ return js.new({
name = "js-jest",
framework = "jest",
command = { "npx", "jest" },
json_args = { "--json" },
json_args = { "--json", "--verbose" },
})

View File

@@ -418,28 +418,123 @@ function M.new(opts)
}
end
local function parse_jest_like(output)
local ok, data = pcall(vim.json.decode, output)
if not ok or type(data) ~= "table" then
local function parse_jest_results(data)
if type(data) ~= "table" then
return nil
end
local passes = {}
local failures = {}
local skips = {}
local display = { passes = {}, failures = {}, skips = {} }
for _, result in ipairs(data.testResults or {}) do
for _, assertion in ipairs(result.assertionResults or {}) do
local title = assertion.fullName or assertion.title
if assertion.status == "passed" and title then
table.insert(passes, title)
elseif assertion.status == "failed" and title then
table.insert(failures, title)
local full = assertion.fullName or assertion.title
local short = assertion.title or assertion.fullName
if assertion.status == "passed" and full then
table.insert(passes, full)
if short then
table.insert(display.passes, short)
end
elseif assertion.status == "failed" and full then
table.insert(failures, full)
if short then
table.insert(display.failures, short)
end
elseif (assertion.status == "pending" or assertion.status == "skipped" or assertion.status == "todo")
and title then
table.insert(skips, title)
and full then
table.insert(skips, full)
if short then
table.insert(display.skips, short)
end
end
end
end
return { passes = passes, failures = failures, skips = skips }
return { passes = passes, failures = failures, skips = skips, display = display }
end
local function parse_jest_like(output)
local ok, data = pcall(vim.json.decode, output)
if ok and type(data) == "table" then
return parse_jest_results(data)
end
if not output or output == "" then
return nil
end
local start_idx = output:find("{", 1, true)
if not start_idx then
return nil
end
local end_idx = nil
for i = #output, start_idx, -1 do
if output:sub(i, i) == "}" then
end_idx = i
break
end
end
if not end_idx then
return nil
end
local candidate = output:sub(start_idx, end_idx)
local ok2, data2 = pcall(vim.json.decode, candidate)
if not ok2 or type(data2) ~= "table" then
return nil
end
return parse_jest_results(data2)
end
local jest_symbols = {
pass = string.char(0xE2, 0x9C, 0x93),
fail = string.char(0xE2, 0x9C, 0x95),
skip = string.char(0xE2, 0x97, 0x8B),
}
local function shorten_js_name(name)
if not name or name == "" then
return nil
end
local last = name:match(".*>%s*([^>]+)$")
if last then
return last:gsub("^%s+", ""):gsub("%s+$", "")
end
last = name:match(".*%s*([^]+)$")
if last then
return last:gsub("^%s+", ""):gsub("%s+$", "")
end
last = name:match(".*»%s*([^»]+)$")
if last then
return last:gsub("^%s+", ""):gsub("%s+$", "")
end
return name
end
local function trim_jest_verbose_name(name)
if not name then
return nil
end
name = name:gsub("%s+%(%d+%.?%d*%s*ms%)%s*$", "")
name = name:gsub("%s+%(%d+%.?%d*%s*sec%)%s*$", "")
name = name:gsub("%s+%b()%s*$", "")
name = name:gsub("%s+$", "")
return name
end
local function parse_jest_verbose_line(line)
if not line or line == "" then
return nil, nil
end
local name = line:match("^%s*" .. jest_symbols.pass .. "%s+(.+)$")
if name then
return "pass", trim_jest_verbose_name(name)
end
name = line:match("^%s*" .. jest_symbols.fail .. "%s+(.+)$")
if name then
return "fail", trim_jest_verbose_name(name)
end
name = line:match("^%s*" .. jest_symbols.skip .. "%s+(.+)$")
if name then
return "skip", trim_jest_verbose_name(name)
end
return nil, nil
end
local function parse_mocha(output)
@@ -448,6 +543,7 @@ function M.new(opts)
local passes = {}
local failures = {}
local skips = {}
local display = { passes = {}, failures = {}, skips = {} }
for line in (output or ""):gmatch("[^\n]+") do
local ok_line, entry = pcall(vim.json.decode, line)
if ok_line and type(entry) == "table" then
@@ -456,56 +552,89 @@ function M.new(opts)
if not entry.event then
payload = entry[2] or entry["2"] or {}
end
local title = payload.fullTitle or payload.title
if event == "pass" and title then
table.insert(passes, title)
elseif event == "fail" and title then
table.insert(failures, title)
elseif event == "pending" and title then
table.insert(skips, title)
local full = payload.fullTitle or payload.title
local short = payload.title or payload.fullTitle
if event == "pass" and full then
table.insert(passes, full)
if short then
table.insert(display.passes, short)
end
elseif event == "fail" and full then
table.insert(failures, full)
if short then
table.insert(display.failures, short)
end
elseif event == "pending" and full then
table.insert(skips, full)
if short then
table.insert(display.skips, short)
end
end
end
end
if #passes == 0 and #failures == 0 and #skips == 0 then
return nil
end
return { passes = passes, failures = failures, skips = skips }
return { passes = passes, failures = failures, skips = skips, display = display }
end
local passes = {}
local failures = {}
local skips = {}
local display = { passes = {}, failures = {}, skips = {} }
if type(data.tests) == "table" then
for _, test in ipairs(data.tests) do
local title = test.fullTitle or test.title
if test.state == "passed" and title then
table.insert(passes, title)
elseif test.state == "failed" and title then
table.insert(failures, title)
elseif test.state == "pending" and title then
table.insert(skips, title)
local full = test.fullTitle or test.title
local short = test.title or test.fullTitle
if test.state == "passed" and full then
table.insert(passes, full)
if short then
table.insert(display.passes, short)
end
elseif test.state == "failed" and full then
table.insert(failures, full)
if short then
table.insert(display.failures, short)
end
elseif test.state == "pending" and full then
table.insert(skips, full)
if short then
table.insert(display.skips, short)
end
end
end
elseif type(data.passes) == "table" or type(data.failures) == "table" then
for _, test in ipairs(data.passes or {}) do
local title = test.fullTitle or test.title
if title then
table.insert(passes, title)
local full = test.fullTitle or test.title
local short = test.title or test.fullTitle
if full then
table.insert(passes, full)
if short then
table.insert(display.passes, short)
end
end
end
for _, test in ipairs(data.failures or {}) do
local title = test.fullTitle or test.title
if title then
table.insert(failures, title)
local full = test.fullTitle or test.title
local short = test.title or test.fullTitle
if full then
table.insert(failures, full)
if short then
table.insert(display.failures, short)
end
end
end
for _, test in ipairs(data.pending or {}) do
local title = test.fullTitle or test.title
if title then
table.insert(skips, title)
local full = test.fullTitle or test.title
local short = test.title or test.fullTitle
if full then
table.insert(skips, full)
if short then
table.insert(display.skips, short)
end
end
end
end
return { passes = passes, failures = failures, skips = skips }
return { passes = passes, failures = failures, skips = skips, display = display }
end
local function parse_output(output)
@@ -523,11 +652,137 @@ function M.new(opts)
local state = { raw = {}, done = false, saw_stream = false }
local failures = {}
local skips = {}
local jest_seen_pass = {}
local jest_seen_fail = {}
local jest_seen_skip = {}
local jest_json_collecting = false
local jest_json_buffer = {}
local jest_streamed = false
local function reset_list(dst, src)
for i = #dst, 1, -1 do
dst[i] = nil
end
for _, item in ipairs(src or {}) do
if item and item ~= "" then
table.insert(dst, item)
end
end
end
local function emit_jest_results(results, emit_output)
if not results then
return nil
end
local display = results.display or {}
if emit_output == false then
reset_list(failures, results.failures or {})
return {
passes = {},
failures = {},
skips = {},
failures_all = vim.deepcopy(failures),
}
end
local out = {
passes = {},
failures = {},
skips = {},
failures_all = {},
display = { passes = {}, failures = {}, skips = {} },
}
for i, title in ipairs(results.passes or {}) do
if title and not jest_seen_pass[title] then
jest_seen_pass[title] = true
table.insert(out.passes, title)
local show = display.passes and display.passes[i] or title
table.insert(out.display.passes, show)
end
end
for i, title in ipairs(results.failures or {}) do
if title and not jest_seen_fail[title] then
jest_seen_fail[title] = true
table.insert(failures, title)
table.insert(out.failures, title)
local show = display.failures and display.failures[i] or title
table.insert(out.display.failures, show)
end
end
for i, title in ipairs(results.skips or {}) do
if title and not jest_seen_skip[title] then
jest_seen_skip[title] = true
table.insert(out.skips, title)
local show = display.skips and display.skips[i] or title
table.insert(out.display.skips, show)
end
end
out.failures_all = vim.deepcopy(failures)
if #out.passes == 0 and #out.failures == 0 and #out.skips == 0 then
return nil
end
return out
end
local function feed_jest_json(line)
if not jest_json_collecting then
if not line:match("^%s*{") then
return nil
end
jest_json_collecting = true
jest_json_buffer = { line }
else
table.insert(jest_json_buffer, line)
end
local candidate = table.concat(jest_json_buffer, "\n")
local ok_buf, data_buf = pcall(vim.json.decode, candidate)
if ok_buf and type(data_buf) == "table" then
jest_json_collecting = false
jest_json_buffer = {}
return data_buf
end
return nil
end
local function handle_jest_json(data)
local results = parse_jest_results(data)
if not results then
return nil
end
state.done = true
state.saw_stream = true
return emit_jest_results(results, not jest_streamed)
end
return {
on_line = function(line, _state)
if state.done then
return nil
end
if runner.framework == "jest" then
local kind, name = parse_jest_verbose_line(line)
if kind and name then
jest_streamed = true
local results = { passes = {}, failures = {}, skips = {} }
if kind == "pass" then
results.passes = { name }
elseif kind == "fail" then
results.failures = { name }
elseif kind == "skip" then
results.skips = { name }
end
results.display = {
passes = kind == "pass" and { name } or {},
failures = kind == "fail" and { name } or {},
skips = kind == "skip" and { name } or {},
}
return emit_jest_results(results, true)
end
local data = feed_jest_json(line)
if data then
return handle_jest_json(data)
end
return nil
end
if runner.framework == "mocha" and runner.json_args then
local uses_stream = false
for i = 1, #runner.json_args - 1 do
@@ -551,6 +806,11 @@ function M.new(opts)
passes = { payload.fullTitle },
failures = {},
skips = {},
display = {
passes = { payload.title or payload.fullTitle },
failures = {},
skips = {},
},
failures_all = vim.deepcopy(failures),
}
elseif event == "fail" and payload.fullTitle then
@@ -560,6 +820,11 @@ function M.new(opts)
passes = {},
failures = { payload.fullTitle },
skips = {},
display = {
passes = {},
failures = { payload.title or payload.fullTitle },
skips = {},
},
failures_all = vim.deepcopy(failures),
}
elseif event == "pending" and payload.fullTitle then
@@ -569,6 +834,11 @@ function M.new(opts)
passes = {},
failures = {},
skips = { payload.fullTitle },
display = {
passes = {},
failures = {},
skips = { payload.title or payload.fullTitle },
},
failures_all = vim.deepcopy(failures),
}
elseif event == "start" or event == "end" then
@@ -599,29 +869,48 @@ function M.new(opts)
if skip_title then
skip_title = skip_title:gsub("%s+$", "")
table.insert(skips, skip_title)
state.saw_stream = true
return {
passes = {},
failures = {},
skips = { skip_title },
display = {
passes = {},
failures = {},
skips = { shorten_js_name(skip_title) or skip_title },
},
failures_all = vim.deepcopy(failures),
}
end
ok_title = ok_title:gsub("%s+#%s+time=.*$", "")
state.saw_stream = true
return {
passes = { ok_title },
failures = {},
skips = {},
display = {
passes = { shorten_js_name(ok_title) or ok_title },
failures = {},
skips = {},
},
failures_all = vim.deepcopy(failures),
}
end
local fail_title = line:match("^not ok%s+%d+%s+%-%s+(.+)")
if fail_title then
fail_title = fail_title:gsub("%s+#%s+time=.*$", "")
local display_name = shorten_js_name(fail_title)
table.insert(failures, fail_title)
state.saw_stream = true
return {
passes = {},
failures = { fail_title },
skips = {},
display = {
passes = {},
failures = { display_name or fail_title },
skips = {},
},
failures_all = vim.deepcopy(failures),
}
end
@@ -637,6 +926,18 @@ function M.new(opts)
return results
end,
on_complete = function(output, _state)
if runner.framework == "jest" then
if state.done then
return nil
end
local results = parse_jest_like(output)
if results then
state.done = true
state.saw_stream = true
return emit_jest_results(results, not jest_streamed)
end
return nil
end
if state.done or state.saw_stream then
return nil
end