mirror of
https://github.com/nodejs/node.git
synced 2025-04-29 14:25:18 +00:00

The test runner is bootstrapped synchronously, with the exception of importing custom reporters. To better handle asynchronously throw errors, this commit introduces an internal error type that can be checked for from the test runner's uncaughtException handler. After https://github.com/nodejs/node/pull/46707 and this change land, the other throw statement in the uncaughtException handler can be removed. This will allow the test runner to handle errors thrown from outside of tests (which currently prevents the test runner from reporting results). PR-URL: https://github.com/nodejs/node/pull/46720 Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
232 lines
5.8 KiB
JavaScript
232 lines
5.8 KiB
JavaScript
'use strict';
|
|
const {
|
|
ArrayPrototypeMap,
|
|
ArrayPrototypePush,
|
|
ObjectGetOwnPropertyDescriptor,
|
|
SafePromiseAllReturnArrayLike,
|
|
RegExp,
|
|
RegExpPrototypeExec,
|
|
SafeMap,
|
|
Symbol,
|
|
} = primordials;
|
|
const { basename } = require('path');
|
|
const { createWriteStream } = require('fs');
|
|
const { pathToFileURL } = require('internal/url');
|
|
const { createDeferredPromise } = require('internal/util');
|
|
const { getOptionValue } = require('internal/options');
|
|
|
|
const {
|
|
codes: {
|
|
ERR_INVALID_ARG_VALUE,
|
|
ERR_TEST_FAILURE,
|
|
},
|
|
kIsNodeError,
|
|
} = require('internal/errors');
|
|
const { compose } = require('stream');
|
|
|
|
const kAsyncBootstrapFailure = Symbol('asyncBootstrapFailure');
|
|
const kMultipleCallbackInvocations = 'multipleCallbackInvocations';
|
|
const kRegExpPattern = /^\/(.*)\/([a-z]*)$/;
|
|
const kSupportedFileExtensions = /\.[cm]?js$/;
|
|
const kTestFilePattern = /((^test(-.+)?)|(.+[.\-_]test))\.[cm]?js$/;
|
|
|
|
function doesPathMatchFilter(p) {
|
|
return RegExpPrototypeExec(kTestFilePattern, basename(p)) !== null;
|
|
}
|
|
|
|
function isSupportedFileType(p) {
|
|
return RegExpPrototypeExec(kSupportedFileExtensions, p) !== null;
|
|
}
|
|
|
|
function createDeferredCallback() {
|
|
let calledCount = 0;
|
|
const { promise, resolve, reject } = createDeferredPromise();
|
|
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 { 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'],
|
|
]);
|
|
|
|
const kDefaultReporter = 'tap';
|
|
const kDefaultDestination = 'stdout';
|
|
|
|
function tryBuiltinReporter(name) {
|
|
const builtinPath = kBuiltinReporters.get(name);
|
|
|
|
if (builtinPath === undefined) {
|
|
return;
|
|
}
|
|
|
|
return require(builtinPath);
|
|
}
|
|
|
|
async function getReportersMap(reporters, destinations) {
|
|
return SafePromiseAllReturnArrayLike(reporters, async (name, i) => {
|
|
const destination = kBuiltinDestinations.get(destinations[i]) ?? createWriteStream(destinations[i]);
|
|
|
|
// 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 { esmLoader } = require('internal/process/esm_loader');
|
|
reporter = await esmLoader.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 };
|
|
});
|
|
}
|
|
|
|
|
|
async function setupTestReporters(testsStream) {
|
|
try {
|
|
const { reporters, destinations } = parseCommandLine();
|
|
const reportersMap = await getReportersMap(reporters, destinations);
|
|
for (let i = 0; i < reportersMap.length; i++) {
|
|
const { reporter, destination } = reportersMap[i];
|
|
compose(testsStream, reporter).pipe(destination);
|
|
}
|
|
} catch (err) {
|
|
throw new ERR_TEST_FAILURE(err, kAsyncBootstrapFailure);
|
|
}
|
|
}
|
|
|
|
let globalTestOptions;
|
|
|
|
function parseCommandLine() {
|
|
if (globalTestOptions) {
|
|
return globalTestOptions;
|
|
}
|
|
|
|
const isTestRunner = getOptionValue('--test');
|
|
const coverage = getOptionValue('--experimental-test-coverage');
|
|
const destinations = getOptionValue('--test-reporter-destination');
|
|
const reporters = getOptionValue('--test-reporter');
|
|
let testNamePatterns;
|
|
let testOnlyFlag;
|
|
|
|
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) {
|
|
testOnlyFlag = false;
|
|
testNamePatterns = null;
|
|
} else {
|
|
const testNamePatternFlag = getOptionValue('--test-name-pattern');
|
|
testOnlyFlag = getOptionValue('--test-only');
|
|
testNamePatterns = testNamePatternFlag?.length > 0 ?
|
|
ArrayPrototypeMap(
|
|
testNamePatternFlag,
|
|
(re) => convertStringToRegExp(re, '--test-name-pattern'),
|
|
) : null;
|
|
}
|
|
|
|
globalTestOptions = {
|
|
__proto__: null,
|
|
isTestRunner,
|
|
coverage,
|
|
testOnlyFlag,
|
|
testNamePatterns,
|
|
reporters,
|
|
destinations,
|
|
};
|
|
|
|
return globalTestOptions;
|
|
}
|
|
|
|
module.exports = {
|
|
convertStringToRegExp,
|
|
createDeferredCallback,
|
|
doesPathMatchFilter,
|
|
isSupportedFileType,
|
|
isTestFailureError,
|
|
kAsyncBootstrapFailure,
|
|
parseCommandLine,
|
|
setupTestReporters,
|
|
};
|