• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2const {
3  ArrayPrototypePush,
4  ArrayPrototypePushApply,
5  ArrayPrototypeReduce,
6  ArrayPrototypeShift,
7  ArrayPrototypeSlice,
8  ArrayPrototypeSome,
9  ArrayPrototypeUnshift,
10  FunctionPrototype,
11  MathMax,
12  Number,
13  ObjectSeal,
14  PromisePrototypeThen,
15  PromiseResolve,
16  SafePromisePrototypeFinally,
17  ReflectApply,
18  RegExpPrototypeExec,
19  SafeMap,
20  SafeSet,
21  SafePromiseAll,
22  SafePromiseRace,
23  SymbolDispose,
24  ObjectDefineProperty,
25  Symbol,
26} = primordials;
27const { getCallerLocation } = internalBinding('util');
28const { addAbortListener } = require('events');
29const { AsyncResource } = require('async_hooks');
30const { AbortController } = require('internal/abort_controller');
31const {
32  codes: {
33    ERR_INVALID_ARG_TYPE,
34    ERR_TEST_FAILURE,
35  },
36  AbortError,
37} = require('internal/errors');
38const { MockTracker } = require('internal/test_runner/mock/mock');
39const { TestsStream } = require('internal/test_runner/tests_stream');
40const {
41  createDeferredCallback,
42  countCompletedTest,
43  isTestFailureError,
44  parseCommandLine,
45} = require('internal/test_runner/utils');
46const {
47  createDeferredPromise,
48  kEmptyObject,
49  once: runOnce,
50} = require('internal/util');
51const { isPromise } = require('internal/util/types');
52const {
53  validateAbortSignal,
54  validateNumber,
55  validateOneOf,
56  validateUint32,
57} = require('internal/validators');
58const { setTimeout } = require('timers');
59const { TIMEOUT_MAX } = require('internal/timers');
60const { availableParallelism } = require('os');
61const { bigint: hrtime } = process.hrtime;
62const kCallbackAndPromisePresent = 'callbackAndPromisePresent';
63const kCancelledByParent = 'cancelledByParent';
64const kAborted = 'testAborted';
65const kParentAlreadyFinished = 'parentAlreadyFinished';
66const kSubtestsFailed = 'subtestsFailed';
67const kTestCodeFailure = 'testCodeFailure';
68const kTestTimeoutFailure = 'testTimeoutFailure';
69const kHookFailure = 'hookFailed';
70const kDefaultTimeout = null;
71const noop = FunctionPrototype;
72const kShouldAbort = Symbol('kShouldAbort');
73const kFilename = process.argv?.[1];
74const kHookNames = ObjectSeal(['before', 'after', 'beforeEach', 'afterEach']);
75const kUnwrapErrors = new SafeSet()
76  .add(kTestCodeFailure).add(kHookFailure)
77  .add('uncaughtException').add('unhandledRejection');
78const { testNamePatterns, testOnlyFlag } = parseCommandLine();
79let kResistStopPropagation;
80
81function stopTest(timeout, signal) {
82  const deferred = createDeferredPromise();
83  const abortListener = addAbortListener(signal, deferred.resolve);
84  let timer;
85  let disposeFunction;
86
87  if (timeout === kDefaultTimeout) {
88    disposeFunction = abortListener[SymbolDispose];
89  } else {
90    timer = setTimeout(() => deferred.resolve(), timeout);
91    timer.unref();
92
93    ObjectDefineProperty(deferred, 'promise', {
94      __proto__: null,
95      configurable: true,
96      writable: true,
97      value: PromisePrototypeThen(deferred.promise, () => {
98        throw new ERR_TEST_FAILURE(
99          `test timed out after ${timeout}ms`,
100          kTestTimeoutFailure,
101        );
102      }),
103    });
104
105    disposeFunction = () => {
106      abortListener[SymbolDispose]();
107      timer[SymbolDispose]();
108    };
109  }
110
111  ObjectDefineProperty(deferred.promise, SymbolDispose, {
112    __proto__: null,
113    configurable: true,
114    writable: true,
115    value: disposeFunction,
116  });
117  return deferred.promise;
118}
119
120class TestContext {
121  #test;
122
123  constructor(test) {
124    this.#test = test;
125  }
126
127  get signal() {
128    return this.#test.signal;
129  }
130
131  get name() {
132    return this.#test.name;
133  }
134
135  diagnostic(message) {
136    this.#test.diagnostic(message);
137  }
138
139  get mock() {
140    this.#test.mock ??= new MockTracker();
141    return this.#test.mock;
142  }
143
144  runOnly(value) {
145    this.#test.runOnlySubtests = !!value;
146  }
147
148  skip(message) {
149    this.#test.skip(message);
150  }
151
152  todo(message) {
153    this.#test.todo(message);
154  }
155
156  test(name, options, fn) {
157    const overrides = {
158      __proto__: null,
159      loc: getCallerLocation(),
160    };
161
162    const subtest = this.#test.createSubtest(
163      // eslint-disable-next-line no-use-before-define
164      Test, name, options, fn, overrides,
165    );
166
167    return subtest.start();
168  }
169
170  before(fn, options) {
171    this.#test.createHook('before', fn, options);
172  }
173
174  after(fn, options) {
175    this.#test.createHook('after', fn, options);
176  }
177
178  beforeEach(fn, options) {
179    this.#test.createHook('beforeEach', fn, options);
180  }
181
182  afterEach(fn, options) {
183    this.#test.createHook('afterEach', fn, options);
184  }
185}
186
187class SuiteContext {
188  #suite;
189
190  constructor(suite) {
191    this.#suite = suite;
192  }
193
194  get signal() {
195    return this.#suite.signal;
196  }
197
198  get name() {
199    return this.#suite.name;
200  }
201}
202
203class Test extends AsyncResource {
204  abortController;
205  outerSignal;
206  #reportedSubtest;
207
208  constructor(options) {
209    super('Test');
210
211    let { fn, name, parent, skip } = options;
212    const { concurrency, loc, only, timeout, todo, signal } = options;
213
214    if (typeof fn !== 'function') {
215      fn = noop;
216    }
217
218    if (typeof name !== 'string' || name === '') {
219      name = fn.name || '<anonymous>';
220    }
221
222    if (!(parent instanceof Test)) {
223      parent = null;
224    }
225
226    if (parent === null) {
227      this.concurrency = 1;
228      this.nesting = 0;
229      this.only = testOnlyFlag;
230      this.reporter = new TestsStream();
231      this.runOnlySubtests = this.only;
232      this.testNumber = 0;
233      this.timeout = kDefaultTimeout;
234      this.root = this;
235      this.hooks = {
236        __proto__: null,
237        before: [],
238        after: [],
239        beforeEach: [],
240        afterEach: [],
241      };
242    } else {
243      const nesting = parent.parent === null ? parent.nesting :
244        parent.nesting + 1;
245
246      this.concurrency = parent.concurrency;
247      this.nesting = nesting;
248      this.only = only ?? !parent.runOnlySubtests;
249      this.reporter = parent.reporter;
250      this.runOnlySubtests = !this.only;
251      this.testNumber = parent.subtests.length + 1;
252      this.timeout = parent.timeout;
253      this.root = parent.root;
254      this.hooks = {
255        __proto__: null,
256        before: [],
257        after: [],
258        beforeEach: ArrayPrototypeSlice(parent.hooks.beforeEach),
259        afterEach: ArrayPrototypeSlice(parent.hooks.afterEach),
260      };
261    }
262
263    switch (typeof concurrency) {
264      case 'number':
265        validateUint32(concurrency, 'options.concurrency', 1);
266        this.concurrency = concurrency;
267        break;
268
269      case 'boolean':
270        if (concurrency) {
271          this.concurrency = parent === null ?
272            MathMax(availableParallelism() - 1, 1) : Infinity;
273        } else {
274          this.concurrency = 1;
275        }
276        break;
277
278      default:
279        if (concurrency != null)
280          throw new ERR_INVALID_ARG_TYPE('options.concurrency', ['boolean', 'number'], concurrency);
281    }
282
283    if (timeout != null && timeout !== Infinity) {
284      validateNumber(timeout, 'options.timeout', 0, TIMEOUT_MAX);
285      this.timeout = timeout;
286    }
287
288    this.name = name;
289    this.parent = parent;
290
291    if (testNamePatterns !== null && !this.matchesTestNamePatterns()) {
292      skip = 'test name does not match pattern';
293    }
294
295    if (testOnlyFlag && !this.only) {
296      skip = '\'only\' option not set';
297    }
298
299    if (skip) {
300      fn = noop;
301    }
302
303    this.abortController = new AbortController();
304    this.outerSignal = signal;
305    this.signal = this.abortController.signal;
306
307    validateAbortSignal(signal, 'options.signal');
308    if (signal) {
309      kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
310    }
311
312    this.outerSignal?.addEventListener(
313      'abort',
314      this.#abortHandler,
315      { __proto__: null, [kResistStopPropagation]: true },
316    );
317
318    this.fn = fn;
319    this.harness = null; // Configured on the root test by the test harness.
320    this.mock = null;
321    this.cancelled = false;
322    this.skipped = skip !== undefined && skip !== false;
323    this.isTodo = todo !== undefined && todo !== false;
324    this.startTime = null;
325    this.endTime = null;
326    this.passed = false;
327    this.error = null;
328    this.diagnostics = [];
329    this.message = typeof skip === 'string' ? skip :
330      typeof todo === 'string' ? todo : null;
331    this.activeSubtests = 0;
332    this.pendingSubtests = [];
333    this.readySubtests = new SafeMap();
334    this.subtests = [];
335    this.waitingOn = 0;
336    this.finished = false;
337
338    if (!testOnlyFlag && (only || this.runOnlySubtests)) {
339      const warning =
340        "'only' and 'runOnly' require the --test-only command-line option.";
341      this.diagnostic(warning);
342    }
343
344    if (loc === undefined || kFilename === undefined) {
345      this.loc = undefined;
346    } else {
347      this.loc = {
348        __proto__: null,
349        line: loc[0],
350        column: loc[1],
351        file: loc[2],
352      };
353    }
354  }
355
356  matchesTestNamePatterns() {
357    return ArrayPrototypeSome(testNamePatterns, (re) => RegExpPrototypeExec(re, this.name) !== null) ||
358      this.parent?.matchesTestNamePatterns();
359  }
360
361  hasConcurrency() {
362    return this.concurrency > this.activeSubtests;
363  }
364
365  addPendingSubtest(deferred) {
366    ArrayPrototypePush(this.pendingSubtests, deferred);
367  }
368
369  async processPendingSubtests() {
370    while (this.pendingSubtests.length > 0 && this.hasConcurrency()) {
371      const deferred = ArrayPrototypeShift(this.pendingSubtests);
372      const test = deferred.test;
373      this.reporter.dequeue(test.nesting, test.loc, test.name);
374      await test.run();
375      deferred.resolve();
376    }
377  }
378
379  addReadySubtest(subtest) {
380    this.readySubtests.set(subtest.testNumber, subtest);
381  }
382
383  processReadySubtestRange(canSend) {
384    const start = this.waitingOn;
385    const end = start + this.readySubtests.size;
386
387    for (let i = start; i < end; i++) {
388      const subtest = this.readySubtests.get(i);
389
390      // Check if the specified subtest is in the map. If it is not, return
391      // early to avoid trying to process any more tests since they would be
392      // out of order.
393      if (subtest === undefined) {
394        return;
395      }
396
397      // Call isClearToSend() in the loop so that it is:
398      // - Only called if there are results to report in the correct order.
399      // - Guaranteed to only be called a maximum of once per call to
400      //   processReadySubtestRange().
401      canSend = canSend || this.isClearToSend();
402
403      if (!canSend) {
404        return;
405      }
406
407      if (i === 1 && this.parent !== null) {
408        this.reportStarted();
409      }
410
411      // Report the subtest's results and remove it from the ready map.
412      subtest.finalize();
413      this.readySubtests.delete(i);
414    }
415  }
416
417  createSubtest(Factory, name, options, fn, overrides) {
418    if (typeof name === 'function') {
419      fn = name;
420    } else if (name !== null && typeof name === 'object') {
421      fn = options;
422      options = name;
423    } else if (typeof options === 'function') {
424      fn = options;
425    }
426
427    if (options === null || typeof options !== 'object') {
428      options = kEmptyObject;
429    }
430
431    let parent = this;
432
433    // If this test has already ended, attach this test to the root test so
434    // that the error can be properly reported.
435    const preventAddingSubtests = this.finished || this.buildPhaseFinished;
436    if (preventAddingSubtests) {
437      while (parent.parent !== null) {
438        parent = parent.parent;
439      }
440    }
441
442    const test = new Factory({ __proto__: null, fn, name, parent, ...options, ...overrides });
443
444    if (parent.waitingOn === 0) {
445      parent.waitingOn = test.testNumber;
446    }
447
448    if (preventAddingSubtests) {
449      test.startTime = test.startTime || hrtime();
450      test.fail(
451        new ERR_TEST_FAILURE(
452          'test could not be started because its parent finished',
453          kParentAlreadyFinished,
454        ),
455      );
456    }
457
458    ArrayPrototypePush(parent.subtests, test);
459    return test;
460  }
461
462  #abortHandler = () => {
463    const error = this.outerSignal?.reason || new AbortError('The test was aborted');
464    error.failureType = kAborted;
465    this.#cancel(error);
466  };
467
468  #cancel(error) {
469    if (this.endTime !== null) {
470      return;
471    }
472
473    this.fail(error ||
474      new ERR_TEST_FAILURE(
475        'test did not finish before its parent and was cancelled',
476        kCancelledByParent,
477      ),
478    );
479    this.startTime = this.startTime || this.endTime; // If a test was canceled before it was started, e.g inside a hook
480    this.cancelled = true;
481    this.abortController.abort();
482  }
483
484  createHook(name, fn, options) {
485    validateOneOf(name, 'hook name', kHookNames);
486    // eslint-disable-next-line no-use-before-define
487    const hook = new TestHook(fn, options);
488    if (name === 'before' || name === 'after') {
489      hook.run = runOnce(hook.run);
490    }
491    ArrayPrototypePush(this.hooks[name], hook);
492    return hook;
493  }
494
495  fail(err) {
496    if (this.error !== null) {
497      return;
498    }
499
500    this.endTime = hrtime();
501    this.passed = false;
502    this.error = err;
503  }
504
505  pass() {
506    if (this.endTime !== null) {
507      return;
508    }
509
510    this.endTime = hrtime();
511    this.passed = true;
512  }
513
514  skip(message) {
515    this.skipped = true;
516    this.message = message;
517  }
518
519  todo(message) {
520    this.isTodo = true;
521    this.message = message;
522  }
523
524  diagnostic(message) {
525    ArrayPrototypePush(this.diagnostics, message);
526  }
527
528  start() {
529    // If there is enough available concurrency to run the test now, then do
530    // it. Otherwise, return a Promise to the caller and mark the test as
531    // pending for later execution.
532    this.reporter.enqueue(this.nesting, this.loc, this.name);
533    if (!this.parent.hasConcurrency()) {
534      const deferred = createDeferredPromise();
535
536      deferred.test = this;
537      this.parent.addPendingSubtest(deferred);
538      return deferred.promise;
539    }
540
541    this.reporter.dequeue(this.nesting, this.loc, this.name);
542    return this.run();
543  }
544
545  [kShouldAbort]() {
546    if (this.signal.aborted) {
547      return true;
548    }
549    if (this.outerSignal?.aborted) {
550      this.#abortHandler();
551      return true;
552    }
553  }
554
555  getRunArgs() {
556    const ctx = new TestContext(this);
557    return { __proto__: null, ctx, args: [ctx] };
558  }
559
560  async runHook(hook, args) {
561    validateOneOf(hook, 'hook name', kHookNames);
562    try {
563      await ArrayPrototypeReduce(this.hooks[hook], async (prev, hook) => {
564        await prev;
565        await hook.run(args);
566        if (hook.error) {
567          throw hook.error;
568        }
569      }, PromiseResolve());
570    } catch (err) {
571      const error = new ERR_TEST_FAILURE(`failed running ${hook} hook`, kHookFailure);
572      error.cause = isTestFailureError(err) ? err.cause : err;
573      throw error;
574    }
575  }
576
577  async run() {
578    if (this.parent !== null) {
579      this.parent.activeSubtests++;
580    }
581    this.startTime = hrtime();
582
583    if (this[kShouldAbort]()) {
584      this.postRun();
585      return;
586    }
587
588    const { args, ctx } = this.getRunArgs();
589    const after = async () => {
590      if (this.hooks.after.length > 0) {
591        await this.runHook('after', { __proto__: null, args, ctx });
592      }
593    };
594    const afterEach = runOnce(async () => {
595      if (this.parent?.hooks.afterEach.length > 0) {
596        await this.parent.runHook('afterEach', { __proto__: null, args, ctx });
597      }
598    });
599
600    let stopPromise;
601
602    try {
603      if (this.parent?.hooks.before.length > 0) {
604        await this.parent.runHook('before', this.parent.getRunArgs());
605      }
606      if (this.parent?.hooks.beforeEach.length > 0) {
607        await this.parent.runHook('beforeEach', { __proto__: null, args, ctx });
608      }
609      stopPromise = stopTest(this.timeout, this.signal);
610      const runArgs = ArrayPrototypeSlice(args);
611      ArrayPrototypeUnshift(runArgs, this.fn, ctx);
612
613      if (this.fn.length === runArgs.length - 1) {
614        // This test is using legacy Node.js error first callbacks.
615        const { promise, cb } = createDeferredCallback();
616
617        ArrayPrototypePush(runArgs, cb);
618        const ret = ReflectApply(this.runInAsyncScope, this, runArgs);
619
620        if (isPromise(ret)) {
621          this.fail(new ERR_TEST_FAILURE(
622            'passed a callback but also returned a Promise',
623            kCallbackAndPromisePresent,
624          ));
625          await SafePromiseRace([ret, stopPromise]);
626        } else {
627          await SafePromiseRace([PromiseResolve(promise), stopPromise]);
628        }
629      } else {
630        // This test is synchronous or using Promises.
631        const promise = ReflectApply(this.runInAsyncScope, this, runArgs);
632        await SafePromiseRace([PromiseResolve(promise), stopPromise]);
633      }
634
635      if (this[kShouldAbort]()) {
636        this.postRun();
637        return;
638      }
639
640      await afterEach();
641      await after();
642      this.pass();
643    } catch (err) {
644      try { await afterEach(); } catch { /* test is already failing, let's ignore the error */ }
645      try { await after(); } catch { /* Ignore error. */ }
646      if (isTestFailureError(err)) {
647        if (err.failureType === kTestTimeoutFailure) {
648          this.#cancel(err);
649        } else {
650          this.fail(err);
651        }
652      } else {
653        this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure));
654      }
655    } finally {
656      stopPromise?.[SymbolDispose]();
657
658      // Do not abort hooks and the root test as hooks instance are shared between tests suite so aborting them will
659      // cause them to not run for further tests.
660      if (this.parent !== null) {
661        this.abortController.abort();
662      }
663    }
664
665    if (this.parent !== null || typeof this.hookType === 'string') {
666      // Clean up the test. Then, try to report the results and execute any
667      // tests that were pending due to available concurrency.
668      //
669      // The root test is skipped here because it is a special case. Its
670      // postRun() method is called when the process is getting ready to exit.
671      // This helps catch any asynchronous activity that occurs after the tests
672      // have finished executing.
673      this.postRun();
674    }
675  }
676
677  postRun(pendingSubtestsError) {
678    // If the test was failed before it even started, then the end time will
679    // be earlier than the start time. Correct that here.
680    if (this.endTime < this.startTime) {
681      this.endTime = hrtime();
682    }
683    this.startTime ??= this.endTime;
684
685    // The test has run, so recursively cancel any outstanding subtests and
686    // mark this test as failed if any subtests failed.
687    this.pendingSubtests = [];
688    let failed = 0;
689    for (let i = 0; i < this.subtests.length; i++) {
690      const subtest = this.subtests[i];
691
692      if (!subtest.finished) {
693        subtest.#cancel(pendingSubtestsError);
694        subtest.postRun(pendingSubtestsError);
695      }
696      if (!subtest.passed && !subtest.isTodo) {
697        failed++;
698      }
699    }
700
701    if ((this.passed || this.parent === null) && failed > 0) {
702      const subtestString = `subtest${failed > 1 ? 's' : ''}`;
703      const msg = `${failed} ${subtestString} failed`;
704
705      this.fail(new ERR_TEST_FAILURE(msg, kSubtestsFailed));
706    }
707
708    this.outerSignal?.removeEventListener('abort', this.#abortHandler);
709    this.mock?.reset();
710
711    if (this.parent !== null) {
712      this.parent.activeSubtests--;
713      this.parent.addReadySubtest(this);
714      this.parent.processReadySubtestRange(false);
715      this.parent.processPendingSubtests();
716
717      if (this.parent === this.root &&
718          this.root.activeSubtests === 0 &&
719          this.root.pendingSubtests.length === 0 &&
720          this.root.readySubtests.size === 0 &&
721          this.root.hooks.after.length > 0) {
722        // This is done so that any global after() hooks are run. At this point
723        // all of the tests have finished running. However, there might be
724        // ref'ed handles keeping the event loop alive. This gives the global
725        // after() hook a chance to clean them up.
726        this.root.run();
727      }
728    } else if (!this.reported) {
729      const {
730        diagnostics,
731        harness,
732        loc,
733        nesting,
734        reporter,
735      } = this;
736
737      this.reported = true;
738      reporter.plan(nesting, loc, harness.counters.topLevel);
739
740      // Call this harness.coverage() before collecting diagnostics, since failure to collect coverage is a diagnostic.
741      const coverage = harness.coverage();
742      for (let i = 0; i < diagnostics.length; i++) {
743        reporter.diagnostic(nesting, loc, diagnostics[i]);
744      }
745
746      reporter.diagnostic(nesting, loc, `tests ${harness.counters.all}`);
747      reporter.diagnostic(nesting, loc, `suites ${harness.counters.suites}`);
748      reporter.diagnostic(nesting, loc, `pass ${harness.counters.passed}`);
749      reporter.diagnostic(nesting, loc, `fail ${harness.counters.failed}`);
750      reporter.diagnostic(nesting, loc, `cancelled ${harness.counters.cancelled}`);
751      reporter.diagnostic(nesting, loc, `skipped ${harness.counters.skipped}`);
752      reporter.diagnostic(nesting, loc, `todo ${harness.counters.todo}`);
753      reporter.diagnostic(nesting, loc, `duration_ms ${this.duration()}`);
754
755      if (coverage) {
756        reporter.coverage(nesting, loc, coverage);
757      }
758
759      reporter.end();
760    }
761  }
762
763  isClearToSend() {
764    return this.parent === null ||
765      (
766        this.parent.waitingOn === this.testNumber && this.parent.isClearToSend()
767      );
768  }
769
770  finalize() {
771    // By the time this function is called, the following can be relied on:
772    // - The current test has completed or been cancelled.
773    // - All of this test's subtests have completed or been cancelled.
774    // - It is the current test's turn to report its results.
775
776    // Report any subtests that have not been reported yet. Since all of the
777    // subtests have finished, it's safe to pass true to
778    // processReadySubtestRange(), which will finalize all remaining subtests.
779    this.processReadySubtestRange(true);
780
781    // Output this test's results and update the parent's waiting counter.
782    this.report();
783    this.parent.waitingOn++;
784    this.finished = true;
785  }
786
787  duration() {
788    // Duration is recorded in BigInt nanoseconds. Convert to milliseconds.
789    return Number(this.endTime - this.startTime) / 1_000_000;
790  }
791
792  report() {
793    countCompletedTest(this);
794    if (this.subtests.length > 0) {
795      this.reporter.plan(this.subtests[0].nesting, this.loc, this.subtests.length);
796    } else {
797      this.reportStarted();
798    }
799    let directive;
800    const details = { __proto__: null, duration_ms: this.duration() };
801
802    if (this.skipped) {
803      directive = this.reporter.getSkip(this.message);
804    } else if (this.isTodo) {
805      directive = this.reporter.getTodo(this.message);
806    }
807
808    if (this.reportedType) {
809      details.type = this.reportedType;
810    }
811
812    if (this.passed) {
813      this.reporter.ok(this.nesting, this.loc, this.testNumber, this.name, details, directive);
814    } else {
815      details.error = this.error;
816      this.reporter.fail(this.nesting, this.loc, this.testNumber, this.name, details, directive);
817    }
818
819    for (let i = 0; i < this.diagnostics.length; i++) {
820      this.reporter.diagnostic(this.nesting, this.loc, this.diagnostics[i]);
821    }
822  }
823
824  reportStarted() {
825    if (this.#reportedSubtest || this.parent === null) {
826      return;
827    }
828    this.#reportedSubtest = true;
829    this.parent.reportStarted();
830    this.reporter.start(this.nesting, this.loc, this.name);
831  }
832}
833
834class TestHook extends Test {
835  #args;
836  constructor(fn, options) {
837    if (options === null || typeof options !== 'object') {
838      options = kEmptyObject;
839    }
840    const { loc, timeout, signal } = options;
841    super({ __proto__: null, fn, loc, timeout, signal });
842
843    this.parentTest = options.parent ?? null;
844    this.hookType = options.hookType;
845  }
846  run(args) {
847    if (this.error && !this.outerSignal?.aborted) {
848      this.passed = false;
849      this.error = null;
850      this.abortController.abort();
851      this.abortController = new AbortController();
852      this.signal = this.abortController.signal;
853    }
854
855    this.#args = args;
856    return super.run();
857  }
858  getRunArgs() {
859    return this.#args;
860  }
861  matchesTestNamePatterns() {
862    return true;
863  }
864  postRun() {
865    const { error, loc, parentTest: parent } = this;
866
867    // Report failures in the root test's after() hook.
868    if (error && parent !== null &&
869        parent === parent.root && this.hookType === 'after') {
870
871      if (isTestFailureError(error)) {
872        error.failureType = kHookFailure;
873      }
874
875      parent.reporter.fail(0, loc, parent.subtests.length + 1, loc.file, {
876        __proto__: null,
877        duration_ms: this.duration(),
878        error,
879      }, undefined);
880    }
881  }
882}
883
884class Suite extends Test {
885  reportedType = 'suite';
886  constructor(options) {
887    super(options);
888
889    if (testNamePatterns !== null && !options.skip && !options.todo) {
890      this.fn = options.fn || this.fn;
891      this.skipped = false;
892    }
893    this.runOnlySubtests = testOnlyFlag;
894
895    try {
896      const { ctx, args } = this.getRunArgs();
897      const runArgs = [this.fn, ctx];
898      ArrayPrototypePushApply(runArgs, args);
899      this.buildSuite = SafePromisePrototypeFinally(
900        PromisePrototypeThen(
901          PromiseResolve(ReflectApply(this.runInAsyncScope, this, runArgs)),
902          undefined,
903          (err) => {
904            this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure));
905          }),
906        () => {
907          this.buildPhaseFinished = true;
908        },
909      );
910    } catch (err) {
911      this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure));
912
913      this.buildPhaseFinished = true;
914    }
915    this.fn = () => {};
916  }
917
918  getRunArgs() {
919    const ctx = new SuiteContext(this);
920    return { __proto__: null, ctx, args: [ctx] };
921  }
922
923  async run() {
924    const hookArgs = this.getRunArgs();
925
926    let stopPromise;
927    try {
928      this.parent.activeSubtests++;
929      await this.buildSuite;
930      this.startTime = hrtime();
931
932      if (this[kShouldAbort]()) {
933        this.subtests = [];
934        this.postRun();
935        return;
936      }
937
938      if (this.parent.hooks.before.length > 0) {
939        await this.parent.runHook('before', this.parent.getRunArgs());
940      }
941
942      await this.runHook('before', hookArgs);
943
944      stopPromise = stopTest(this.timeout, this.signal);
945      const subtests = this.skipped || this.error ? [] : this.subtests;
946      const promise = SafePromiseAll(subtests, (subtests) => subtests.start());
947
948      await SafePromiseRace([promise, stopPromise]);
949      await this.runHook('after', hookArgs);
950
951      this.pass();
952    } catch (err) {
953      if (isTestFailureError(err)) {
954        this.fail(err);
955      } else {
956        this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure));
957      }
958    } finally {
959      stopPromise?.[SymbolDispose]();
960    }
961
962    this.postRun();
963  }
964}
965
966module.exports = {
967  kCancelledByParent,
968  kSubtestsFailed,
969  kTestCodeFailure,
970  kTestTimeoutFailure,
971  kAborted,
972  kUnwrapErrors,
973  Suite,
974  Test,
975};
976