node/test/parallel/test-runner-worker-id.js
Ali Hassan 2ebe49600e
test_runner: expose worker ID for concurrent test execution
This adds support for identifying which worker is running a test file
when tests execute concurrently, similar to JEST_WORKER_ID in Jest,
VITEST_POOL_ID in Vitest, and MOCHA_WORKER_ID in Mocha.

When running with --test-isolation=process (default), each test file
runs in a separate child process and receives a unique worker ID from
1 to N. When running with --test-isolation=none, all tests run in the
same process and the worker ID is always 1.

This enables users to allocate separate resources (databases, ports,
etc.) for each test worker to avoid conflicts during concurrent
execution.

Changes:
- Add WorkerIdPool class to manage worker ID allocation and reuse
- Set NODE_TEST_WORKER_ID environment variable for child processes
- Add context.workerId getter to TestContext class
- Add tests for worker ID functionality
- Add documentation for context.workerId

Fixes: https://github.com/nodejs/node/issues/55842
PR-URL: https://github.com/nodejs/node/pull/61394
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com>
Reviewed-By: Jacob Smith <jacob@frende.me>
2026-02-24 23:37:22 +00:00

141 lines
4.5 KiB
JavaScript

'use strict';
require('../common');
const fixtures = require('../common/fixtures');
const assert = require('node:assert');
const { spawnSync } = require('node:child_process');
const { test } = require('node:test');
test('NODE_TEST_WORKER_ID is set for concurrent test files', async () => {
const args = [
'--test',
fixtures.path('test-runner', 'worker-id', 'test-1.mjs'),
fixtures.path('test-runner', 'worker-id', 'test-2.mjs'),
fixtures.path('test-runner', 'worker-id', 'test-3.mjs'),
];
const result = spawnSync(process.execPath, args, {
cwd: fixtures.path(),
env: { ...process.env }
});
assert.strictEqual(result.status, 0, `Test failed: ${result.stderr.toString()}`);
});
test('NODE_TEST_WORKER_ID is set with explicit concurrency', async () => {
const args = [
'--test',
'--test-concurrency=2',
fixtures.path('test-runner', 'worker-id', 'test-1.mjs'),
fixtures.path('test-runner', 'worker-id', 'test-2.mjs'),
];
const result = spawnSync(process.execPath, args, {
cwd: fixtures.path(),
env: { ...process.env }
});
assert.strictEqual(result.status, 0, `Test failed: ${result.stderr.toString()}`);
});
test('NODE_TEST_WORKER_ID is 1 with concurrency=1', async () => {
const args = ['--test', '--test-concurrency=1', fixtures.path('test-runner', 'worker-id', 'test-1.mjs')];
const result = spawnSync(process.execPath, args, {
cwd: fixtures.path(),
env: { ...process.env }
});
assert.strictEqual(result.status, 0, `Test failed: ${result.stderr.toString()}`);
});
test('NODE_TEST_WORKER_ID with explicit isolation=process', async () => {
const args = [
'--test',
'--test-isolation=process',
fixtures.path('test-runner', 'worker-id', 'test-1.mjs'),
fixtures.path('test-runner', 'worker-id', 'test-2.mjs'),
];
const result = spawnSync(process.execPath, args, {
cwd: fixtures.path(),
env: { ...process.env }
});
assert.strictEqual(result.status, 0, `Test failed: ${result.stderr.toString()}`);
});
test('NODE_TEST_WORKER_ID is 1 with isolation=none', async () => {
const args = [
'--test',
'--test-isolation=none',
fixtures.path('test-runner', 'worker-id', 'test-1.mjs'),
fixtures.path('test-runner', 'worker-id', 'test-2.mjs'),
];
const result = spawnSync(process.execPath, args, {
cwd: fixtures.path(),
env: { ...process.env }
});
assert.strictEqual(result.status, 0, `Test failed: ${result.stderr.toString()}`);
});
test('context.workerId matches NODE_TEST_WORKER_ID', async () => {
const args = ['--test', fixtures.path('test-runner', 'worker-id', 'test-1.mjs')];
const result = spawnSync(process.execPath, args, {
cwd: fixtures.path(),
env: { ...process.env }
});
// The fixture tests already verify that context.workerId matches the env var
assert.strictEqual(result.status, 0, `Test failed: ${result.stderr.toString()}`);
});
test('worker IDs are reused when more tests than concurrency', async () => {
const tmpdir = require('../common/tmpdir');
const { writeFileSync } = require('node:fs');
tmpdir.refresh();
// Create 9 separate test files dynamically
const testFiles = [];
const usageFile = tmpdir.resolve('worker-usage.txt');
for (let i = 1; i <= 9; i++) {
const testFile = tmpdir.resolve(`reuse-test-${i}.mjs`);
writeFileSync(
testFile,
`import { test } from 'node:test';
import { appendFileSync } from 'node:fs';
test('track worker ${i}', () => {
const workerId = process.env.NODE_TEST_WORKER_ID;
const usageFile = process.env.WORKER_USAGE_FILE;
appendFileSync(usageFile, workerId + '\\n');
});
`,
);
testFiles.push(testFile);
}
const args = ['--test', '--test-concurrency=3', ...testFiles];
const result = spawnSync(process.execPath, args, {
env: { ...process.env, WORKER_USAGE_FILE: usageFile }
});
assert.strictEqual(result.status, 0, `Test failed: ${result.stderr.toString()}`);
// Read and analyze worker IDs used
const { readFileSync } = require('node:fs');
const workerIds = readFileSync(usageFile, 'utf8').trim().split('\n');
// Count occurrences of each worker ID
const workerCounts = {};
workerIds.forEach((id) => {
workerCounts[id] = (workerCounts[id] || 0) + 1;
});
const uniqueWorkers = Object.keys(workerCounts);
assert.strictEqual(
uniqueWorkers.length,
3,
`Should have exactly 3 unique worker IDs, got ${uniqueWorkers.length}: ${uniqueWorkers.join(', ')}`
);
Object.entries(workerCounts).forEach(([id, count]) => {
assert.strictEqual(count, 3, `Worker ID ${id} should be used 3 times, got ${count}`);
});
});