UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

1,356 lines (1,178 loc) 36.4 kB
// 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 };