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
6 changes: 6 additions & 0 deletions lib/internal/test_runner/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1484,6 +1484,12 @@

this.harness.teardown();
await SafePromiseAllReturnVoid(promises);
// Allow the event loop to drain any remaining write operations
// on the reporter destinations (e.g., child→parent report streams

Check failure on line 1488 in lib/internal/test_runner/test.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Non-ASCII character '→' detected
// that are still flushing) before calling process.exit().
// Without this, --test-force-exit with --test-concurrency can
// silently lose test verdicts. See nodejs/node#64833.
await new Promise((resolve) => setImmediate(resolve));

Check failure on line 1492 in lib/internal/test_runner/test.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

'setImmediate' is not defined

Check failure on line 1492 in lib/internal/test_runner/test.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Unexpected use of 'setImmediate'. Use `const { setImmediate } = require('timers');` instead of the global
process.exit();
}
}
Expand Down
81 changes: 81 additions & 0 deletions test/parallel/test-runner-force-exit-concurrency.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use strict';

Check failure on line 1 in test/parallel/test-runner-force-exit-concurrency.mjs

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Mandatory module "common" must be loaded before any other modules

Check failure on line 1 in test/parallel/test-runner-force-exit-concurrency.mjs

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Mandatory module "common" must be loaded

Check failure on line 1 in test/parallel/test-runner-force-exit-concurrency.mjs

View workflow job for this annotation

GitHub Actions / lint-js-and-md

'use strict' is unnecessary inside of modules
// This test verifies that --test-force-exit with --test-concurrency > 1
// does not silently lose test verdicts from child processes.
// Regression test for: https://github.com/nodejs/node/issues/64833

require('../common');
const fixtures = require('../common/fixtures');

Check failure on line 7 in test/parallel/test-runner-force-exit-concurrency.mjs

View workflow job for this annotation

GitHub Actions / lint-js-and-md

'fixtures' is assigned a value but never used
const tmpdir = require('../common/tmpdir');
const assert = require('node:assert');
const { spawnSync } = require('node:child_process');
const { readFileSync, writeFileSync, mkdirSync, rmSync } = require('node:fs');

Check failure on line 11 in test/parallel/test-runner-force-exit-concurrency.mjs

View workflow job for this annotation

GitHub Actions / lint-js-and-md

'readFileSync' is assigned a value but never used
const { resolve } = require('node:path');

Check failure on line 12 in test/parallel/test-runner-force-exit-concurrency.mjs

View workflow job for this annotation

GitHub Actions / lint-js-and-md

'resolve' is assigned a value but never used
const { test } = require('node:test');

tmpdir.refresh();

// Generate test files: N files × M tests each.
// We use a modest number to keep the test quick while still exercising
// the concurrent process-isolation path with force-exit.
const NUM_FILES = 8;
const TESTS_PER_FILE = 10;
const EXPECTED_TOTAL = NUM_FILES * TESTS_PER_FILE;

const testDir = tmpdir.resolve('force-exit-concurrency-tests');
rmSync(testDir, { recursive: true, force: true });
mkdirSync(testDir);

for (let f = 0; f < NUM_FILES; f++) {
const lines = ["import { test } from 'node:test';"];
for (let t = 0; t < TESTS_PER_FILE; t++) {
lines.push(
`test('file ${f} test ${t} — concurrency force-exit regression test', () => {});`
);
}
writeFileSync(`${testDir}/f${f}.test.mjs`, lines.join('\n'));
}

// Custom reporter that counts leaf test verdicts.
// Uses synchronous writes to rule out reporter-side buffering.
const countReporter = tmpdir.resolve('count-reporter.mjs');
writeFileSync(countReporter, `
import fs from 'node:fs';
export default async function* countReporter(source) {
let pass = 0, fail = 0;
for await (const event of source) {
if (event.type === 'test:pass' || event.type === 'test:fail') {
if (event.data.details?.type === 'test') {
if (event.type === 'test:pass') pass++; else fail++;
}
}
}
fs.writeSync(process.stderr.fd, JSON.stringify({ pass, fail, total: pass + fail }) + '\\n');
}
`);

test('--test-force-exit with --test-concurrency > 1 reports all tests', () => {
const args = [
'--test',
'--test-force-exit',
'--test-concurrency=4',
'--test-reporter', countReporter,
'--test-reporter-destination', 'stderr',
`${testDir}/*.test.mjs`,
];

// Run multiple times to catch the race.
for (let run = 0; run < 5; run++) {
const child = spawnSync(process.execPath, args, { encoding: 'utf8' });
const stderr = child.stderr.toString();
const match = stderr.match(/\{"pass":(\d+),"fail":(\d+),"total":(\d+)\}/);
assert.ok(match, `Run ${run}: count-reporter did not produce output. stderr: ${stderr}`);
const total = parseInt(match[3], 10);

assert.strictEqual(
total,
EXPECTED_TOTAL,
`Run ${run}: expected ${EXPECTED_TOTAL} tests reported, got ${total}. ` +
`stderr: ${stderr.slice(0, 500)}`
);
}
});
Loading