diff --git a/doc/api/test.md b/doc/api/test.md index f88d3542f954bf7..662f7176c0f4a88 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -466,6 +466,15 @@ each other in ways that are not possible when isolation is enabled. For example, if a test relies on global state, it is possible for that state to be modified by a test originating from another file. +## Environment Variables + +### `NODE_TEST_WORKER_ID` + +when running the test in parallel using the `--experimental-test-isolation=process` +flag, this environment variable will be set to a number between 1 to N (where N is the +number of files). When running the test in isolation using the +`--experimental-test-isolation=none` flag, this environment variable will be set to `1`. + ## Collecting code coverage > Stability: 1 - Experimental diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 43a62b5b4307e4e..9f54ed0a6d9415c 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -358,14 +358,14 @@ class FileTest extends Test { } } -function runTestFile(path, filesWatcher, opts) { +function runTestFile(path, filesWatcher, opts, workerId = 1) { const watchMode = filesWatcher != null; const testPath = path === kIsolatedProcessName ? '' : path; const testOpts = { __proto__: null, signal: opts.signal }; const subtest = opts.root.createSubtest(FileTest, testPath, testOpts, async (t) => { const args = getRunArgs(path, opts); const stdio = ['pipe', 'pipe', 'pipe']; - const env = { __proto__: null, ...process.env, NODE_TEST_CONTEXT: 'child-v8' }; + const env = { __proto__: null, ...process.env, NODE_TEST_CONTEXT: 'child-v8', NODE_TEST_WORKER_ID: workerId }; if (watchMode) { stdio.push('ipc'); env.WATCH_REPORT_DEPENDENCIES = '1'; @@ -724,8 +724,10 @@ function run(options = kEmptyObject) { runFiles = () => { root.harness.bootstrapPromise = null; root.harness.buildPromise = null; + let workerId = 1; return SafePromiseAllSettledReturnVoid(testFiles, (path) => { - const subtest = runTestFile(path, filesWatcher, opts); + const subtest = runTestFile(path, filesWatcher, opts, workerId); + workerId++; filesWatcher?.runningSubtests.set(path, subtest); return subtest; }); @@ -766,6 +768,7 @@ function run(options = kEmptyObject) { root.entryFile = resolve(testFile); debug('loading test file:', fileURL.href); + process.env.NODE_TEST_WORKER_ID = 1; try { await cascadedLoader.import(fileURL, parent, { __proto__: null }); } catch (err) { diff --git a/test/fixtures/test-runner/NODE_TEST_WORKER_ID/1.test.js b/test/fixtures/test-runner/NODE_TEST_WORKER_ID/1.test.js new file mode 100644 index 000000000000000..5680299a7554c49 --- /dev/null +++ b/test/fixtures/test-runner/NODE_TEST_WORKER_ID/1.test.js @@ -0,0 +1,7 @@ +// This file tests the `NODE_TEST_WORKER_ID` feature. +// The test logic is implemented in `test/parallel/test-runner-cli.js`. +const test = require('node:test'); + +test('test1', t => { + console.log('NODE_TEST_WORKER_ID', process.env.NODE_TEST_WORKER_ID) +}); \ No newline at end of file diff --git a/test/fixtures/test-runner/NODE_TEST_WORKER_ID/2.test.js b/test/fixtures/test-runner/NODE_TEST_WORKER_ID/2.test.js new file mode 100644 index 000000000000000..51e0b63dbc29525 --- /dev/null +++ b/test/fixtures/test-runner/NODE_TEST_WORKER_ID/2.test.js @@ -0,0 +1,7 @@ +// This file tests the `NODE_TEST_WORKER_ID` feature. +// The test logic is implemented in `test/parallel/test-runner-cli.js`. +const test = require('node:test'); + +test('test2', t => { + console.log('NODE_TEST_WORKER_ID', process.env.NODE_TEST_WORKER_ID) +}); \ No newline at end of file diff --git a/test/fixtures/test-runner/NODE_TEST_WORKER_ID/3.test.js b/test/fixtures/test-runner/NODE_TEST_WORKER_ID/3.test.js new file mode 100644 index 000000000000000..27edc8f2d8213b6 --- /dev/null +++ b/test/fixtures/test-runner/NODE_TEST_WORKER_ID/3.test.js @@ -0,0 +1,7 @@ +// This file tests the `NODE_TEST_WORKER_ID` feature. +// The test logic is implemented in `test/parallel/test-runner-cli.js`. +const test = require('node:test'); + +test('test3', t => { + console.log('NODE_TEST_WORKER_ID', process.env.NODE_TEST_WORKER_ID) +}); \ No newline at end of file diff --git a/test/fixtures/test-runner/NODE_TEST_WORKER_ID/4.test.js b/test/fixtures/test-runner/NODE_TEST_WORKER_ID/4.test.js new file mode 100644 index 000000000000000..9c39b30ec02759a --- /dev/null +++ b/test/fixtures/test-runner/NODE_TEST_WORKER_ID/4.test.js @@ -0,0 +1,7 @@ +// This file tests the `NODE_TEST_WORKER_ID` feature. +// The test logic is implemented in `test/parallel/test-runner-cli.js`. +const test = require('node:test'); + +test('test4', t => { + console.log('NODE_TEST_WORKER_ID', process.env.NODE_TEST_WORKER_ID) +}); \ No newline at end of file diff --git a/test/parallel/test-runner-cli.js b/test/parallel/test-runner-cli.js index 3e8e14b747a22f1..017b9fff9790036 100644 --- a/test/parallel/test-runner-cli.js +++ b/test/parallel/test-runner-cli.js @@ -428,3 +428,31 @@ for (const isolation of ['none', 'process']) { assert.strictEqual(child.status, 0); assert.strictEqual(child.signal, null); } + +{ + const args = ['--test', '--experimental-test-isolation=process']; + const child = spawnSync(process.execPath, args, { cwd: join(testFixtures, 'NODE_TEST_WORKER_ID') }); + + assert.strictEqual(child.stderr.toString(), ''); + const stdout = child.stdout.toString(); + + assert.match(stdout, /NODE_TEST_WORKER_ID 1/); + assert.match(stdout, /NODE_TEST_WORKER_ID 2/); + assert.match(stdout, /NODE_TEST_WORKER_ID 3/); + assert.match(stdout, /NODE_TEST_WORKER_ID 4/); + + assert.strictEqual(child.status, 0); +} + +{ + const args = ['--test', '--experimental-test-isolation=none']; + const child = spawnSync(process.execPath, args, { cwd: join(testFixtures, 'NODE_TEST_WORKER_ID') }); + + assert.strictEqual(child.stderr.toString(), ''); + const stdout = child.stdout.toString(); + const regex = /NODE_TEST_WORKER_ID 1/g; + const result = stdout.match(regex); + + assert.strictEqual(result.length, 4); + assert.strictEqual(child.status, 0); +}