Skip to content

Commit 83c163f

Browse files
committed
test_runner: preserve signal exit codes on interruption
1 parent 69fdff9 commit 83c163f

2 files changed

Lines changed: 16 additions & 12 deletions

File tree

lib/internal/test_runner/harness.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const {
2020
ERR_TEST_FAILURE,
2121
},
2222
} = require('internal/errors');
23+
const signalNumbers = internalBinding('constants').os.signals;
2324
const { exitCodes: { kGenericUserError } } = internalBinding('errors');
2425
const { kCancelledByParent, Test, Suite } = require('internal/test_runner/test');
2526
const {
@@ -285,8 +286,8 @@ function setupProcessState(root, globalOptions) {
285286
process.removeListener('unhandledRejection', rejectionHandler);
286287
process.removeListener('beforeExit', exitHandler);
287288
if (globalOptions.isTestRunner) {
288-
process.removeListener('SIGINT', terminationHandler);
289-
process.removeListener('SIGTERM', terminationHandler);
289+
process.removeListener('SIGINT', sigintHandler);
290+
process.removeListener('SIGTERM', sigtermHandler);
290291
}
291292
};
292293

@@ -310,24 +311,27 @@ function setupProcessState(root, globalOptions) {
310311
return running;
311312
};
312313

313-
const terminationHandler = async () => {
314+
const terminationHandler = async (signal) => {
314315
const runningTests = findRunningTests(root);
315316
if (runningTests.length > 0) {
316317
root.reporter.interrupted(runningTests);
317318
// Allow the reporter stream to process the interrupted event
318319
await new Promise((resolve) => setImmediate(resolve));
319320
}
321+
process.exitCode = 128 + signalNumbers[signal];
320322
await exitHandler(true);
321323
process.exit();
322324
};
325+
const sigintHandler = FunctionPrototypeBind(terminationHandler, null, 'SIGINT');
326+
const sigtermHandler = FunctionPrototypeBind(terminationHandler, null, 'SIGTERM');
323327

324328
process.on('uncaughtException', exceptionHandler);
325329
process.on('unhandledRejection', rejectionHandler);
326330
process.on('beforeExit', exitHandler);
327331
// TODO(MoLow): Make it configurable to hook when isTestRunner === false.
328332
if (globalOptions.isTestRunner) {
329-
process.on('SIGINT', terminationHandler);
330-
process.on('SIGTERM', terminationHandler);
333+
process.on('SIGINT', sigintHandler);
334+
process.on('SIGTERM', sigtermHandler);
331335
}
332336

333337
root.harness.coverage = FunctionPrototypeBind(collectCoverage, null, root, coverage);

test/parallel/test-runner-exit-code.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const { spawnSync, spawn } = require('child_process');
66
const { once } = require('events');
77
const { finished } = require('stream/promises');
88

9-
async function runAndKill(file, expectedTestName) {
9+
async function runAndKill(file, expectedTestName, killSignal, expectedCode) {
1010
if (common.isWindows) {
1111
common.printSkipMessage(`signals are not supported in windows, skipping ${file}`);
1212
return;
@@ -15,17 +15,17 @@ async function runAndKill(file, expectedTestName) {
1515
const child = spawn(process.execPath, ['--test', '--test-reporter=tap', file]);
1616
child.stdout.setEncoding('utf8');
1717
child.stdout.on('data', (chunk) => {
18-
if (!stdout.length) child.kill('SIGINT');
18+
if (!stdout.length) child.kill(killSignal);
1919
stdout += chunk;
2020
});
21-
const [code, signal] = await once(child, 'exit');
21+
const [code, exitSignal] = await once(child, 'exit');
2222
await finished(child.stdout);
2323
assert(stdout.startsWith('TAP version 13\n'));
2424
// Verify interrupted test message
2525
assert(stdout.includes(`Interrupted while running: ${expectedTestName}`),
2626
`Expected output to contain interrupted test name`);
27-
assert.strictEqual(signal, null);
28-
assert.strictEqual(code, 1);
27+
assert.strictEqual(exitSignal, null);
28+
assert.strictEqual(code, expectedCode);
2929
}
3030

3131
if (process.argv[2] === 'child') {
@@ -82,6 +82,6 @@ if (process.argv[2] === 'child') {
8282
// because the parent runner only knows about file-level tests
8383
const neverEndingSync = fixtures.path('test-runner', 'never_ending_sync.js');
8484
const neverEndingAsync = fixtures.path('test-runner', 'never_ending_async.js');
85-
runAndKill(neverEndingSync, neverEndingSync).then(common.mustCall());
86-
runAndKill(neverEndingAsync, neverEndingAsync).then(common.mustCall());
85+
runAndKill(neverEndingSync, neverEndingSync, 'SIGINT', 130).then(common.mustCall());
86+
runAndKill(neverEndingAsync, neverEndingAsync, 'SIGTERM', 143).then(common.mustCall());
8787
}

0 commit comments

Comments
 (0)