From 017d30c27d1151d07d28165419a3dfb0c86253ae Mon Sep 17 00:00:00 2001 From: Mete0rfish Date: Fri, 29 Aug 2025 20:49:36 +0900 Subject: [PATCH 1/7] test_runner: make harness hook configurable --- lib/internal/test_runner/harness.js | 3 +-- lib/internal/test_runner/runner.js | 1 + test/parallel/test-runner-exit-code.js | 34 +++++++++++++++++++++++--- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index bd4b35f1d64033..d0b43611651391 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -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) { process.on('SIGINT', terminationHandler); process.on('SIGTERM', terminationHandler); } diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 1340b817a57e5b..98d49d480948d1 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -708,6 +708,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, diff --git a/test/parallel/test-runner-exit-code.js b/test/parallel/test-runner-exit-code.js index d2f0251e5fb30c..5c11446e41222a 100644 --- a/test/parallel/test-runner-exit-code.js +++ b/test/parallel/test-runner-exit-code.js @@ -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'); @@ -25,18 +25,35 @@ async function runAndKill(file) { assert.strictEqual(code, 1); } +async function spawnAndKillProgrammatic(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')], signal: true }); + console.log('child started'); } else assert.fail('unreachable'); } else { let child = spawnSync(process.execPath, [__filename, 'child', 'pass']); @@ -69,4 +86,15 @@ if (process.argv[2] === 'child') { 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 spawnAndKillProgrammatic( + [__filename, 'child', 'run-signal-false'], + { signal: 'SIGINT', code: null }, + ); + await spawnAndKillProgrammatic( + [__filename, 'child', 'run-signal-true'], + { signal: null, code: 1 }, + ); + })().then(common.mustCall()); } From b3bdab59affd4ab1bc77be23d9571d841bd1cc8b Mon Sep 17 00:00:00 2001 From: Mete0rfish Date: Fri, 29 Aug 2025 21:25:02 +0900 Subject: [PATCH 2/7] test_runner: change configure parameter name --- lib/internal/test_runner/runner.js | 1 + test/parallel/test-runner-exit-code.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 98d49d480948d1..b7eb40e7203ab3 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -593,6 +593,7 @@ function run(options = kEmptyObject) { argv = [], cwd = process.cwd(), rerunFailuresFilePath, + hookSignal = false, } = options; if (files != null) { diff --git a/test/parallel/test-runner-exit-code.js b/test/parallel/test-runner-exit-code.js index 5c11446e41222a..3667b567745819 100644 --- a/test/parallel/test-runner-exit-code.js +++ b/test/parallel/test-runner-exit-code.js @@ -52,7 +52,7 @@ if (process.argv[2] === 'child') { 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')], signal: true }); + run({ files: [fixtures.path('test-runner', 'never_ending_async.js')], hookSignal: true }); console.log('child started'); } else assert.fail('unreachable'); } else { @@ -94,7 +94,7 @@ if (process.argv[2] === 'child') { ); await spawnAndKillProgrammatic( [__filename, 'child', 'run-signal-true'], - { signal: null, code: 1 }, + { signal: null, code: 0 }, ); })().then(common.mustCall()); } From f7fba083d956386e4b72943550a82e7037a1a417 Mon Sep 17 00:00:00 2001 From: Mete0rfish Date: Fri, 29 Aug 2025 21:37:39 +0900 Subject: [PATCH 3/7] test_runner: use strict check for hookSignal option --- lib/internal/test_runner/harness.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index d0b43611651391..228bffc4caf766 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -297,7 +297,7 @@ function setupProcessState(root, globalOptions) { process.on('uncaughtException', exceptionHandler); process.on('unhandledRejection', rejectionHandler); process.on('beforeExit', exitHandler); - if (globalOptions.isTestRunner || globalOptions.hookSignal) { + if (globalOptions.isTestRunner || globalOptions.hookSignal === true) { process.on('SIGINT', terminationHandler); process.on('SIGTERM', terminationHandler); } From 46e4fcc783c3ccafe58e44989b217bdf5875f8ef Mon Sep 17 00:00:00 2001 From: Mete0rfish Date: Fri, 29 Aug 2025 21:54:00 +0900 Subject: [PATCH 4/7] test: rename spawnAndKill method --- test/parallel/test-runner-exit-code.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/parallel/test-runner-exit-code.js b/test/parallel/test-runner-exit-code.js index 3667b567745819..6bd0e37f16e98a 100644 --- a/test/parallel/test-runner-exit-code.js +++ b/test/parallel/test-runner-exit-code.js @@ -25,7 +25,7 @@ async function runAndKill(file) { assert.strictEqual(code, 1); } -async function spawnAndKillProgrammatic(childArgs, { code: expectedCode, signal: expectedSignal }) { +async function spawnAndKill(childArgs, { code: expectedCode, signal: expectedSignal }) { if (common.isWindows) { common.printSkipMessage('signals are not supported on windows'); return; @@ -88,11 +88,11 @@ if (process.argv[2] === 'child') { runAndKill(fixtures.path('test-runner', 'never_ending_async.js')).then(common.mustCall()); (async () => { - await spawnAndKillProgrammatic( + await spawnAndKill( [__filename, 'child', 'run-signal-false'], { signal: 'SIGINT', code: null }, ); - await spawnAndKillProgrammatic( + await spawnAndKill( [__filename, 'child', 'run-signal-true'], { signal: null, code: 0 }, ); From ff0d560b462372ac625c0c451a254b0f51e6243a Mon Sep 17 00:00:00 2001 From: Mete0rfish Date: Fri, 29 Aug 2025 23:16:38 +0900 Subject: [PATCH 5/7] test: remove trailing spaces --- test/parallel/test-runner-exit-code.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/parallel/test-runner-exit-code.js b/test/parallel/test-runner-exit-code.js index 6bd0e37f16e98a..b7a03e6acd76d8 100644 --- a/test/parallel/test-runner-exit-code.js +++ b/test/parallel/test-runner-exit-code.js @@ -50,10 +50,10 @@ if (process.argv[2] === 'child') { }); } else if (process.argv[3] === 'run-signal-false') { run({ files: [fixtures.path('test-runner', 'never_ending_async.js')] }); - console.log('child started'); + 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'); + console.log('child started'); } else assert.fail('unreachable'); } else { let child = spawnSync(process.execPath, [__filename, 'child', 'pass']); From 3922f4115a4370c8f0a86cb4bdec2922653899e3 Mon Sep 17 00:00:00 2001 From: Mete0rfish Date: Mon, 1 Sep 2025 08:55:57 +0900 Subject: [PATCH 6/7] doc: update run method option about hookSignal --- doc/api/test.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/api/test.md b/doc/api/test.md index 88a602758f4f7b..1c1b6bc3b8019a 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -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 From 54a6e25e7362f0bf476a708278875a5d3141ba6c Mon Sep 17 00:00:00 2001 From: Mete0rfish Date: Mon, 1 Sep 2025 08:57:30 +0900 Subject: [PATCH 7/7] test_runner: change default behavior of hookSignal option --- lib/internal/test_runner/harness.js | 2 +- lib/internal/test_runner/runner.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index 228bffc4caf766..e1c072d6747ea9 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -297,7 +297,7 @@ function setupProcessState(root, globalOptions) { process.on('uncaughtException', exceptionHandler); process.on('unhandledRejection', rejectionHandler); process.on('beforeExit', exitHandler); - if (globalOptions.isTestRunner || globalOptions.hookSignal === true) { + if (globalOptions.isTestRunner && globalOptions.hookSignal !== true) { process.on('SIGINT', terminationHandler); process.on('SIGTERM', terminationHandler); } diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index b7eb40e7203ab3..ff1ebf1879f01f 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -593,7 +593,7 @@ function run(options = kEmptyObject) { argv = [], cwd = process.cwd(), rerunFailuresFilePath, - hookSignal = false, + hookSignal = true, } = options; if (files != null) {