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
2 changes: 2 additions & 0 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,8 @@ changes:
* `functionCoverage` {number} Require a minimum percent of covered functions. If code
coverage does not reach the threshold specified, the process will exit with code `1`.
**Default:** `0`.
* `hookSignal` {boolean} Configures the test runner to handle termination signals like `SIGINT` and `SIGTERM`.
**Default:** `true`.
* Returns: {TestsStream}

**Note:** `shard` is used to horizontally parallelize test running across
Expand Down
3 changes: 1 addition & 2 deletions lib/internal/test_runner/harness.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,7 @@ function setupProcessState(root, globalOptions) {
process.on('uncaughtException', exceptionHandler);
process.on('unhandledRejection', rejectionHandler);
process.on('beforeExit', exitHandler);
// TODO(MoLow): Make it configurable to hook when isTestRunner === false.
if (globalOptions.isTestRunner) {
if (globalOptions.isTestRunner && globalOptions.hookSignal !== true) {
process.on('SIGINT', terminationHandler);
process.on('SIGTERM', terminationHandler);
}
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/test_runner/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@ function run(options = kEmptyObject) {
argv = [],
cwd = process.cwd(),
rerunFailuresFilePath,
hookSignal = true,
} = options;

if (files != null) {
Expand Down Expand Up @@ -708,6 +709,7 @@ function run(options = kEmptyObject) {
// behavior has relied on it, so removing it must be done in a semver major.
...parseCommandLine(),
setup, // This line can be removed when parseCommandLine() is removed here.
hookSignal,
coverage,
coverageExcludeGlobs,
coverageIncludeGlobs,
Expand Down
34 changes: 31 additions & 3 deletions test/parallel/test-runner-exit-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
const common = require('../common');
const fixtures = require('../common/fixtures');
const assert = require('assert');
const { spawnSync, spawn } = require('child_process');
const { spawnSync, spawn } = require('node:child_process');
const { once } = require('events');
const { finished } = require('stream/promises');

Expand All @@ -21,22 +21,39 @@
const [code, signal] = await once(child, 'exit');
await finished(child.stdout);
assert(stdout.startsWith('TAP version 13\n'));
assert.strictEqual(signal, null);

Check failure on line 24 in test/parallel/test-runner-exit-code.js

View workflow job for this annotation

GitHub Actions / test-macOS

--- stderr --- node:internal/process/promises:332 triggerUncaughtException(err, true /* fromPromise */); ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 'SIGINT' !== null at runAndKill (/Users/runner/work/node/node/node/test/parallel/test-runner-exit-code.js:24:10) at process.processTicksAndRejections (node:internal/process/task_queues:105:5) { generatedMessage: true, code: 'ERR_ASSERTION', actual: 'SIGINT', expected: null, operator: 'strictEqual', diff: 'simple' } Node.js v25.0.0-pre Command: out/Release/node --test-reporter=./test/common/test-error-reporter.js --test-reporter-destination=stdout /Users/runner/work/node/node/node/test/parallel/test-runner-exit-code.js

Check failure on line 24 in test/parallel/test-runner-exit-code.js

View workflow job for this annotation

GitHub Actions / test-linux (ubuntu-24.04)

--- stderr --- node:internal/process/promises:332 triggerUncaughtException(err, true /* fromPromise */); ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 'SIGINT' !== null at runAndKill (/home/runner/work/node/node/node/test/parallel/test-runner-exit-code.js:24:10) at process.processTicksAndRejections (node:internal/process/task_queues:105:5) { generatedMessage: true, code: 'ERR_ASSERTION', actual: 'SIGINT', expected: null, operator: 'strictEqual', diff: 'simple' } Node.js v25.0.0-pre Command: out/Release/node --test-reporter=./test/common/test-error-reporter.js --test-reporter-destination=stdout /home/runner/work/node/node/node/test/parallel/test-runner-exit-code.js

Check failure on line 24 in test/parallel/test-runner-exit-code.js

View workflow job for this annotation

GitHub Actions / test-linux (ubuntu-24.04-arm)

--- stderr --- node:internal/process/promises:332 triggerUncaughtException(err, true /* fromPromise */); ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 'SIGINT' !== null at runAndKill (/home/runner/work/node/node/node/test/parallel/test-runner-exit-code.js:24:10) at process.processTicksAndRejections (node:internal/process/task_queues:105:5) { generatedMessage: true, code: 'ERR_ASSERTION', actual: 'SIGINT', expected: null, operator: 'strictEqual', diff: 'simple' } Node.js v25.0.0-pre Command: out/Release/node --test-reporter=./test/common/test-error-reporter.js --test-reporter-destination=stdout /home/runner/work/node/node/node/test/parallel/test-runner-exit-code.js
assert.strictEqual(code, 1);
}

async function spawnAndKill(childArgs, { code: expectedCode, signal: expectedSignal }) {
if (common.isWindows) {
common.printSkipMessage('signals are not supported on windows');
return;
}
const child = spawn(process.execPath, childArgs);
child.stdout.once('data', () => child.kill('SIGINT'));
const [code, signal] = await once(child, 'exit');
assert.strictEqual(signal, expectedSignal);
assert.strictEqual(code, expectedCode);
}

if (process.argv[2] === 'child') {
const test = require('node:test');
const { test, run } = require('node:test');

if (process.argv[3] === 'pass') {
test('passing test', () => {
assert.strictEqual(true, true);
});
} else if (process.argv[3] === 'fail') {
assert.strictEqual(process.argv[3], 'fail');
test('failing test', () => {
assert.strictEqual(true, false);
});
} else if (process.argv[3] === 'run-signal-false') {
run({ files: [fixtures.path('test-runner', 'never_ending_async.js')] });
console.log('child started');
} else if (process.argv[3] === 'run-signal-true') {
run({ files: [fixtures.path('test-runner', 'never_ending_async.js')], hookSignal: true });
console.log('child started');
} else assert.fail('unreachable');
} else {
let child = spawnSync(process.execPath, [__filename, 'child', 'pass']);
Expand Down Expand Up @@ -69,4 +86,15 @@

runAndKill(fixtures.path('test-runner', 'never_ending_sync.js')).then(common.mustCall());
runAndKill(fixtures.path('test-runner', 'never_ending_async.js')).then(common.mustCall());

(async () => {
await spawnAndKill(
[__filename, 'child', 'run-signal-false'],
{ signal: 'SIGINT', code: null },
);
await spawnAndKill(
[__filename, 'child', 'run-signal-true'],
{ signal: null, code: 0 },
);
})().then(common.mustCall());
}
Loading