Compare commits

...

3 Commits

Author SHA1 Message Date
Meghan Denny
b059f03c74 fix claudecode-flag.test.ts 2025-10-08 00:29:03 -07:00
Meghan Denny
17d279ca15 fix lint 2025-10-08 00:28:24 -07:00
Meghan Denny
7855d792eb js: add node:test/reporters module 2025-10-07 23:12:04 -07:00
12 changed files with 1008 additions and 1 deletions

View File

@@ -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"),

View File

@@ -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

View File

@@ -79,6 +79,7 @@ static constexpr ASCIILiteral builtinModuleNamesSortedLength[] = {
"inspector/promises"_s,
"_stream_passthrough"_s,
"diagnostics_channel"_s,
"node:test/reporters"_s,
};
namespace Bun {

View File

@@ -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,
};

View 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;

View 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, "&amp;"), "&lt;");
}
function escapeComment(s = "") {
return RegExpPrototypeSymbolReplace.$apply(/--/g, s, "&#45;&#45;");
}
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;

View 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;

View 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;

View 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;

View 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,
};

View 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;

View File

@@ -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 () => {