From 67ad62d4bcdfca441145ae9ff0b809517b23d53f Mon Sep 17 00:00:00 2001 From: inoway46 Date: Sun, 1 Mar 2026 02:46:42 +0900 Subject: [PATCH] test_runner: fix run() none-isolation teardown hang --- lib/internal/test_runner/runner.js | 13 +++++- .../run-isolation-none-in-cluster.js | 45 +++++++++++++++++++ test/parallel/test-runner-run.mjs | 14 ++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/test-runner/run-isolation-none-in-cluster.js diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index f90c7dcad10346..2922cd76b7a74e 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -942,8 +942,19 @@ function run(options = kEmptyObject) { debug('beginning test execution'); root.entryFile = null; finishBootstrap(); - return root.processPendingSubtests(); + const pendingSubtestsPromise = root.processPendingSubtests(); + if (!isTestRunner) { + PromisePrototypeThen(pendingSubtestsPromise, undefined, triggerUncaughtException); + return; + } + return pendingSubtestsPromise; }; + + if (!isTestRunner) { + // run() API consumers can keep handles alive (e.g., IPC). Finalize + // without waiting for beforeExit so the stream can close promptly. + teardown = () => root.harness.teardown(); + } } } diff --git a/test/fixtures/test-runner/run-isolation-none-in-cluster.js b/test/fixtures/test-runner/run-isolation-none-in-cluster.js new file mode 100644 index 00000000000000..de550d102d6698 --- /dev/null +++ b/test/fixtures/test-runner/run-isolation-none-in-cluster.js @@ -0,0 +1,45 @@ +'use strict'; + +const cluster = require('node:cluster'); +const { join } = require('node:path'); +const { run } = require('node:test'); + +if (cluster.isPrimary) { + const worker = cluster.fork(); + worker.on('exit', (code, signal) => { + if (signal !== null) { + process.stderr.write(`worker exited with signal ${signal}\n`); + process.exit(1); + } + + process.exit(code ?? 0); + }); +} else { + // Repro based on: https://github.com/nodejs/node/issues/60020 + // We pin `files` for deterministic test discovery in CI. + const stream = run({ + isolation: 'none', + files: [ + join(__dirname, 'default-behavior', 'test', 'random.cjs'), + ], + }); + + stream.on('error', (err) => { + process.stderr.write(`worker error: ${err}\n`); + process.exit(1); + }); + + stream.on('data', (data) => { + process.stdout.write(`on data ${data.type}\n`); + }); + + stream.on('end', () => { + process.stdout.write('on end\n'); + process.exit(0); + }); + + setTimeout(() => { + process.stderr.write('worker timed out waiting for end\n'); + process.exit(1); + }, 3000).unref(); +} diff --git a/test/parallel/test-runner-run.mjs b/test/parallel/test-runner-run.mjs index e9bb6c4a260160..621cd914b0a961 100644 --- a/test/parallel/test-runner-run.mjs +++ b/test/parallel/test-runner-run.mjs @@ -1,6 +1,7 @@ import * as common from '../common/index.mjs'; import * as fixtures from '../common/fixtures.mjs'; import { join } from 'node:path'; +import { spawnSync } from 'node:child_process'; import { describe, it, run } from 'node:test'; import { dot, spec, tap } from 'node:test/reporters'; import consumers from 'node:stream/consumers'; @@ -646,6 +647,19 @@ describe('require(\'node:test\').run', { concurrency: true }, () => { assert.strictEqual(diagnostics.includes(entry), true); } }); + + // Regression test for https://github.com/nodejs/node/issues/60020 + it('should not hang in cluster workers when isolation is none', () => { + const fixture = fixtures.path('test-runner', 'run-isolation-none-in-cluster.js'); + const { status, signal, stdout, stderr } = spawnSync(process.execPath, [fixture], { + encoding: 'utf8', + timeout: common.platformTimeout(15_000), + }); + + assert.strictEqual(signal, null); + assert.strictEqual(status, 0, stderr); + assert.match(stdout, /on end/); + }); }); describe('env', () => {