nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
1,356 lines (1,178 loc) • 36.4 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/test_runner/test.js
import * as __hoisted_assert__ from "nstdlib/lib/assert";
import * as __hoisted_internal_options__ from "nstdlib/lib/internal/options";
import * as __hoisted_internal_test_runner_snapshot__ from "nstdlib/lib/internal/test_runner/snapshot";
import * as __hoisted_internal_event_target__ from "nstdlib/lib/internal/event_target";
import { getCallerLocation } from "nstdlib/stub/binding/util";
import { addAbortListener } from "nstdlib/lib/internal/events/abort_listener";
import { queueMicrotask } from "nstdlib/lib/internal/process/task_queues";
import { AsyncResource } from "nstdlib/lib/async_hooks";
import { AbortController } from "nstdlib/lib/internal/abort_controller";
import { AbortError, codes as __codes__ } from "nstdlib/lib/internal/errors";
import { MockTracker } from "nstdlib/lib/internal/test_runner/mock/mock";
import { TestsStream } from "nstdlib/lib/internal/test_runner/tests_stream";
import {
createDeferredCallback,
countCompletedTest,
isTestFailureError,
} from "nstdlib/lib/internal/test_runner/utils";
import {
createDeferredPromise,
kEmptyObject,
once as runOnce,
} from "nstdlib/lib/internal/util";
import { isPromise } from "nstdlib/lib/internal/util/types";
import {
validateAbortSignal,
validateNumber,
validateOneOf,
validateUint32,
} from "nstdlib/lib/internal/validators";
import { setTimeout } from "nstdlib/lib/timers";
import { TIMEOUT_MAX } from "nstdlib/lib/internal/timers";
import { fileURLToPath } from "nstdlib/lib/internal/url";
import { availableParallelism } from "nstdlib/lib/os";
import * as __hoisted_internal_source_map_source_map_cache__ from "nstdlib/lib/internal/source_map/source_map_cache";
const { ERR_INVALID_ARG_TYPE, ERR_TEST_FAILURE } = __codes__;
const { bigint: hrtime } = process.hrtime;
const kCallbackAndPromisePresent = "callbackAndPromisePresent";
const kCancelledByParent = "cancelledByParent";
const kAborted = "testAborted";
const kParentAlreadyFinished = "parentAlreadyFinished";
const kSubtestsFailed = "subtestsFailed";
const kTestCodeFailure = "testCodeFailure";
const kTestTimeoutFailure = "testTimeoutFailure";
const kHookFailure = "hookFailed";
const kDefaultTimeout = null;
const noop = Function.prototype;
const kShouldAbort = Symbol("kShouldAbort");
const kHookNames = Object.seal(["before", "after", "beforeEach", "afterEach"]);
const kUnwrapErrors = new Set()
.add(kTestCodeFailure)
.add(kHookFailure)
.add("uncaughtException")
.add("unhandledRejection");
let kResistStopPropagation;
let assertObj;
let findSourceMap;
let noopTestStream;
const kRunOnceOptions = { __proto__: null, preserveReturnValue: true };
function lazyFindSourceMap(file) {
if (findSourceMap === undefined) {
({ findSourceMap } = __hoisted_internal_source_map_source_map_cache__);
}
return findSourceMap(file);
}
function lazyAssertObject(harness) {
if (assertObj === undefined) {
assertObj = new Map();
const assert = __hoisted_assert__;
const methodsToCopy = [
"deepEqual",
"deepStrictEqual",
"doesNotMatch",
"doesNotReject",
"doesNotThrow",
"equal",
"fail",
"ifError",
"match",
"notDeepEqual",
"notDeepStrictEqual",
"notEqual",
"notStrictEqual",
"ok",
"rejects",
"strictEqual",
"throws",
];
for (let i = 0; i < methodsToCopy.length; i++) {
assertObj.set(methodsToCopy[i], assert[methodsToCopy[i]]);
}
const { getOptionValue } = __hoisted_internal_options__;
if (getOptionValue("--experimental-test-snapshots")) {
const { SnapshotManager } = __hoisted_internal_test_runner_snapshot__;
harness.snapshotManager = new SnapshotManager(
harness.config.updateSnapshots,
);
assertObj.set("snapshot", harness.snapshotManager.createAssert());
}
}
return assertObj;
}
function stopTest(timeout, signal) {
const deferred = createDeferredPromise();
const abortListener = addAbortListener(signal, deferred.resolve);
let timer;
let disposeFunction;
if (timeout === kDefaultTimeout) {
disposeFunction = abortListener[Symbol.for("nodejs.dispose")];
} else {
timer = setTimeout(() => deferred.resolve(), timeout);
timer.unref();
Object.defineProperty(deferred, "promise", {
__proto__: null,
configurable: true,
writable: true,
value: Promise.prototype.then.call(deferred.promise, () => {
throw new ERR_TEST_FAILURE(
`test timed out after ${timeout}ms`,
kTestTimeoutFailure,
);
}),
});
disposeFunction = () => {
abortListener[Symbol.for("nodejs.dispose")]();
timer[Symbol.for("nodejs.dispose")]();
};
}
Object.defineProperty(deferred.promise, Symbol.for("nodejs.dispose"), {
__proto__: null,
configurable: true,
writable: true,
value: disposeFunction,
});
return deferred.promise;
}
function testMatchesPattern(test, patterns) {
const matchesByNameOrParent =
Array.prototype.some.call(
patterns,
(re) => RegExp.prototype.exec.call(re, test.name) !== null,
) ||
(test.parent && testMatchesPattern(test.parent, patterns));
if (matchesByNameOrParent) return true;
const testNameWithAncestors = String.prototype.trim.call(
test.getTestNameWithAncestors(),
);
return Array.prototype.some.call(
patterns,
(re) => RegExp.prototype.exec.call(re, testNameWithAncestors) !== null,
);
}
class TestPlan {
constructor(count) {
validateUint32(count, "count");
this.expected = count;
this.actual = 0;
}
check() {
if (this.actual !== this.expected) {
throw new ERR_TEST_FAILURE(
`plan expected ${this.expected} assertions but received ${this.actual}`,
kTestCodeFailure,
);
}
}
}
class TestContext {
#assert;
#test;
constructor(test) {
this.#test = test;
}
get signal() {
return this.#test.signal;
}
get name() {
return this.#test.name;
}
get filePath() {
return this.#test.entryFile;
}
get fullName() {
return getFullName(this.#test);
}
get error() {
return this.#test.error;
}
get passed() {
return this.#test.passed;
}
diagnostic(message) {
this.#test.diagnostic(message);
}
plan(count) {
if (this.#test.plan !== null) {
throw new ERR_TEST_FAILURE(
"cannot set plan more than once",
kTestCodeFailure,
);
}
this.#test.plan = new TestPlan(count);
}
get assert() {
if (this.#assert === undefined) {
const { plan } = this.#test;
const map = lazyAssertObject(this.#test.root.harness);
const assert = { __proto__: null };
this.#assert = assert;
map.forEach((method, name) => {
assert[name] = (...args) => {
if (plan !== null) {
plan.actual++;
}
return ReflectApply(method, this, args);
};
});
}
return this.#assert;
}
get mock() {
this.#test.mock ??= new MockTracker();
return this.#test.mock;
}
runOnly(value) {
this.#test.runOnlySubtests = !!value;
}
skip(message) {
this.#test.skip(message);
}
todo(message) {
this.#test.todo(message);
}
test(name, options, fn) {
const overrides = {
__proto__: null,
loc: getCallerLocation(),
};
const { plan } = this.#test;
if (plan !== null) {
plan.actual++;
}
const subtest = this.#test.createSubtest(
// eslint-disable-next-line no-use-before-define
Test,
name,
options,
fn,
overrides,
);
return subtest.start();
}
before(fn, options) {
this.#test.createHook("before", fn, {
__proto__: null,
...options,
parent: this.#test,
hookType: "before",
loc: getCallerLocation(),
});
}
after(fn, options) {
this.#test.createHook("after", fn, {
__proto__: null,
...options,
parent: this.#test,
hookType: "after",
loc: getCallerLocation(),
});
}
beforeEach(fn, options) {
this.#test.createHook("beforeEach", fn, {
__proto__: null,
...options,
parent: this.#test,
hookType: "beforeEach",
loc: getCallerLocation(),
});
}
afterEach(fn, options) {
this.#test.createHook("afterEach", fn, {
__proto__: null,
...options,
parent: this.#test,
hookType: "afterEach",
loc: getCallerLocation(),
});
}
}
class SuiteContext {
#suite;
constructor(suite) {
this.#suite = suite;
}
get signal() {
return this.#suite.signal;
}
get name() {
return this.#suite.name;
}
get filePath() {
return this.#suite.entryFile;
}
get fullName() {
return getFullName(this.#suite);
}
}
class Test extends AsyncResource {
abortController;
outerSignal;
#reportedSubtest;
constructor(options) {
super("Test");
let { fn, name, parent } = options;
const {
concurrency,
entryFile,
loc,
only,
timeout,
todo,
skip,
signal,
plan,
} = options;
if (typeof fn !== "function") {
fn = noop;
}
if (typeof name !== "string" || name === "") {
name = fn.name || "<anonymous>";
}
if (!(parent instanceof Test)) {
parent = null;
}
this.name = name;
this.parent = parent;
this.testNumber = 0;
this.outputSubtestCount = 0;
this.filteredSubtestCount = 0;
this.filtered = false;
if (parent === null) {
this.root = this;
this.harness = options.harness;
this.config = this.harness.config;
this.concurrency = 1;
this.nesting = 0;
this.only = this.config.only;
this.reporter = new TestsStream();
this.runOnlySubtests = this.only;
this.childNumber = 0;
this.timeout = kDefaultTimeout;
this.entryFile = entryFile;
this.hooks = {
__proto__: null,
before: [],
after: [],
beforeEach: [],
afterEach: [],
ownAfterEachCount: 0,
};
} else {
const nesting =
parent.parent === null ? parent.nesting : parent.nesting + 1;
this.root = parent.root;
this.harness = null;
this.config = this.root.harness.config;
this.concurrency = parent.concurrency;
this.nesting = nesting;
this.only = only ?? (parent.only && !parent.runOnlySubtests);
this.reporter = parent.reporter;
this.runOnlySubtests = false;
this.childNumber = parent.subtests.length + 1;
this.timeout = parent.timeout;
this.entryFile = parent.entryFile;
this.hooks = {
__proto__: null,
before: [],
after: [],
beforeEach: Array.prototype.slice.call(parent.hooks.beforeEach),
afterEach: Array.prototype.slice.call(parent.hooks.afterEach),
ownAfterEachCount: 0,
};
if (this.willBeFiltered()) {
this.filtered = true;
this.parent.filteredSubtestCount++;
}
if (this.config.only && only === false) {
fn = noop;
}
}
switch (typeof concurrency) {
case "number":
validateUint32(concurrency, "options.concurrency", true);
this.concurrency = concurrency;
break;
case "boolean":
if (concurrency) {
this.concurrency =
parent === null
? Math.max(availableParallelism() - 1, 1)
: Infinity;
} else {
this.concurrency = 1;
}
break;
default:
if (concurrency != null)
throw new ERR_INVALID_ARG_TYPE(
"options.concurrency",
["boolean", "number"],
concurrency,
);
}
if (timeout != null && timeout !== Infinity) {
validateNumber(timeout, "options.timeout", 0, TIMEOUT_MAX);
this.timeout = timeout;
}
if (skip) {
fn = noop;
}
this.abortController = new AbortController();
this.outerSignal = signal;
this.signal = this.abortController.signal;
validateAbortSignal(signal, "options.signal");
if (signal) {
kResistStopPropagation ??=
__hoisted_internal_event_target__.kResistStopPropagation;
}
this.outerSignal?.addEventListener("abort", this.#abortHandler, {
__proto__: null,
[kResistStopPropagation]: true,
});
this.fn = fn;
this.mock = null;
this.plan = null;
this.expectedAssertions = plan;
this.cancelled = false;
this.skipped = skip !== undefined && skip !== false;
this.isTodo = todo !== undefined && todo !== false;
this.startTime = null;
this.endTime = null;
this.passed = false;
this.error = null;
this.diagnostics = [];
this.message =
typeof skip === "string" ? skip : typeof todo === "string" ? todo : null;
this.activeSubtests = 0;
this.pendingSubtests = [];
this.readySubtests = new Map();
this.subtests = [];
this.waitingOn = 0;
this.finished = false;
if (!this.config.only && (only || this.parent?.runOnlySubtests)) {
const warning =
"'only' and 'runOnly' require the --test-only command-line option.";
this.diagnostic(warning);
}
if (loc === undefined) {
this.loc = undefined;
} else {
this.loc = {
__proto__: null,
line: loc[0],
column: loc[1],
file: loc[2],
};
if (this.config.sourceMaps === true) {
const map = lazyFindSourceMap(this.loc.file);
const entry = map?.findEntry(this.loc.line - 1, this.loc.column - 1);
if (entry !== undefined) {
this.loc.line = entry.originalLine + 1;
this.loc.column = entry.originalColumn + 1;
this.loc.file = entry.originalSource;
}
}
if (String.prototype.startsWith.call(this.loc.file, "file://")) {
this.loc.file = fileURLToPath(this.loc.file);
}
}
}
willBeFiltered() {
if (this.config.only && !this.only) return true;
const { testNamePatterns, testSkipPatterns } = this.config;
if (testNamePatterns && !testMatchesPattern(this, testNamePatterns)) {
return true;
}
if (testSkipPatterns && testMatchesPattern(this, testSkipPatterns)) {
return true;
}
return false;
}
/**
* Returns a name of the test prefixed by name of all its ancestors in ascending order, separated by a space
* Ex."grandparent parent test"
*
* It's needed to match a single test with non-unique name by pattern
*/
getTestNameWithAncestors() {
if (!this.parent) return "";
return `${this.parent.getTestNameWithAncestors()} ${this.name}`;
}
hasConcurrency() {
return this.concurrency > this.activeSubtests;
}
addPendingSubtest(deferred) {
Array.prototype.push.call(this.pendingSubtests, deferred);
}
async processPendingSubtests() {
while (this.pendingSubtests.length > 0 && this.hasConcurrency()) {
const deferred = Array.prototype.shift.call(this.pendingSubtests);
const test = deferred.test;
test.reporter.dequeue(test.nesting, test.loc, test.name);
await test.run();
deferred.resolve();
}
}
addReadySubtest(subtest) {
this.readySubtests.set(subtest.childNumber, subtest);
}
processReadySubtestRange(canSend) {
const start = this.waitingOn;
const end = start + this.readySubtests.size;
for (let i = start; i < end; i++) {
const subtest = this.readySubtests.get(i);
// Check if the specified subtest is in the map. If it is not, return
// early to avoid trying to process any more tests since they would be
// out of order.
if (subtest === undefined) {
return;
}
// Call isClearToSend() in the loop so that it is:
// - Only called if there are results to report in the correct order.
// - Guaranteed to only be called a maximum of once per call to
// processReadySubtestRange().
canSend = canSend || this.isClearToSend();
if (!canSend) {
return;
}
// Report the subtest's results and remove it from the ready map.
subtest.finalize();
this.readySubtests.delete(i);
}
}
createSubtest(Factory, name, options, fn, overrides) {
if (typeof name === "function") {
fn = name;
} else if (name !== null && typeof name === "object") {
fn = options;
options = name;
} else if (typeof options === "function") {
fn = options;
}
if (options === null || typeof options !== "object") {
options = kEmptyObject;
}
let parent = this;
// If this test has already ended, attach this test to the root test so
// that the error can be properly reported.
const preventAddingSubtests = this.finished || this.buildPhaseFinished;
if (preventAddingSubtests) {
while (parent.parent !== null) {
parent = parent.parent;
}
}
const test = new Factory({
__proto__: null,
fn,
name,
parent,
...options,
...overrides,
});
if (parent.waitingOn === 0) {
parent.waitingOn = test.childNumber;
}
if (preventAddingSubtests) {
test.fail(
new ERR_TEST_FAILURE(
"test could not be started because its parent finished",
kParentAlreadyFinished,
),
);
}
Array.prototype.push.call(parent.subtests, test);
return test;
}
#abortHandler = () => {
const error =
this.outerSignal?.reason || new AbortError("The test was aborted");
error.failureType = kAborted;
this.#cancel(error);
};
#cancel(error) {
if (this.endTime !== null || this.error !== null) {
return;
}
this.fail(
error ||
new ERR_TEST_FAILURE(
"test did not finish before its parent and was cancelled",
kCancelledByParent,
),
);
this.cancelled = true;
this.abortController.abort();
}
createHook(name, fn, options) {
validateOneOf(name, "hook name", kHookNames);
// eslint-disable-next-line no-use-before-define
const hook = new TestHook(fn, options);
if (name === "before" || name === "after") {
hook.run = runOnce(hook.run, kRunOnceOptions);
}
if (name === "before" && this.startTime !== null) {
// Test has already started, run the hook immediately
Promise.prototype.then.call(hook.run(this.getRunArgs()), () => {
if (hook.error != null) {
this.fail(hook.error);
}
});
}
if (name === "afterEach") {
// afterEach hooks for the current test should run in the order that they
// are created. However, the current test's afterEach hooks should run
// prior to any ancestor afterEach hooks.
Array.prototype.splice.call(
this.hooks[name],
this.hooks.ownAfterEachCount,
0,
hook,
);
this.hooks.ownAfterEachCount++;
} else {
Array.prototype.push.call(this.hooks[name], hook);
}
return hook;
}
fail(err) {
if (this.error !== null) {
return;
}
this.passed = false;
this.error = err;
}
pass() {
if (this.error !== null) {
return;
}
this.passed = true;
}
skip(message) {
this.skipped = true;
this.message = message;
}
todo(message) {
this.isTodo = true;
this.message = message;
}
diagnostic(message) {
Array.prototype.push.call(this.diagnostics, message);
}
get shouldFilter() {
return this.filtered && this.parent?.filteredSubtestCount > 0;
}
start() {
if (this.shouldFilter) {
noopTestStream ??= new TestsStream();
this.reporter = noopTestStream;
this.run = this.filteredRun;
} else {
this.testNumber = ++this.parent.outputSubtestCount;
}
// If there is enough available concurrency to run the test now, then do
// it. Otherwise, return a Promise to the caller and mark the test as
// pending for later execution.
this.reporter.enqueue(this.nesting, this.loc, this.name);
if (!this.root.harness.allowTestsToRun || !this.parent.hasConcurrency()) {
const deferred = createDeferredPromise();
deferred.test = this;
this.parent.addPendingSubtest(deferred);
return deferred.promise;
}
this.reporter.dequeue(this.nesting, this.loc, this.name);
return this.run();
}
[kShouldAbort]() {
if (this.signal.aborted || this.outerSignal?.aborted) {
this.#abortHandler();
return true;
}
}
getRunArgs() {
const ctx = new TestContext(this);
return { __proto__: null, ctx, args: [ctx] };
}
async runHook(hook, args) {
validateOneOf(hook, "hook name", kHookNames);
try {
await Array.prototype.reduce.call(
this.hooks[hook],
async (prev, hook) => {
await prev;
await hook.run(args);
if (hook.error) {
throw hook.error;
}
},
Promise.resolve(),
);
} catch (err) {
const error = new ERR_TEST_FAILURE(
`failed running ${hook} hook`,
kHookFailure,
);
error.cause = isTestFailureError(err) ? err.cause : err;
throw error;
}
}
async filteredRun() {
this.pass();
this.subtests = [];
this.report = noop;
queueMicrotask(() => this.postRun());
}
async run() {
if (this.parent !== null) {
this.parent.activeSubtests++;
}
this.startTime ??= hrtime();
if (this[kShouldAbort]()) {
this.postRun();
return;
}
const hookArgs = this.getRunArgs();
const { args, ctx } = hookArgs;
if (this.plan === null && this.expectedAssertions) {
ctx.plan(this.expectedAssertions);
}
const after = async () => {
if (this.hooks.after.length > 0) {
await this.runHook("after", hookArgs);
}
};
const afterEach = runOnce(async () => {
if (this.parent?.hooks.afterEach.length > 0 && !this.skipped) {
await this.parent.runHook("afterEach", hookArgs);
}
}, kRunOnceOptions);
let stopPromise;
try {
if (this.parent?.hooks.before.length > 0) {
// This hook usually runs immediately, we need to wait for it to finish
await this.parent.runHook("before", this.parent.getRunArgs());
}
if (this.parent?.hooks.beforeEach.length > 0 && !this.skipped) {
await this.parent.runHook("beforeEach", hookArgs);
}
stopPromise = stopTest(this.timeout, this.signal);
const runArgs = Array.prototype.slice.call(args);
Array.prototype.unshift.call(runArgs, this.fn, ctx);
if (this.fn.length === runArgs.length - 1) {
// This test is using legacy Node.js error first callbacks.
const { promise, cb } = createDeferredCallback();
Array.prototype.push.call(runArgs, cb);
const ret = ReflectApply(this.runInAsyncScope, this, runArgs);
if (isPromise(ret)) {
this.fail(
new ERR_TEST_FAILURE(
"passed a callback but also returned a Promise",
kCallbackAndPromisePresent,
),
);
await Promise.race([ret, stopPromise]);
} else {
await Promise.race([Promise.resolve(promise), stopPromise]);
}
} else {
// This test is synchronous or using Promises.
const promise = ReflectApply(this.runInAsyncScope, this, runArgs);
await Promise.race([Promise.resolve(promise), stopPromise]);
}
this[kShouldAbort]();
this.plan?.check();
this.pass();
await afterEach();
await after();
} catch (err) {
if (isTestFailureError(err)) {
if (err.failureType === kTestTimeoutFailure) {
this.#cancel(err);
} else {
this.fail(err);
}
} else {
this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure));
}
try {
await afterEach();
} catch {
/* test is already failing, let's ignore the error */
}
try {
await after();
} catch {
/* Ignore error. */
}
} finally {
stopPromise?.[Symbol.for("nodejs.dispose")]();
// Do not abort hooks and the root test as hooks instance are shared between tests suite so aborting them will
// cause them to not run for further tests.
if (this.parent !== null) {
this.abortController.abort();
}
}
if (this.parent !== null || typeof this.hookType === "string") {
// Clean up the test. Then, try to report the results and execute any
// tests that were pending due to available concurrency.
//
// The root test is skipped here because it is a special case. Its
// postRun() method is called when the process is getting ready to exit.
// This helps catch any asynchronous activity that occurs after the tests
// have finished executing.
this.postRun();
} else if (this.config.forceExit) {
// This is the root test, and all known tests and hooks have finished
// executing. If the user wants to force exit the process regardless of
// any remaining ref'ed handles, then do that now. It is theoretically
// possible that a ref'ed handle could asynchronously create more tests,
// but the user opted into this behavior.
this.reporter.once("close", () => {
process.exit();
});
this.harness.teardown();
}
}
postRun(pendingSubtestsError) {
// If the test was cancelled before it started, then the start and end
// times need to be corrected.
this.endTime ??= hrtime();
this.startTime ??= this.endTime;
// The test has run, so recursively cancel any outstanding subtests and
// mark this test as failed if any subtests failed.
this.pendingSubtests = [];
let failed = 0;
for (let i = 0; i < this.subtests.length; i++) {
const subtest = this.subtests[i];
if (!subtest.finished) {
subtest.#cancel(pendingSubtestsError);
subtest.postRun(pendingSubtestsError);
}
if (!subtest.passed && !subtest.isTodo) {
failed++;
}
}
if ((this.passed || this.parent === null) && failed > 0) {
const subtestString = `subtest${failed > 1 ? "s" : ""}`;
const msg = `${failed} ${subtestString} failed`;
this.fail(new ERR_TEST_FAILURE(msg, kSubtestsFailed));
}
this.outerSignal?.removeEventListener("abort", this.#abortHandler);
this.mock?.reset();
if (this.parent !== null) {
if (!this.shouldFilter) {
const report = this.getReportDetails();
report.details.passed = this.passed;
this.testNumber ||= ++this.parent.outputSubtestCount;
this.reporter.complete(
this.nesting,
this.loc,
this.testNumber,
this.name,
report.details,
report.directive,
);
this.parent.activeSubtests--;
}
this.parent.addReadySubtest(this);
this.parent.processReadySubtestRange(false);
this.parent.processPendingSubtests();
} else if (!this.reported) {
const { diagnostics, harness, loc, nesting, reporter } = this;
this.reported = true;
reporter.plan(nesting, loc, harness.counters.topLevel);
// Call this harness.coverage() before collecting diagnostics, since failure to collect coverage is a diagnostic.
const coverage = harness.coverage();
harness.snapshotManager?.writeSnapshotFiles();
for (let i = 0; i < diagnostics.length; i++) {
reporter.diagnostic(nesting, loc, diagnostics[i]);
}
reporter.diagnostic(nesting, loc, `tests ${harness.counters.all}`);
reporter.diagnostic(nesting, loc, `suites ${harness.counters.suites}`);
reporter.diagnostic(nesting, loc, `pass ${harness.counters.passed}`);
reporter.diagnostic(nesting, loc, `fail ${harness.counters.failed}`);
reporter.diagnostic(
nesting,
loc,
`cancelled ${harness.counters.cancelled}`,
);
reporter.diagnostic(nesting, loc, `skipped ${harness.counters.skipped}`);
reporter.diagnostic(nesting, loc, `todo ${harness.counters.todo}`);
reporter.diagnostic(nesting, loc, `duration_ms ${this.duration()}`);
if (coverage) {
reporter.coverage(nesting, loc, coverage);
}
if (harness.watching) {
this.reported = false;
harness.resetCounters();
assertObj = undefined;
} else {
reporter.end();
}
}
}
isClearToSend() {
return (
this.parent === null ||
(this.parent.waitingOn === this.childNumber &&
this.parent.isClearToSend())
);
}
finalize() {
// By the time this function is called, the following can be relied on:
// - The current test has completed or been cancelled.
// - All of this test's subtests have completed or been cancelled.
// - It is the current test's turn to report its results.
// Report any subtests that have not been reported yet. Since all of the
// subtests have finished, it's safe to pass true to
// processReadySubtestRange(), which will finalize all remaining subtests.
this.processReadySubtestRange(true);
// Output this test's results and update the parent's waiting counter.
this.report();
this.parent.waitingOn++;
this.finished = true;
if (
this.parent === this.root &&
this.root.waitingOn > this.root.subtests.length
) {
// At this point all of the tests have finished running. However, there
// might be ref'ed handles keeping the event loop alive. This gives the
// global after() hook a chance to clean them up. The user may also
// want to force the test runner to exit despite ref'ed handles.
this.root.run();
}
}
duration() {
// Duration is recorded in BigInt nanoseconds. Convert to milliseconds.
return Number(this.endTime - this.startTime) / 1_000_000;
}
getReportDetails() {
let directive;
const details = { __proto__: null, duration_ms: this.duration() };
if (this.skipped) {
directive = this.reporter.getSkip(this.message);
} else if (this.isTodo) {
directive = this.reporter.getTodo(this.message);
}
if (this.reportedType) {
details.type = this.reportedType;
}
if (!this.passed) {
details.error = this.error;
}
return { __proto__: null, details, directive };
}
report() {
countCompletedTest(this);
if (this.outputSubtestCount > 0) {
this.reporter.plan(
this.subtests[0].nesting,
this.loc,
this.outputSubtestCount,
);
} else {
this.reportStarted();
}
const report = this.getReportDetails();
if (this.passed) {
this.reporter.ok(
this.nesting,
this.loc,
this.testNumber,
this.name,
report.details,
report.directive,
);
} else {
this.reporter.fail(
this.nesting,
this.loc,
this.testNumber,
this.name,
report.details,
report.directive,
);
}
for (let i = 0; i < this.diagnostics.length; i++) {
this.reporter.diagnostic(this.nesting, this.loc, this.diagnostics[i]);
}
}
reportStarted() {
if (this.#reportedSubtest || this.parent === null) {
return;
}
this.#reportedSubtest = true;
this.parent.reportStarted();
this.reporter.start(this.nesting, this.loc, this.name);
}
}
class TestHook extends Test {
#args;
constructor(fn, options) {
const { hookType, loc, parent, timeout, signal } = options;
super({
__proto__: null,
fn,
loc,
timeout,
signal,
harness: parent.root.harness,
});
this.parentTest = parent;
this.hookType = hookType;
}
run(args) {
if (this.error && !this.outerSignal?.aborted) {
this.passed = false;
this.error = null;
this.abortController.abort();
this.abortController = new AbortController();
this.signal = this.abortController.signal;
}
this.#args = args;
return super.run();
}
getRunArgs() {
return this.#args;
}
willBeFiltered() {
return false;
}
postRun() {
const { error, loc, parentTest: parent } = this;
// Report failures in the root test's after() hook.
if (error && parent === parent.root && this.hookType === "after") {
if (isTestFailureError(error)) {
error.failureType = kHookFailure;
}
this.endTime ??= hrtime();
parent.reporter.fail(
0,
loc,
parent.subtests.length + 1,
loc.file,
{
__proto__: null,
duration_ms: this.duration(),
error,
},
undefined,
);
}
}
}
class Suite extends Test {
reportedType = "suite";
constructor(options) {
super(options);
if (
this.config.testNamePatterns !== null &&
this.config.testSkipPatterns !== null &&
!options.skip
) {
this.fn = options.fn || this.fn;
this.skipped = false;
}
this.runOnlySubtests = this.config.only;
try {
const { ctx, args } = this.getRunArgs();
const runArgs = [this.fn, ctx];
Array.prototype.push.apply(runArgs, args);
this.buildSuite = Promise.prototype.finally.call(
Promise.prototype.then.call(
Promise.resolve(ReflectApply(this.runInAsyncScope, this, runArgs)),
undefined,
(err) => {
this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure));
},
),
() => this.postBuild(),
);
} catch (err) {
this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure));
this.postBuild();
}
this.fn = noop;
}
postBuild() {
this.buildPhaseFinished = true;
if (
this.filtered &&
(this.filteredSubtestCount !== this.subtests.length || this.error)
) {
// A suite can transition from filtered to unfiltered based on the
// tests that it contains - in case of children matching patterns.
this.filtered = false;
this.parent.filteredSubtestCount--;
} else if (
this.config.only &&
this.config.testNamePatterns == null &&
this.config.testSkipPatterns == null &&
this.filteredSubtestCount === this.subtests.length
) {
// If no subtests are marked as "only", run them all
this.filteredSubtestCount = 0;
}
}
getRunArgs() {
const ctx = new SuiteContext(this);
return { __proto__: null, ctx, args: [ctx] };
}
async run() {
const hookArgs = this.getRunArgs();
let stopPromise;
const after = runOnce(
() => this.runHook("after", hookArgs),
kRunOnceOptions,
);
try {
this.parent.activeSubtests++;
await this.buildSuite;
this.startTime = hrtime();
if (this[kShouldAbort]()) {
this.subtests = [];
this.postRun();
return;
}
if (this.parent.hooks.before.length > 0) {
await this.parent.runHook("before", this.parent.getRunArgs());
}
await this.runHook("before", hookArgs);
stopPromise = stopTest(this.timeout, this.signal);
const subtests = this.skipped || this.error ? [] : this.subtests;
const promise = Promise.all(subtests, (subtests) => subtests.start());
await Promise.race([promise, stopPromise]);
await after();
this.pass();
} catch (err) {
try {
await after();
} catch {
/* suite is already failing */
}
if (isTestFailureError(err)) {
this.fail(err);
} else {
this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure));
}
} finally {
stopPromise?.[Symbol.for("nodejs.dispose")]();
}
this.postRun();
}
}
function getFullName(test) {
let fullName = test.name;
for (let t = test.parent; t !== t.root; t = t.parent) {
fullName = `${t.name} > ${fullName}`;
}
return fullName;
}
export { kCancelledByParent };
export { kSubtestsFailed };
export { kTestCodeFailure };
export { kTestTimeoutFailure };
export { kAborted };
export { kUnwrapErrors };
export { Suite };
export { Test };