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>
141 lines
4.5 KiB
JavaScript
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}`);
|
|
});
|
|
});
|