mirror of
https://github.com/oven-sh/bun
synced 2026-02-06 08:58:52 +00:00
Compare commits
3 Commits
dylan/pyth
...
nektro-pat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b059f03c74 | ||
|
|
17d279ca15 | ||
|
|
7855d792eb |
@@ -2678,6 +2678,7 @@ pub const HardcodedModule = enum {
|
||||
@"node:stream/web",
|
||||
@"node:string_decoder",
|
||||
@"node:test",
|
||||
@"node:test/reporters",
|
||||
@"node:timers",
|
||||
@"node:timers/promises",
|
||||
@"node:tls",
|
||||
@@ -2760,6 +2761,7 @@ pub const HardcodedModule = enum {
|
||||
.{ "node:net", .@"node:net" },
|
||||
.{ "node:readline", .@"node:readline" },
|
||||
.{ "node:test", .@"node:test" },
|
||||
.{ "node:test/reporters", .@"node:test/reporters" },
|
||||
.{ "node:os", .@"node:os" },
|
||||
.{ "node:path", .@"node:path" },
|
||||
.{ "node:path/posix", .@"node:path/posix" },
|
||||
@@ -2899,6 +2901,7 @@ pub const HardcodedModule = enum {
|
||||
nodeEntry("node:zlib"),
|
||||
// New Node.js builtins only resolve from the prefixed one.
|
||||
nodeEntryOnlyPrefix("node:test"),
|
||||
nodeEntryOnlyPrefix("node:test/reporters"),
|
||||
|
||||
nodeEntry("assert"),
|
||||
nodeEntry("assert/strict"),
|
||||
|
||||
@@ -151,6 +151,12 @@ static JSValue processBindingNativesReturnUndefined(VM& vm, JSObject* bindingObj
|
||||
internal/streams/transform processBindingNativesGetter PropertyCallback
|
||||
internal/streams/utils processBindingNativesGetter PropertyCallback
|
||||
internal/streams/writable processBindingNativesGetter PropertyCallback
|
||||
internal/test/reporter/dot processBindingNativesGetter PropertyCallback
|
||||
internal/test/reporter/junit processBindingNativesGetter PropertyCallback
|
||||
internal/test/reporter/lcov processBindingNativesGetter PropertyCallback
|
||||
internal/test/reporter/spec processBindingNativesGetter PropertyCallback
|
||||
internal/test/reporter/tap processBindingNativesGetter PropertyCallback
|
||||
internal/test/reporter/utils processBindingNativesGetter PropertyCallback
|
||||
internal/timers processBindingNativesGetter PropertyCallback
|
||||
internal/tls processBindingNativesGetter PropertyCallback
|
||||
internal/tty processBindingNativesGetter PropertyCallback
|
||||
|
||||
@@ -79,6 +79,7 @@ static constexpr ASCIILiteral builtinModuleNamesSortedLength[] = {
|
||||
"inspector/promises"_s,
|
||||
"_stream_passthrough"_s,
|
||||
"diagnostics_channel"_s,
|
||||
"node:test/reporters"_s,
|
||||
};
|
||||
|
||||
namespace Bun {
|
||||
|
||||
@@ -84,6 +84,27 @@ const SafeArrayIterator = createSafeIterator(ArrayPrototypeSymbolIterator, Array
|
||||
|
||||
const ArrayPrototypeMap = Array.prototype.map;
|
||||
const PromisePrototypeThen = $Promise.prototype.$then;
|
||||
const RegExpPrototype = RegExp.prototype;
|
||||
const SymbolMatch = Symbol.match;
|
||||
const SymbolMatchAll = Symbol.matchAll;
|
||||
const SymbolReplace = Symbol.replace;
|
||||
const SymbolSearch = Symbol.search;
|
||||
const SymbolSplit = Symbol.split;
|
||||
const RegExpPrototypeSymbolMatch = RegExpPrototype[SymbolMatch];
|
||||
const RegExpPrototypeSymbolMatchAll = RegExpPrototype[SymbolMatchAll];
|
||||
const RegExpPrototypeSymbolReplace = RegExpPrototype[SymbolReplace];
|
||||
const RegExpPrototypeSymbolSearch = RegExpPrototype[SymbolSearch];
|
||||
const RegExpPrototypeSymbolSplit = RegExpPrototype[SymbolSplit];
|
||||
const RegExpPrototypeExec = RegExpPrototype.exec;
|
||||
const RegExpPrototypeGetDotAll = getGetter(RegExp, "dotAll");
|
||||
const RegExpPrototypeGetGlobal = getGetter(RegExp, "global");
|
||||
const RegExpPrototypeGetHasIndices = getGetter(RegExp, "hasIndices");
|
||||
const RegExpPrototypeGetIgnoreCase = getGetter(RegExp, "ignoreCase");
|
||||
const RegExpPrototypeGetMultiline = getGetter(RegExp, "multiline");
|
||||
const RegExpPrototypeGetSource = getGetter(RegExp, "source");
|
||||
const RegExpPrototypeGetSticky = getGetter(RegExp, "sticky");
|
||||
const RegExpPrototypeGetUnicode = getGetter(RegExp, "unicode");
|
||||
const RegExpPrototypeGetFlags = getGetter(RegExp, "flags");
|
||||
|
||||
const arrayToSafePromiseIterable = (promises, mapFn) =>
|
||||
new SafeArrayIterator(
|
||||
@@ -118,6 +139,113 @@ const SafePromiseAllReturnArrayLike = (promises, mapFn) =>
|
||||
}
|
||||
});
|
||||
|
||||
class RegExpLikeForStringSplitting {
|
||||
#regex;
|
||||
constructor() {
|
||||
this.#regex = ReflectConstruct(RegExp, arguments);
|
||||
}
|
||||
|
||||
get lastIndex() {
|
||||
return ReflectGet(this.#regex, "lastIndex");
|
||||
}
|
||||
set lastIndex(value) {
|
||||
ReflectSet(this.#regex, "lastIndex", value);
|
||||
}
|
||||
|
||||
exec() {
|
||||
return ReflectApply(RegExpPrototypeExec, this.#regex, arguments);
|
||||
}
|
||||
}
|
||||
ObjectSetPrototypeOf(RegExpLikeForStringSplitting.prototype, null);
|
||||
|
||||
function hardenRegExp(pattern) {
|
||||
ObjectDefineProperties(pattern, {
|
||||
[SymbolMatch]: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: RegExpPrototypeSymbolMatch,
|
||||
},
|
||||
[SymbolMatchAll]: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: RegExpPrototypeSymbolMatchAll,
|
||||
},
|
||||
[SymbolReplace]: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: RegExpPrototypeSymbolReplace,
|
||||
},
|
||||
[SymbolSearch]: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: RegExpPrototypeSymbolSearch,
|
||||
},
|
||||
[SymbolSplit]: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: RegExpPrototypeSymbolSplit,
|
||||
},
|
||||
constructor: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: {
|
||||
[SymbolSpecies]: RegExpLikeForStringSplitting,
|
||||
},
|
||||
},
|
||||
dotAll: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: RegExpPrototypeGetDotAll(pattern),
|
||||
},
|
||||
exec: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: OriginalRegExpPrototypeExec,
|
||||
},
|
||||
global: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: RegExpPrototypeGetGlobal(pattern),
|
||||
},
|
||||
hasIndices: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: RegExpPrototypeGetHasIndices(pattern),
|
||||
},
|
||||
ignoreCase: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: RegExpPrototypeGetIgnoreCase(pattern),
|
||||
},
|
||||
multiline: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: RegExpPrototypeGetMultiline(pattern),
|
||||
},
|
||||
source: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: RegExpPrototypeGetSource(pattern),
|
||||
},
|
||||
sticky: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: RegExpPrototypeGetSticky(pattern),
|
||||
},
|
||||
unicode: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: RegExpPrototypeGetUnicode(pattern),
|
||||
},
|
||||
});
|
||||
ObjectDefineProperty(pattern, "flags", {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
value: RegExpPrototypeGetFlags(pattern),
|
||||
});
|
||||
return pattern;
|
||||
}
|
||||
|
||||
export default {
|
||||
Array,
|
||||
SafeArrayIterator,
|
||||
@@ -177,4 +305,5 @@ export default {
|
||||
BigUint64Array,
|
||||
BigInt64Array,
|
||||
uncurryThis,
|
||||
hardenRegExp,
|
||||
};
|
||||
|
||||
40
src/js/internal/test/reporter/dot.ts
Normal file
40
src/js/internal/test/reporter/dot.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
const colors = require("internal/util/colors");
|
||||
const { formatTestReport } = require("internal/test/reporter/utils");
|
||||
|
||||
const ArrayPrototypePush = Array.prototype.push;
|
||||
const MathMax = Math.max;
|
||||
|
||||
async function* dot(source) {
|
||||
let count = 0;
|
||||
let columns = getLineLength();
|
||||
const failedTests = [];
|
||||
for await (const { type, data } of source) {
|
||||
if (type === "test:pass") {
|
||||
yield `${colors.green}.${colors.reset}`;
|
||||
}
|
||||
if (type === "test:fail") {
|
||||
yield `${colors.red}X${colors.reset}`;
|
||||
ArrayPrototypePush.$apply(failedTests, data);
|
||||
}
|
||||
if ((type === "test:fail" || type === "test:pass") && ++count === columns) {
|
||||
yield "\n";
|
||||
|
||||
// Getting again in case the terminal was resized.
|
||||
columns = getLineLength();
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
yield "\n";
|
||||
if (failedTests.length > 0) {
|
||||
yield `\n${colors.red}Failed tests:${colors.white}\n\n`;
|
||||
for (const test of failedTests) {
|
||||
yield formatTestReport("test:fail", test);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLineLength() {
|
||||
return MathMax(process.stdout.columns ?? 20, 20);
|
||||
}
|
||||
|
||||
export default dot;
|
||||
170
src/js/internal/test/reporter/junit.ts
Normal file
170
src/js/internal/test/reporter/junit.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
const { inspectWithNoCustomRetry } = require("internal/test/reporter/utils");
|
||||
const { hostname } = require("node:os");
|
||||
|
||||
const ArrayPrototypeFilter = Array.prototype.filter;
|
||||
const ArrayPrototypeJoin = Array.prototype.join;
|
||||
const ArrayPrototypeMap = Array.prototype.map;
|
||||
const ArrayPrototypePush = Array.prototype.push;
|
||||
const ArrayPrototypeSome = Array.prototype.some;
|
||||
const NumberPrototypeToFixed = Number.prototype.toFixed;
|
||||
const ObjectEntries = Object.entries;
|
||||
const RegExpPrototypeSymbolReplace = RegExp.prototype[Symbol.replace];
|
||||
const StringPrototypeRepeat = String.prototype.repeat;
|
||||
|
||||
const inspectOptions = { __proto__: null, colors: false, breakLength: Infinity };
|
||||
const HOSTNAME = hostname();
|
||||
|
||||
function escapeAttribute(s = "") {
|
||||
return escapeContent(
|
||||
RegExpPrototypeSymbolReplace.$apply(/"/g, RegExpPrototypeSymbolReplace.$apply(/\n/g, s, ""), """),
|
||||
);
|
||||
}
|
||||
|
||||
function escapeContent(s = "") {
|
||||
return RegExpPrototypeSymbolReplace.$apply(/</g, RegExpPrototypeSymbolReplace.$apply(/&/g, s, "&"), "<");
|
||||
}
|
||||
|
||||
function escapeComment(s = "") {
|
||||
return RegExpPrototypeSymbolReplace.$apply(/--/g, s, "--");
|
||||
}
|
||||
|
||||
function treeToXML(tree) {
|
||||
if (typeof tree === "string") {
|
||||
return `${escapeContent(tree)}\n`;
|
||||
}
|
||||
const { tag, attrs, nesting, children, comment } = tree;
|
||||
const indent = StringPrototypeRepeat.$apply("\t", nesting + 1);
|
||||
if (comment) {
|
||||
return `${indent}<!-- ${escapeComment(comment)} -->\n`;
|
||||
}
|
||||
const attrsString = ArrayPrototypeJoin.$apply(
|
||||
ArrayPrototypeMap.$apply(
|
||||
ObjectEntries(attrs),
|
||||
({ 0: key, 1: value }) => `${key}="${escapeAttribute(String(value))}"`,
|
||||
),
|
||||
" ",
|
||||
);
|
||||
if (!children?.length) {
|
||||
return `${indent}<${tag} ${attrsString}/>\n`;
|
||||
}
|
||||
const childrenString = ArrayPrototypeJoin.$apply(ArrayPrototypeMap.$apply(children ?? [], treeToXML), "");
|
||||
return `${indent}<${tag} ${attrsString}>\n${childrenString}${indent}</${tag}>\n`;
|
||||
}
|
||||
|
||||
function isFailure(node) {
|
||||
return (
|
||||
(node?.children && ArrayPrototypeSome.$apply(node.children, c => c.tag === "failure")) || node?.attrs?.failures
|
||||
);
|
||||
}
|
||||
|
||||
function isSkipped(node) {
|
||||
return (
|
||||
(node?.children && ArrayPrototypeSome.$apply(node.children, c => c.tag === "skipped")) || node?.attrs?.failures
|
||||
);
|
||||
}
|
||||
|
||||
async function* junitReporter(source) {
|
||||
yield '<?xml version="1.0" encoding="utf-8"?>\n';
|
||||
yield "<testsuites>\n";
|
||||
let currentSuite = null;
|
||||
const roots = [];
|
||||
|
||||
function startTest(event) {
|
||||
const originalSuite = currentSuite;
|
||||
currentSuite = {
|
||||
__proto__: null,
|
||||
attrs: { __proto__: null, name: event.data.name },
|
||||
nesting: event.data.nesting,
|
||||
parent: currentSuite,
|
||||
children: [],
|
||||
};
|
||||
if (originalSuite?.children) {
|
||||
ArrayPrototypePush.$apply(originalSuite.children, currentSuite);
|
||||
}
|
||||
if (!currentSuite.parent) {
|
||||
ArrayPrototypePush.$apply(roots, currentSuite);
|
||||
}
|
||||
}
|
||||
|
||||
for await (const event of source) {
|
||||
switch (event.type) {
|
||||
case "test:start": {
|
||||
startTest(event);
|
||||
break;
|
||||
}
|
||||
case "test:pass":
|
||||
case "test:fail": {
|
||||
if (!currentSuite) {
|
||||
startTest({ __proto__: null, data: { __proto__: null, name: "root", nesting: 0 } });
|
||||
}
|
||||
if (currentSuite.attrs.name !== event.data.name || currentSuite.nesting !== event.data.nesting) {
|
||||
startTest(event);
|
||||
}
|
||||
const currentTest = currentSuite;
|
||||
if (currentSuite?.nesting === event.data.nesting) {
|
||||
currentSuite = currentSuite.parent;
|
||||
}
|
||||
currentTest.attrs.time = NumberPrototypeToFixed.$apply(event.data.details.duration_ms / 1000, 6);
|
||||
const nonCommentChildren = ArrayPrototypeFilter.$apply(currentTest.children, c => c.comment == null);
|
||||
if (nonCommentChildren.length > 0) {
|
||||
currentTest.tag = "testsuite";
|
||||
currentTest.attrs.disabled = 0;
|
||||
currentTest.attrs.errors = 0;
|
||||
currentTest.attrs.tests = nonCommentChildren.length;
|
||||
currentTest.attrs.failures = ArrayPrototypeFilter.$apply(currentTest.children, isFailure).length;
|
||||
currentTest.attrs.skipped = ArrayPrototypeFilter.$apply(currentTest.children, isSkipped).length;
|
||||
currentTest.attrs.hostname = HOSTNAME;
|
||||
} else {
|
||||
currentTest.tag = "testcase";
|
||||
currentTest.attrs.classname = event.data.classname ?? "test";
|
||||
if (event.data.skip) {
|
||||
ArrayPrototypePush.$apply(currentTest.children, {
|
||||
__proto__: null,
|
||||
nesting: event.data.nesting + 1,
|
||||
tag: "skipped",
|
||||
attrs: { __proto__: null, type: "skipped", message: event.data.skip },
|
||||
});
|
||||
}
|
||||
if (event.data.todo) {
|
||||
ArrayPrototypePush.$apply(currentTest.children, {
|
||||
__proto__: null,
|
||||
nesting: event.data.nesting + 1,
|
||||
tag: "skipped",
|
||||
attrs: { __proto__: null, type: "todo", message: event.data.todo },
|
||||
});
|
||||
}
|
||||
if (event.type === "test:fail") {
|
||||
const error = event.data.details?.error;
|
||||
currentTest.children.push({
|
||||
__proto__: null,
|
||||
nesting: event.data.nesting + 1,
|
||||
tag: "failure",
|
||||
attrs: { __proto__: null, type: error?.failureType || error?.code, message: error?.message ?? "" },
|
||||
children: [inspectWithNoCustomRetry(error, inspectOptions)],
|
||||
});
|
||||
currentTest.failures = 1;
|
||||
currentTest.attrs.failure = error?.message ?? "";
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "test:diagnostic": {
|
||||
const parent = currentSuite?.children ?? roots;
|
||||
ArrayPrototypePush.$apply(parent, {
|
||||
__proto__: null,
|
||||
nesting: event.data.nesting,
|
||||
comment: event.data.message,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const suite of roots) {
|
||||
yield treeToXML(suite);
|
||||
}
|
||||
yield "</testsuites>\n";
|
||||
}
|
||||
|
||||
export default junitReporter;
|
||||
105
src/js/internal/test/reporter/lcov.ts
Normal file
105
src/js/internal/test/reporter/lcov.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
const { relative } = require("node:path");
|
||||
const Transform = require("internal/streams/transform");
|
||||
|
||||
// This reporter is based on the LCOV format, as described here:
|
||||
// https://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
|
||||
// Excerpts from this documentation are included in the comments that make up
|
||||
// the _transform function below.
|
||||
class LcovReporter extends Transform {
|
||||
constructor(options) {
|
||||
super({ ...options, writableObjectMode: true, __proto__: null });
|
||||
}
|
||||
|
||||
_transform(event, _encoding, callback) {
|
||||
if (event.type !== "test:coverage") {
|
||||
return callback(null);
|
||||
}
|
||||
let lcov = "";
|
||||
// A tracefile is made up of several human-readable lines of text, divided
|
||||
// into sections. If available, a tracefile begins with the testname which
|
||||
// is stored in the following format:
|
||||
// ## TN:\<test name\>
|
||||
lcov += "TN:\n";
|
||||
const {
|
||||
data: {
|
||||
summary: { workingDirectory },
|
||||
},
|
||||
} = event;
|
||||
try {
|
||||
for (let i = 0; i < event.data.summary.files.length; i++) {
|
||||
const file = event.data.summary.files[i];
|
||||
// For each source file referenced in the .da file, there is a section
|
||||
// containing filename and coverage data:
|
||||
// ## SF:\<path to the source file\>
|
||||
lcov += `SF:${relative(workingDirectory, file.path)}\n`;
|
||||
|
||||
// Following is a list of line numbers for each function name found in
|
||||
// the source file:
|
||||
// ## FN:\<line number of function start\>,\<function name\>
|
||||
//
|
||||
// After, there is a list of execution counts for each instrumented
|
||||
// function:
|
||||
// ## FNDA:\<execution count\>,\<function name\>
|
||||
//
|
||||
// This loop adds the FN lines to the lcov variable as it goes and
|
||||
// gathers the FNDA lines to be added later. This way we only loop
|
||||
// through the list of functions once.
|
||||
let fnda = "";
|
||||
for (let j = 0; j < file.functions.length; j++) {
|
||||
const func = file.functions[j];
|
||||
const name = func.name || `anonymous_${j}`;
|
||||
lcov += `FN:${func.line},${name}\n`;
|
||||
fnda += `FNDA:${func.count},${name}\n`;
|
||||
}
|
||||
lcov += fnda;
|
||||
|
||||
// This list is followed by two lines containing the number of
|
||||
// functions found and hit:
|
||||
// ## FNF:\<number of functions found\>
|
||||
// ## FNH:\<number of function hit\>
|
||||
lcov += `FNF:${file.totalFunctionCount}\n`;
|
||||
lcov += `FNH:${file.coveredFunctionCount}\n`;
|
||||
|
||||
// Branch coverage information is stored which one line per branch:
|
||||
// ## BRDA:\<line number\>,\<block number\>,\<branch number\>,\<taken\>
|
||||
// Block number and branch number are gcc internal IDs for the branch.
|
||||
// Taken is either '-' if the basic block containing the branch was
|
||||
// never executed or a number indicating how often that branch was
|
||||
// taken.
|
||||
for (let j = 0; j < file.branches.length; j++) {
|
||||
lcov += `BRDA:${file.branches[j].line},${j},0,${file.branches[j].count}\n`;
|
||||
}
|
||||
|
||||
// Branch coverage summaries are stored in two lines:
|
||||
// ## BRF:\<number of branches found\>
|
||||
// ## BRH:\<number of branches hit\>
|
||||
lcov += `BRF:${file.totalBranchCount}\n`;
|
||||
lcov += `BRH:${file.coveredBranchCount}\n`;
|
||||
|
||||
// Then there is a list of execution counts for each instrumented line
|
||||
// (i.e. a line which resulted in executable code):
|
||||
// ## DA:\<line number\>,\<execution count\>[,\<checksum\>]
|
||||
const sortedLines = file.lines.toSorted((a, b) => a.line - b.line);
|
||||
for (let j = 0; j < sortedLines.length; j++) {
|
||||
lcov += `DA:${sortedLines[j].line},${sortedLines[j].count}\n`;
|
||||
}
|
||||
|
||||
// At the end of a section, there is a summary about how many lines
|
||||
// were found and how many were actually instrumented:
|
||||
// ## LH:\<number of lines with a non-zero execution count\>
|
||||
// ## LF:\<number of instrumented lines\>
|
||||
lcov += `LH:${file.coveredLineCount}\n`;
|
||||
lcov += `LF:${file.totalLineCount}\n`;
|
||||
|
||||
// Each sections ends with:
|
||||
// end_of_record
|
||||
lcov += "end_of_record\n";
|
||||
}
|
||||
} catch (error) {
|
||||
return callback(error);
|
||||
}
|
||||
return callback(null, lcov);
|
||||
}
|
||||
}
|
||||
|
||||
export default LcovReporter;
|
||||
119
src/js/internal/test/reporter/spec.ts
Normal file
119
src/js/internal/test/reporter/spec.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
const Transform = require("internal/streams/transform");
|
||||
const colors = require("internal/util/colors");
|
||||
const { relative } = require("node:path");
|
||||
const {
|
||||
formatTestReport,
|
||||
indent,
|
||||
reporterColorMap,
|
||||
reporterUnicodeSymbolMap,
|
||||
} = require("internal/test/reporter/utils");
|
||||
|
||||
const ArrayPrototypeJoin = Array.prototype.join;
|
||||
const ArrayPrototypePop = Array.prototype.pop;
|
||||
const ArrayPrototypePush = Array.prototype.push;
|
||||
const ArrayPrototypeShift = Array.prototype.shift;
|
||||
const ArrayPrototypeUnshift = Array.prototype.unshift;
|
||||
|
||||
class SpecReporter extends Transform {
|
||||
#stack = [];
|
||||
#reported = [];
|
||||
#failedTests = [];
|
||||
#cwd = process.cwd();
|
||||
|
||||
constructor() {
|
||||
super({ __proto__: null, writableObjectMode: true });
|
||||
colors.refresh();
|
||||
}
|
||||
|
||||
#formatFailedTestResults() {
|
||||
if (this.#failedTests.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const results = [
|
||||
`\n${reporterColorMap["test:fail"]}${reporterUnicodeSymbolMap["test:fail"]}failing tests:${colors.white}\n`,
|
||||
];
|
||||
|
||||
for (let i = 0; i < this.#failedTests.length; i++) {
|
||||
const test = this.#failedTests[i];
|
||||
const formattedErr = formatTestReport("test:fail", test);
|
||||
|
||||
if (test.file) {
|
||||
const relPath = relative(this.#cwd, test.file);
|
||||
const location = `test at ${relPath}:${test.line}:${test.column}`;
|
||||
ArrayPrototypePush.$apply(results, location);
|
||||
}
|
||||
|
||||
ArrayPrototypePush.$apply(results, formattedErr);
|
||||
}
|
||||
|
||||
this.#failedTests = []; // Clean up the failed tests
|
||||
return ArrayPrototypeJoin.$apply(results, "\n");
|
||||
}
|
||||
#handleTestReportEvent(type, data) {
|
||||
const subtest = ArrayPrototypeShift.$apply(this.#stack); // This is the matching `test:start` event
|
||||
if (subtest) {
|
||||
$assert(subtest.type === "test:start");
|
||||
$assert(subtest.data.nesting === data.nesting);
|
||||
$assert(subtest.data.name === data.name);
|
||||
}
|
||||
let prefix = "";
|
||||
while (this.#stack.length) {
|
||||
// Report all the parent `test:start` events
|
||||
const parent = ArrayPrototypePop.$apply(this.#stack);
|
||||
$assert(parent.type === "test:start");
|
||||
const msg = parent.data;
|
||||
ArrayPrototypeUnshift.$apply(this.#reported, msg);
|
||||
prefix += `${indent(msg.nesting)}${reporterUnicodeSymbolMap["arrow:right"]}${msg.name}\n`;
|
||||
}
|
||||
let hasChildren = false;
|
||||
if (this.#reported[0] && this.#reported[0].nesting === data.nesting && this.#reported[0].name === data.name) {
|
||||
ArrayPrototypeShift.$apply(this.#reported);
|
||||
hasChildren = true;
|
||||
}
|
||||
const indentation = indent(data.nesting);
|
||||
return `${formatTestReport(type, data, prefix, indentation, hasChildren, false)}\n`;
|
||||
}
|
||||
#handleEvent({ type, data }) {
|
||||
switch (type) {
|
||||
case "test:fail":
|
||||
if (data.details?.error?.failureType !== kSubtestsFailed) {
|
||||
ArrayPrototypePush.$apply(this.#failedTests, data);
|
||||
}
|
||||
return this.#handleTestReportEvent(type, data);
|
||||
case "test:pass":
|
||||
return this.#handleTestReportEvent(type, data);
|
||||
case "test:start":
|
||||
ArrayPrototypeUnshift.$apply(this.#stack, { __proto__: null, data, type });
|
||||
break;
|
||||
case "test:stderr":
|
||||
case "test:stdout":
|
||||
return data.message;
|
||||
case "test:diagnostic": {
|
||||
const diagnosticColor = reporterColorMap[data.level] || reporterColorMap["test:diagnostic"];
|
||||
return `${diagnosticColor}${indent(data.nesting)}${reporterUnicodeSymbolMap[type]}${data.message}${colors.white}\n`;
|
||||
}
|
||||
case "test:coverage":
|
||||
return getCoverageReport(
|
||||
indent(data.nesting),
|
||||
data.summary,
|
||||
reporterUnicodeSymbolMap["test:coverage"],
|
||||
colors.blue,
|
||||
true,
|
||||
);
|
||||
case "test:summary":
|
||||
// We report only the root test summary
|
||||
if (data.file === undefined) {
|
||||
return this.#formatFailedTestResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
_transform({ type, data }, encoding, callback) {
|
||||
callback(null, this.#handleEvent({ __proto__: null, type, data }));
|
||||
}
|
||||
_flush(callback) {
|
||||
callback(null, this.#formatFailedTestResults());
|
||||
}
|
||||
}
|
||||
|
||||
export default SpecReporter;
|
||||
262
src/js/internal/test/reporter/tap.ts
Normal file
262
src/js/internal/test/reporter/tap.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
const { kEmptyObject } = require("internal/shared");
|
||||
const { inspectWithNoCustomRetry } = require("internal/test/reporter/utils");
|
||||
const { SafeMap, SafeSet } = require("internal/primordials");
|
||||
const { isDate } = require("node:util/types");
|
||||
|
||||
const ArrayPrototypeForEach = Array.prototype.forEach;
|
||||
const ArrayPrototypeJoin = Array.prototype.join;
|
||||
const ArrayPrototypePush = Array.prototype.push;
|
||||
const DatePrototypeToISOString = Date.prototype.toISOString;
|
||||
const ObjectEntries = Object.entries;
|
||||
const RegExpPrototypeSymbolReplace = RegExp.prototype[Symbol.replace];
|
||||
const RegExpPrototypeSymbolSplit = RegExp.prototype[Symbol.split];
|
||||
const StringPrototypeRepeat = String.prototype.repeat;
|
||||
const StringPrototypeReplaceAll = String.prototype.replaceAll;
|
||||
|
||||
const kDefaultIndent = " "; // 4 spaces
|
||||
const kFrameStartRegExp = /^ {4}at /;
|
||||
const kLineBreakRegExp = /\n|\r\n/;
|
||||
const kDefaultTAPVersion = 13;
|
||||
const inspectOptions = { __proto__: null, colors: false, breakLength: Infinity };
|
||||
|
||||
let testModule = undefined; // Lazy loaded due to circular dependency.
|
||||
function lazyLoadTest() {
|
||||
// testModule ??= require("internal/test_runner/test");
|
||||
return testModule;
|
||||
}
|
||||
|
||||
async function* tapReporter(source) {
|
||||
yield `TAP version ${kDefaultTAPVersion}\n`;
|
||||
for await (const { type, data } of source) {
|
||||
switch (type) {
|
||||
case "test:fail": {
|
||||
yield reportTest(data.nesting, data.testNumber, "not ok", data.name, data.skip, data.todo);
|
||||
const location = data.file ? `${data.file}:${data.line}:${data.column}` : null;
|
||||
yield reportDetails(data.nesting, data.details, location);
|
||||
break;
|
||||
}
|
||||
case "test:pass":
|
||||
yield reportTest(data.nesting, data.testNumber, "ok", data.name, data.skip, data.todo);
|
||||
yield reportDetails(data.nesting, data.details, null);
|
||||
break;
|
||||
case "test:plan":
|
||||
yield `${indent(data.nesting)}1..${data.count}\n`;
|
||||
break;
|
||||
case "test:start":
|
||||
yield `${indent(data.nesting)}# Subtest: ${tapEscape(data.name)}\n`;
|
||||
break;
|
||||
case "test:stderr":
|
||||
case "test:stdout": {
|
||||
const lines = RegExpPrototypeSymbolSplit.$apply(kLineBreakRegExp, data.message);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].length === 0) continue;
|
||||
yield `# ${tapEscape(lines[i])}\n`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "test:diagnostic":
|
||||
yield `${indent(data.nesting)}# ${tapEscape(data.message)}\n`;
|
||||
break;
|
||||
case "test:coverage":
|
||||
yield getCoverageReport(indent(data.nesting), data.summary, "# ", "", true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reportTest(nesting, testNumber, status, name, skip, todo) {
|
||||
let line = `${indent(nesting)}${status} ${testNumber}`;
|
||||
|
||||
if (name) {
|
||||
line += ` ${tapEscape(`- ${name}`)}`;
|
||||
}
|
||||
|
||||
if (skip !== undefined) {
|
||||
line += ` # SKIP${typeof skip === "string" && skip.length ? ` ${tapEscape(skip)}` : ""}`;
|
||||
} else if (todo !== undefined) {
|
||||
line += ` # TODO${typeof todo === "string" && todo.length ? ` ${tapEscape(todo)}` : ""}`;
|
||||
}
|
||||
|
||||
line += "\n";
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
function reportDetails(nesting, data = kEmptyObject, location) {
|
||||
const { error, duration_ms } = data;
|
||||
const _indent = indent(nesting);
|
||||
let details = `${_indent} ---\n`;
|
||||
|
||||
details += jsToYaml(_indent, "duration_ms", duration_ms);
|
||||
details += jsToYaml(_indent, "type", data.type);
|
||||
|
||||
if (location) {
|
||||
details += jsToYaml(_indent, "location", location);
|
||||
}
|
||||
|
||||
details += jsToYaml(_indent, null, error, new SafeSet());
|
||||
details += `${_indent} ...\n`;
|
||||
return details;
|
||||
}
|
||||
|
||||
const memo = new SafeMap();
|
||||
function indent(nesting) {
|
||||
let value = memo.get(nesting);
|
||||
if (value === undefined) {
|
||||
value = StringPrototypeRepeat.$apply(kDefaultIndent, nesting);
|
||||
memo.set(nesting, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// In certain places, # and \ need to be escaped as \# and \\.
|
||||
function tapEscape(input) {
|
||||
let result = StringPrototypeReplaceAll.$apply(input, "\b", "\\b");
|
||||
result = StringPrototypeReplaceAll.$apply(result, "\f", "\\f");
|
||||
result = StringPrototypeReplaceAll.$apply(result, "\t", "\\t");
|
||||
result = StringPrototypeReplaceAll.$apply(result, "\n", "\\n");
|
||||
result = StringPrototypeReplaceAll.$apply(result, "\r", "\\r");
|
||||
result = StringPrototypeReplaceAll.$apply(result, "\v", "\\v");
|
||||
result = StringPrototypeReplaceAll.$apply(result, "\\", "\\\\");
|
||||
result = StringPrototypeReplaceAll.$apply(result, "#", "\\#");
|
||||
return result;
|
||||
}
|
||||
|
||||
function jsToYaml(indent, name, value, seen) {
|
||||
if (value === undefined) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const prefix = `${indent} ${name}:`;
|
||||
|
||||
if (value === null) {
|
||||
return `${prefix} ~\n`;
|
||||
}
|
||||
|
||||
if (typeof value !== "object") {
|
||||
if (typeof value !== "string") {
|
||||
return `${prefix} ${inspectWithNoCustomRetry(value, inspectOptions)}\n`;
|
||||
}
|
||||
|
||||
const lines = RegExpPrototypeSymbolSplit.$apply(kLineBreakRegExp, value);
|
||||
|
||||
if (lines.length === 1) {
|
||||
return `${prefix} ${inspectWithNoCustomRetry(value, inspectOptions)}\n`;
|
||||
}
|
||||
|
||||
let str = `${prefix} |-\n`;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
str += `${indent} ${lines[i]}\n`;
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
seen.add(value);
|
||||
const entries = ObjectEntries(value);
|
||||
const isErrorObj = Error.isError(value);
|
||||
let propsIndent = indent;
|
||||
let result = "";
|
||||
|
||||
if (name != null) {
|
||||
result += prefix;
|
||||
if (isDate(value)) {
|
||||
// YAML uses the ISO-8601 standard to express dates.
|
||||
result += " " + DatePrototypeToISOString.$apply(value);
|
||||
}
|
||||
result += "\n";
|
||||
propsIndent += " ";
|
||||
}
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const { 0: key, 1: value } = entries[i];
|
||||
|
||||
if (isErrorObj && (key === "cause" || key === "code")) {
|
||||
continue;
|
||||
}
|
||||
if (seen.has(value)) {
|
||||
result += `${propsIndent} ${key}: <Circular>\n`;
|
||||
continue;
|
||||
}
|
||||
|
||||
result += jsToYaml(propsIndent, key, value, seen);
|
||||
}
|
||||
|
||||
if (isErrorObj) {
|
||||
const { kUnwrapErrors } = lazyLoadTest();
|
||||
const { cause, code, failureType, message, expected, actual, operator, stack, name } = value;
|
||||
let errMsg = message ?? "<unknown error>";
|
||||
let errName = name;
|
||||
let errStack = stack;
|
||||
let errCode = code;
|
||||
let errExpected = expected;
|
||||
let errActual = actual;
|
||||
let errOperator = operator;
|
||||
let errIsAssertion = isAssertionLike(value);
|
||||
|
||||
// If the ERR_TEST_FAILURE came from an error provided by user code,
|
||||
// then try to unwrap the original error message and stack.
|
||||
if (code === "ERR_TEST_FAILURE" && kUnwrapErrors.has(failureType)) {
|
||||
errStack = cause?.stack ?? errStack;
|
||||
errCode = cause?.code ?? errCode;
|
||||
errName = cause?.name ?? errName;
|
||||
errMsg = cause?.message ?? errMsg;
|
||||
|
||||
if (isAssertionLike(cause)) {
|
||||
errExpected = cause.expected;
|
||||
errActual = cause.actual;
|
||||
errOperator = cause.operator ?? errOperator;
|
||||
errIsAssertion = true;
|
||||
}
|
||||
}
|
||||
|
||||
result += jsToYaml(indent, "error", errMsg, seen);
|
||||
|
||||
if (errCode) {
|
||||
result += jsToYaml(indent, "code", errCode, seen);
|
||||
}
|
||||
if (errName && errName !== "Error") {
|
||||
result += jsToYaml(indent, "name", errName, seen);
|
||||
}
|
||||
|
||||
if (errIsAssertion) {
|
||||
// Note that we're deliberately creating shallow copies of the `seen`
|
||||
// set here in order to isolate the discovery of circular references
|
||||
// within the expected and actual properties respectively.
|
||||
result += jsToYaml(indent, "expected", errExpected, new SafeSet(seen));
|
||||
result += jsToYaml(indent, "actual", errActual, new SafeSet(seen));
|
||||
if (errOperator) {
|
||||
result += jsToYaml(indent, "operator", errOperator, seen);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof errStack === "string") {
|
||||
const frames = [];
|
||||
|
||||
ArrayPrototypeForEach.$apply(RegExpPrototypeSymbolSplit.$apply(kLineBreakRegExp, errStack), frame => {
|
||||
const processed = RegExpPrototypeSymbolReplace.$apply(kFrameStartRegExp, frame, "");
|
||||
|
||||
if (processed.length > 0 && processed.length !== frame.length) {
|
||||
ArrayPrototypePush.$apply(frames, processed);
|
||||
}
|
||||
});
|
||||
|
||||
if (frames.length > 0) {
|
||||
const frameDelimiter = `\n${indent} `;
|
||||
|
||||
result += `${indent} stack: |-${frameDelimiter}`;
|
||||
result += `${ArrayPrototypeJoin.$apply(frames, frameDelimiter)}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function isAssertionLike(value) {
|
||||
return value && typeof value === "object" && "expected" in value && "actual" in value;
|
||||
}
|
||||
|
||||
export default tapReporter;
|
||||
112
src/js/internal/test/reporter/utils.ts
Normal file
112
src/js/internal/test/reporter/utils.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
const colors = require("internal/util/colors");
|
||||
const { SafeMap, hardenRegExp } = require("internal/primordials");
|
||||
|
||||
const ArrayPrototypeJoin = Array.prototype.join;
|
||||
const RegExpPrototypeSymbolSplit = RegExp.prototype[Symbol.split];
|
||||
const StringPrototypeRepeat = String.prototype.repeat;
|
||||
|
||||
const indentMemo = new SafeMap();
|
||||
|
||||
const inspectOptions = {
|
||||
__proto__: null,
|
||||
colors: colors.shouldColorize(process.stdout),
|
||||
breakLength: Infinity,
|
||||
};
|
||||
|
||||
const reporterUnicodeSymbolMap = {
|
||||
"__proto__": null,
|
||||
"test:fail": "\u2716 ",
|
||||
"test:pass": "\u2714 ",
|
||||
"test:diagnostic": "\u2139 ",
|
||||
"test:coverage": "\u2139 ",
|
||||
"arrow:right": "\u25B6 ",
|
||||
"hyphen:minus": "\uFE63 ",
|
||||
};
|
||||
|
||||
const reporterColorMap = {
|
||||
"__proto__": null,
|
||||
get "test:fail"() {
|
||||
return colors.red;
|
||||
},
|
||||
get "test:pass"() {
|
||||
return colors.green;
|
||||
},
|
||||
get "test:diagnostic"() {
|
||||
return colors.blue;
|
||||
},
|
||||
get "info"() {
|
||||
return colors.blue;
|
||||
},
|
||||
get "warn"() {
|
||||
return colors.yellow;
|
||||
},
|
||||
get "error"() {
|
||||
return colors.red;
|
||||
},
|
||||
};
|
||||
|
||||
function indent(nesting) {
|
||||
let value = indentMemo.get(nesting);
|
||||
if (value === undefined) {
|
||||
value = StringPrototypeRepeat.$apply(" ", nesting);
|
||||
indentMemo.set(nesting, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function formatError(error, indent) {
|
||||
if (!error) return "";
|
||||
const err = error.code === "ERR_TEST_FAILURE" ? error.cause : error;
|
||||
const message = ArrayPrototypeJoin.$apply(
|
||||
RegExpPrototypeSymbolSplit.$apply(hardenRegExp(/\r?\n/), inspectWithNoCustomRetry(err, inspectOptions)),
|
||||
`\n${indent} `,
|
||||
);
|
||||
return `\n${indent} ${message}\n`;
|
||||
}
|
||||
|
||||
function formatTestReport(type, data, prefix = "", indent = "", hasChildren = false, showErrorDetails = true) {
|
||||
let color = reporterColorMap[type] ?? colors.white;
|
||||
let symbol = reporterUnicodeSymbolMap[type] ?? " ";
|
||||
const { skip, todo } = data;
|
||||
const duration_ms = data.details?.duration_ms ? ` ${colors.gray}(${data.details.duration_ms}ms)${colors.white}` : "";
|
||||
let title = `${data.name}${duration_ms}`;
|
||||
|
||||
if (skip !== undefined) {
|
||||
title += ` # ${typeof skip === "string" && skip.length ? skip : "SKIP"}`;
|
||||
} else if (todo !== undefined) {
|
||||
title += ` # ${typeof todo === "string" && todo.length ? todo : "TODO"}`;
|
||||
}
|
||||
|
||||
const error = showErrorDetails ? formatError(data.details?.error, indent) : "";
|
||||
const err = hasChildren
|
||||
? !error || data.details?.error?.failureType === "subtestsFailed"
|
||||
? ""
|
||||
: `\n${error}`
|
||||
: error;
|
||||
|
||||
if (skip !== undefined) {
|
||||
color = colors.gray;
|
||||
symbol = reporterUnicodeSymbolMap["hyphen:minus"];
|
||||
}
|
||||
return `${prefix}${indent}${color}${symbol}${title}${colors.white}${err}`;
|
||||
}
|
||||
|
||||
let utilInspect;
|
||||
function inspectWithNoCustomRetry(obj, options) {
|
||||
utilInspect ??= require("internal/util/inspect");
|
||||
const { inspect } = utilInspect;
|
||||
|
||||
try {
|
||||
return inspect(obj, options);
|
||||
} catch {
|
||||
return inspect(obj, { ...options, customInspect: false });
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
reporterUnicodeSymbolMap,
|
||||
reporterColorMap,
|
||||
formatTestReport,
|
||||
indent,
|
||||
inspectWithNoCustomRetry,
|
||||
};
|
||||
60
src/js/node/test.reporters.ts
Normal file
60
src/js/node/test.reporters.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// Hardcoded module "node:test/reporters"
|
||||
|
||||
const ObjectDefineProperties = Object.defineProperties;
|
||||
|
||||
let dot;
|
||||
let junit;
|
||||
let spec;
|
||||
let tap;
|
||||
let lcov;
|
||||
|
||||
const default_exports = {};
|
||||
ObjectDefineProperties(default_exports, {
|
||||
dot: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
dot ??= require("internal/test/reporter/dot");
|
||||
return dot;
|
||||
},
|
||||
},
|
||||
junit: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
junit ??= require("internal/test/reporter/junit");
|
||||
return junit;
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
value: function value() {
|
||||
spec ??= require("internal/test/reporter/spec");
|
||||
return new spec(...arguments);
|
||||
},
|
||||
},
|
||||
tap: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
tap ??= require("internal/test/reporter/tap");
|
||||
return tap;
|
||||
},
|
||||
},
|
||||
lcov: {
|
||||
__proto__: null,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
value: function value() {
|
||||
lcov ??= require("internal/test/reporter/lcov");
|
||||
return new lcov(...arguments);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default default_exports;
|
||||
@@ -6,7 +6,7 @@ let testEnv: NodeJS.Dict<string>;
|
||||
|
||||
beforeAll(() => {
|
||||
testEnv = { ...bunEnv };
|
||||
delete testEnv.AGENTS;
|
||||
delete testEnv.AGENT;
|
||||
});
|
||||
|
||||
test("CLAUDECODE=1 shows quiet test output (only failures)", async () => {
|
||||
|
||||
Reference in New Issue
Block a user