Upload Kmake

This commit is contained in:
Gorochu
2026-05-26 23:36:42 -07:00
parent ba051b2f74
commit 555ec72358
41615 changed files with 13344630 additions and 1 deletions

View File

@ -0,0 +1,50 @@
'use strict';
const {
SafeMap,
} = primordials;
const {
validateFunction,
validateString,
} = require('internal/validators');
const assert = require('assert');
const methodsToCopy = [
'deepEqual',
'deepStrictEqual',
'doesNotMatch',
'doesNotReject',
'doesNotThrow',
'equal',
'fail',
'ifError',
'match',
'notDeepEqual',
'notDeepStrictEqual',
'notEqual',
'notStrictEqual',
'partialDeepStrictEqual',
'rejects',
'strictEqual',
'throws',
];
let assertMap;
function getAssertionMap() {
if (assertMap === undefined) {
assertMap = new SafeMap();
for (let i = 0; i < methodsToCopy.length; i++) {
assertMap.set(methodsToCopy[i], assert[methodsToCopy[i]]);
}
}
return assertMap;
}
function register(name, fn) {
validateString(name, 'name');
validateFunction(fn, 'fn');
const map = getAssertionMap();
map.set(name, fn);
}
module.exports = { getAssertionMap, register };

View File

@ -0,0 +1,692 @@
'use strict';
const {
ArrayFrom,
ArrayPrototypeMap,
ArrayPrototypePush,
JSONParse,
MathFloor,
MathMax,
MathMin,
NumberParseInt,
ObjectAssign,
RegExpPrototypeExec,
RegExpPrototypeSymbolSplit,
SafeMap,
SafeSet,
StringPrototypeIncludes,
StringPrototypeLocaleCompare,
StringPrototypeStartsWith,
} = primordials;
const {
copyFileSync,
mkdirSync,
mkdtempSync,
opendirSync,
readFileSync,
rmSync,
} = require('fs');
const { setupCoverageHooks } = require('internal/util');
const { tmpdir } = require('os');
const { join, resolve, relative } = require('path');
const { fileURLToPath } = require('internal/url');
const { kMappings, SourceMap } = require('internal/source_map/source_map');
const {
codes: {
ERR_SOURCE_MAP_CORRUPT,
ERR_SOURCE_MAP_MISSING_SOURCE,
},
} = require('internal/errors');
const { matchGlobPattern } = require('internal/fs/glob');
const kCoverageFileRegex = /^coverage-(\d+)-(\d{13})-(\d+)\.json$/;
const kIgnoreRegex = /\/\* node:coverage ignore next (?<count>\d+ )?\*\//;
const kLineEndingRegex = /\r?\n$/u;
const kLineSplitRegex = /(?<=\r?\n)/u;
const kStatusRegex = /\/\* node:coverage (?<status>enable|disable) \*\//;
class CoverageLine {
constructor(line, startOffset, src, length = src?.length) {
const newlineLength = src == null ? 0 :
RegExpPrototypeExec(kLineEndingRegex, src)?.[0].length ?? 0;
this.line = line;
this.src = src;
this.startOffset = startOffset;
this.endOffset = startOffset + length - newlineLength;
this.ignore = false;
this.count = this.startOffset === this.endOffset ? 1 : 0;
}
}
class TestCoverage {
constructor(coverageDirectory,
originalCoverageDirectory,
options) {
this.coverageDirectory = coverageDirectory;
this.originalCoverageDirectory = originalCoverageDirectory;
this.options = options;
}
#sourceLines = new SafeMap();
getLines(fileUrl, source) {
// Split the file source into lines. Make sure the lines maintain their
// original line endings because those characters are necessary for
// determining offsets in the file.
if (this.#sourceLines.has(fileUrl)) {
return this.#sourceLines.get(fileUrl);
}
try {
source ??= readFileSync(fileURLToPath(fileUrl), 'utf8');
} catch {
// The file can no longer be read. It may have been deleted among
// other possibilities. Leave it out of the coverage report.
this.#sourceLines.set(fileUrl, null);
return;
}
const linesWithBreaks =
RegExpPrototypeSymbolSplit(kLineSplitRegex, source);
let ignoreCount = 0;
let enabled = true;
let offset = 0;
const lines = ArrayPrototypeMap(linesWithBreaks, (line, i) => {
const startOffset = offset;
const coverageLine = new CoverageLine(i + 1, startOffset, line);
offset += line.length;
// Determine if this line is being ignored.
if (ignoreCount > 0) {
ignoreCount--;
coverageLine.ignore = true;
} else if (!enabled) {
coverageLine.ignore = true;
}
if (!coverageLine.ignore) {
// If this line is not already being ignored, check for ignore
// comments.
const match = RegExpPrototypeExec(kIgnoreRegex, line);
if (match !== null) {
ignoreCount = NumberParseInt(match.groups?.count ?? 1, 10);
}
}
// Check for comments to enable/disable coverage no matter what. These
// take precedence over ignore comments.
const match = RegExpPrototypeExec(kStatusRegex, line);
const status = match?.groups?.status;
if (status) {
ignoreCount = 0;
enabled = status === 'enable';
}
return coverageLine;
});
this.#sourceLines.set(fileUrl, lines);
return lines;
}
summary() {
internalBinding('profiler').takeCoverage();
const coverage = this.getCoverageFromDirectory();
const coverageSummary = {
__proto__: null,
workingDirectory: this.options.cwd,
files: [],
totals: {
__proto__: null,
totalLineCount: 0,
totalBranchCount: 0,
totalFunctionCount: 0,
coveredLineCount: 0,
coveredBranchCount: 0,
coveredFunctionCount: 0,
coveredLinePercent: 0,
coveredBranchPercent: 0,
coveredFunctionPercent: 0,
},
thresholds: {
__proto__: null,
line: this.options.lineCoverage,
branch: this.options.branchCoverage,
function: this.options.functionCoverage,
},
};
if (!coverage) {
return coverageSummary;
}
for (let i = 0; i < coverage.length; ++i) {
const { functions, url } = coverage[i];
let totalBranches = 0;
let totalFunctions = 0;
let branchesCovered = 0;
let functionsCovered = 0;
const functionReports = [];
const branchReports = [];
const lines = this.getLines(url);
if (!lines) {
continue;
}
for (let j = 0; j < functions.length; ++j) {
const { isBlockCoverage, ranges } = functions[j];
let maxCountPerFunction = 0;
for (let k = 0; k < ranges.length; ++k) {
const range = ranges[k];
maxCountPerFunction = MathMax(maxCountPerFunction, range.count);
// Add some useful data to the range. The test runner has read these ranges
// from a file, so we own the data structures and can do what we want.
ObjectAssign(range, mapRangeToLines(range, lines));
if (isBlockCoverage) {
ArrayPrototypePush(branchReports, {
__proto__: null,
line: range.lines[0]?.line,
count: range.count,
});
if (range.count !== 0 ||
range.ignoredLines === range.lines.length) {
branchesCovered++;
}
totalBranches++;
}
}
if (j > 0 && ranges.length > 0) {
const range = ranges[0];
ArrayPrototypePush(functionReports, {
__proto__: null,
name: functions[j].functionName,
count: maxCountPerFunction,
line: range.lines[0]?.line,
});
if (range.count !== 0 || range.ignoredLines === range.lines.length) {
functionsCovered++;
}
totalFunctions++;
}
}
let coveredCnt = 0;
const lineReports = [];
for (let j = 0; j < lines.length; ++j) {
const line = lines[j];
if (!line.ignore) {
ArrayPrototypePush(lineReports, {
__proto__: null,
line: line.line,
count: line.count,
});
}
if (line.count > 0 || line.ignore) {
coveredCnt++;
}
}
ArrayPrototypePush(coverageSummary.files, {
__proto__: null,
path: fileURLToPath(url),
totalLineCount: lines.length,
totalBranchCount: totalBranches,
totalFunctionCount: totalFunctions,
coveredLineCount: coveredCnt,
coveredBranchCount: branchesCovered,
coveredFunctionCount: functionsCovered,
coveredLinePercent: toPercentage(coveredCnt, lines.length),
coveredBranchPercent: toPercentage(branchesCovered, totalBranches),
coveredFunctionPercent: toPercentage(functionsCovered, totalFunctions),
functions: functionReports,
branches: branchReports,
lines: lineReports,
});
coverageSummary.totals.totalLineCount += lines.length;
coverageSummary.totals.totalBranchCount += totalBranches;
coverageSummary.totals.totalFunctionCount += totalFunctions;
coverageSummary.totals.coveredLineCount += coveredCnt;
coverageSummary.totals.coveredBranchCount += branchesCovered;
coverageSummary.totals.coveredFunctionCount += functionsCovered;
}
coverageSummary.totals.coveredLinePercent = toPercentage(
coverageSummary.totals.coveredLineCount,
coverageSummary.totals.totalLineCount,
);
coverageSummary.totals.coveredBranchPercent = toPercentage(
coverageSummary.totals.coveredBranchCount,
coverageSummary.totals.totalBranchCount,
);
coverageSummary.totals.coveredFunctionPercent = toPercentage(
coverageSummary.totals.coveredFunctionCount,
coverageSummary.totals.totalFunctionCount,
);
coverageSummary.files.sort(sortCoverageFiles);
return coverageSummary;
}
cleanup() {
// Restore the original value of process.env.NODE_V8_COVERAGE. Then, copy
// all of the created coverage files to the original coverage directory.
internalBinding('profiler').endCoverage();
if (this.originalCoverageDirectory === undefined) {
delete process.env.NODE_V8_COVERAGE;
} else {
process.env.NODE_V8_COVERAGE = this.originalCoverageDirectory;
let dir;
try {
mkdirSync(this.originalCoverageDirectory, { __proto__: null, recursive: true });
dir = opendirSync(this.coverageDirectory);
for (let entry; (entry = dir.readSync()) !== null;) {
const src = join(this.coverageDirectory, entry.name);
const dst = join(this.originalCoverageDirectory, entry.name);
copyFileSync(src, dst);
}
} finally {
if (dir) {
dir.closeSync();
}
}
}
try {
rmSync(this.coverageDirectory, { __proto__: null, recursive: true });
} catch {
// Ignore cleanup errors.
}
}
getCoverageFromDirectory() {
const result = new SafeMap();
let dir;
try {
dir = opendirSync(this.coverageDirectory);
for (let entry; (entry = dir.readSync()) !== null;) {
if (RegExpPrototypeExec(kCoverageFileRegex, entry.name) === null) {
continue;
}
const coverageFile = join(this.coverageDirectory, entry.name);
const coverage = JSONParse(readFileSync(coverageFile, 'utf8'));
this.mergeCoverage(result, this.mapCoverageWithSourceMap(coverage));
}
return ArrayFrom(result.values());
} finally {
if (dir) {
dir.closeSync();
}
}
}
mapCoverageWithSourceMap(coverage) {
const { result } = coverage;
const sourceMapCache = coverage['source-map-cache'];
if (!this.options.sourceMaps || !sourceMapCache) {
return result;
}
const newResult = new SafeMap();
for (let i = 0; i < result.length; ++i) {
const script = result[i];
const { url, functions } = script;
if (this.shouldSkipFileCoverage(url) || sourceMapCache[url] == null) {
newResult.set(url, script);
continue;
}
const { data, lineLengths } = sourceMapCache[url];
if (!data) throw new ERR_SOURCE_MAP_CORRUPT(url);
let offset = 0;
const executedLines = ArrayPrototypeMap(lineLengths, (length, i) => {
const coverageLine = new CoverageLine(i + 1, offset, null, length + 1);
offset += length + 1;
return coverageLine;
});
if (data.sourcesContent != null) {
for (let j = 0; j < data.sources.length; ++j) {
this.getLines(data.sources[j], data.sourcesContent[j]);
}
}
const sourceMap = new SourceMap(data, { __proto__: null, lineLengths });
for (let j = 0; j < functions.length; ++j) {
const { ranges, functionName, isBlockCoverage } = functions[j];
if (ranges == null) {
continue;
}
let newUrl;
const newRanges = [];
for (let k = 0; k < ranges.length; ++k) {
const { startOffset, endOffset, count } = ranges[k];
const { lines } = mapRangeToLines(ranges[k], executedLines);
let startEntry = sourceMap
.findEntry(lines[0].line - 1, MathMax(0, startOffset - lines[0].startOffset));
const endEntry = sourceMap
.findEntry(lines[lines.length - 1].line - 1, (endOffset - lines[lines.length - 1].startOffset) - 1);
if (!startEntry.originalSource && endEntry.originalSource &&
lines[0].line === 1 && startOffset === 0 && lines[0].startOffset === 0) {
// Edge case when the first line is not mappable
const { 2: originalSource, 3: originalLine, 4: originalColumn } = sourceMap[kMappings][0];
startEntry = { __proto__: null, originalSource, originalLine, originalColumn };
}
if (!startEntry.originalSource || startEntry.originalSource !== endEntry.originalSource) {
// The range is not mappable. Skip it.
continue;
}
newUrl ??= startEntry?.originalSource;
const mappedLines = this.getLines(newUrl);
if (!mappedLines) {
throw new ERR_SOURCE_MAP_MISSING_SOURCE(newUrl, url);
}
const mappedStartOffset = this.entryToOffset(startEntry, mappedLines);
const mappedEndOffset = this.entryToOffset(endEntry, mappedLines) + 1;
if (mappedStartOffset < 0 || mappedEndOffset < 1) {
// The range is not mappable. Skip it.
continue;
}
for (let l = startEntry.originalLine; l <= endEntry.originalLine; l++) {
mappedLines[l].count = count;
}
ArrayPrototypePush(newRanges, {
__proto__: null, startOffset: mappedStartOffset, endOffset: mappedEndOffset, count,
});
}
if (!newUrl) {
// No mappable ranges. Skip the function.
continue;
}
const newScript = newResult.get(newUrl) ?? { __proto__: null, url: newUrl, functions: [] };
ArrayPrototypePush(newScript.functions, { __proto__: null, functionName, ranges: newRanges, isBlockCoverage });
newResult.set(newUrl, newScript);
}
}
return ArrayFrom(newResult.values());
}
entryToOffset(entry, lines) {
const line = MathMax(entry.originalLine, 0);
const mappedLine = lines[line];
if (!mappedLine) {
// Return -1 if the line is not mappable.
return -1;
}
return MathMin(mappedLine.startOffset + entry.originalColumn, mappedLine.endOffset);
}
mergeCoverage(merged, coverage) {
for (let i = 0; i < coverage.length; ++i) {
const newScript = coverage[i];
const { url } = newScript;
if (this.shouldSkipFileCoverage(url)) {
continue;
}
const oldScript = merged.get(url);
if (oldScript === undefined) {
merged.set(url, newScript);
} else {
mergeCoverageScripts(oldScript, newScript);
}
}
}
shouldSkipFileCoverage(url) {
// This check filters out core modules, which start with 'node:' in
// coverage reports, as well as any invalid coverages which have been
// observed on Windows.
if (!StringPrototypeStartsWith(url, 'file:')) return true;
const absolutePath = fileURLToPath(url);
const relativePath = relative(this.options.cwd, absolutePath);
const {
coverageExcludeGlobs: excludeGlobs,
coverageIncludeGlobs: includeGlobs,
} = this.options;
// This check filters out files that match the exclude globs.
if (excludeGlobs?.length > 0) {
for (let i = 0; i < excludeGlobs.length; ++i) {
if (
matchGlobPattern(relativePath, excludeGlobs[i]) ||
matchGlobPattern(absolutePath, excludeGlobs[i])
) return true;
}
}
// This check filters out files that do not match the include globs.
if (includeGlobs?.length > 0) {
for (let i = 0; i < includeGlobs.length; ++i) {
if (
matchGlobPattern(relativePath, includeGlobs[i]) ||
matchGlobPattern(absolutePath, includeGlobs[i])
) return false;
}
return true;
}
// This check filters out the node_modules/ directory, unless it is explicitly included.
return StringPrototypeIncludes(url, '/node_modules/');
}
}
function toPercentage(covered, total) {
return total === 0 ? 100 : (covered / total) * 100;
}
function sortCoverageFiles(a, b) {
return StringPrototypeLocaleCompare(a.path, b.path);
}
function setupCoverage(options) {
let originalCoverageDirectory = process.env.NODE_V8_COVERAGE;
// If NODE_V8_COVERAGE was already specified, convert it to an absolute path
// and store it for later. The test runner will use a temporary directory
// so that no preexisting coverage files interfere with the results of the
// coverage report. Then, once the coverage is computed, move the coverage
// files back to the original NODE_V8_COVERAGE directory.
originalCoverageDirectory &&= resolve(options.cwd, originalCoverageDirectory);
const coverageDirectory = mkdtempSync(join(tmpdir(), 'node-coverage-'));
const enabled = setupCoverageHooks(coverageDirectory);
if (!enabled) {
return null;
}
// Ensure that NODE_V8_COVERAGE is set so that coverage can propagate to
// child processes.
process.env.NODE_V8_COVERAGE = coverageDirectory;
return new TestCoverage(
coverageDirectory,
originalCoverageDirectory,
options,
);
}
function mapRangeToLines(range, lines) {
const { startOffset, endOffset, count } = range;
const mappedLines = [];
let ignoredLines = 0;
let start = 0;
let end = lines.length;
let mid;
while (start <= end) {
mid = MathFloor((start + end) / 2);
let line = lines[mid];
if (startOffset >= line?.startOffset && startOffset <= line?.endOffset) {
while (endOffset > line?.startOffset) {
// If the range is not covered, and the range covers the entire line,
// then mark that line as not covered.
if (startOffset <= line.startOffset && endOffset >= line.endOffset) {
line.count = count;
}
ArrayPrototypePush(mappedLines, line);
if (line.ignore) {
ignoredLines++;
}
mid++;
line = lines[mid];
}
break;
} else if (startOffset >= line?.endOffset) {
start = mid + 1;
} else {
end = mid - 1;
}
}
return { __proto__: null, lines: mappedLines, ignoredLines };
}
function mergeCoverageScripts(oldScript, newScript) {
// Merge the functions from the new coverage into the functions from the
// existing (merged) coverage.
for (let i = 0; i < newScript.functions.length; ++i) {
const newFn = newScript.functions[i];
let found = false;
for (let j = 0; j < oldScript.functions.length; ++j) {
const oldFn = oldScript.functions[j];
if (newFn.functionName === oldFn.functionName &&
newFn.ranges?.[0].startOffset === oldFn.ranges?.[0].startOffset &&
newFn.ranges?.[0].endOffset === oldFn.ranges?.[0].endOffset) {
// These are the same functions.
found = true;
// If newFn is block level coverage, then it will:
// - Replace oldFn if oldFn is not block level coverage.
// - Merge with oldFn if it is also block level coverage.
// If newFn is not block level coverage, then it has no new data.
if (newFn.isBlockCoverage) {
if (oldFn.isBlockCoverage) {
// Merge the oldFn ranges with the newFn ranges.
mergeCoverageRanges(oldFn, newFn);
} else {
// Replace oldFn with newFn.
oldFn.isBlockCoverage = true;
oldFn.ranges = newFn.ranges;
}
}
break;
}
}
if (!found) {
// This is a new function to track. This is possible because V8 can
// generate a different list of functions depending on which code paths
// are executed. For example, if a code path dynamically creates a
// function, but that code path is not executed then the function does
// not show up in the coverage report. Unfortunately, this also means
// that the function counts in the coverage summary can never be
// guaranteed to be 100% accurate.
ArrayPrototypePush(oldScript.functions, newFn);
}
}
}
function mergeCoverageRanges(oldFn, newFn) {
const mergedRanges = new SafeSet();
// Keep all of the existing covered ranges.
for (let i = 0; i < oldFn.ranges.length; ++i) {
const oldRange = oldFn.ranges[i];
if (oldRange.count > 0) {
mergedRanges.add(oldRange);
}
}
// Merge in the new ranges where appropriate.
for (let i = 0; i < newFn.ranges.length; ++i) {
const newRange = newFn.ranges[i];
let exactMatch = false;
for (let j = 0; j < oldFn.ranges.length; ++j) {
const oldRange = oldFn.ranges[j];
if (doesRangeEqualOtherRange(newRange, oldRange)) {
// These are the same ranges, so keep the existing one.
oldRange.count += newRange.count;
mergedRanges.add(oldRange);
exactMatch = true;
break;
}
// Look at ranges representing missing coverage and add ranges that
// represent the intersection.
if (oldRange.count === 0 && newRange.count === 0) {
if (doesRangeContainOtherRange(oldRange, newRange)) {
// The new range is completely within the old range. Discard the
// larger (old) range, and keep the smaller (new) range.
mergedRanges.add(newRange);
} else if (doesRangeContainOtherRange(newRange, oldRange)) {
// The old range is completely within the new range. Discard the
// larger (new) range, and keep the smaller (old) range.
mergedRanges.add(oldRange);
}
}
}
// Add new ranges that do not represent missing coverage.
if (newRange.count > 0 && !exactMatch) {
mergedRanges.add(newRange);
}
}
oldFn.ranges = ArrayFrom(mergedRanges);
}
function doesRangeEqualOtherRange(range, otherRange) {
return range.startOffset === otherRange.startOffset &&
range.endOffset === otherRange.endOffset;
}
function doesRangeContainOtherRange(range, otherRange) {
return range.startOffset <= otherRange.startOffset &&
range.endOffset >= otherRange.endOffset;
}
module.exports = { setupCoverage, TestCoverage };

View File

@ -0,0 +1,391 @@
'use strict';
const {
ArrayPrototypeForEach,
ArrayPrototypePush,
FunctionPrototypeBind,
PromiseResolve,
PromiseWithResolvers,
SafeMap,
SafePromiseAllReturnVoid,
} = primordials;
const { getCallerLocation } = internalBinding('util');
const {
createHook,
executionAsyncId,
} = require('async_hooks');
const { relative } = require('path');
const {
codes: {
ERR_TEST_FAILURE,
},
} = require('internal/errors');
const { exitCodes: { kGenericUserError } } = internalBinding('errors');
const { kCancelledByParent, Test, Suite } = require('internal/test_runner/test');
const {
parseCommandLine,
reporterScope,
shouldColorizeTestFiles,
setupGlobalSetupTeardownFunctions,
} = require('internal/test_runner/utils');
const { queueMicrotask } = require('internal/process/task_queues');
const { TIMEOUT_MAX } = require('internal/timers');
const { clearInterval, setInterval } = require('timers');
const { bigint: hrtime } = process.hrtime;
const testResources = new SafeMap();
let globalRoot;
let globalSetupExecuted = false;
testResources.set(reporterScope.asyncId(), reporterScope);
function createTestTree(rootTestOptions, globalOptions) {
const buildPhaseDeferred = PromiseWithResolvers();
const isFilteringByName = globalOptions.testNamePatterns ||
globalOptions.testSkipPatterns;
const isFilteringByOnly = (globalOptions.isolation === 'process' || process.env.NODE_TEST_CONTEXT) ?
globalOptions.only : true;
const harness = {
__proto__: null,
buildPromise: buildPhaseDeferred.promise,
buildSuites: [],
isWaitingForBuildPhase: false,
watching: false,
config: globalOptions,
coverage: null,
resetCounters() {
harness.counters = {
__proto__: null,
tests: 0,
failed: 0,
passed: 0,
cancelled: 0,
skipped: 0,
todo: 0,
topLevel: 0,
suites: 0,
};
},
success: true,
counters: null,
shouldColorizeTestFiles: shouldColorizeTestFiles(globalOptions.destinations),
teardown: null,
snapshotManager: null,
isFilteringByName,
isFilteringByOnly,
async runBootstrap() {
if (globalSetupExecuted) {
return PromiseResolve();
}
globalSetupExecuted = true;
const globalSetupFunctions = await setupGlobalSetupTeardownFunctions(
globalOptions.globalSetupPath,
globalOptions.cwd,
);
harness.globalTeardownFunction = globalSetupFunctions.globalTeardownFunction;
if (typeof globalSetupFunctions.globalSetupFunction === 'function') {
return globalSetupFunctions.globalSetupFunction();
}
return PromiseResolve();
},
async waitForBuildPhase() {
if (harness.buildSuites.length > 0) {
await SafePromiseAllReturnVoid(harness.buildSuites);
}
buildPhaseDeferred.resolve();
},
};
harness.resetCounters();
harness.bootstrapPromise = harness.runBootstrap();
globalRoot = new Test({
__proto__: null,
...rootTestOptions,
harness,
name: '<root>',
});
setupProcessState(globalRoot, globalOptions, harness);
globalRoot.startTime = hrtime();
return globalRoot;
}
function createProcessEventHandler(eventName, rootTest) {
return (err) => {
if (rootTest.harness.bootstrapPromise) {
// Something went wrong during the asynchronous portion of bootstrapping
// the test runner. Since the test runner is not setup properly, we can't
// do anything but throw the error.
throw err;
}
const test = testResources.get(executionAsyncId());
// Check if this error is coming from a reporter. If it is, throw it.
if (test === reporterScope) {
throw err;
}
// Check if this error is coming from a test or test hook. If it is, fail the test.
if (!test || test.finished || test.hookType) {
// If the test is already finished or the resource that created the error
// is not mapped to a Test, report this as a top level diagnostic.
let msg;
if (test) {
const name = test.hookType ? `Test hook "${test.hookType}"` : `Test "${test.name}"`;
let locInfo = '';
if (test.loc) {
const relPath = relative(rootTest.config.cwd, test.loc.file);
locInfo = ` at ${relPath}:${test.loc.line}:${test.loc.column}`;
}
msg = `Error: ${name}${locInfo} generated asynchronous ` +
'activity after the test ended. This activity created the error ' +
`"${err}" and would have caused the test to fail, but instead ` +
`triggered an ${eventName} event.`;
} else {
msg = 'Error: A resource generated asynchronous activity after ' +
`the test ended. This activity created the error "${err}" which ` +
`triggered an ${eventName} event, caught by the test runner.`;
}
rootTest.diagnostic(msg);
rootTest.harness.success = false;
process.exitCode = kGenericUserError;
return;
}
test.fail(new ERR_TEST_FAILURE(err, eventName));
test.abortController.abort();
};
}
function configureCoverage(rootTest, globalOptions) {
if (!globalOptions.coverage) {
return null;
}
const { setupCoverage } = require('internal/test_runner/coverage');
try {
return setupCoverage(globalOptions);
} catch (err) {
const msg = `Warning: Code coverage could not be enabled. ${err}`;
rootTest.diagnostic(msg);
rootTest.harness.success = false;
process.exitCode = kGenericUserError;
}
}
function collectCoverage(rootTest, coverage) {
if (!coverage) {
return null;
}
let summary = null;
try {
summary = coverage.summary();
} catch (err) {
rootTest.diagnostic(`Warning: Could not report code coverage. ${err}`);
rootTest.harness.success = false;
process.exitCode = kGenericUserError;
}
try {
coverage.cleanup();
} catch (err) {
rootTest.diagnostic(`Warning: Could not clean up code coverage. ${err}`);
rootTest.harness.success = false;
process.exitCode = kGenericUserError;
}
return summary;
}
function setupProcessState(root, globalOptions) {
const hook = createHook({
__proto__: null,
init(asyncId, type, triggerAsyncId, resource) {
if (resource instanceof Test) {
testResources.set(asyncId, resource);
return;
}
const parent = testResources.get(triggerAsyncId);
if (parent !== undefined) {
testResources.set(asyncId, parent);
}
},
destroy(asyncId) {
testResources.delete(asyncId);
},
});
hook.enable();
const exceptionHandler =
createProcessEventHandler('uncaughtException', root);
const rejectionHandler =
createProcessEventHandler('unhandledRejection', root);
const coverage = configureCoverage(root, globalOptions);
const exitHandler = async (kill) => {
if (root.subtests.length === 0 && (root.hooks.before.length > 0 || root.hooks.after.length > 0)) {
// Run global before/after hooks in case there are no tests
await root.run();
}
if (kill !== true && root.subtestsPromise !== null) {
// Wait for all subtests to finish, but keep the process alive in case
// there are no ref'ed handles left.
const keepAlive = setInterval(() => {}, TIMEOUT_MAX);
await root.subtestsPromise.promise;
clearInterval(keepAlive);
}
root.postRun(new ERR_TEST_FAILURE(
'Promise resolution is still pending but the event loop has already resolved',
kCancelledByParent));
if (root.harness.globalTeardownFunction) {
await root.harness.globalTeardownFunction();
root.harness.globalTeardownFunction = null;
}
hook.disable();
process.removeListener('uncaughtException', exceptionHandler);
process.removeListener('unhandledRejection', rejectionHandler);
process.removeListener('beforeExit', exitHandler);
if (globalOptions.isTestRunner) {
process.removeListener('SIGINT', terminationHandler);
process.removeListener('SIGTERM', terminationHandler);
}
};
const terminationHandler = async () => {
await exitHandler(true);
process.exit();
};
process.on('uncaughtException', exceptionHandler);
process.on('unhandledRejection', rejectionHandler);
process.on('beforeExit', exitHandler);
// TODO(MoLow): Make it configurable to hook when isTestRunner === false.
if (globalOptions.isTestRunner) {
process.on('SIGINT', terminationHandler);
process.on('SIGTERM', terminationHandler);
}
root.harness.coverage = FunctionPrototypeBind(collectCoverage, null, root, coverage);
root.harness.teardown = exitHandler;
}
function lazyBootstrapRoot() {
if (!globalRoot) {
// This is where the test runner is bootstrapped when node:test is used
// without the --test flag or the run() API.
const entryFile = process.argv?.[1];
const rootTestOptions = {
__proto__: null,
entryFile,
loc: entryFile ? [1, 1, entryFile] : undefined,
};
const globalOptions = parseCommandLine();
globalOptions.cwd = process.cwd();
createTestTree(rootTestOptions, globalOptions);
globalRoot.reporter.on('test:summary', (data) => {
if (!data.success) {
process.exitCode = kGenericUserError;
}
});
globalRoot.harness.bootstrapPromise = SafePromiseAllReturnVoid([
globalRoot.harness.bootstrapPromise,
globalOptions.setup(globalRoot.reporter),
]);
}
return globalRoot;
}
async function startSubtestAfterBootstrap(subtest) {
if (subtest.root.harness.buildPromise) {
if (subtest.root.harness.bootstrapPromise) {
await subtest.root.harness.bootstrapPromise;
subtest.root.harness.bootstrapPromise = null;
}
if (subtest.buildSuite) {
ArrayPrototypePush(subtest.root.harness.buildSuites, subtest.buildSuite);
}
if (!subtest.root.harness.isWaitingForBuildPhase) {
subtest.root.harness.isWaitingForBuildPhase = true;
queueMicrotask(() => {
subtest.root.harness.waitForBuildPhase();
});
}
await subtest.root.harness.buildPromise;
subtest.root.harness.buildPromise = null;
}
await subtest.start();
}
function runInParentContext(Factory) {
function run(name, options, fn, overrides) {
const parent = testResources.get(executionAsyncId()) || lazyBootstrapRoot();
const subtest = parent.createSubtest(Factory, name, options, fn, overrides);
if (parent instanceof Suite) {
return;
}
startSubtestAfterBootstrap(subtest);
}
const test = (name, options, fn) => {
const overrides = {
__proto__: null,
loc: getCallerLocation(),
};
run(name, options, fn, overrides);
};
ArrayPrototypeForEach(['skip', 'todo', 'only'], (keyword) => {
test[keyword] = (name, options, fn) => {
const overrides = {
__proto__: null,
[keyword]: true,
loc: getCallerLocation(),
};
run(name, options, fn, overrides);
};
});
return test;
}
function hook(hook) {
return (fn, options) => {
const parent = testResources.get(executionAsyncId()) || lazyBootstrapRoot();
parent.createHook(hook, fn, {
__proto__: null,
...options,
parent,
hookType: hook,
loc: getCallerLocation(),
});
};
}
module.exports = {
createTestTree,
test: runInParentContext(Test),
suite: runInParentContext(Suite),
before: hook('before'),
after: hook('after'),
beforeEach: hook('beforeEach'),
afterEach: hook('afterEach'),
startSubtestAfterBootstrap,
};

View File

@ -0,0 +1,208 @@
'use strict';
const {
JSONStringify,
SafeMap,
globalThis: {
Atomics: {
notify: AtomicsNotify,
store: AtomicsStore,
},
},
} = primordials;
const {
kBadExportsMessage,
kMockSearchParam,
kMockSuccess,
kMockExists,
kMockUnknownMessage,
} = require('internal/test_runner/mock/mock');
const { URL, URLParse } = require('internal/url');
let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
debug = fn;
});
// TODO(cjihrig): The mocks need to be thread aware because the exports are
// evaluated on the thread that creates the mock. Before marking this API as
// stable, one of the following issues needs to be implemented:
// https://github.com/nodejs/node/issues/49472
// or https://github.com/nodejs/node/issues/52219
const mocks = new SafeMap();
async function initialize(data) {
data?.port.on('message', ({ type, payload }) => {
debug('mock loader received message type "%s" with payload %o', type, payload);
if (type === 'node:test:register') {
const { baseURL } = payload;
const mock = mocks.get(baseURL);
if (mock?.active) {
debug('already mocking "%s"', baseURL);
sendAck(payload.ack, kMockExists);
return;
}
const localVersion = mock?.localVersion ?? 0;
debug('new mock version %d for "%s"', localVersion, baseURL);
mocks.set(baseURL, {
__proto__: null,
active: true,
cache: payload.cache,
exportNames: payload.exportNames,
format: payload.format,
hasDefaultExport: payload.hasDefaultExport,
localVersion,
url: baseURL,
});
sendAck(payload.ack);
} else if (type === 'node:test:unregister') {
const mock = mocks.get(payload.baseURL);
if (mock !== undefined) {
mock.active = false;
mock.localVersion++;
}
sendAck(payload.ack);
} else {
sendAck(payload.ack, kMockUnknownMessage);
}
});
}
async function resolve(specifier, context, nextResolve) {
debug('resolve hook entry, specifier = "%s", context = %o', specifier, context);
const nextResolveResult = await nextResolve(specifier, context);
const mockSpecifier = nextResolveResult.url;
const mock = mocks.get(mockSpecifier);
debug('resolve hook, specifier = "%s", mock = %o', specifier, mock);
if (mock?.active !== true) {
return nextResolveResult;
}
const url = new URL(mockSpecifier);
url.searchParams.set(kMockSearchParam, mock.localVersion);
if (!mock.cache) {
// With ESM, we can't remove modules from the cache. Bump the module's
// version instead so that the next import will be uncached.
mock.localVersion++;
}
const { href } = url;
debug('resolve hook finished, url = "%s"', href);
return { __proto__: null, url: href, format: nextResolveResult.format };
}
async function load(url, context, nextLoad) {
debug('load hook entry, url = "%s", context = %o', url, context);
const parsedURL = URLParse(url);
if (parsedURL) {
parsedURL.searchParams.delete(kMockSearchParam);
}
const baseURL = parsedURL ? parsedURL.href : url;
const mock = mocks.get(baseURL);
const original = await nextLoad(url, context);
debug('load hook, mock = %o', mock);
if (mock?.active !== true) {
return original;
}
// Treat builtins as commonjs because customization hooks do not allow a
// core module to be replaced.
// Also collapse 'commonjs-sync' and 'require-commonjs' to 'commonjs'.
let format = original.format;
switch (original.format) {
case 'builtin': // Deliberate fallthrough
case 'commonjs-sync': // Deliberate fallthrough
case 'require-commonjs':
format = 'commonjs';
break;
case 'json':
format = 'module';
break;
}
const result = {
__proto__: null,
format,
shortCircuit: true,
source: await createSourceFromMock(mock, format),
};
debug('load hook finished, result = %o', result);
return result;
}
async function createSourceFromMock(mock, format) {
// Create mock implementation from provided exports.
const { exportNames, hasDefaultExport, url } = mock;
const useESM = format === 'module' || format === 'module-typescript';
const source = `${testImportSource(useESM)}
if (!$__test.mock._mockExports.has(${JSONStringify(url)})) {
throw new Error(${JSONStringify(`mock exports not found for "${url}"`)});
}
const $__exports = $__test.mock._mockExports.get(${JSONStringify(url)});
${defaultExportSource(useESM, hasDefaultExport)}
${namedExportsSource(useESM, exportNames)}
`;
return source;
}
function testImportSource(useESM) {
if (useESM) {
return "import $__test from 'node:test';";
}
return "const $__test = require('node:test');";
}
function defaultExportSource(useESM, hasDefaultExport) {
if (!hasDefaultExport) {
return '';
} else if (useESM) {
return 'export default $__exports.defaultExport;';
}
return 'module.exports = $__exports.defaultExport;';
}
function namedExportsSource(useESM, exportNames) {
let source = '';
if (!useESM && exportNames.length > 0) {
source += `
if (module.exports === null || typeof module.exports !== 'object') {
throw new Error('${JSONStringify(kBadExportsMessage)}');
}
`;
}
for (let i = 0; i < exportNames.length; ++i) {
const name = exportNames[i];
if (useESM) {
source += `export let ${name} = $__exports.namedExports[${JSONStringify(name)}];\n`;
} else {
source += `module.exports[${JSONStringify(name)}] = $__exports.namedExports[${JSONStringify(name)}];\n`;
}
}
return source;
}
function sendAck(buf, status = kMockSuccess) {
AtomicsStore(buf, 0, status);
AtomicsNotify(buf, 0);
}
module.exports = { initialize, load, resolve };

View File

@ -0,0 +1,811 @@
'use strict';
const {
ArrayPrototypePush,
ArrayPrototypeSlice,
Error,
FunctionPrototypeBind,
FunctionPrototypeCall,
Int32Array,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
ObjectGetPrototypeOf,
ObjectKeys,
Proxy,
ReflectApply,
ReflectConstruct,
ReflectGet,
SafeMap,
StringPrototypeSlice,
StringPrototypeStartsWith,
globalThis: {
Atomics: {
store: AtomicsStore,
wait: AtomicsWait,
},
SharedArrayBuffer,
},
} = primordials;
const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_STATE,
},
} = require('internal/errors');
const esmLoader = require('internal/modules/esm/loader');
const { getOptionValue } = require('internal/options');
const {
fileURLToPath,
isURL,
pathToFileURL,
toPathIfFileURL,
URL,
} = require('internal/url');
const {
emitExperimentalWarning,
getStructuredStack,
kEmptyObject,
} = require('internal/util');
let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
debug = fn;
});
const {
validateBoolean,
validateFunction,
validateInteger,
validateObject,
validateOneOf,
} = require('internal/validators');
const { MockTimers } = require('internal/test_runner/mock/mock_timers');
const { strictEqual, notStrictEqual } = require('assert');
const { Module } = require('internal/modules/cjs/loader');
const { MessageChannel } = require('worker_threads');
const { _load, _nodeModulePaths, _resolveFilename, isBuiltin } = Module;
function kDefaultFunction() {}
const enableModuleMocking = getOptionValue('--experimental-test-module-mocks');
const kMockSearchParam = 'node-test-mock';
const kMockSuccess = 1;
const kMockExists = 2;
const kMockUnknownMessage = 3;
const kWaitTimeout = 5_000;
const kBadExportsMessage = 'Cannot create mock because named exports ' +
'cannot be applied to the provided default export.';
const kSupportedFormats = [
'builtin',
'commonjs-typescript',
'commonjs',
'json',
'module-typescript',
'module',
];
let sharedModuleState;
class MockFunctionContext {
#calls;
#mocks;
#implementation;
#restore;
#times;
constructor(implementation, restore, times) {
this.#calls = [];
this.#mocks = new SafeMap();
this.#implementation = implementation;
this.#restore = restore;
this.#times = times;
}
/**
* Gets an array of recorded calls made to the mock function.
* @returns {Array} An array of recorded calls.
*/
get calls() {
return ArrayPrototypeSlice(this.#calls, 0);
}
/**
* Retrieves the number of times the mock function has been called.
* @returns {number} The call count.
*/
callCount() {
return this.#calls.length;
}
/**
* Sets a new implementation for the mock function.
* @param {Function} implementation - The new implementation for the mock function.
*/
mockImplementation(implementation) {
validateFunction(implementation, 'implementation');
this.#implementation = implementation;
}
/**
* Replaces the implementation of the function only once.
* @param {Function} implementation - The substitute function.
* @param {number} [onCall] - The call index to be replaced.
*/
mockImplementationOnce(implementation, onCall) {
validateFunction(implementation, 'implementation');
const nextCall = this.#calls.length;
const call = onCall ?? nextCall;
validateInteger(call, 'onCall', nextCall);
this.#mocks.set(call, implementation);
}
/**
* Restores the original function that was mocked.
*/
restore() {
const { descriptor, object, original, methodName } = this.#restore;
if (typeof methodName === 'string') {
// This is an object method spy.
ObjectDefineProperty(object, methodName, descriptor);
} else {
// This is a bare function spy. There isn't much to do here but make
// the mock call the original function.
this.#implementation = original;
}
}
/**
* Resets the recorded calls to the mock function
*/
resetCalls() {
this.#calls = [];
}
/**
* Tracks a call made to the mock function.
* @param {object} call - The call details.
*/
trackCall(call) {
ArrayPrototypePush(this.#calls, call);
}
/**
* Gets the next implementation to use for the mock function.
* @returns {Function} The next implementation.
*/
nextImpl() {
const nextCall = this.#calls.length;
const mock = this.#mocks.get(nextCall);
const impl = mock ?? this.#implementation;
if (nextCall + 1 === this.#times) {
this.restore();
}
this.#mocks.delete(nextCall);
return impl;
}
}
const {
nextImpl,
restore: restoreFn,
trackCall,
} = MockFunctionContext.prototype;
delete MockFunctionContext.prototype.trackCall;
delete MockFunctionContext.prototype.nextImpl;
class MockModuleContext {
#restore;
#sharedState;
constructor({
baseURL,
cache,
caller,
defaultExport,
format,
fullPath,
hasDefaultExport,
namedExports,
sharedState,
}) {
const ack = new Int32Array(new SharedArrayBuffer(4));
const config = {
__proto__: null,
cache,
defaultExport,
hasDefaultExport,
namedExports,
caller: toPathIfFileURL(caller),
};
sharedState.mockMap.set(baseURL, config);
sharedState.mockMap.set(fullPath, config);
this.#sharedState = sharedState;
this.#restore = {
__proto__: null,
ack,
baseURL,
cached: fullPath in Module._cache,
format,
fullPath,
value: Module._cache[fullPath],
};
sharedState.loaderPort.postMessage({
__proto__: null,
type: 'node:test:register',
payload: {
__proto__: null,
ack,
baseURL,
cache,
exportNames: ObjectKeys(namedExports),
hasDefaultExport,
format,
},
});
waitForAck(ack);
delete Module._cache[fullPath];
sharedState.mockExports.set(baseURL, {
__proto__: null,
defaultExport,
namedExports,
});
}
restore() {
if (this.#restore === undefined) {
return;
}
// Delete the mock CJS cache entry. If the module was previously in the
// cache then restore the old value.
delete Module._cache[this.#restore.fullPath];
if (this.#restore.cached) {
Module._cache[this.#restore.fullPath] = this.#restore.value;
}
AtomicsStore(this.#restore.ack, 0, 0);
this.#sharedState.loaderPort.postMessage({
__proto__: null,
type: 'node:test:unregister',
payload: {
__proto__: null,
ack: this.#restore.ack,
baseURL: this.#restore.baseURL,
},
});
waitForAck(this.#restore.ack);
this.#sharedState.mockMap.delete(this.#restore.baseURL);
this.#sharedState.mockMap.delete(this.#restore.fullPath);
this.#restore = undefined;
}
}
const { restore: restoreModule } = MockModuleContext.prototype;
class MockTracker {
#mocks = [];
#timers;
/**
* Returns the mock timers of this MockTracker instance.
* @returns {MockTimers} The mock timers instance.
*/
get timers() {
this.#timers ??= new MockTimers();
return this.#timers;
}
/**
* Creates a mock function tracker.
* @param {Function} [original] - The original function to be tracked.
* @param {Function} [implementation] - An optional replacement function for the original one.
* @param {object} [options] - Additional tracking options.
* @param {number} [options.times=Infinity] - The maximum number of times the mock function can be called.
* @returns {ProxyConstructor} The mock function tracker.
*/
fn(
original = function() {},
implementation = original,
options = kEmptyObject,
) {
if (original !== null && typeof original === 'object') {
options = original;
original = function() {};
implementation = original;
} else if (implementation !== null && typeof implementation === 'object') {
options = implementation;
implementation = original;
}
validateFunction(original, 'original');
validateFunction(implementation, 'implementation');
validateObject(options, 'options');
const { times = Infinity } = options;
validateTimes(times, 'options.times');
const ctx = new MockFunctionContext(implementation, { __proto__: null, original }, times);
return this.#setupMock(ctx, original);
}
/**
* Creates a method tracker for a specified object or function.
* @param {(object | Function)} objectOrFunction - The object or function containing the method to be tracked.
* @param {string} methodName - The name of the method to be tracked.
* @param {Function} [implementation] - An optional replacement function for the original method.
* @param {object} [options] - Additional tracking options.
* @param {boolean} [options.getter=false] - Indicates whether this is a getter method.
* @param {boolean} [options.setter=false] - Indicates whether this is a setter method.
* @param {number} [options.times=Infinity] - The maximum number of times the mock method can be called.
* @returns {ProxyConstructor} The mock method tracker.
*/
method(
objectOrFunction,
methodName,
implementation = kDefaultFunction,
options = kEmptyObject,
) {
validateStringOrSymbol(methodName, 'methodName');
if (typeof objectOrFunction !== 'function') {
validateObject(objectOrFunction, 'object');
}
if (implementation !== null && typeof implementation === 'object') {
options = implementation;
implementation = kDefaultFunction;
}
validateFunction(implementation, 'implementation');
validateObject(options, 'options');
const {
getter = false,
setter = false,
times = Infinity,
} = options;
validateBoolean(getter, 'options.getter');
validateBoolean(setter, 'options.setter');
validateTimes(times, 'options.times');
if (setter && getter) {
throw new ERR_INVALID_ARG_VALUE(
'options.setter', setter, "cannot be used with 'options.getter'",
);
}
const descriptor = findMethodOnPrototypeChain(objectOrFunction, methodName);
let original;
if (getter) {
original = descriptor?.get;
} else if (setter) {
original = descriptor?.set;
} else {
original = descriptor?.value;
}
if (typeof original !== 'function') {
throw new ERR_INVALID_ARG_VALUE(
'methodName', original, 'must be a method',
);
}
const restore = { __proto__: null, descriptor, object: objectOrFunction, methodName };
const impl = implementation === kDefaultFunction ?
original : implementation;
const ctx = new MockFunctionContext(impl, restore, times);
const mock = this.#setupMock(ctx, original);
const mockDescriptor = {
__proto__: null,
configurable: descriptor.configurable,
enumerable: descriptor.enumerable,
};
if (getter) {
mockDescriptor.get = mock;
mockDescriptor.set = descriptor.set;
} else if (setter) {
mockDescriptor.get = descriptor.get;
mockDescriptor.set = mock;
} else {
mockDescriptor.writable = descriptor.writable;
mockDescriptor.value = mock;
}
ObjectDefineProperty(objectOrFunction, methodName, mockDescriptor);
return mock;
}
/**
* Mocks a getter method of an object.
* This is a syntax sugar for the MockTracker.method with options.getter set to true
* @param {object} object - The target object.
* @param {string} methodName - The name of the getter method to be mocked.
* @param {Function} [implementation] - An optional replacement function for the targeted method.
* @param {object} [options] - Additional tracking options.
* @param {boolean} [options.getter=true] - Indicates whether this is a getter method.
* @param {boolean} [options.setter=false] - Indicates whether this is a setter method.
* @param {number} [options.times=Infinity] - The maximum number of times the mock method can be called.
* @returns {ProxyConstructor} The mock method tracker.
*/
getter(
object,
methodName,
implementation = kDefaultFunction,
options = kEmptyObject,
) {
if (implementation !== null && typeof implementation === 'object') {
options = implementation;
implementation = kDefaultFunction;
} else {
validateObject(options, 'options');
}
const { getter = true } = options;
if (getter === false) {
throw new ERR_INVALID_ARG_VALUE(
'options.getter', getter, 'cannot be false',
);
}
return this.method(object, methodName, implementation, {
__proto__: null,
...options,
getter,
});
}
/**
* Mocks a setter method of an object.
* This function is a syntax sugar for MockTracker.method with options.setter set to true.
* @param {object} object - The target object.
* @param {string} methodName - The setter method to be mocked.
* @param {Function} [implementation] - An optional replacement function for the targeted method.
* @param {object} [options] - Additional tracking options.
* @param {boolean} [options.getter=false] - Indicates whether this is a getter method.
* @param {boolean} [options.setter=true] - Indicates whether this is a setter method.
* @param {number} [options.times=Infinity] - The maximum number of times the mock method can be called.
* @returns {ProxyConstructor} The mock method tracker.
*/
setter(
object,
methodName,
implementation = kDefaultFunction,
options = kEmptyObject,
) {
if (implementation !== null && typeof implementation === 'object') {
options = implementation;
implementation = kDefaultFunction;
} else {
validateObject(options, 'options');
}
const { setter = true } = options;
if (setter === false) {
throw new ERR_INVALID_ARG_VALUE(
'options.setter', setter, 'cannot be false',
);
}
return this.method(object, methodName, implementation, {
__proto__: null,
...options,
setter,
});
}
module(specifier, options = kEmptyObject) {
emitExperimentalWarning('Module mocking');
if (typeof specifier !== 'string') {
if (!isURL(specifier))
throw new ERR_INVALID_ARG_TYPE('specifier', ['string', 'URL'], specifier);
specifier = `${specifier}`;
}
validateObject(options, 'options');
debug('module mock entry, specifier = "%s", options = %o', specifier, options);
const {
cache = false,
namedExports = kEmptyObject,
defaultExport,
} = options;
const hasDefaultExport = 'defaultExport' in options;
validateBoolean(cache, 'options.cache');
validateObject(namedExports, 'options.namedExports');
const sharedState = setupSharedModuleState();
const mockSpecifier = StringPrototypeStartsWith(specifier, 'node:') ?
StringPrototypeSlice(specifier, 5) : specifier;
// Get the file that called this function. We need four stack frames:
// vm context -> getStructuredStack() -> this function -> actual caller.
const filename = getStructuredStack()[3]?.getFileName();
// If the caller is already a file URL, use it as is. Otherwise, convert it.
const hasFileProtocol = StringPrototypeStartsWith(filename, 'file://');
const caller = hasFileProtocol ? filename : pathToFileURL(filename).href;
const { format, url } = sharedState.moduleLoader.resolveSync(
mockSpecifier, caller, null,
);
debug('module mock, url = "%s", format = "%s", caller = "%s"', url, format, caller);
if (format) { // Format is not yet known for ambiguous files when detection is enabled.
validateOneOf(format, 'format', kSupportedFormats);
}
const baseURL = URL.parse(url);
if (!baseURL) {
throw new ERR_INVALID_ARG_VALUE(
'specifier', specifier, 'cannot compute URL',
);
}
if (baseURL.searchParams.has(kMockSearchParam)) {
throw new ERR_INVALID_STATE(
`Cannot mock '${specifier}.' The module is already mocked.`,
);
}
const fullPath = StringPrototypeStartsWith(url, 'file://') ?
fileURLToPath(url) : null;
const ctx = new MockModuleContext({
__proto__: null,
baseURL: baseURL.href,
cache,
caller,
defaultExport,
format,
fullPath,
hasDefaultExport,
namedExports,
sharedState,
specifier: mockSpecifier,
});
ArrayPrototypePush(this.#mocks, {
__proto__: null,
ctx,
restore: restoreModule,
});
return ctx;
}
/**
* Resets the mock tracker, restoring all mocks and clearing timers.
*/
reset() {
this.restoreAll();
this.#timers?.reset();
this.#mocks = [];
}
/**
* Restore all mocks created by this MockTracker instance.
*/
restoreAll() {
for (let i = 0; i < this.#mocks.length; i++) {
const { ctx, restore } = this.#mocks[i];
FunctionPrototypeCall(restore, ctx);
}
}
#setupMock(ctx, fnToMatch) {
const mock = new Proxy(fnToMatch, {
__proto__: null,
apply(_fn, thisArg, argList) {
const fn = FunctionPrototypeCall(nextImpl, ctx);
let result;
let error;
try {
result = ReflectApply(fn, thisArg, argList);
} catch (err) {
error = err;
throw err;
} finally {
FunctionPrototypeCall(trackCall, ctx, {
__proto__: null,
arguments: argList,
error,
result,
// eslint-disable-next-line no-restricted-syntax
stack: new Error(),
target: undefined,
this: thisArg,
});
}
return result;
},
construct(target, argList, newTarget) {
const realTarget = FunctionPrototypeCall(nextImpl, ctx);
let result;
let error;
try {
result = ReflectConstruct(realTarget, argList, newTarget);
} catch (err) {
error = err;
throw err;
} finally {
FunctionPrototypeCall(trackCall, ctx, {
__proto__: null,
arguments: argList,
error,
result,
// eslint-disable-next-line no-restricted-syntax
stack: new Error(),
target,
this: result,
});
}
return result;
},
get(target, property, receiver) {
if (property === 'mock') {
return ctx;
}
return ReflectGet(target, property, receiver);
},
});
ArrayPrototypePush(this.#mocks, {
__proto__: null,
ctx,
restore: restoreFn,
});
return mock;
}
}
function setupSharedModuleState() {
if (sharedModuleState === undefined) {
const { mock } = require('test');
const mockExports = new SafeMap();
const { port1, port2 } = new MessageChannel();
const moduleLoader = esmLoader.getOrInitializeCascadedLoader();
moduleLoader.register(
'internal/test_runner/mock/loader',
'node:',
{ __proto__: null, port: port2 },
[port2],
true,
);
sharedModuleState = {
__proto__: null,
loaderPort: port1,
mockExports,
mockMap: new SafeMap(),
moduleLoader,
};
mock._mockExports = mockExports;
Module._load = FunctionPrototypeBind(cjsMockModuleLoad, sharedModuleState);
}
return sharedModuleState;
}
function cjsMockModuleLoad(request, parent, isMain) {
let resolved;
if (isBuiltin(request)) {
resolved = ensureNodeScheme(request);
} else {
resolved = _resolveFilename(request, parent, isMain);
}
const config = this.mockMap.get(resolved);
if (config === undefined) {
return _load(request, parent, isMain);
}
const {
cache,
caller,
defaultExport,
hasDefaultExport,
namedExports,
} = config;
if (cache && Module._cache[resolved]) {
// The CJS cache entry is deleted when the mock is configured. If it has
// been repopulated, return the exports from that entry.
return Module._cache[resolved].exports;
}
// eslint-disable-next-line node-core/set-proto-to-null-in-object
const modExports = hasDefaultExport ? defaultExport : {};
const exportNames = ObjectKeys(namedExports);
if ((typeof modExports !== 'object' || modExports === null) &&
exportNames.length > 0) {
// eslint-disable-next-line no-restricted-syntax
throw new Error(kBadExportsMessage);
}
for (let i = 0; i < exportNames.length; ++i) {
const name = exportNames[i];
const descriptor = ObjectGetOwnPropertyDescriptor(namedExports, name);
ObjectDefineProperty(modExports, name, descriptor);
}
if (cache) {
const entry = new Module(resolved, caller);
entry.exports = modExports;
entry.filename = resolved;
entry.loaded = true;
entry.paths = _nodeModulePaths(entry.path);
Module._cache[resolved] = entry;
}
return modExports;
}
function validateStringOrSymbol(value, name) {
if (typeof value !== 'string' && typeof value !== 'symbol') {
throw new ERR_INVALID_ARG_TYPE(name, ['string', 'symbol'], value);
}
}
function validateTimes(value, name) {
if (value === Infinity) {
return;
}
validateInteger(value, name, 1);
}
function findMethodOnPrototypeChain(instance, methodName) {
let host = instance;
let descriptor;
while (host !== null) {
descriptor = ObjectGetOwnPropertyDescriptor(host, methodName);
if (descriptor) {
break;
}
host = ObjectGetPrototypeOf(host);
}
return descriptor;
}
function waitForAck(buf) {
const result = AtomicsWait(buf, 0, 0, kWaitTimeout);
notStrictEqual(result, 'timed-out', 'test mocking synchronization failed');
strictEqual(buf[0], kMockSuccess);
}
function ensureNodeScheme(specifier) {
if (!StringPrototypeStartsWith(specifier, 'node:')) {
return `node:${specifier}`;
}
return specifier;
}
if (!enableModuleMocking) {
delete MockTracker.prototype.module;
}
module.exports = {
ensureNodeScheme,
kBadExportsMessage,
kMockSearchParam,
kMockSuccess,
kMockExists,
kMockUnknownMessage,
MockTracker,
};

View File

@ -0,0 +1,796 @@
'use strict';
const {
ArrayPrototypeAt,
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
DatePrototypeGetTime,
DatePrototypeToString,
FunctionPrototypeApply,
FunctionPrototypeBind,
FunctionPrototypeToString,
NumberIsNaN,
ObjectDefineProperties,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
ObjectGetOwnPropertyDescriptors,
Promise,
Symbol,
SymbolAsyncIterator,
SymbolDispose,
globalThis,
} = primordials;
const {
validateAbortSignal,
validateNumber,
validateStringArray,
} = require('internal/validators');
const {
AbortError,
codes: {
ERR_INVALID_ARG_VALUE,
ERR_INVALID_STATE,
},
} = require('internal/errors');
const { TIMEOUT_MAX } = require('internal/timers');
const PriorityQueue = require('internal/priority_queue');
const nodeTimers = require('timers');
const nodeTimersPromises = require('timers/promises');
const EventEmitter = require('events');
let kResistStopPropagation;
// Internal reference to the MockTimers class inside MockDate
let kMock;
// Initial epoch to which #now should be set to
const kInitialEpoch = 0;
function compareTimersLists(a, b) {
return (a.runAt - b.runAt) || (a.id - b.id);
}
function setPosition(node, pos) {
node.priorityQueuePosition = pos;
}
function abortIt(signal) {
return new AbortError(undefined, { __proto__: null, cause: signal.reason });
}
/**
* @enum {('setTimeout'|'setInterval'|'setImmediate'|'Date', 'scheduler.wait')[]} Supported timers
*/
const SUPPORTED_APIS = ['setTimeout', 'setInterval', 'setImmediate', 'Date', 'scheduler.wait'];
const TIMERS_DEFAULT_INTERVAL = {
__proto__: null,
setImmediate: -1,
};
class Timeout {
constructor(opts) {
this.id = opts.id;
this.callback = opts.callback;
this.runAt = opts.runAt;
this.interval = opts.interval;
this.args = opts.args;
}
hasRef() {
return true;
}
ref() {
return this;
}
unref() {
return this;
}
refresh() {
return this;
}
}
class MockTimers {
#realSetTimeout;
#realClearTimeout;
#realSetInterval;
#realClearInterval;
#realSetImmediate;
#realClearImmediate;
#realPromisifiedSetTimeout;
#realPromisifiedSetInterval;
#realTimersPromisifiedSchedulerWait;
#realTimersSetTimeout;
#realTimersClearTimeout;
#realTimersSetInterval;
#realTimersClearInterval;
#realTimersSetImmediate;
#realTimersClearImmediate;
#realPromisifiedSetImmediate;
#nativeDateDescriptor;
#timersInContext = [];
#isEnabled = false;
#currentTimer = 1;
#now = kInitialEpoch;
#executionQueue = new PriorityQueue(compareTimersLists, setPosition);
#setTimeout = FunctionPrototypeBind(this.#createTimer, this, false);
#clearTimeout = FunctionPrototypeBind(this.#clearTimer, this);
#setInterval = FunctionPrototypeBind(this.#createTimer, this, true);
#clearInterval = FunctionPrototypeBind(this.#clearTimer, this);
#clearImmediate = FunctionPrototypeBind(this.#clearTimer, this);
#restoreSetImmediate() {
ObjectDefineProperty(
globalThis,
'setImmediate',
this.#realSetImmediate,
);
ObjectDefineProperty(
globalThis,
'clearImmediate',
this.#realClearImmediate,
);
ObjectDefineProperty(
nodeTimers,
'setImmediate',
this.#realTimersSetImmediate,
);
ObjectDefineProperty(
nodeTimers,
'clearImmediate',
this.#realTimersClearImmediate,
);
ObjectDefineProperty(
nodeTimersPromises,
'setImmediate',
this.#realPromisifiedSetImmediate,
);
}
#restoreOriginalSetInterval() {
ObjectDefineProperty(
globalThis,
'setInterval',
this.#realSetInterval,
);
ObjectDefineProperty(
globalThis,
'clearInterval',
this.#realClearInterval,
);
ObjectDefineProperty(
nodeTimers,
'setInterval',
this.#realTimersSetInterval,
);
ObjectDefineProperty(
nodeTimers,
'clearInterval',
this.#realTimersClearInterval,
);
ObjectDefineProperty(
nodeTimersPromises,
'setInterval',
this.#realPromisifiedSetInterval,
);
}
#restoreOriginalSchedulerWait() {
nodeTimersPromises.scheduler.wait = FunctionPrototypeBind(
this.#realTimersPromisifiedSchedulerWait,
this,
);
}
#restoreOriginalSetTimeout() {
ObjectDefineProperty(
globalThis,
'setTimeout',
this.#realSetTimeout,
);
ObjectDefineProperty(
globalThis,
'clearTimeout',
this.#realClearTimeout,
);
ObjectDefineProperty(
nodeTimers,
'setTimeout',
this.#realTimersSetTimeout,
);
ObjectDefineProperty(
nodeTimers,
'clearTimeout',
this.#realTimersClearTimeout,
);
ObjectDefineProperty(
nodeTimersPromises,
'setTimeout',
this.#realPromisifiedSetTimeout,
);
}
#storeOriginalSetImmediate() {
this.#realSetImmediate = ObjectGetOwnPropertyDescriptor(
globalThis,
'setImmediate',
);
this.#realClearImmediate = ObjectGetOwnPropertyDescriptor(
globalThis,
'clearImmediate',
);
this.#realTimersSetImmediate = ObjectGetOwnPropertyDescriptor(
nodeTimers,
'setImmediate',
);
this.#realTimersClearImmediate = ObjectGetOwnPropertyDescriptor(
nodeTimers,
'clearImmediate',
);
this.#realPromisifiedSetImmediate = ObjectGetOwnPropertyDescriptor(
nodeTimersPromises,
'setImmediate',
);
}
#storeOriginalSetInterval() {
this.#realSetInterval = ObjectGetOwnPropertyDescriptor(
globalThis,
'setInterval',
);
this.#realClearInterval = ObjectGetOwnPropertyDescriptor(
globalThis,
'clearInterval',
);
this.#realTimersSetInterval = ObjectGetOwnPropertyDescriptor(
nodeTimers,
'setInterval',
);
this.#realTimersClearInterval = ObjectGetOwnPropertyDescriptor(
nodeTimers,
'clearInterval',
);
this.#realPromisifiedSetInterval = ObjectGetOwnPropertyDescriptor(
nodeTimersPromises,
'setInterval',
);
}
#storeOriginalSchedulerWait() {
this.#realTimersPromisifiedSchedulerWait = FunctionPrototypeBind(
nodeTimersPromises.scheduler.wait,
this,
);
}
#storeOriginalSetTimeout() {
this.#realSetTimeout = ObjectGetOwnPropertyDescriptor(
globalThis,
'setTimeout',
);
this.#realClearTimeout = ObjectGetOwnPropertyDescriptor(
globalThis,
'clearTimeout',
);
this.#realTimersSetTimeout = ObjectGetOwnPropertyDescriptor(
nodeTimers,
'setTimeout',
);
this.#realTimersClearTimeout = ObjectGetOwnPropertyDescriptor(
nodeTimers,
'clearTimeout',
);
this.#realPromisifiedSetTimeout = ObjectGetOwnPropertyDescriptor(
nodeTimersPromises,
'setTimeout',
);
}
#createTimer(isInterval, callback, delay, ...args) {
if (delay > TIMEOUT_MAX) {
delay = 1;
}
const timerId = this.#currentTimer++;
const opts = {
__proto__: null,
id: timerId,
callback,
runAt: this.#now + delay,
interval: isInterval ? delay : undefined,
args,
};
const timer = new Timeout(opts);
this.#executionQueue.insert(timer);
return timer;
}
#clearTimer(timer) {
if (timer?.priorityQueuePosition !== undefined) {
this.#executionQueue.removeAt(timer.priorityQueuePosition);
timer.priorityQueuePosition = undefined;
}
}
#createDate() {
kMock ??= Symbol('MockTimers');
const NativeDateConstructor = this.#nativeDateDescriptor.value;
if (NativeDateConstructor.isMock) {
throw new ERR_INVALID_STATE('Date is already being mocked!');
}
/**
* Function to mock the Date constructor, treats cases as per ECMA-262
* and returns a Date object with a mocked implementation
* @typedef {Date} MockDate
* @returns {MockDate} a mocked Date object
*/
function MockDate(year, month, date, hours, minutes, seconds, ms) {
const mockTimersSource = MockDate[kMock];
const nativeDate = mockTimersSource.#nativeDateDescriptor.value;
// As of the fake-timers implementation for Sinon
// ref https://github.com/sinonjs/fake-timers/blob/a4c757f80840829e45e0852ea1b17d87a998388e/src/fake-timers-src.js#L456
// This covers the Date constructor called as a function ref.
// ECMA-262 Edition 5.1 section 15.9.2.
// and ECMA-262 Edition 14 Section 21.4.2.1
// replaces 'this instanceof MockDate' with a more reliable check
// from ECMA-262 Edition 14 Section 13.3.12.1 NewTarget
if (!new.target) {
return DatePrototypeToString(new nativeDate(mockTimersSource.#now));
}
// Cases where Date is called as a constructor
// This is intended as a defensive implementation to avoid
// having unexpected returns
switch (arguments.length) {
case 0:
return new nativeDate(MockDate[kMock].#now);
case 1:
return new nativeDate(year);
case 2:
return new nativeDate(year, month);
case 3:
return new nativeDate(year, month, date);
case 4:
return new nativeDate(year, month, date, hours);
case 5:
return new nativeDate(year, month, date, hours, minutes);
case 6:
return new nativeDate(year, month, date, hours, minutes, seconds);
default:
return new nativeDate(year, month, date, hours, minutes, seconds, ms);
}
}
// Prototype is read-only, and non assignable through Object.defineProperties
// eslint-disable-next-line no-unused-vars -- used to get the prototype out of the object
const { prototype, ...dateProps } = ObjectGetOwnPropertyDescriptors(NativeDateConstructor);
// Binds all the properties of Date to the MockDate function
ObjectDefineProperties(
MockDate,
dateProps,
);
MockDate.now = function now() {
return MockDate[kMock].#now;
};
// This is just to print the function { native code } in the console
// when the user prints the function and not the internal code
MockDate.toString = function toString() {
return FunctionPrototypeToString(MockDate[kMock].#nativeDateDescriptor.value);
};
// We need to pollute the prototype of this
ObjectDefineProperties(MockDate, {
__proto__: null,
[kMock]: {
__proto__: null,
enumerable: false,
configurable: false,
writable: false,
value: this,
},
isMock: {
__proto__: null,
enumerable: true,
configurable: false,
writable: false,
value: true,
},
});
MockDate.prototype = NativeDateConstructor.prototype;
MockDate.parse = NativeDateConstructor.parse;
MockDate.UTC = NativeDateConstructor.UTC;
MockDate.prototype.toUTCString = NativeDateConstructor.prototype.toUTCString;
return MockDate;
}
async * #setIntervalPromisified(interval, result, options) {
const context = this;
const emitter = new EventEmitter();
if (options?.signal) {
validateAbortSignal(options.signal, 'options.signal');
if (options.signal.aborted) {
throw abortIt(options.signal);
}
const onAbort = (reason) => {
emitter.emit('data', { __proto__: null, aborted: true, reason });
};
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
options.signal.addEventListener('abort', onAbort, {
__proto__: null,
once: true,
[kResistStopPropagation]: true,
});
}
const eventIt = EventEmitter.on(emitter, 'data');
const callback = () => {
emitter.emit('data', result);
};
const timer = this.#createTimer(true, callback, interval, options);
const clearListeners = () => {
emitter.removeAllListeners();
context.#clearTimer(timer);
};
const iterator = {
__proto__: null,
[SymbolAsyncIterator]() {
return this;
},
async next() {
const result = await eventIt.next();
const value = ArrayPrototypeAt(result.value, 0);
if (value?.aborted) {
iterator.return();
throw abortIt(options.signal);
}
return {
__proto__: null,
done: result.done,
value,
};
},
async return() {
clearListeners();
return eventIt.return();
},
};
yield* iterator;
}
#setImmediate(callback, ...args) {
return this.#createTimer(
false,
callback,
TIMERS_DEFAULT_INTERVAL.setImmediate,
...args,
);
}
#promisifyTimer({ timerFn, clearFn, ms, result, options }) {
return new Promise((resolve, reject) => {
if (options?.signal) {
try {
validateAbortSignal(options.signal, 'options.signal');
} catch (err) {
return reject(err);
}
if (options.signal.aborted) {
return reject(abortIt(options.signal));
}
}
const onabort = () => {
clearFn(timer);
return reject(abortIt(options.signal));
};
const timer = timerFn(() => {
return resolve(result);
}, ms);
if (options?.signal) {
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
options.signal.addEventListener('abort', onabort, {
__proto__: null,
once: true,
[kResistStopPropagation]: true,
});
}
});
}
#setImmediatePromisified(result, options) {
return this.#promisifyTimer({
__proto__: null,
timerFn: FunctionPrototypeBind(this.#setImmediate, this),
clearFn: FunctionPrototypeBind(this.#clearImmediate, this),
ms: TIMERS_DEFAULT_INTERVAL.setImmediate,
result,
options,
});
}
#setTimeoutPromisified(ms, result, options) {
return this.#promisifyTimer({
__proto__: null,
timerFn: FunctionPrototypeBind(this.#setTimeout, this),
clearFn: FunctionPrototypeBind(this.#clearTimeout, this),
ms,
result,
options,
});
}
#assertTimersAreEnabled() {
if (!this.#isEnabled) {
throw new ERR_INVALID_STATE(
'You should enable MockTimers first by calling the .enable function',
);
}
}
#assertTimeArg(time) {
if (time < 0) {
throw new ERR_INVALID_ARG_VALUE('time', 'positive integer', time);
}
}
#isValidDateWithGetTime(maybeDate) {
// Validation inspired on https://github.com/inspect-js/is-date-object/blob/main/index.js#L3-L11
try {
DatePrototypeGetTime(maybeDate);
return true;
} catch {
return false;
}
}
#toggleEnableTimers(activate) {
const options = {
__proto__: null,
toFake: {
'__proto__': null,
'scheduler.wait': () => {
this.#storeOriginalSchedulerWait();
nodeTimersPromises.scheduler.wait = (delay, options) =>
this.#setTimeoutPromisified(delay, undefined, options);
},
'setTimeout': () => {
this.#storeOriginalSetTimeout();
globalThis.setTimeout = this.#setTimeout;
globalThis.clearTimeout = this.#clearTimeout;
nodeTimers.setTimeout = this.#setTimeout;
nodeTimers.clearTimeout = this.#clearTimeout;
nodeTimersPromises.setTimeout = FunctionPrototypeBind(
this.#setTimeoutPromisified,
this,
);
},
'setInterval': () => {
this.#storeOriginalSetInterval();
globalThis.setInterval = this.#setInterval;
globalThis.clearInterval = this.#clearInterval;
nodeTimers.setInterval = this.#setInterval;
nodeTimers.clearInterval = this.#clearInterval;
nodeTimersPromises.setInterval = FunctionPrototypeBind(
this.#setIntervalPromisified,
this,
);
},
'setImmediate': () => {
this.#storeOriginalSetImmediate();
// setImmediate functions needs to bind MockTimers
// otherwise it will throw an error when called
// "Receiver must be an instance of MockTimers"
// because #setImmediate is the only function here
// that calls #createTimer and it's not bound to MockTimers
globalThis.setImmediate = FunctionPrototypeBind(
this.#setImmediate,
this,
);
globalThis.clearImmediate = this.#clearImmediate;
nodeTimers.setImmediate = FunctionPrototypeBind(
this.#setImmediate,
this,
);
nodeTimers.clearImmediate = this.#clearImmediate;
nodeTimersPromises.setImmediate = FunctionPrototypeBind(
this.#setImmediatePromisified,
this,
);
},
'Date': () => {
this.#nativeDateDescriptor = ObjectGetOwnPropertyDescriptor(globalThis, 'Date');
globalThis.Date = this.#createDate();
},
},
toReal: {
'__proto__': null,
'scheduler.wait': () => {
this.#restoreOriginalSchedulerWait();
},
'setTimeout': () => {
this.#restoreOriginalSetTimeout();
},
'setInterval': () => {
this.#restoreOriginalSetInterval();
},
'setImmediate': () => {
this.#restoreSetImmediate();
},
'Date': () => {
ObjectDefineProperty(globalThis, 'Date', this.#nativeDateDescriptor);
},
},
};
const target = activate ? options.toFake : options.toReal;
ArrayPrototypeForEach(this.#timersInContext, (timer) => target[timer]());
this.#isEnabled = activate;
}
/**
* Advances the virtual time of MockTimers by the specified duration (in milliseconds).
* This method simulates the passage of time and triggers any scheduled timers that are due.
* @param {number} [time=1] - The amount of time (in milliseconds) to advance the virtual time.
* @throws {ERR_INVALID_STATE} If MockTimers are not enabled.
* @throws {ERR_INVALID_ARG_VALUE} If a negative time value is provided.
*/
tick(time = 1) {
this.#assertTimersAreEnabled();
this.#assertTimeArg(time);
this.#now += time;
let timer = this.#executionQueue.peek();
while (timer) {
if (timer.runAt > this.#now) break;
FunctionPrototypeApply(timer.callback, undefined, timer.args);
// Check if the timeout was cleared by calling clearTimeout inside its own callback
const afterCallback = this.#executionQueue.peek();
if (afterCallback?.id === timer.id) {
this.#executionQueue.shift();
timer.priorityQueuePosition = undefined;
}
if (timer.interval !== undefined) {
timer.runAt += timer.interval;
this.#executionQueue.insert(timer);
}
timer = this.#executionQueue.peek();
}
}
/**
* @typedef {{apis: SUPPORTED_APIS;now: number | Date;}} EnableOptions Options to enable the timers
* @property {SUPPORTED_APIS} apis List of timers to enable, defaults to all
* @property {number | Date} now The epoch to which the timers should be set to, defaults to 0
*/
/**
* Enables the MockTimers replacing the native timers with the fake ones.
* @param {EnableOptions} [options]
*/
enable(options = { __proto__: null, apis: SUPPORTED_APIS, now: 0 }) {
const internalOptions = { __proto__: null, ...options };
if (this.#isEnabled) {
throw new ERR_INVALID_STATE('MockTimers is already enabled!');
}
if (NumberIsNaN(internalOptions.now)) {
throw new ERR_INVALID_ARG_VALUE('now', internalOptions.now, `epoch must be a positive integer received ${internalOptions.now}`);
}
internalOptions.now ||= 0;
internalOptions.apis ||= SUPPORTED_APIS;
validateStringArray(internalOptions.apis, 'options.apis');
// Check that the timers passed are supported
ArrayPrototypeForEach(internalOptions.apis, (timer) => {
if (!ArrayPrototypeIncludes(SUPPORTED_APIS, timer)) {
throw new ERR_INVALID_ARG_VALUE(
'options.apis',
timer,
`option ${timer} is not supported`,
);
}
});
this.#timersInContext = internalOptions.apis;
// Checks if the second argument is the initial time
if (this.#isValidDateWithGetTime(internalOptions.now)) {
this.#now = DatePrototypeGetTime(internalOptions.now);
} else if (validateNumber(internalOptions.now, 'initialTime') === undefined) {
this.#assertTimeArg(internalOptions.now);
this.#now = internalOptions.now;
}
this.#toggleEnableTimers(true);
}
/**
* Sets the current time to the given epoch.
* @param {number} time The epoch to set the current time to.
*/
setTime(time = kInitialEpoch) {
validateNumber(time, 'time');
this.#assertTimeArg(time);
this.#assertTimersAreEnabled();
this.#now = time;
}
/**
* An alias for `this.reset()`, allowing the disposal of the `MockTimers` instance.
*/
[SymbolDispose]() {
this.reset();
}
/**
* Resets MockTimers, disabling any enabled timers and clearing the execution queue.
* Does nothing if MockTimers are not enabled.
*/
reset() {
// Ignore if not enabled
if (!this.#isEnabled) return;
this.#toggleEnableTimers(false);
this.#timersInContext = [];
this.#now = kInitialEpoch;
let timer = this.#executionQueue.peek();
while (timer) {
this.#executionQueue.shift();
timer = this.#executionQueue.peek();
}
}
/**
* Runs all scheduled timers until there are no more pending timers.
* @throws {ERR_INVALID_STATE} If MockTimers are not enabled.
*/
runAll() {
this.#assertTimersAreEnabled();
const longestTimer = this.#executionQueue.peekBottom();
if (!longestTimer) return;
this.tick(longestTimer.runAt - this.#now);
}
}
module.exports = { MockTimers };

View File

@ -0,0 +1,40 @@
'use strict';
const {
ArrayPrototypePush,
MathMax,
} = primordials;
const colors = require('internal/util/colors');
const { formatTestReport } = require('internal/test_runner/reporter/utils');
module.exports = 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(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);
}

View File

@ -0,0 +1,160 @@
'use strict';
const {
ArrayPrototypeFilter,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypeSome,
NumberPrototypeToFixed,
ObjectEntries,
RegExpPrototypeSymbolReplace,
String,
StringPrototypeRepeat,
} = primordials;
const { inspectWithNoCustomRetry } = require('internal/errors');
const { hostname } = require('os');
const inspectOptions = { __proto__: null, colors: false, breakLength: Infinity };
const HOSTNAME = hostname();
function escapeAttribute(s = '') {
return escapeContent(RegExpPrototypeSymbolReplace(/"/g, RegExpPrototypeSymbolReplace(/\n/g, s, ''), '&quot;'));
}
function escapeContent(s = '') {
return RegExpPrototypeSymbolReplace(/</g, RegExpPrototypeSymbolReplace(/&/g, s, '&amp;'), '&lt;');
}
function escapeComment(s = '') {
return RegExpPrototypeSymbolReplace(/--/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('\t', nesting + 1);
if (comment) {
return `${indent}<!-- ${escapeComment(comment)} -->\n`;
}
const attrsString = ArrayPrototypeJoin(
ArrayPrototypeMap(
ObjectEntries(attrs),
({ 0: key, 1: value }) => `${key}="${escapeAttribute(String(value))}"`),
' ');
if (!children?.length) {
return `${indent}<${tag} ${attrsString}/>\n`;
}
const childrenString = ArrayPrototypeJoin(ArrayPrototypeMap(children ?? [], treeToXML), '');
return `${indent}<${tag} ${attrsString}>\n${childrenString}${indent}</${tag}>\n`;
}
function isFailure(node) {
return (node?.children && ArrayPrototypeSome(node.children, (c) => c.tag === 'failure')) || node?.attrs?.failures;
}
function isSkipped(node) {
return (node?.children && ArrayPrototypeSome(node.children, (c) => c.tag === 'skipped')) || node?.attrs?.failures;
}
module.exports = 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(originalSuite.children, currentSuite);
}
if (!currentSuite.parent) {
ArrayPrototypePush(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(event.data.details.duration_ms / 1000, 6);
const nonCommentChildren = ArrayPrototypeFilter(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(currentTest.children, isFailure).length;
currentTest.attrs.skipped = ArrayPrototypeFilter(currentTest.children, isSkipped).length;
currentTest.attrs.hostname = HOSTNAME;
} else {
currentTest.tag = 'testcase';
currentTest.attrs.classname = event.data.classname ?? 'test';
if (event.data.skip) {
ArrayPrototypePush(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(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(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';
};

View File

@ -0,0 +1,107 @@
'use strict';
const { relative } = require('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);
}
}
module.exports = LcovReporter;

View File

@ -0,0 +1,119 @@
'use strict';
const {
ArrayPrototypeJoin,
ArrayPrototypePop,
ArrayPrototypePush,
ArrayPrototypeShift,
ArrayPrototypeUnshift,
} = primordials;
const assert = require('assert');
const Transform = require('internal/streams/transform');
const colors = require('internal/util/colors');
const { kSubtestsFailed } = require('internal/test_runner/test');
const { getCoverageReport } = require('internal/test_runner/utils');
const { relative } = require('path');
const {
formatTestReport,
indent,
reporterColorMap,
reporterUnicodeSymbolMap,
} = require('internal/test_runner/reporter/utils');
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(results, location);
}
ArrayPrototypePush(results, formattedErr);
}
this.#failedTests = []; // Clean up the failed tests
return ArrayPrototypeJoin(results, '\n'); ;
}
#handleTestReportEvent(type, data) {
const subtest = ArrayPrototypeShift(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(this.#stack);
assert(parent.type === 'test:start');
const msg = parent.data;
ArrayPrototypeUnshift(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(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(this.#failedTests, data);
}
return this.#handleTestReportEvent(type, data);
case 'test:pass':
return this.#handleTestReportEvent(type, data);
case 'test:start':
ArrayPrototypeUnshift(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());
}
}
module.exports = SpecReporter;

View File

@ -0,0 +1,281 @@
'use strict';
const {
ArrayPrototypeForEach,
ArrayPrototypeJoin,
ArrayPrototypePush,
DatePrototypeToISOString,
ObjectEntries,
RegExpPrototypeSymbolReplace,
RegExpPrototypeSymbolSplit,
SafeMap,
SafeSet,
StringPrototypeRepeat,
StringPrototypeReplaceAll,
} = primordials;
const { inspectWithNoCustomRetry } = require('internal/errors');
const { isError, kEmptyObject } = require('internal/util');
const { getCoverageReport } = require('internal/test_runner/utils');
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; // 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(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(kDefaultIndent, nesting);
memo.set(nesting, value);
}
return value;
}
// In certain places, # and \ need to be escaped as \# and \\.
function tapEscape(input) {
let result = StringPrototypeReplaceAll(input, '\b', '\\b');
result = StringPrototypeReplaceAll(result, '\f', '\\f');
result = StringPrototypeReplaceAll(result, '\t', '\\t');
result = StringPrototypeReplaceAll(result, '\n', '\\n');
result = StringPrototypeReplaceAll(result, '\r', '\\r');
result = StringPrototypeReplaceAll(result, '\v', '\\v');
result = StringPrototypeReplaceAll(result, '\\', '\\\\');
result = StringPrototypeReplaceAll(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(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 = isError(value);
let propsIndent = indent;
let result = '';
if (name != null) {
result += prefix;
if (internalBinding('types').isDate(value)) {
// YAML uses the ISO-8601 standard to express dates.
result += ' ' + DatePrototypeToISOString(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(
RegExpPrototypeSymbolSplit(kLineBreakRegExp, errStack),
(frame) => {
const processed = RegExpPrototypeSymbolReplace(
kFrameStartRegExp,
frame,
'',
);
if (processed.length > 0 && processed.length !== frame.length) {
ArrayPrototypePush(frames, processed);
}
},
);
if (frames.length > 0) {
const frameDelimiter = `\n${indent} `;
result += `${indent} stack: |-${frameDelimiter}`;
result += `${ArrayPrototypeJoin(frames, frameDelimiter)}\n`;
}
}
}
return result;
}
function isAssertionLike(value) {
return value && typeof value === 'object' && 'expected' in value && 'actual' in value;
}
module.exports = tapReporter;

View File

@ -0,0 +1,102 @@
'use strict';
const {
ArrayPrototypeJoin,
RegExpPrototypeSymbolSplit,
SafeMap,
StringPrototypeRepeat,
hardenRegExp,
} = primordials;
const colors = require('internal/util/colors');
const { inspectWithNoCustomRetry } = require('internal/errors');
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(' ', 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(
RegExpPrototypeSymbolSplit(
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}`;
}
module.exports = {
__proto__: null,
reporterUnicodeSymbolMap,
reporterColorMap,
formatTestReport,
indent,
};

View File

@ -0,0 +1,45 @@
'use strict';
const {
TypedArrayPrototypeGetLength,
} = primordials;
const { DefaultSerializer } = require('v8');
const { Buffer } = require('buffer');
const { serializeError } = require('internal/error_serdes');
module.exports = async function* v8Reporter(source) {
const serializer = new DefaultSerializer();
serializer.writeHeader();
const headerLength = TypedArrayPrototypeGetLength(serializer.releaseBuffer());
for await (const item of source) {
const originalError = item.data.details?.error;
if (originalError) {
// Error is overridden with a serialized version, so that it can be
// deserialized in the parent process.
// Error is restored after serialization.
item.data.details.error = serializeError(originalError);
}
serializer.writeHeader();
// Add 4 bytes, to later populate with message length
serializer.writeRawBytes(Buffer.allocUnsafe(4));
serializer.writeHeader();
serializer.writeValue(item);
if (originalError) {
item.data.details.error = originalError;
}
const serializedMessage = serializer.releaseBuffer();
const serializedMessageLength = serializedMessage.length - (4 + headerLength);
serializedMessage.set([
serializedMessageLength >> 24 & 0xFF,
serializedMessageLength >> 16 & 0xFF,
serializedMessageLength >> 8 & 0xFF,
serializedMessageLength & 0xFF,
], headerLength);
yield serializedMessage;
}
};

View File

@ -0,0 +1,857 @@
'use strict';
const {
ArrayIsArray,
ArrayPrototypeEvery,
ArrayPrototypeFilter,
ArrayPrototypeFind,
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeShift,
ArrayPrototypeSlice,
ArrayPrototypeSome,
ArrayPrototypeSort,
ObjectAssign,
PromisePrototypeThen,
PromiseWithResolvers,
SafeMap,
SafePromiseAll,
SafePromiseAllReturnVoid,
SafePromiseAllSettledReturnVoid,
SafeSet,
StringPrototypeIndexOf,
StringPrototypeSlice,
StringPrototypeStartsWith,
Symbol,
TypedArrayPrototypeGetLength,
TypedArrayPrototypeSubarray,
} = primordials;
const { spawn } = require('child_process');
const { finished } = require('internal/streams/end-of-stream');
const { resolve, sep, isAbsolute } = require('path');
const { DefaultDeserializer, DefaultSerializer } = require('v8');
const { getOptionValue } = require('internal/options');
const { Interface } = require('internal/readline/interface');
const { deserializeError } = require('internal/error_serdes');
const { Buffer } = require('buffer');
const { FilesWatcher } = require('internal/watch_mode/files_watcher');
const console = require('internal/console/global');
const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_TEST_FAILURE,
},
} = require('internal/errors');
const esmLoader = require('internal/modules/esm/loader');
const {
validateArray,
validateBoolean,
validateFunction,
validateObject,
validateOneOf,
validateInteger,
validateString,
validateStringArray,
} = require('internal/validators');
const { getInspectPort, isUsingInspector, isInspectorMessage } = require('internal/util/inspector');
const { isRegExp } = require('internal/util/types');
const { pathToFileURL } = require('internal/url');
const {
kEmptyObject,
} = require('internal/util');
const { kEmitMessage } = require('internal/test_runner/tests_stream');
const {
createTestTree,
startSubtestAfterBootstrap,
} = require('internal/test_runner/harness');
const {
kAborted,
kCancelledByParent,
kSubtestsFailed,
kTestCodeFailure,
kTestTimeoutFailure,
Test,
} = require('internal/test_runner/test');
const {
convertStringToRegExp,
countCompletedTest,
kDefaultPattern,
parseCommandLine,
} = require('internal/test_runner/utils');
const { Glob } = require('internal/fs/glob');
const { once } = require('events');
const { validatePath } = require('internal/fs/utils');
const { loadPreloadModules } = require('internal/process/pre_execution');
const {
triggerUncaughtException,
exitCodes: { kGenericUserError },
} = internalBinding('errors');
let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
debug = fn;
});
const kIsolatedProcessName = Symbol('kIsolatedProcessName');
const kFilterArgs = [
'--test',
'--experimental-test-coverage',
'--watch',
'--experimental-default-config-file',
'--experimental-config-file',
];
const kFilterArgValues = ['--test-reporter', '--test-reporter-destination'];
const kDiagnosticsFilterArgs = ['tests', 'suites', 'pass', 'fail', 'cancelled', 'skipped', 'todo', 'duration_ms'];
const kCanceledTests = new SafeSet()
.add(kCancelledByParent).add(kAborted).add(kTestTimeoutFailure);
let kResistStopPropagation;
function createTestFileList(patterns, cwd) {
const hasUserSuppliedPattern = patterns != null;
if (!patterns || patterns.length === 0) {
patterns = [kDefaultPattern];
}
const glob = new Glob(patterns, {
__proto__: null,
cwd,
exclude: (name) => name === 'node_modules',
});
const results = glob.globSync();
if (hasUserSuppliedPattern && results.length === 0 && ArrayPrototypeEvery(glob.matchers, (m) => !m.hasMagic())) {
console.error(`Could not find '${ArrayPrototypeJoin(patterns, ', ')}'`);
process.exit(kGenericUserError);
}
return ArrayPrototypeSort(results);
}
function filterExecArgv(arg, i, arr) {
return !ArrayPrototypeIncludes(kFilterArgs, arg) &&
!ArrayPrototypeSome(kFilterArgValues, (p) => arg === p || (i > 0 && arr[i - 1] === p) || StringPrototypeStartsWith(arg, `${p}=`));
}
function getRunArgs(path, { forceExit,
inspectPort,
testNamePatterns,
testSkipPatterns,
only,
argv: suppliedArgs,
execArgv,
cwd }) {
const argv = ArrayPrototypeFilter(process.execArgv, filterExecArgv);
if (forceExit === true) {
ArrayPrototypePush(argv, '--test-force-exit');
}
if (isUsingInspector()) {
ArrayPrototypePush(argv, `--inspect-port=${getInspectPort(inspectPort)}`);
}
if (testNamePatterns != null) {
ArrayPrototypeForEach(testNamePatterns, (pattern) => ArrayPrototypePush(argv, `--test-name-pattern=${pattern}`));
}
if (testSkipPatterns != null) {
ArrayPrototypeForEach(testSkipPatterns, (pattern) => ArrayPrototypePush(argv, `--test-skip-pattern=${pattern}`));
}
if (only === true) {
ArrayPrototypePush(argv, '--test-only');
}
ArrayPrototypePushApply(argv, execArgv);
if (path === kIsolatedProcessName) {
ArrayPrototypePush(argv, '--test');
ArrayPrototypePushApply(argv, ArrayPrototypeSlice(process.argv, 1));
} else {
ArrayPrototypePush(argv, path);
}
ArrayPrototypePushApply(argv, suppliedArgs);
return argv;
}
const serializer = new DefaultSerializer();
serializer.writeHeader();
const v8Header = serializer.releaseBuffer();
const kV8HeaderLength = TypedArrayPrototypeGetLength(v8Header);
const kSerializedSizeHeader = 4 + kV8HeaderLength;
class FileTest extends Test {
// This class maintains two buffers:
#reportBuffer = []; // Parsed items waiting for this.isClearToSend()
#rawBuffer = []; // Raw data waiting to be parsed
#rawBufferSize = 0;
#reportedChildren = 0;
failedSubtests = false;
constructor(options) {
super(options);
this.loc ??= {
__proto__: null,
line: 1,
column: 1,
file: resolve(this.name),
};
this.timeout = null;
}
#skipReporting() {
return this.#reportedChildren > 0 && (!this.error || this.error.failureType === kSubtestsFailed);
}
#checkNestedComment(comment) {
const firstSpaceIndex = StringPrototypeIndexOf(comment, ' ');
if (firstSpaceIndex === -1) return false;
const secondSpaceIndex = StringPrototypeIndexOf(comment, ' ', firstSpaceIndex + 1);
return secondSpaceIndex === -1 &&
ArrayPrototypeIncludes(kDiagnosticsFilterArgs, StringPrototypeSlice(comment, 0, firstSpaceIndex));
}
#handleReportItem(item) {
const isTopLevel = item.data.nesting === 0;
if (isTopLevel) {
if (item.type === 'test:plan' && this.#skipReporting()) {
return;
}
if (item.type === 'test:diagnostic' && this.#checkNestedComment(item.data.message)) {
return;
}
}
if (item.data.details?.error) {
item.data.details.error = deserializeError(item.data.details.error);
}
if (item.type === 'test:pass' || item.type === 'test:fail') {
item.data.testNumber = isTopLevel ? (this.root.harness.counters.topLevel + 1) : item.data.testNumber;
countCompletedTest({
__proto__: null,
name: item.data.name,
finished: true,
skipped: item.data.skip !== undefined,
isTodo: item.data.todo !== undefined,
passed: item.type === 'test:pass',
cancelled: kCanceledTests.has(item.data.details?.error?.failureType),
nesting: item.data.nesting,
reportedType: item.data.details?.type,
}, this.root.harness);
}
this.reporter[kEmitMessage](item.type, item.data);
}
#accumulateReportItem(item) {
if (item.type !== 'test:pass' && item.type !== 'test:fail') {
return;
}
this.#reportedChildren++;
if (item.data.nesting === 0 && item.type === 'test:fail') {
this.failedSubtests = true;
}
}
#drainReportBuffer() {
if (this.#reportBuffer.length > 0) {
ArrayPrototypeForEach(this.#reportBuffer, (ast) => this.#handleReportItem(ast));
this.#reportBuffer = [];
}
}
addToReport(item) {
this.#accumulateReportItem(item);
if (!this.isClearToSend()) {
ArrayPrototypePush(this.#reportBuffer, item);
return;
}
this.#drainReportBuffer();
this.#handleReportItem(item);
}
reportStarted() {}
drain() {
this.#drainRawBuffer();
this.#drainReportBuffer();
}
report() {
this.drain();
const skipReporting = this.#skipReporting();
if (!skipReporting) {
super.reportStarted();
super.report();
}
}
parseMessage(readData) {
let dataLength = TypedArrayPrototypeGetLength(readData);
if (dataLength === 0) return;
const partialV8Header = readData[dataLength - 1] === v8Header[0];
if (partialV8Header) {
// This will break if v8Header length (2 bytes) is changed.
// However it is covered by tests.
readData = TypedArrayPrototypeSubarray(readData, 0, dataLength - 1);
dataLength--;
}
if (this.#rawBuffer[0] && TypedArrayPrototypeGetLength(this.#rawBuffer[0]) < kSerializedSizeHeader) {
this.#rawBuffer[0] = Buffer.concat([this.#rawBuffer[0], readData]);
} else {
ArrayPrototypePush(this.#rawBuffer, readData);
}
this.#rawBufferSize += dataLength;
this.#processRawBuffer();
if (partialV8Header) {
ArrayPrototypePush(this.#rawBuffer, TypedArrayPrototypeSubarray(v8Header, 0, 1));
this.#rawBufferSize++;
}
}
#drainRawBuffer() {
while (this.#rawBuffer.length > 0) {
this.#processRawBuffer();
}
}
#processRawBuffer() {
// This method is called when it is known that there is at least one message
let bufferHead = this.#rawBuffer[0];
let headerIndex = bufferHead.indexOf(v8Header);
let nonSerialized = Buffer.alloc(0);
while (bufferHead && headerIndex !== 0) {
const nonSerializedData = headerIndex === -1 ?
bufferHead :
bufferHead.slice(0, headerIndex);
nonSerialized = Buffer.concat([nonSerialized, nonSerializedData]);
this.#rawBufferSize -= TypedArrayPrototypeGetLength(nonSerializedData);
if (headerIndex === -1) {
ArrayPrototypeShift(this.#rawBuffer);
} else {
this.#rawBuffer[0] = TypedArrayPrototypeSubarray(bufferHead, headerIndex);
}
bufferHead = this.#rawBuffer[0];
headerIndex = bufferHead?.indexOf(v8Header);
}
if (TypedArrayPrototypeGetLength(nonSerialized) > 0) {
this.addToReport({
__proto__: null,
type: 'test:stdout',
data: { __proto__: null, file: this.name, message: nonSerialized.toString('utf-8') },
});
}
while (bufferHead?.length >= kSerializedSizeHeader) {
// We call `readUInt32BE` manually here, because this is faster than first converting
// it to a buffer and using `readUInt32BE` on that.
const fullMessageSize = (
bufferHead[kV8HeaderLength] << 24 |
bufferHead[kV8HeaderLength + 1] << 16 |
bufferHead[kV8HeaderLength + 2] << 8 |
bufferHead[kV8HeaderLength + 3]
) + kSerializedSizeHeader;
if (this.#rawBufferSize < fullMessageSize) break;
const concatenatedBuffer = this.#rawBuffer.length === 1 ?
this.#rawBuffer[0] : Buffer.concat(this.#rawBuffer, this.#rawBufferSize);
const deserializer = new DefaultDeserializer(
TypedArrayPrototypeSubarray(concatenatedBuffer, kSerializedSizeHeader, fullMessageSize),
);
bufferHead = TypedArrayPrototypeSubarray(concatenatedBuffer, fullMessageSize);
this.#rawBufferSize = TypedArrayPrototypeGetLength(bufferHead);
this.#rawBuffer = this.#rawBufferSize !== 0 ? [bufferHead] : [];
deserializer.readHeader();
const item = deserializer.readValue();
this.addToReport(item);
}
}
}
function runTestFile(path, filesWatcher, opts) {
const watchMode = filesWatcher != null;
const testPath = path === kIsolatedProcessName ? '' : path;
const testOpts = { __proto__: null, signal: opts.signal };
const subtest = opts.root.createSubtest(FileTest, testPath, testOpts, async (t) => {
const args = getRunArgs(path, opts);
const stdio = ['pipe', 'pipe', 'pipe'];
const env = { __proto__: null, ...process.env, NODE_TEST_CONTEXT: 'child-v8' };
if (watchMode) {
stdio.push('ipc');
env.WATCH_REPORT_DEPENDENCIES = '1';
}
if (opts.root.harness.shouldColorizeTestFiles) {
env.FORCE_COLOR = '1';
}
const child = spawn(
process.execPath, args,
{
__proto__: null,
signal: t.signal,
encoding: 'utf8',
env,
stdio,
cwd: opts.cwd,
},
);
if (watchMode) {
filesWatcher.runningProcesses.set(path, child);
filesWatcher.watcher.watchChildProcessModules(child, path);
}
let err;
child.on('error', (error) => {
err = error;
});
child.stdout.on('data', (data) => {
subtest.parseMessage(data);
});
const rl = new Interface({ __proto__: null, input: child.stderr });
rl.on('line', (line) => {
if (isInspectorMessage(line)) {
process.stderr.write(line + '\n');
return;
}
// stderr cannot be treated as TAP, per the spec. However, we want to
// surface stderr lines to improve the DX. Inject each line into the
// test output as an unknown token as if it came from the TAP parser.
subtest.addToReport({
__proto__: null,
type: 'test:stderr',
data: { __proto__: null, file: path, message: line + '\n' },
});
});
const { 0: { 0: code, 1: signal } } = await SafePromiseAll([
once(child, 'exit', { __proto__: null, signal: t.signal }),
finished(child.stdout, { __proto__: null, signal: t.signal }),
]);
if (watchMode) {
filesWatcher.runningProcesses.delete(path);
filesWatcher.runningSubtests.delete(path);
(async () => {
try {
await subTestEnded;
} finally {
if (filesWatcher.runningSubtests.size === 0) {
opts.root.reporter[kEmitMessage]('test:watch:drained');
opts.root.postRun();
}
}
})();
}
if (code !== 0 || signal !== null) {
if (!err) {
const failureType = subtest.failedSubtests ? kSubtestsFailed : kTestCodeFailure;
err = ObjectAssign(new ERR_TEST_FAILURE('test failed', failureType), {
__proto__: null,
exitCode: code,
signal: signal,
// The stack will not be useful since the failures came from tests
// in a child process.
stack: undefined,
});
}
throw err;
}
});
const subTestEnded = subtest.start();
return subTestEnded;
}
function watchFiles(testFiles, opts) {
const runningProcesses = new SafeMap();
const runningSubtests = new SafeMap();
const watcherMode = opts.hasFiles ? 'filter' : 'all';
const watcher = new FilesWatcher({ __proto__: null, debounce: 200, mode: watcherMode, signal: opts.signal });
if (!opts.hasFiles) {
watcher.watchPath(opts.cwd);
}
const filesWatcher = { __proto__: null, watcher, runningProcesses, runningSubtests };
opts.root.harness.watching = true;
async function restartTestFile(file) {
const runningProcess = runningProcesses.get(file);
if (runningProcess) {
runningProcess.kill();
await once(runningProcess, 'exit');
}
if (!runningSubtests.size) {
// Reset the topLevel counter
opts.root.harness.counters.topLevel = 0;
}
await runningSubtests.get(file);
runningSubtests.set(file, runTestFile(file, filesWatcher, opts));
}
// Watch for changes in current filtered files
watcher.on('changed', ({ owners, eventType }) => {
if (!opts.hasFiles && (eventType === 'rename' || eventType === 'change')) {
const updatedTestFiles = createTestFileList(opts.globPatterns, opts.cwd);
const newFileName = ArrayPrototypeFind(updatedTestFiles, (x) => !ArrayPrototypeIncludes(testFiles, x));
const previousFileName = ArrayPrototypeFind(testFiles, (x) => !ArrayPrototypeIncludes(updatedTestFiles, x));
testFiles = updatedTestFiles;
// When file renamed (created / deleted) we need to update the watcher
if (newFileName) {
owners = new SafeSet().add(newFileName);
const resolveFileName = isAbsolute(newFileName) ? newFileName : resolve(opts.cwd, newFileName);
watcher.filterFile(resolveFileName, owners);
}
if (!newFileName && previousFileName) {
return; // Avoid rerunning files when file deleted
}
}
// Reset the root start time to recalculate the duration
// of the run
opts.root.clearExecutionTime();
opts.root.reporter[kEmitMessage]('test:watch:restarted');
// Restart test files
if (opts.isolation === 'none') {
PromisePrototypeThen(restartTestFile(kIsolatedProcessName), undefined, (error) => {
triggerUncaughtException(error, true /* fromPromise */);
});
} else {
watcher.unfilterFilesOwnedBy(owners);
PromisePrototypeThen(SafePromiseAllReturnVoid(testFiles, async (file) => {
if (!owners.has(file)) {
return;
}
await restartTestFile(file);
}, undefined, (error) => {
triggerUncaughtException(error, true /* fromPromise */);
}));
}
});
if (opts.signal) {
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
opts.signal.addEventListener(
'abort',
() => {
opts.root.harness.watching = false;
opts.root.postRun();
},
{ __proto__: null, once: true, [kResistStopPropagation]: true },
);
}
return filesWatcher;
}
function run(options = kEmptyObject) {
validateObject(options, 'options');
let {
testNamePatterns,
testSkipPatterns,
shard,
coverageExcludeGlobs,
coverageIncludeGlobs,
} = options;
const {
concurrency,
timeout,
signal,
files,
forceExit,
inspectPort,
isolation = 'process',
watch,
setup,
globalSetupPath,
only,
globPatterns,
coverage = false,
lineCoverage = 0,
branchCoverage = 0,
functionCoverage = 0,
execArgv = [],
argv = [],
cwd = process.cwd(),
} = options;
if (files != null) {
validateArray(files, 'options.files');
}
if (watch != null) {
validateBoolean(watch, 'options.watch');
}
if (forceExit != null) {
validateBoolean(forceExit, 'options.forceExit');
if (forceExit && watch) {
throw new ERR_INVALID_ARG_VALUE(
'options.forceExit', watch, 'is not supported with watch mode',
);
}
}
if (only != null) {
validateBoolean(only, 'options.only');
}
if (globPatterns != null) {
validateArray(globPatterns, 'options.globPatterns');
}
validateString(cwd, 'options.cwd');
if (globPatterns?.length > 0 && files?.length > 0) {
throw new ERR_INVALID_ARG_VALUE(
'options.globPatterns', globPatterns, 'is not supported when specifying \'options.files\'',
);
}
if (shard != null) {
validateObject(shard, 'options.shard');
// Avoid re-evaluating the shard object in case it's a getter
shard = { __proto__: null, index: shard.index, total: shard.total };
validateInteger(shard.total, 'options.shard.total', 1);
validateInteger(shard.index, 'options.shard.index', 1, shard.total);
if (watch) {
throw new ERR_INVALID_ARG_VALUE('options.shard', watch, 'shards not supported with watch mode');
}
}
if (setup != null) {
validateFunction(setup, 'options.setup');
}
if (testNamePatterns != null) {
if (!ArrayIsArray(testNamePatterns)) {
testNamePatterns = [testNamePatterns];
}
testNamePatterns = ArrayPrototypeMap(testNamePatterns, (value, i) => {
if (isRegExp(value)) {
return value;
}
const name = `options.testNamePatterns[${i}]`;
if (typeof value === 'string') {
return convertStringToRegExp(value, name);
}
throw new ERR_INVALID_ARG_TYPE(name, ['string', 'RegExp'], value);
});
}
if (testSkipPatterns != null) {
if (!ArrayIsArray(testSkipPatterns)) {
testSkipPatterns = [testSkipPatterns];
}
testSkipPatterns = ArrayPrototypeMap(testSkipPatterns, (value, i) => {
if (isRegExp(value)) {
return value;
}
const name = `options.testSkipPatterns[${i}]`;
if (typeof value === 'string') {
return convertStringToRegExp(value, name);
}
throw new ERR_INVALID_ARG_TYPE(name, ['string', 'RegExp'], value);
});
}
validateOneOf(isolation, 'options.isolation', ['process', 'none']);
validateBoolean(coverage, 'options.coverage');
if (coverageExcludeGlobs != null) {
if (!ArrayIsArray(coverageExcludeGlobs)) {
coverageExcludeGlobs = [coverageExcludeGlobs];
}
validateStringArray(coverageExcludeGlobs, 'options.coverageExcludeGlobs');
}
if (coverageIncludeGlobs != null) {
if (!ArrayIsArray(coverageIncludeGlobs)) {
coverageIncludeGlobs = [coverageIncludeGlobs];
}
validateStringArray(coverageIncludeGlobs, 'options.coverageIncludeGlobs');
}
validateInteger(lineCoverage, 'options.lineCoverage', 0, 100);
validateInteger(branchCoverage, 'options.branchCoverage', 0, 100);
validateInteger(functionCoverage, 'options.functionCoverage', 0, 100);
validateStringArray(argv, 'options.argv');
validateStringArray(execArgv, 'options.execArgv');
if (globalSetupPath != null) {
validatePath(globalSetupPath, 'options.globalSetupPath');
}
const rootTestOptions = { __proto__: null, concurrency, timeout, signal };
const globalOptions = {
__proto__: null,
// parseCommandLine() should not be used here. However, The existing run()
// behavior has relied on it, so removing it must be done in a semver major.
...parseCommandLine(),
setup, // This line can be removed when parseCommandLine() is removed here.
coverage,
coverageExcludeGlobs,
coverageIncludeGlobs,
lineCoverage: lineCoverage,
branchCoverage: branchCoverage,
functionCoverage: functionCoverage,
cwd,
globalSetupPath,
};
const root = createTestTree(rootTestOptions, globalOptions);
let testFiles = files ?? createTestFileList(globPatterns, cwd);
const { isTestRunner } = globalOptions;
if (shard) {
testFiles = ArrayPrototypeFilter(testFiles, (_, index) => index % shard.total === shard.index - 1);
}
let teardown;
let postRun;
let filesWatcher;
let runFiles;
const opts = {
__proto__: null,
root,
signal,
inspectPort,
testNamePatterns,
testSkipPatterns,
hasFiles: files != null,
globPatterns,
only,
forceExit,
cwd,
isolation,
argv,
execArgv,
};
if (isolation === 'process') {
if (process.env.NODE_TEST_CONTEXT !== undefined) {
process.emitWarning('node:test run() is being called recursively within a test file. skipping running files.');
root.postRun();
return root.reporter;
}
if (watch) {
filesWatcher = watchFiles(testFiles, opts);
} else {
postRun = () => root.postRun();
teardown = () => root.harness.teardown();
}
runFiles = () => {
root.harness.bootstrapPromise = null;
root.harness.buildPromise = null;
return SafePromiseAllSettledReturnVoid(testFiles, (path) => {
const subtest = runTestFile(path, filesWatcher, opts);
filesWatcher?.runningSubtests.set(path, subtest);
return subtest;
});
};
} else if (isolation === 'none') {
if (watch) {
const absoluteTestFiles = ArrayPrototypeMap(testFiles, (file) => (isAbsolute(file) ? file : resolve(cwd, file)));
filesWatcher = watchFiles(absoluteTestFiles, opts);
runFiles = async () => {
root.harness.bootstrapPromise = null;
root.harness.buildPromise = null;
const subtest = runTestFile(kIsolatedProcessName, filesWatcher, opts);
filesWatcher?.runningSubtests.set(kIsolatedProcessName, subtest);
return subtest;
};
} else {
runFiles = async () => {
const { promise, resolve: finishBootstrap } = PromiseWithResolvers();
await root.runInAsyncScope(async () => {
const parentURL = pathToFileURL(cwd + sep).href;
const cascadedLoader = esmLoader.getOrInitializeCascadedLoader();
let topLevelTestCount = 0;
root.harness.bootstrapPromise = root.harness.bootstrapPromise ?
SafePromiseAllReturnVoid([root.harness.bootstrapPromise, promise]) :
promise;
// We need to setup the user modules in the test runner if we are running with
// --test-isolation=none and --test in order to avoid loading the user modules
// BEFORE the creation of the root test (that would cause them to get lost).
if (isTestRunner) {
// If we are not coming from the test runner entry point, the user-required and imported
// modules have already been loaded.
// Since it's possible to delete modules from require.cache, a CommonJS module
// could otherwise be executed twice.
loadPreloadModules();
}
const userImports = getOptionValue('--import');
for (let i = 0; i < userImports.length; i++) {
await cascadedLoader.import(userImports[i], parentURL, kEmptyObject);
}
for (let i = 0; i < testFiles.length; ++i) {
const testFile = testFiles[i];
const fileURL = pathToFileURL(resolve(cwd, testFile));
const parent = i === 0 ? undefined : parentURL;
let threw = false;
let importError;
root.entryFile = resolve(testFile);
debug('loading test file:', fileURL.href);
try {
await cascadedLoader.import(fileURL, parent, { __proto__: null });
} catch (err) {
threw = true;
importError = err;
}
debug(
'loaded "%s": top level test count before = %d and after = %d',
testFile,
topLevelTestCount,
root.subtests.length,
);
if (topLevelTestCount === root.subtests.length) {
// This file had no tests in it. Add the placeholder test.
const subtest = root.createSubtest(Test, testFile);
if (threw) {
subtest.fail(importError);
}
startSubtestAfterBootstrap(subtest);
}
topLevelTestCount = root.subtests.length;
}
});
debug('beginning test execution');
root.entryFile = null;
finishBootstrap();
return root.processPendingSubtests();
};
}
}
const runChain = async () => {
if (root.harness?.bootstrapPromise) {
await root.harness.bootstrapPromise;
}
if (typeof setup === 'function') {
await setup(root.reporter);
}
await runFiles();
postRun?.();
teardown?.();
};
runChain();
return root.reporter;
}
module.exports = {
FileTest, // Exported for tests only
run,
};

View File

@ -0,0 +1,314 @@
'use strict';
const {
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypeSlice,
ArrayPrototypeSort,
JSONStringify,
ObjectKeys,
SafeMap,
String,
StringPrototypeReplaceAll,
} = primordials;
const {
codes: {
ERR_INVALID_STATE,
},
} = require('internal/errors');
const { kEmptyObject } = require('internal/util');
let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
debug = fn;
});
const {
validateArray,
validateFunction,
validateObject,
validateString,
} = require('internal/validators');
const { strictEqual } = require('assert');
const { mkdirSync, readFileSync, writeFileSync } = require('fs');
const { dirname } = require('path');
const { createContext, runInContext } = require('vm');
const kMissingSnapshotTip = 'Missing snapshots can be generated by rerunning ' +
'the command with the --test-update-snapshots flag.';
const defaultSerializers = [
(value) => { return JSONStringify(value, null, 2); },
];
function defaultResolveSnapshotPath(testPath) {
if (typeof testPath !== 'string') {
return testPath;
}
return `${testPath}.snapshot`;
}
let resolveSnapshotPathFn = defaultResolveSnapshotPath;
let serializerFns = defaultSerializers;
function setResolveSnapshotPath(fn) {
validateFunction(fn, 'fn');
resolveSnapshotPathFn = fn;
}
function setDefaultSnapshotSerializers(serializers) {
validateFunctionArray(serializers, 'serializers');
serializerFns = ArrayPrototypeSlice(serializers);
}
class SnapshotFile {
constructor(snapshotFile) {
this.snapshotFile = snapshotFile;
this.snapshots = { __proto__: null };
this.nameCounts = new SafeMap();
this.loaded = false;
}
getSnapshot(id) {
if (!(id in this.snapshots)) {
const err = new ERR_INVALID_STATE(`Snapshot '${id}' not found in ` +
`'${this.snapshotFile}.' ${kMissingSnapshotTip}`);
err.snapshot = id;
err.filename = this.snapshotFile;
throw err;
}
return this.snapshots[id];
}
setSnapshot(id, value) {
this.snapshots[escapeSnapshotKey(id)] = value;
}
nextId(name) {
const count = this.nameCounts.get(name) ?? 1;
this.nameCounts.set(name, count + 1);
return `${name} ${count}`;
}
readFile() {
if (this.loaded) {
debug('skipping read of snapshot file');
return;
}
try {
const source = readFileSync(this.snapshotFile, 'utf8');
const context = { __proto__: null, exports: { __proto__: null } };
createContext(context);
runInContext(source, context);
if (context.exports === null || typeof context.exports !== 'object') {
throw new ERR_INVALID_STATE(
`Malformed snapshot file '${this.snapshotFile}'.`,
);
}
for (const key in context.exports) {
this.snapshots[key] = templateEscape(context.exports[key]);
}
this.loaded = true;
} catch (err) {
throwReadError(err, this.snapshotFile);
}
}
writeFile() {
try {
const keys = ArrayPrototypeSort(ObjectKeys(this.snapshots));
const snapshotStrings = ArrayPrototypeMap(keys, (key) => {
return `exports[\`${key}\`] = \`${this.snapshots[key]}\`;\n`;
});
const output = ArrayPrototypeJoin(snapshotStrings, '\n');
mkdirSync(dirname(this.snapshotFile), { __proto__: null, recursive: true });
writeFileSync(this.snapshotFile, output, 'utf8');
} catch (err) {
throwWriteError(err, this.snapshotFile);
}
}
}
class SnapshotManager {
constructor(updateSnapshots) {
// A manager instance will only read or write snapshot files based on the
// updateSnapshots argument.
this.updateSnapshots = updateSnapshots;
this.cache = new SafeMap();
}
resolveSnapshotFile(entryFile) {
let snapshotFile = this.cache.get(entryFile);
if (snapshotFile === undefined) {
const resolved = resolveSnapshotPathFn(entryFile);
if (typeof resolved !== 'string') {
const err = new ERR_INVALID_STATE('Invalid snapshot filename.');
err.filename = resolved;
throw err;
}
snapshotFile = new SnapshotFile(resolved);
snapshotFile.loaded = this.updateSnapshots;
this.cache.set(entryFile, snapshotFile);
}
return snapshotFile;
}
serialize(input, serializers = serializerFns) {
try {
const value = serializeValue(input, serializers);
return `\n${templateEscape(value)}\n`;
} catch (err) {
throwSerializationError(input, err);
}
}
serializeWithoutEscape(input, serializers = serializerFns) {
try {
return serializeValue(input, serializers);
} catch (err) {
throwSerializationError(input, err);
}
}
writeSnapshotFiles() {
if (!this.updateSnapshots) {
debug('skipping write of snapshot files');
return;
}
this.cache.forEach((snapshotFile) => {
snapshotFile.writeFile();
});
}
createAssert() {
const manager = this;
return function snapshotAssertion(actual, options = kEmptyObject) {
validateObject(options, 'options');
const {
serializers = serializerFns,
} = options;
validateFunctionArray(serializers, 'options.serializers');
const { filePath, fullName } = this;
const snapshotFile = manager.resolveSnapshotFile(filePath);
const value = manager.serialize(actual, serializers);
const id = snapshotFile.nextId(fullName);
if (manager.updateSnapshots) {
snapshotFile.setSnapshot(id, value);
} else {
snapshotFile.readFile();
strictEqual(value, snapshotFile.getSnapshot(id));
}
};
}
createFileAssert() {
const manager = this;
return function fileSnapshotAssertion(actual, path, options = kEmptyObject) {
validateString(path, 'path');
validateObject(options, 'options');
const {
serializers = serializerFns,
} = options;
validateFunctionArray(serializers, 'options.serializers');
const value = manager.serializeWithoutEscape(actual, serializers);
if (manager.updateSnapshots) {
try {
mkdirSync(dirname(path), { __proto__: null, recursive: true });
writeFileSync(path, value, 'utf8');
} catch (err) {
throwWriteError(err, path);
}
} else {
let expected;
try {
expected = readFileSync(path, 'utf8');
} catch (err) {
throwReadError(err, path);
}
strictEqual(value, expected);
}
};
}
}
function throwReadError(err, filename) {
let msg = `Cannot read snapshot file '${filename}.'`;
if (err?.code === 'ENOENT') {
msg += ` ${kMissingSnapshotTip}`;
}
const error = new ERR_INVALID_STATE(msg);
error.cause = err;
error.filename = filename;
throw error;
}
function throwWriteError(err, filename) {
const msg = `Cannot write snapshot file '${filename}.'`;
const error = new ERR_INVALID_STATE(msg);
error.cause = err;
error.filename = filename;
throw error;
}
function throwSerializationError(input, err) {
const error = new ERR_INVALID_STATE(
'The provided serializers did not generate a string.',
);
error.input = input;
error.cause = err;
throw error;
}
function serializeValue(value, serializers) {
let v = value;
for (let i = 0; i < serializers.length; ++i) {
const fn = serializers[i];
v = fn(v);
}
return v;
}
function validateFunctionArray(fns, name) {
validateArray(fns, name);
for (let i = 0; i < fns.length; ++i) {
validateFunction(fns[i], `${name}[${i}]`);
}
}
function escapeSnapshotKey(str) {
let result = JSONStringify(str, null, 2).slice(1, -1);
result = StringPrototypeReplaceAll(result, '`', '\\`');
result = StringPrototypeReplaceAll(result, '${', '\\${');
return result;
}
function templateEscape(str) {
let result = String(str);
result = StringPrototypeReplaceAll(result, '\\', '\\\\');
result = StringPrototypeReplaceAll(result, '`', '\\`');
result = StringPrototypeReplaceAll(result, '${', '\\${');
return result;
}
module.exports = {
SnapshotManager,
defaultResolveSnapshotPath, // Exported for testing only.
defaultSerializers, // Exported for testing only.
setDefaultSnapshotSerializers,
setResolveSnapshotPath,
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,170 @@
'use strict';
const {
ArrayPrototypePush,
ArrayPrototypeShift,
NumberMAX_SAFE_INTEGER,
Symbol,
} = primordials;
const Readable = require('internal/streams/readable');
const kEmitMessage = Symbol('kEmitMessage');
class TestsStream extends Readable {
#buffer;
#canPush;
constructor() {
super({
__proto__: null,
objectMode: true,
highWaterMark: NumberMAX_SAFE_INTEGER,
});
this.#buffer = [];
this.#canPush = true;
}
_read() {
this.#canPush = true;
while (this.#buffer.length > 0) {
const obj = ArrayPrototypeShift(this.#buffer);
if (!this.#tryPush(obj)) {
return;
}
}
}
fail(nesting, loc, testNumber, name, details, directive) {
this[kEmitMessage]('test:fail', {
__proto__: null,
name,
nesting,
testNumber,
details,
...loc,
...directive,
});
}
ok(nesting, loc, testNumber, name, details, directive) {
this[kEmitMessage]('test:pass', {
__proto__: null,
name,
nesting,
testNumber,
details,
...loc,
...directive,
});
}
complete(nesting, loc, testNumber, name, details, directive) {
this[kEmitMessage]('test:complete', {
__proto__: null,
name,
nesting,
testNumber,
details,
...loc,
...directive,
});
}
plan(nesting, loc, count) {
this[kEmitMessage]('test:plan', {
__proto__: null,
nesting,
count,
...loc,
});
}
getSkip(reason = undefined) {
return { __proto__: null, skip: reason ?? true };
}
getTodo(reason = undefined) {
return { __proto__: null, todo: reason ?? true };
}
enqueue(nesting, loc, name, type) {
this[kEmitMessage]('test:enqueue', {
__proto__: null,
nesting,
name,
type,
...loc,
});
}
dequeue(nesting, loc, name, type) {
this[kEmitMessage]('test:dequeue', {
__proto__: null,
nesting,
name,
type,
...loc,
});
}
start(nesting, loc, name) {
this[kEmitMessage]('test:start', {
__proto__: null,
nesting,
name,
...loc,
});
}
diagnostic(nesting, loc, message, level = 'info') {
this[kEmitMessage]('test:diagnostic', {
__proto__: null,
nesting,
message,
level,
...loc,
});
}
coverage(nesting, loc, summary) {
this[kEmitMessage]('test:coverage', {
__proto__: null,
nesting,
summary,
...loc,
});
}
summary(nesting, file, success, counts, duration_ms) {
this[kEmitMessage]('test:summary', {
__proto__: null,
success,
counts,
duration_ms,
file,
});
}
end() {
this.#tryPush(null);
}
[kEmitMessage](type, data) {
this.emit(type, data);
// Disabling as this going to the user-land
// eslint-disable-next-line node-core/set-proto-to-null-in-object
this.#tryPush({ type, data });
}
#tryPush(message) {
if (this.#canPush) {
this.#canPush = this.push(message);
} else {
ArrayPrototypePush(this.#buffer, message);
}
return this.#canPush;
}
}
module.exports = { TestsStream, kEmitMessage };

View File

@ -0,0 +1,640 @@
'use strict';
const {
ArrayPrototypeFlatMap,
ArrayPrototypeForEach,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePop,
ArrayPrototypePush,
ArrayPrototypeReduce,
ArrayPrototypeSome,
MathFloor,
MathMax,
MathMin,
NumberParseInt,
NumberPrototypeToFixed,
ObjectGetOwnPropertyDescriptor,
PromiseWithResolvers,
RegExp,
RegExpPrototypeExec,
SafeMap,
SafePromiseAllReturnArrayLike,
StringPrototypePadEnd,
StringPrototypePadStart,
StringPrototypeRepeat,
StringPrototypeSlice,
StringPrototypeSplit,
} = primordials;
const { AsyncResource } = require('async_hooks');
const { relative, sep, resolve } = require('path');
const { createWriteStream } = require('fs');
const { pathToFileURL } = require('internal/url');
const { getOptionValue } = require('internal/options');
const { green, yellow, red, white, shouldColorize } = require('internal/util/colors');
const {
codes: {
ERR_INVALID_ARG_VALUE,
ERR_TEST_FAILURE,
},
kIsNodeError,
} = require('internal/errors');
const { compose } = require('stream');
const {
validateInteger,
validateFunction,
} = require('internal/validators');
const { validatePath } = require('internal/fs/utils');
const { kEmptyObject } = require('internal/util');
const coverageColors = {
__proto__: null,
high: green,
medium: yellow,
low: red,
};
const kMultipleCallbackInvocations = 'multipleCallbackInvocations';
const kRegExpPattern = /^\/(.*)\/([a-z]*)$/;
const kPatterns = ['test', 'test/**/*', 'test-*', '*[._-]test'];
const kFileExtensions = ['js', 'mjs', 'cjs'];
if (getOptionValue('--experimental-strip-types')) {
ArrayPrototypePush(kFileExtensions, 'ts', 'mts', 'cts');
}
const kDefaultPattern = `**/{${ArrayPrototypeJoin(kPatterns, ',')}}.{${ArrayPrototypeJoin(kFileExtensions, ',')}}`;
function createDeferredCallback() {
let calledCount = 0;
const { promise, resolve, reject } = PromiseWithResolvers();
const cb = (err) => {
calledCount++;
// If the callback is called a second time, let the user know, but
// don't let them know more than once.
if (calledCount > 1) {
if (calledCount === 2) {
throw new ERR_TEST_FAILURE(
'callback invoked multiple times',
kMultipleCallbackInvocations,
);
}
return;
}
if (err) {
return reject(err);
}
resolve();
};
return { __proto__: null, promise, cb };
}
function isTestFailureError(err) {
return err?.code === 'ERR_TEST_FAILURE' && kIsNodeError in err;
}
function convertStringToRegExp(str, name) {
const match = RegExpPrototypeExec(kRegExpPattern, str);
const pattern = match?.[1] ?? str;
const flags = match?.[2] || '';
try {
return new RegExp(pattern, flags);
} catch (err) {
const msg = err?.message;
throw new ERR_INVALID_ARG_VALUE(
name,
str,
`is an invalid regular expression.${msg ? ` ${msg}` : ''}`,
);
}
}
const kBuiltinDestinations = new SafeMap([
['stdout', process.stdout],
['stderr', process.stderr],
]);
const kBuiltinReporters = new SafeMap([
['spec', 'internal/test_runner/reporter/spec'],
['dot', 'internal/test_runner/reporter/dot'],
['tap', 'internal/test_runner/reporter/tap'],
['junit', 'internal/test_runner/reporter/junit'],
['lcov', 'internal/test_runner/reporter/lcov'],
]);
const kDefaultReporter = 'spec';
const kDefaultDestination = 'stdout';
function tryBuiltinReporter(name) {
const builtinPath = kBuiltinReporters.get(name);
if (builtinPath === undefined) {
return;
}
return require(builtinPath);
}
function shouldColorizeTestFiles(destinations) {
// This function assumes only built-in destinations (stdout/stderr) supports coloring
return ArrayPrototypeSome(destinations, (_, index) => {
const destination = kBuiltinDestinations.get(destinations[index]);
return destination && shouldColorize(destination);
});
}
async function getReportersMap(reporters, destinations) {
return SafePromiseAllReturnArrayLike(reporters, async (name, i) => {
const destination = kBuiltinDestinations.get(destinations[i]) ??
createWriteStream(destinations[i], { __proto__: null, flush: true });
// Load the test reporter passed to --test-reporter
let reporter = tryBuiltinReporter(name);
if (reporter === undefined) {
let parentURL;
try {
parentURL = pathToFileURL(process.cwd() + '/').href;
} catch {
parentURL = 'file:///';
}
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
reporter = await cascadedLoader.import(name, parentURL, { __proto__: null });
}
if (reporter?.default) {
reporter = reporter.default;
}
if (reporter?.prototype && ObjectGetOwnPropertyDescriptor(reporter.prototype, 'constructor')) {
reporter = new reporter();
}
if (!reporter) {
throw new ERR_INVALID_ARG_VALUE('Reporter', name, 'is not a valid reporter');
}
return { __proto__: null, reporter, destination };
});
}
const reporterScope = new AsyncResource('TestReporterScope');
let globalTestOptions;
function parseCommandLine() {
if (globalTestOptions) {
return globalTestOptions;
}
const isTestRunner = getOptionValue('--test');
const coverage = getOptionValue('--experimental-test-coverage');
const forceExit = getOptionValue('--test-force-exit');
const sourceMaps = getOptionValue('--enable-source-maps');
const updateSnapshots = getOptionValue('--test-update-snapshots');
const watch = getOptionValue('--watch');
const timeout = getOptionValue('--test-timeout') || Infinity;
const isChildProcess = process.env.NODE_TEST_CONTEXT === 'child';
const isChildProcessV8 = process.env.NODE_TEST_CONTEXT === 'child-v8';
let globalSetupPath;
let concurrency;
let coverageExcludeGlobs;
let coverageIncludeGlobs;
let lineCoverage;
let branchCoverage;
let functionCoverage;
let destinations;
let isolation;
let only = getOptionValue('--test-only');
let reporters;
let shard;
let testNamePatterns = mapPatternFlagToRegExArray('--test-name-pattern');
let testSkipPatterns = mapPatternFlagToRegExArray('--test-skip-pattern');
if (isChildProcessV8) {
kBuiltinReporters.set('v8-serializer', 'internal/test_runner/reporter/v8-serializer');
reporters = ['v8-serializer'];
destinations = [kDefaultDestination];
} else if (isChildProcess) {
reporters = ['tap'];
destinations = [kDefaultDestination];
} else {
destinations = getOptionValue('--test-reporter-destination');
reporters = getOptionValue('--test-reporter');
globalSetupPath = getOptionValue('--test-global-setup');
if (reporters.length === 0 && destinations.length === 0) {
ArrayPrototypePush(reporters, kDefaultReporter);
}
if (reporters.length === 1 && destinations.length === 0) {
ArrayPrototypePush(destinations, kDefaultDestination);
}
if (destinations.length !== reporters.length) {
throw new ERR_INVALID_ARG_VALUE(
'--test-reporter',
reporters,
'must match the number of specified \'--test-reporter-destination\'',
);
}
}
if (isTestRunner) {
isolation = getOptionValue('--test-isolation');
if (isolation === 'none') {
concurrency = 1;
} else {
concurrency = getOptionValue('--test-concurrency') || true;
only = false;
testNamePatterns = null;
testSkipPatterns = null;
}
const shardOption = getOptionValue('--test-shard');
if (shardOption) {
if (!RegExpPrototypeExec(/^\d+\/\d+$/, shardOption)) {
throw new ERR_INVALID_ARG_VALUE(
'--test-shard',
shardOption,
'must be in the form of <index>/<total>',
);
}
const indexAndTotal = StringPrototypeSplit(shardOption, '/', 2);
shard = {
__proto__: null,
index: NumberParseInt(indexAndTotal[0], 10),
total: NumberParseInt(indexAndTotal[1], 10),
};
}
} else {
concurrency = 1;
const testNamePatternFlag = getOptionValue('--test-name-pattern');
only = getOptionValue('--test-only');
testNamePatterns = testNamePatternFlag?.length > 0 ?
ArrayPrototypeMap(
testNamePatternFlag,
(re) => convertStringToRegExp(re, '--test-name-pattern'),
) : null;
const testSkipPatternFlag = getOptionValue('--test-skip-pattern');
testSkipPatterns = testSkipPatternFlag?.length > 0 ?
ArrayPrototypeMap(testSkipPatternFlag, (re) => convertStringToRegExp(re, '--test-skip-pattern')) : null;
}
if (coverage) {
coverageExcludeGlobs = getOptionValue('--test-coverage-exclude');
if (!coverageExcludeGlobs || coverageExcludeGlobs.length === 0) {
// TODO(pmarchini): this default should follow something similar to c8 defaults
// Default exclusions should be also exported to be used by other tools / users
coverageExcludeGlobs = [kDefaultPattern];
}
coverageIncludeGlobs = getOptionValue('--test-coverage-include');
branchCoverage = getOptionValue('--test-coverage-branches');
lineCoverage = getOptionValue('--test-coverage-lines');
functionCoverage = getOptionValue('--test-coverage-functions');
validateInteger(branchCoverage, '--test-coverage-branches', 0, 100);
validateInteger(lineCoverage, '--test-coverage-lines', 0, 100);
validateInteger(functionCoverage, '--test-coverage-functions', 0, 100);
}
const setup = reporterScope.bind(async (rootReporter) => {
const reportersMap = await getReportersMap(reporters, destinations);
for (let i = 0; i < reportersMap.length; i++) {
const { reporter, destination } = reportersMap[i];
compose(rootReporter, reporter).pipe(destination);
}
reporterScope.reporters = reportersMap;
});
globalTestOptions = {
__proto__: null,
isTestRunner,
concurrency,
coverage,
coverageExcludeGlobs,
coverageIncludeGlobs,
destinations,
forceExit,
isolation,
branchCoverage,
functionCoverage,
lineCoverage,
only,
reporters,
setup,
globalSetupPath,
shard,
sourceMaps,
testNamePatterns,
testSkipPatterns,
timeout,
updateSnapshots,
watch,
};
return globalTestOptions;
}
function mapPatternFlagToRegExArray(flagName) {
const patterns = getOptionValue(flagName);
if (patterns?.length > 0) {
return ArrayPrototypeMap(patterns, (re) => convertStringToRegExp(re, flagName));
}
return null;
}
function countCompletedTest(test, harness = test.root.harness) {
if (test.nesting === 0) {
harness.counters.topLevel++;
}
if (test.reportedType === 'suite') {
harness.counters.suites++;
return;
}
// Check SKIP and TODO tests first, as those should not be counted as
// failures.
if (test.skipped) {
harness.counters.skipped++;
} else if (test.isTodo) {
harness.counters.todo++;
} else if (test.cancelled) {
harness.counters.cancelled++;
harness.success = false;
} else if (!test.passed) {
harness.counters.failed++;
harness.success = false;
} else {
harness.counters.passed++;
}
harness.counters.tests++;
}
const memo = new SafeMap();
function addTableLine(prefix, width) {
const key = `${prefix}-${width}`;
let value = memo.get(key);
if (value === undefined) {
value = `${prefix}${StringPrototypeRepeat('-', width)}\n`;
memo.set(key, value);
}
return value;
}
const kHorizontalEllipsis = '\u2026';
function truncateStart(string, width) {
return string.length > width ? `${kHorizontalEllipsis}${StringPrototypeSlice(string, string.length - width + 1)}` : string;
}
function truncateEnd(string, width) {
return string.length > width ? `${StringPrototypeSlice(string, 0, width - 1)}${kHorizontalEllipsis}` : string;
}
function formatLinesToRanges(values) {
return ArrayPrototypeMap(ArrayPrototypeReduce(values, (prev, current, index, array) => {
if ((index > 0) && ((current - array[index - 1]) === 1)) {
prev[prev.length - 1][1] = current;
} else {
prev.push([current]);
}
return prev;
}, []), (range) => ArrayPrototypeJoin(range, '-'));
}
function getUncoveredLines(lines) {
return ArrayPrototypeFlatMap(lines, (line) => (line.count === 0 ? line.line : []));
}
function formatUncoveredLines(lines, table) {
if (table) return ArrayPrototypeJoin(formatLinesToRanges(lines), ' ');
return ArrayPrototypeJoin(lines, ', ');
}
const kColumns = ['line %', 'branch %', 'funcs %'];
const kColumnsKeys = ['coveredLinePercent', 'coveredBranchPercent', 'coveredFunctionPercent'];
const kSeparator = ' | ';
function buildFileTree(summary) {
const tree = { __proto__: null };
let treeDepth = 1;
let longestFile = 0;
ArrayPrototypeForEach(summary.files, (file) => {
let longestPart = 0;
const parts = StringPrototypeSplit(relative(summary.workingDirectory, file.path), sep);
let current = tree;
ArrayPrototypeForEach(parts, (part, index) => {
current[part] ||= { __proto__: null };
current = current[part];
// If this is the last part, add the file to the tree
if (index === parts.length - 1) {
current.file = file;
}
// Keep track of the longest part for padding
longestPart = MathMax(longestPart, part.length);
});
treeDepth = MathMax(treeDepth, parts.length);
longestFile = MathMax(longestPart, longestFile);
});
return { __proto__: null, tree, treeDepth, longestFile };
}
function getCoverageReport(pad, summary, symbol, color, table) {
const prefix = `${pad}${symbol}`;
let report = `${color}${prefix}start of coverage report\n`;
let filePadLength;
let columnPadLengths = [];
let uncoveredLinesPadLength;
let tableWidth;
// Create a tree of file paths
const { tree, treeDepth, longestFile } = buildFileTree(summary);
if (table) {
// Calculate expected column sizes based on the tree
filePadLength = table && longestFile;
filePadLength += (treeDepth - 1);
if (color) {
filePadLength += 2;
}
filePadLength = MathMax(filePadLength, 'all files'.length);
if (filePadLength > (process.stdout.columns / 2)) {
filePadLength = MathFloor(process.stdout.columns / 2);
}
const fileWidth = filePadLength + 2;
columnPadLengths = ArrayPrototypeMap(kColumns, (column) => (table ? MathMax(column.length, 6) : 0));
const columnsWidth = ArrayPrototypeReduce(columnPadLengths, (acc, columnPadLength) => acc + columnPadLength + 3, 0);
uncoveredLinesPadLength = table && ArrayPrototypeReduce(summary.files, (acc, file) =>
MathMax(acc, formatUncoveredLines(getUncoveredLines(file.lines), table).length), 0);
uncoveredLinesPadLength = MathMax(uncoveredLinesPadLength, 'uncovered lines'.length);
const uncoveredLinesWidth = uncoveredLinesPadLength + 2;
tableWidth = fileWidth + columnsWidth + uncoveredLinesWidth;
const availableWidth = (process.stdout.columns || Infinity) - prefix.length;
const columnsExtras = tableWidth - availableWidth;
if (table && columnsExtras > 0) {
filePadLength = MathMin(availableWidth * 0.5, filePadLength);
uncoveredLinesPadLength = MathMax(availableWidth - columnsWidth - (filePadLength + 2) - 2, 1);
tableWidth = availableWidth;
} else {
uncoveredLinesPadLength = Infinity;
}
}
function getCell(string, width, pad, truncate, coverage) {
if (!table) return string;
let result = string;
if (pad) result = pad(result, width);
if (truncate) result = truncate(result, width);
if (color && coverage !== undefined) {
if (coverage > 90) return `${coverageColors.high}${result}${color}`;
if (coverage > 50) return `${coverageColors.medium}${result}${color}`;
return `${coverageColors.low}${result}${color}`;
}
return result;
}
function writeReportLine({ file, depth = 0, coveragesColumns, fileCoverage, uncoveredLines }) {
const fileColumn = `${prefix}${StringPrototypeRepeat(' ', depth)}${getCell(file, filePadLength - depth, StringPrototypePadEnd, truncateStart, fileCoverage)}`;
const coverageColumns = ArrayPrototypeJoin(ArrayPrototypeMap(coveragesColumns, (coverage, j) => {
const coverageText = typeof coverage === 'number' ? NumberPrototypeToFixed(coverage, 2) : coverage;
return getCell(coverageText, columnPadLengths[j], StringPrototypePadStart, false, coverage);
}), kSeparator);
const uncoveredLinesColumn = getCell(uncoveredLines, uncoveredLinesPadLength, false, truncateEnd);
return `${fileColumn}${kSeparator}${coverageColumns}${kSeparator}${uncoveredLinesColumn}\n`;
}
function printCoverageBodyTree(tree, depth = 0) {
for (const key in tree) {
if (tree[key].file) {
const file = tree[key].file;
const fileName = ArrayPrototypePop(StringPrototypeSplit(file.path, sep));
let fileCoverage = 0;
const coverages = ArrayPrototypeMap(kColumnsKeys, (columnKey) => {
const percent = file[columnKey];
fileCoverage += percent;
return percent;
});
fileCoverage /= kColumnsKeys.length;
const uncoveredLines = formatUncoveredLines(getUncoveredLines(file.lines), table);
report += writeReportLine({
__proto__: null,
file: fileName,
depth: depth,
coveragesColumns: coverages,
fileCoverage: fileCoverage,
uncoveredLines: uncoveredLines,
});
} else {
report += writeReportLine({
__proto__: null,
file: key,
depth: depth,
coveragesColumns: ArrayPrototypeMap(columnPadLengths, () => ''),
fileCoverage: undefined,
uncoveredLines: '',
});
printCoverageBodyTree(tree[key], depth + 1);
}
}
}
// -------------------------- Coverage Report --------------------------
if (table) report += addTableLine(prefix, tableWidth);
// Print the header
report += writeReportLine({
__proto__: null,
file: 'file',
coveragesColumns: kColumns,
fileCoverage: undefined,
uncoveredLines: 'uncovered lines',
});
if (table) report += addTableLine(prefix, tableWidth);
// Print the body
printCoverageBodyTree(tree);
if (table) report += addTableLine(prefix, tableWidth);
// Print the footer
const allFilesCoverages = ArrayPrototypeMap(kColumnsKeys, (columnKey) => summary.totals[columnKey]);
report += writeReportLine({
__proto__: null,
file: 'all files',
coveragesColumns: allFilesCoverages,
fileCoverage: undefined,
uncoveredLines: '',
});
if (table) report += addTableLine(prefix, tableWidth);
report += `${prefix}end of coverage report\n`;
if (color) {
report += white;
}
return report;
}
async function setupGlobalSetupTeardownFunctions(globalSetupPath, cwd) {
let globalSetupFunction;
let globalTeardownFunction;
if (globalSetupPath) {
validatePath(globalSetupPath, 'options.globalSetupPath');
const fileURL = pathToFileURL(resolve(cwd, globalSetupPath));
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
const globalSetupModule = await cascadedLoader
.import(fileURL, pathToFileURL(cwd + sep).href, kEmptyObject);
if (globalSetupModule.globalSetup) {
validateFunction(globalSetupModule.globalSetup, 'globalSetupModule.globalSetup');
globalSetupFunction = globalSetupModule.globalSetup;
}
if (globalSetupModule.globalTeardown) {
validateFunction(globalSetupModule.globalTeardown, 'globalSetupModule.globalTeardown');
globalTeardownFunction = globalSetupModule.globalTeardown;
}
}
return { __proto__: null, globalSetupFunction, globalTeardownFunction };
}
module.exports = {
convertStringToRegExp,
countCompletedTest,
createDeferredCallback,
isTestFailureError,
kDefaultPattern,
parseCommandLine,
reporterScope,
shouldColorizeTestFiles,
getCoverageReport,
setupGlobalSetupTeardownFunctions,
};