Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions lib/internal/test_runner/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
});

const kIsolatedProcessName = Symbol('kIsolatedProcessName');
// NODE_TEST_CONTEXT is set on a child's environment in runTestFile(). It is
// inherited at process creation, before any user code runs, and is never
// mutated in-process, so capturing it once keeps run() from reading ambient
// process state on every call.
const isTestRunnerChildProcess = process.env.NODE_TEST_CONTEXT !== undefined;
const kFilterArgs = [
'--test',
'--experimental-test-coverage',
Expand Down Expand Up @@ -193,7 +198,8 @@ function getRunArgs(path, { forceExit,
randomize,
randomSeed,
root: { timeout },
cwd }) {
cwd,
processExecArgv }) {
const processNodeOptions = getOptionsAsFlagsFromBinding();
const runArgs = ArrayPrototypeFilter(processNodeOptions, filterExecArgv);

Expand All @@ -207,7 +213,7 @@ function getRunArgs(path, { forceExit,
*/
const nodeOptionsSet = new SafeSet(processNodeOptions);
const unknownProcessExecArgv = ArrayPrototypeFilter(
process.execArgv,
processExecArgv,
(arg) => !nodeOptionsSet.has(arg),
);
ArrayPrototypePushApply(runArgs, unknownProcessExecArgv);
Expand Down Expand Up @@ -496,7 +502,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 || opts.processEnv) };

// Acquire a worker ID from the pool for process isolation mode
let workerId;
Expand Down Expand Up @@ -967,6 +973,17 @@ function run(options = kEmptyObject) {
testFiles.length,
);

// Capture references to ambient process state once, at the run() boundary, so
// the exec args and environment forwarded to child test processes are fixed
// here rather than re-read later inside the per-file subtest callback.
// processExecArgv carries the parent's V8/exec-only flags (e.g.
// --allow-natives-syntax) and is distinct from the public `execArgv` option;
// processEnv is what children inherit when the `env` option is omitted. These
// stay live references (not copies) so later process.env mutations remain
// visible when each child's env is built.
const processExecArgv = process.execArgv;
const processEnv = process.env;

const opts = {
__proto__: null,
root,
Expand All @@ -990,10 +1007,12 @@ function run(options = kEmptyObject) {
randomize,
randomSeed,
testFiles,
processExecArgv,
processEnv,
};

if (isolation === 'process') {
if (process.env.NODE_TEST_CONTEXT !== undefined) {
if (isTestRunnerChildProcess) {
process.emitWarning('node:test run() is being called recursively within a test file. skipping running files.');
root.postRun();
return root.reporter;
Expand Down
6 changes: 6 additions & 0 deletions test/fixtures/test-runner/process-env-inherited.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const { test } = require('node:test');

test('process.env is inherited when no env option is provided', (t) => {
t.assert.strictEqual(process.env.INHERITED_VAR, 'XYZ', 'parent env var should be inherited by default');
t.assert.strictEqual(process.env.NODE_TEST_CONTEXT, 'child-v8', 'NODE_TEST_CONTEXT should be set by run()');
});
16 changes: 16 additions & 0 deletions test/parallel/test-runner-run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,22 @@ describe('env', () => {
delete process.env.ABC;
});

it('should inherit process.env when the env option is omitted', async () => {
// Set a variable on the main process env and confirm the spawned test
// inherits it when the env option is omitted (the default behavior).
process.env.INHERITED_VAR = 'XYZ';

try {
const stream = run({ files: [join(testFixtures, 'process-env-inherited.js')] });
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);
} finally {
delete process.env.INHERITED_VAR;
}
});

it('should throw error when env is specified with isolation=none', async () => {
assert.throws(() => run({ env: { foo: 'bar' }, isolation: 'none' }), {
code: 'ERR_INVALID_ARG_VALUE',
Expand Down
Loading