in packages/bun-internal-test/runners/bun/runner.ts [275:552]
export function parseTest(stderr: string, options: ParseTestOptions = {}): ParseTestResult {
const { cwd, knownPaths } = options;
const linesAnsi = stderr.split("\n");
const lines = linesAnsi.map(stripAnsi);
let info: TestInfo | undefined;
const parseInfo = (line: string): TestInfo | undefined => {
const match = /^(bun (?:wip)?test) v([0-9\.]+) \(([0-9a-z]+)\)$/.exec(line);
if (!match) {
return undefined;
}
const [, name, version, sha] = match;
return {
name,
version,
revision: sha,
};
};
let files: TestFile[] = [];
let file: TestFile | undefined;
const parseFile = (line: string): TestFile | undefined => {
let file = line.slice(0, -1);
if (!isJavaScript(file) || !line.endsWith(":")) {
return undefined;
}
for (const path of knownPaths ?? []) {
if (path.endsWith(file)) {
file = path;
break;
}
}
return {
file,
tests: [],
status: "pass",
summary: {
files: 1,
tests: 0,
pass: 0,
fail: 0,
skip: 0,
todo: 0,
duration: 0,
},
};
};
const parseTestLine = (line: string): Test | undefined => {
const match = /^(✓|‚úì|✗|‚úó|»|-|✎) (.*)$/.exec(line);
if (!match) {
return undefined;
}
const [, icon, name] = match;
let status: TestStatus = "fail";
switch (icon) {
case "✓":
case "‚úì":
status = "pass";
break;
case "✗":
case "‚úó":
status = "fail";
break;
case "»":
case "-":
status = "skip";
break;
case "✎":
status = "todo";
break;
}
const match2 = /^(.*) \[([0-9]+\.[0-9]+)(m?s)\]$/.exec(name);
if (!match2) {
return {
name,
status,
duration: 0,
};
}
const [, title, duration, unit] = match2;
return {
name: title,
status,
duration: parseFloat(duration ?? "0") * (unit === "ms" ? 1000 : 1) || 0,
};
};
let errors: TestError[] = [];
let error: TestError | undefined;
const parseError = (line: string): TestError | undefined => {
const match = /^(.*error|timeout)\: (.*)$/i.exec(line);
if (!match) {
return undefined;
}
const [, name, message] = match;
return {
name: name === "error" ? "Error" : name,
message,
};
};
const parseErrorStack = (line: string): TestErrorStack | undefined => {
let match = /^\s*at (.*) \((.*)\:([0-9]+)\:([0-9]+)\)$/.exec(line);
if (!match) {
match = /^\s*at (.*)\:([0-9]+)\:([0-9]+)$/.exec(line);
if (!match) {
return undefined;
}
}
const [columnNo, lineNo, path, func] = match.reverse();
let file = path;
if (cwd && path.startsWith(cwd)) {
file = path.slice(cwd.length);
if (file.startsWith("/")) {
file = file.slice(1);
}
}
return {
file,
function: func !== line ? func : undefined,
line: parseInt(lineNo),
column: parseInt(columnNo),
};
};
const parseErrorPreview = (line: string): string | undefined => {
if (line.endsWith("^") || /^[0-9]+ \| /.test(line)) {
return line;
}
return undefined;
};
let summary: TestSummary | undefined;
const parseSummary = (line: string): TestSummary | undefined => {
const match = /^Ran ([0-9]+) tests across ([0-9]+) files\. .* \[([0-9]+\.[0-9]+)(m?s)\]$/.exec(line);
if (!match) {
return undefined;
}
const [, tests, files, duration, unit] = match;
return {
pass: 0,
fail: 0,
skip: 0,
todo: 0,
tests: parseInt(tests),
files: parseInt(files),
duration: parseFloat(duration) * (unit === "s" ? 1000 : 1),
};
};
const createSummary = (files: TestFile[]): TestSummary => {
const summary = {
pass: 0,
fail: 0,
skip: 0,
todo: 0,
tests: 0,
files: 0,
duration: 0,
};
for (const file of files) {
summary.files++;
summary.duration += file.summary.duration;
for (const test of file.tests) {
summary.tests++;
summary[test.status]++;
}
if (file.errors?.length) {
summary.fail++;
}
}
return summary;
};
const parseSkip = (line: string): number => {
const match = /^([0-9]+) tests (?:skipped|failed|todo)\:$/.exec(line);
if (match) {
return parseInt(match[1]);
}
return 0;
};
const endOfFile = (file?: TestFile): void => {
if (file && !file.tests.length && errors.length) {
file.errors = errors;
errors = [];
}
};
let errorStart = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!info && !(info = parseInfo(line))) {
continue;
}
const newFile = parseFile(line);
if (newFile) {
endOfFile(file);
files.push((file = newFile));
continue;
}
const newError = parseError(line);
if (newError) {
errorStart = i;
errors.push((error = newError));
for (let j = 1; j < 8 && i - j >= 0; j++) {
const line = lines[i - j];
const preview = parseErrorPreview(line);
if (!preview) {
break;
}
if (error.preview) {
error.preview = preview + "\n" + error.preview;
} else {
error.preview = preview;
}
}
continue;
}
const newStack = parseErrorStack(line);
if (newStack) {
if (error) {
error.stack ||= [];
error.stack.push(newStack);
for (let j = errorStart + 1; j < i && error.stack.length === 1; j++) {
error.message += "\n" + lines[j];
}
} else {
// TODO: newStack and !error
}
continue;
}
const newTest = parseTestLine(line);
if (newTest) {
if (error && newTest.status === "skip") {
continue; // Likely a false positive from error message
}
if (error) {
for (let j = errorStart + 1; j < i - 1 && !error.stack?.length; j++) {
error.message += "\n" + lines[j];
}
error = undefined;
}
if (errors.length) {
newTest.errors = errors;
errors = [];
}
file!.tests.push(newTest);
continue;
}
const newSummary = parseSummary(line);
if (newSummary) {
summary = newSummary;
break;
}
i += parseSkip(line);
}
endOfFile(file);
if (!info) {
throw new Error("No tests found; did the test runner crash?");
}
summary ||= createSummary(files);
const count = (status: TestStatus): number => {
return files.reduce((n, file) => n + file.tests.filter(test => test.status === status).length, 0);
};
summary.pass ||= count("pass");
summary.fail ||= count("fail");
summary.skip ||= count("skip");
summary.todo ||= count("todo");
const getStatus = (summary: TestSummary) => {
return summary.fail ? "fail" : !summary.pass && summary.skip ? "skip" : "pass";
};
if (files.length === 1) {
files[0].summary = { ...summary };
files[0].status = getStatus(summary);
} else {
for (const file of files) {
const summary = createSummary([file]);
file.summary = summary;
file.status = getStatus(summary);
}
}
return {
info,
files,
summary,
};
}