diff --git a/lib/internal/main/test_runner.js b/lib/internal/main/test_runner.js index a42c1e5dff64cc..cc853da7388821 100644 --- a/lib/internal/main/test_runner.js +++ b/lib/internal/main/test_runner.js @@ -10,10 +10,7 @@ const { } = require('internal/process/pre_execution'); const { isUsingInspector } = require('internal/util/inspector'); const { run } = require('internal/test_runner/runner'); -const { - parseCommandLine, - setupTestReporters, -} = require('internal/test_runner/utils'); +const { parseCommandLine } = require('internal/test_runner/utils'); const { exitCodes: { kGenericUserError } } = internalBinding('errors'); let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => { debug = fn; @@ -22,32 +19,17 @@ let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => { prepareMainThreadExecution(false); markBootstrapComplete(); -const { - perFileTimeout, - runnerConcurrency, - shard, - watchMode, -} = parseCommandLine(); - -let concurrency = runnerConcurrency; -let inspectPort; +const options = parseCommandLine(); if (isUsingInspector()) { process.emitWarning('Using the inspector with --test forces running at a concurrency of 1. ' + 'Use the inspectPort option to run with concurrency'); - concurrency = 1; - inspectPort = process.debugPort; + options.concurrency = 1; + options.inspectPort = process.debugPort; } -const options = { - concurrency, - inspectPort, - watch: watchMode, - setup: setupTestReporters, - timeout: perFileTimeout, - shard, - globPatterns: ArrayPrototypeSlice(process.argv, 1), -}; +options.globPatterns = ArrayPrototypeSlice(process.argv, 1); + debug('test runner configuration:', options); run(options).on('test:fail', (data) => { if (data.todo === undefined || data.todo === false) { diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index ac52307cc38be5..9c372c115e90f2 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -17,13 +17,10 @@ const { }, } = require('internal/errors'); const { exitCodes: { kGenericUserError } } = internalBinding('errors'); - -const { kEmptyObject } = require('internal/util'); const { kCancelledByParent, Test, Suite } = require('internal/test_runner/test'); const { parseCommandLine, reporterScope, - setupTestReporters, shouldColorizeTestFiles, } = require('internal/test_runner/utils'); const { queueMicrotask } = require('internal/process/task_queues'); @@ -34,8 +31,42 @@ let globalRoot; testResources.set(reporterScope.asyncId(), reporterScope); -function createTestTree(options = kEmptyObject) { - globalRoot = setup(new Test({ __proto__: null, ...options, name: '' })); +function createTestTree(rootTestOptions, globalOptions) { + const harness = { + __proto__: null, + allowTestsToRun: false, + bootstrapPromise: resolvedPromise, + watching: false, + config: globalOptions, + coverage: null, + resetCounters() { + harness.counters = { + __proto__: null, + all: 0, + failed: 0, + passed: 0, + cancelled: 0, + skipped: 0, + todo: 0, + topLevel: 0, + suites: 0, + }; + }, + counters: null, + shouldColorizeTestFiles: shouldColorizeTestFiles(globalOptions.destinations), + teardown: null, + snapshotManager: null, + }; + + harness.resetCounters(); + globalRoot = new Test({ + __proto__: null, + ...rootTestOptions, + harness, + name: '', + }); + setupProcessState(globalRoot, globalOptions, harness); + globalRoot.startTime = hrtime(); return globalRoot; } @@ -127,15 +158,7 @@ function collectCoverage(rootTest, coverage) { return summary; } -function setup(root) { - if (root.startTime !== null) { - return root; - } - - // Parse the command line options before the hook is enabled. We don't want - // global input validation errors to end up in the uncaughtException handler. - const globalOptions = parseCommandLine(); - +function setupProcessState(root, globalOptions) { const hook = createHook({ __proto__: null, init(asyncId, type, triggerAsyncId, resource) { @@ -195,46 +218,26 @@ function setup(root) { process.on('SIGTERM', terminationHandler); } - root.harness = { - __proto__: null, - allowTestsToRun: false, - bootstrapPromise: resolvedPromise, - watching: false, - coverage: FunctionPrototypeBind(collectCoverage, null, root, coverage), - resetCounters() { - root.harness.counters = { - __proto__: null, - all: 0, - failed: 0, - passed: 0, - cancelled: 0, - skipped: 0, - todo: 0, - topLevel: 0, - suites: 0, - }; - }, - counters: null, - shouldColorizeTestFiles: shouldColorizeTestFiles(globalOptions.destinations), - teardown: exitHandler, - snapshotManager: null, - }; - root.harness.resetCounters(); - root.startTime = hrtime(); - return root; + root.harness.coverage = FunctionPrototypeBind(collectCoverage, null, root, coverage); + root.harness.teardown = exitHandler; } function lazyBootstrapRoot() { if (!globalRoot) { // This is where the test runner is bootstrapped when node:test is used // without the --test flag or the run() API. - createTestTree({ __proto__: null, entryFile: process.argv?.[1] }); + const rootTestOptions = { + __proto__: null, + entryFile: process.argv?.[1], + }; + const globalOptions = parseCommandLine(); + createTestTree(rootTestOptions, globalOptions); globalRoot.reporter.on('test:fail', (data) => { if (data.todo === undefined || data.todo === false) { process.exitCode = kGenericUserError; } }); - globalRoot.harness.bootstrapPromise = setupTestReporters(globalRoot.reporter); + globalRoot.harness.bootstrapPromise = globalOptions.setup(globalRoot.reporter); } return globalRoot; } diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index a14cc97ce8690c..80601beeb64571 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -69,6 +69,7 @@ const { convertStringToRegExp, countCompletedTest, kDefaultPattern, + parseCommandLine, } = require('internal/test_runner/utils'); const { Glob } = require('internal/fs/glob'); const { once } = require('events'); @@ -561,7 +562,15 @@ function run(options = kEmptyObject) { }); } - const root = createTestTree({ __proto__: null, concurrency, timeout, signal }); + const rootTestOptions = { __proto__: null, concurrency, timeout, signal }; + const globalOptions = { + __proto__: null, + // parseCommandLine() should not be used here. However, The existing run() + // behavior has relied on it, so removing it must be done in a semver major. + ...parseCommandLine(), + setup, // This line can be removed when parseCommandLine() is removed here. + }; + const root = createTestTree(rootTestOptions, globalOptions); if (process.env.NODE_TEST_CONTEXT !== undefined) { process.emitWarning('node:test run() is being called recursively within a test file. skipping running files.'); diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 00922c7b529272..99872c1e1d773a 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -45,7 +45,6 @@ const { createDeferredCallback, countCompletedTest, isTestFailureError, - parseCommandLine, } = require('internal/test_runner/utils'); const { createDeferredPromise, @@ -79,14 +78,6 @@ const kHookNames = ObjectSeal(['before', 'after', 'beforeEach', 'afterEach']); const kUnwrapErrors = new SafeSet() .add(kTestCodeFailure).add(kHookFailure) .add('uncaughtException').add('unhandledRejection'); -const { - forceExit, - sourceMaps, - testNamePatterns, - testSkipPatterns, - testOnlyFlag, - updateSnapshots, -} = parseCommandLine(); let kResistStopPropagation; let assertObj; let findSourceMap; @@ -132,7 +123,7 @@ function lazyAssertObject(harness) { const { getOptionValue } = require('internal/options'); if (getOptionValue('--experimental-test-snapshots')) { const { SnapshotManager } = require('internal/test_runner/snapshot'); - harness.snapshotManager = new SnapshotManager(updateSnapshots); + harness.snapshotManager = new SnapshotManager(harness.config.updateSnapshots); assertObj.set('snapshot', harness.snapshotManager.createAssert()); } } @@ -311,23 +302,43 @@ class TestContext { } before(fn, options) { - this.#test - .createHook('before', fn, { __proto__: null, ...options, hookType: 'before', loc: getCallerLocation() }); + 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, hookType: 'after', loc: getCallerLocation() }); + 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, hookType: 'beforeEach', loc: getCallerLocation() }); + 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, hookType: 'afterEach', loc: getCallerLocation() }); + this.#test.createHook('afterEach', fn, { + __proto__: null, + ...options, + parent: this.#test, + hookType: 'afterEach', + loc: getCallerLocation(), + }); } } @@ -386,15 +397,17 @@ class Test extends AsyncResource { 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 = testOnlyFlag; + this.only = this.config.only; this.reporter = new TestsStream(); this.runOnlySubtests = this.only; this.childNumber = 0; this.timeout = kDefaultTimeout; this.entryFile = entryFile; - this.root = this; this.hooks = { __proto__: null, before: [], @@ -407,6 +420,9 @@ class Test extends AsyncResource { 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); @@ -415,7 +431,6 @@ class Test extends AsyncResource { this.childNumber = parent.subtests.length + 1; this.timeout = parent.timeout; this.entryFile = parent.entryFile; - this.root = parent.root; this.hooks = { __proto__: null, before: [], @@ -430,7 +445,7 @@ class Test extends AsyncResource { this.parent.filteredSubtestCount++; } - if (testOnlyFlag && only === false) { + if (this.config.only && only === false) { fn = noop; } } @@ -480,7 +495,6 @@ class Test extends AsyncResource { ); this.fn = fn; - this.harness = null; // Configured on the root test by the test harness. this.mock = null; this.plan = null; this.expectedAssertions = plan; @@ -501,7 +515,7 @@ class Test extends AsyncResource { this.waitingOn = 0; this.finished = false; - if (!testOnlyFlag && (only || this.parent?.runOnlySubtests)) { + if (!this.config.only && (only || this.parent?.runOnlySubtests)) { const warning = "'only' and 'runOnly' require the --test-only command-line option."; this.diagnostic(warning); @@ -517,7 +531,7 @@ class Test extends AsyncResource { file: loc[2], }; - if (sourceMaps === true) { + if (this.config.sourceMaps === true) { const map = lazyFindSourceMap(this.loc.file); const entry = map?.findEntry(this.loc.line - 1, this.loc.column - 1); @@ -535,7 +549,9 @@ class Test extends AsyncResource { } willBeFiltered() { - if (testOnlyFlag && !this.only) return true; + if (this.config.only && !this.only) return true; + + const { testNamePatterns, testSkipPatterns } = this.config; if (testNamePatterns && !testMatchesPattern(this, testNamePatterns)) { return true; @@ -899,7 +915,7 @@ class Test extends AsyncResource { // This helps catch any asynchronous activity that occurs after the tests // have finished executing. this.postRun(); - } else if (forceExit) { + } 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 @@ -1088,14 +1104,17 @@ class Test extends AsyncResource { class TestHook extends Test { #args; constructor(fn, options) { - if (options === null || typeof options !== 'object') { - options = kEmptyObject; - } - const { loc, timeout, signal } = options; - super({ __proto__: null, fn, loc, timeout, signal }); - - this.parentTest = options.parent ?? null; - this.hookType = options.hookType; + 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) { @@ -1119,9 +1138,7 @@ class TestHook extends Test { const { error, loc, parentTest: parent } = this; // Report failures in the root test's after() hook. - if (error && parent !== null && - parent === parent.root && this.hookType === 'after') { - + if (error && parent === parent.root && this.hookType === 'after') { if (isTestFailureError(error)) { error.failureType = kHookFailure; } @@ -1141,11 +1158,13 @@ class Suite extends Test { constructor(options) { super(options); - if (testNamePatterns !== null && testSkipPatterns !== null && !options.skip) { + if (this.config.testNamePatterns !== null && + this.config.testSkipPatterns !== null && + !options.skip) { this.fn = options.fn || this.fn; this.skipped = false; } - this.runOnlySubtests = testOnlyFlag; + this.runOnlySubtests = this.config.only; try { const { ctx, args } = this.getRunArgs(); @@ -1176,9 +1195,9 @@ class Suite extends Test { this.filtered = false; this.parent.filteredSubtestCount--; } else if ( - testOnlyFlag && - testNamePatterns == null && - testSkipPatterns == null && + 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 diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index aae2a756800a0f..e6c421ff870bbd 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -176,15 +176,6 @@ async function getReportersMap(reporters, destinations) { } const reporterScope = new AsyncResource('TestReporterScope'); -const setupTestReporters = reporterScope.bind(async (rootReporter) => { - const { reporters, destinations } = parseCommandLine(); - const reportersMap = await getReportersMap(reporters, destinations); - for (let i = 0; i < reportersMap.length; i++) { - const { reporter, destination } = reportersMap[i]; - compose(rootReporter, reporter).pipe(destination); - } -}); - let globalTestOptions; function parseCommandLine() { @@ -197,19 +188,19 @@ function parseCommandLine() { const forceExit = getOptionValue('--test-force-exit'); const sourceMaps = getOptionValue('--enable-source-maps'); const updateSnapshots = getOptionValue('--test-update-snapshots'); - const watchMode = getOptionValue('--watch'); + const watch = getOptionValue('--watch'); const isChildProcess = process.env.NODE_TEST_CONTEXT === 'child'; const isChildProcessV8 = process.env.NODE_TEST_CONTEXT === 'child-v8'; + let concurrency; let coverageExcludeGlobs; let coverageIncludeGlobs; let destinations; - let perFileTimeout; + let only; let reporters; - let runnerConcurrency; + let shard; let testNamePatterns; let testSkipPatterns; - let testOnlyFlag; - let shard; + let timeout; if (isChildProcessV8) { kBuiltinReporters.set('v8-serializer', 'internal/test_runner/reporter/v8-serializer'); @@ -239,9 +230,9 @@ function parseCommandLine() { } if (isTestRunner) { - perFileTimeout = getOptionValue('--test-timeout') || Infinity; - runnerConcurrency = getOptionValue('--test-concurrency') || true; - testOnlyFlag = false; + timeout = getOptionValue('--test-timeout') || Infinity; + concurrency = getOptionValue('--test-concurrency') || true; + only = false; testNamePatterns = null; const shardOption = getOptionValue('--test-shard'); @@ -262,10 +253,10 @@ function parseCommandLine() { }; } } else { - perFileTimeout = Infinity; - runnerConcurrency = 1; + timeout = Infinity; + concurrency = 1; const testNamePatternFlag = getOptionValue('--test-name-pattern'); - testOnlyFlag = getOptionValue('--test-only'); + only = getOptionValue('--test-only'); testNamePatterns = testNamePatternFlag?.length > 0 ? ArrayPrototypeMap( testNamePatternFlag, @@ -281,24 +272,34 @@ function parseCommandLine() { coverageIncludeGlobs = getOptionValue('--test-coverage-include'); } + const setup = reporterScope.bind(async (rootReporter) => { + const reportersMap = await getReportersMap(reporters, destinations); + + for (let i = 0; i < reportersMap.length; i++) { + const { reporter, destination } = reportersMap[i]; + compose(rootReporter, reporter).pipe(destination); + } + }); + globalTestOptions = { __proto__: null, isTestRunner, + concurrency, coverage, coverageExcludeGlobs, coverageIncludeGlobs, + destinations, forceExit, - perFileTimeout, - runnerConcurrency, + only, + reporters, + setup, shard, sourceMaps, - testOnlyFlag, testNamePatterns, testSkipPatterns, + timeout, updateSnapshots, - reporters, - destinations, - watchMode, + watch, }; return globalTestOptions; @@ -480,7 +481,6 @@ module.exports = { kDefaultPattern, parseCommandLine, reporterScope, - setupTestReporters, shouldColorizeTestFiles, getCoverageReport, }; diff --git a/test/parallel/test-runner-v8-deserializer.mjs b/test/parallel/test-runner-v8-deserializer.mjs index 9b4447d5a24291..42a8a84d1fe09f 100644 --- a/test/parallel/test-runner-v8-deserializer.mjs +++ b/test/parallel/test-runner-v8-deserializer.mjs @@ -26,7 +26,10 @@ describe('v8 deserializer', () => { let reported; beforeEach(() => { reported = []; - fileTest = new runner.FileTest({ name: 'filetest' }); + fileTest = new runner.FileTest({ + name: 'filetest', + harness: { config: {} }, + }); fileTest.reporter.on('data', (data) => reported.push(data)); assert(fileTest.isClearToSend()); });