diff --git a/doc/api/test.md b/doc/api/test.md index 4213d3816422bc..6a180782005e38 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -681,6 +681,9 @@ added: v18.9.0 **Default:** `true`. * `files`: {Array} An array containing the list of files to run. **Default** matching files from [test runner execution model][]. + * `setup` {Function} A function that accepts the `TestsStream` instance + and can be used to setup listeners before any tests are run. + **Default:** `undefined`. * `signal` {AbortSignal} Allows aborting an in-progress test execution. * `timeout` {number} A number of milliseconds the test execution will fail after. diff --git a/lib/internal/main/test_runner.js b/lib/internal/main/test_runner.js index cefe2f9fad0ce8..04a79343e0b4cf 100644 --- a/lib/internal/main/test_runner.js +++ b/lib/internal/main/test_runner.js @@ -21,8 +21,7 @@ if (isUsingInspector()) { inspectPort = process.debugPort; } -const testsStream = run({ concurrency, inspectPort, watch: getOptionValue('--watch') }); -testsStream.once('test:fail', () => { +run({ concurrency, inspectPort, watch: getOptionValue('--watch'), setup: setupTestReporters }) +.once('test:fail', () => { process.exitCode = 1; }); -setupTestReporters(testsStream); diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index d37b4ebab4248f..26ea67d9982ff0 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -159,21 +159,27 @@ function setup(root) { } let globalRoot; +let reportersSetup; function getGlobalRoot() { if (!globalRoot) { globalRoot = createTestTree(); globalRoot.reporter.once('test:fail', () => { process.exitCode = 1; }); - setupTestReporters(globalRoot.reporter); + reportersSetup = setupTestReporters(globalRoot.reporter); } return globalRoot; } +async function startSubtest(subtest) { + await reportersSetup; + await subtest.start(); +} + function test(name, options, fn) { const parent = testResources.get(executionAsyncId()) || getGlobalRoot(); const subtest = parent.createSubtest(Test, name, options, fn); - return subtest.start(); + return startSubtest(subtest); } function runInParentContext(Factory) { @@ -181,7 +187,7 @@ function runInParentContext(Factory) { const parent = testResources.get(executionAsyncId()) || getGlobalRoot(); const subtest = parent.createSubtest(Factory, name, options, fn, overrides); if (parent === getGlobalRoot()) { - subtest.start(); + startSubtest(subtest); } } diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 2424235cd48d15..66d2431d00c030 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -13,8 +13,10 @@ const { ObjectAssign, ObjectKeys, PromisePrototypeThen, + SafePromiseAll, SafePromiseAllReturnVoid, SafePromiseAllSettledReturnVoid, + PromiseResolve, SafeMap, SafeSet, StringPrototypeIndexOf, @@ -24,6 +26,7 @@ const { const { spawn } = require('child_process'); const { readdirSync, statSync } = require('fs'); +const { finished } = require('internal/streams/end-of-stream'); // TODO(aduh95): switch to internal/readline/interface when backporting to Node.js 16.x is no longer a concern. const { createInterface } = require('readline'); const { FilesWatcher } = require('internal/watch_mode/files_watcher'); @@ -33,7 +36,7 @@ const { ERR_TEST_FAILURE, }, } = require('internal/errors'); -const { validateArray, validateBoolean } = require('internal/validators'); +const { validateArray, validateBoolean, validateFunction } = require('internal/validators'); const { getInspectPort, isUsingInspector, isInspectorMessage } = require('internal/util/inspector'); const { kEmptyObject } = require('internal/util'); const { createTestTree } = require('internal/test_runner/harness'); @@ -298,7 +301,10 @@ function runTestFile(path, root, inspectPort, filesWatcher) { subtest.addToReport(ast); }); - const { 0: code, 1: signal } = await once(child, 'exit', { signal: t.signal }); + const { 0: { 0: code, 1: signal } } = await SafePromiseAll([ + once(child, 'exit', { signal: t.signal }), + finished(parser, { signal: t.signal }), + ]); runningProcesses.delete(path); runningSubtests.delete(path); @@ -347,7 +353,7 @@ function run(options) { if (options === null || typeof options !== 'object') { options = kEmptyObject; } - const { concurrency, timeout, signal, files, inspectPort, watch } = options; + const { concurrency, timeout, signal, files, inspectPort, watch, setup } = options; if (files != null) { validateArray(files, 'options.files'); @@ -355,6 +361,9 @@ function run(options) { if (watch != null) { validateBoolean(watch, 'options.watch'); } + if (setup != null) { + validateFunction(setup, 'options.setup'); + } const root = createTestTree({ concurrency, timeout, signal }); const testFiles = files ?? createTestFileList(); @@ -365,13 +374,13 @@ function run(options) { filesWatcher = watchFiles(testFiles, root, inspectPort); postRun = undefined; } - - PromisePrototypeThen(SafePromiseAllSettledReturnVoid(testFiles, (path) => { + const runFiles = () => SafePromiseAllSettledReturnVoid(testFiles, (path) => { const subtest = runTestFile(path, root, inspectPort, filesWatcher); runningSubtests.set(path, subtest); return subtest; - }), postRun); + }); + PromisePrototypeThen(PromisePrototypeThen(PromiseResolve(setup?.(root.reporter)), runFiles), postRun); return root.reporter; }