diff --git a/doc/api/test.md b/doc/api/test.md index 3d245c2303b185..40c0653555f230 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -1546,6 +1546,9 @@ changes: * `setup` {Function} A function that accepts the `TestsStream` instance and can be used to setup listeners before any tests are run. **Default:** `undefined`. + * `env` {Object} Environment variables to pass to test child processes. + This option has no effect when `isolation` is `'none'`. + **Default:** `process.env`. * `execArgv` {Array} An array of CLI flags to pass to the `node` executable when spawning the subprocesses. This option has no effect when `isolation` is `'none`'. **Default:** `[]` diff --git a/lib/internal/main/test_runner.js b/lib/internal/main/test_runner.js index fda47897da9f06..19b62fc62a4bb3 100644 --- a/lib/internal/main/test_runner.js +++ b/lib/internal/main/test_runner.js @@ -30,7 +30,12 @@ if (isUsingInspector() && options.isolation === 'process') { options.inspectPort = process.debugPort; } +// Capture process state explicitly so run() does not need to access it. options.globPatterns = ArrayPrototypeSlice(process.argv, 1); +options.cwd = process.cwd(); +if (options.isolation !== 'none') { + options.env = process.env; +} debug('test runner configuration:', options); run(options).on('test:summary', (data) => { diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index f90c7dcad10346..3b1a10005ad604 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -167,6 +167,7 @@ function getRunArgs(path, { forceExit, only, argv: suppliedArgs, execArgv, + globPatterns, rerunFailuresFilePath, root: { timeout }, cwd }) { @@ -214,7 +215,9 @@ function getRunArgs(path, { forceExit, if (path === kIsolatedProcessName) { ArrayPrototypePush(runArgs, '--test'); - ArrayPrototypePushApply(runArgs, ArrayPrototypeSlice(process.argv, 1)); + if (globPatterns != null) { + ArrayPrototypePushApply(runArgs, globPatterns); + } } else { ArrayPrototypePush(runArgs, path); } @@ -421,7 +424,7 @@ function runTestFile(path, filesWatcher, opts) { const subtest = opts.root.createSubtest(FileTest, testPath, testOpts, async (t) => { const args = getRunArgs(path, opts); const stdio = ['pipe', 'pipe', 'pipe']; - const env = { __proto__: null, NODE_TEST_CONTEXT: 'child-v8', ...(opts.env || process.env) }; + const env = { __proto__: null, NODE_TEST_CONTEXT: 'child-v8', ...opts.env }; // Acquire a worker ID from the pool for process isolation mode let workerId; @@ -648,11 +651,22 @@ function run(options = kEmptyObject) { functionCoverage = 0, execArgv = [], argv = [], - cwd = process.cwd(), + cwd, rerunFailuresFilePath, env, } = options; + // Default to process state when not explicitly provided, maintaining + // backwards compatibility for public API users while allowing the + // internal CLI entry point to pass these values explicitly. + const userProvidedEnv = env !== undefined; + if (cwd === undefined) { + cwd = process.cwd(); + } + if (env === undefined) { + env = process.env; + } + if (files != null) { validateArray(files, 'options.files'); } @@ -759,7 +773,7 @@ function run(options = kEmptyObject) { validatePath(globalSetupPath, 'options.globalSetupPath'); } - if (env != null) { + if (userProvidedEnv) { validateObject(env); if (isolation === 'none') { @@ -836,7 +850,7 @@ function run(options = kEmptyObject) { }; if (isolation === 'process') { - if (process.env.NODE_TEST_CONTEXT !== undefined) { + if (env.NODE_TEST_CONTEXT !== undefined) { process.emitWarning('node:test run() is being called recursively within a test file. skipping running files.'); root.postRun(); return root.reporter; diff --git a/test/parallel/test-runner-run.mjs b/test/parallel/test-runner-run.mjs index e9bb6c4a260160..96575ba824a0b2 100644 --- a/test/parallel/test-runner-run.mjs +++ b/test/parallel/test-runner-run.mjs @@ -687,6 +687,27 @@ describe('forceExit', () => { message: /The property 'options\.forceExit' is not supported with watch mode\./ }); }); + + it('should accept env option and pass it to child processes', async () => { + const stream = run({ + files: [join(testFixtures, 'default-behavior/test/random.cjs')], + env: { ...process.env, NODE_TEST_CUSTOM_ENV_VAR: '1' }, + }); + stream.on('test:fail', common.mustNotCall()); + stream.on('test:pass', common.mustCall(1)); + // eslint-disable-next-line no-unused-vars + for await (const _ of stream); + }); + + it('should reject env option with isolation none', () => { + assert.throws(() => run({ + files: [join(testFixtures, 'default-behavior/test/random.cjs')], + isolation: 'none', + env: { FOO: 'bar' }, + }), { + code: 'ERR_INVALID_ARG_VALUE', + }); + }); });