From c8bf2a8bbdbf05f6ae7905523de1e365a9331d0d Mon Sep 17 00:00:00 2001 From: inoway46 Date: Sun, 8 Mar 2026 23:20:28 +0900 Subject: [PATCH] test_runner: fix run() none-isolation teardown hang --- lib/internal/test_runner/runner.js | 6 +++ .../run-isolation-none-in-cluster.js | 44 +++++++++++++++++++ test/parallel/test-runner-run.mjs | 14 ++++++ 3 files changed, 64 insertions(+) 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..e4b86149912fc5 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -944,6 +944,12 @@ function run(options = kEmptyObject) { finishBootstrap(); return root.processPendingSubtests(); }; + + 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..3279422fa6841c --- /dev/null +++ b/test/fixtures/test-runner/run-isolation-none-in-cluster.js @@ -0,0 +1,44 @@ +'use strict'; + +const cluster = require('node:cluster'); +const { join } = require('node:path'); +const { run } = require('node:test'); + +if (cluster.isPrimary) { + cluster.fork().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 + const stream = run({ + isolation: 'none', + files: [ + join(__dirname, 'default-behavior', 'test', 'random.cjs'), + ], + }); + + async function consumeStream() { + for await (const data of stream) { + if (data === undefined) { + continue; + } + } + process.stdout.write('on end\n'); + process.exit(0); + } + + consumeStream().catch((err) => { + process.stderr.write(`worker error: ${err}\n`); + process.exit(1); + }); + + 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..1b5bc5429976e2 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(5000), + }); + + assert.strictEqual(signal, null); + assert.strictEqual(status, 0, stderr); + assert.match(stdout, /on end/); + }); }); describe('env', () => {