From 68d14e48f54678d65c6c2c16a20bbf4570ff902d Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Thu, 10 Jul 2025 19:24:08 +0200 Subject: [PATCH 1/2] fix: Failing with remaining tests (Windows) This change ensures that all messages from the worker are received by the main process before the worker exits. It solves issues on Windows that are occurring with Node.js v20 and later when `workerThreads: false` is set in the AVA configuration. Fixes: https://github.com/avajs/ava/issues/3390 --- lib/fork.js | 5 +++++ lib/worker/base.js | 9 +++++++++ lib/worker/channel.cjs | 2 ++ test/child-process/fixtures/package.json | 3 +++ test/child-process/fixtures/test.js | 7 +++++++ test/child-process/test.js | 9 +++++++++ 6 files changed, 35 insertions(+) create mode 100644 test/child-process/fixtures/package.json create mode 100644 test/child-process/fixtures/test.js create mode 100644 test/child-process/test.js diff --git a/lib/fork.js b/lib/fork.js index 8317774ff..6002bca2c 100644 --- a/lib/fork.js +++ b/lib/fork.js @@ -104,6 +104,11 @@ export default function loadFork(file, options, execArgv = process.execArgv) { } switch (message.ava.type) { + case 'worker-finished': { + send({type: 'worker-should-exit'}); + break; + } + case 'ready-for-options': { send({type: 'options', options}); break; diff --git a/lib/worker/base.js b/lib/worker/base.js index 520107dd3..f31fea8bb 100644 --- a/lib/worker/base.js +++ b/lib/worker/base.js @@ -120,6 +120,15 @@ const run = async options => { return; } + channel.send({type: 'worker-finished'}); + + // As the channel has already been unreferenced + // we need to reference it again to wait for the worker-should-exit message. + // Otherwise the process may exit prematurely, especially on Windows. + channel.ref(); + await channel.workerShouldExit; + channel.unref(); + nowAndTimers.setImmediate(() => { const unhandled = currentlyUnhandled(); if (unhandled.length === 0) { diff --git a/lib/worker/channel.cjs b/lib/worker/channel.cjs index 0479eeca6..c9544cb1b 100644 --- a/lib/worker/channel.cjs +++ b/lib/worker/channel.cjs @@ -107,7 +107,9 @@ handle.ref(); exports.options = selectAvaMessage(handle.channel, 'options').then(message => message.ava.options); exports.peerFailed = selectAvaMessage(handle.channel, 'peer-failed'); +exports.workerShouldExit = selectAvaMessage(handle.channel, 'worker-should-exit'); exports.send = handle.send.bind(handle); +exports.ref = handle.ref.bind(handle); exports.unref = handle.unref.bind(handle); let channelCounter = 0; diff --git a/test/child-process/fixtures/package.json b/test/child-process/fixtures/package.json new file mode 100644 index 000000000..bedb411a9 --- /dev/null +++ b/test/child-process/fixtures/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test/child-process/fixtures/test.js b/test/child-process/fixtures/test.js new file mode 100644 index 000000000..f7c5e8175 --- /dev/null +++ b/test/child-process/fixtures/test.js @@ -0,0 +1,7 @@ +import test from 'ava'; + +for (let i = 0; i < 50; i++) { + test('Test ' + (i + 1), t => { + t.true(true); + }); +} diff --git a/test/child-process/test.js b/test/child-process/test.js new file mode 100644 index 000000000..e75cb29a7 --- /dev/null +++ b/test/child-process/test.js @@ -0,0 +1,9 @@ +import test from '@ava/test'; + +import {fixture} from '../helpers/exec.js'; + +// Regression test for https://github.com/avajs/ava/issues/3390 +test('running 50 tests in a child process works as expected', async t => { + const result = await fixture(['--no-worker-threads']); + t.is(result.stats.passed.length, 50); +}); From 7d12953f3759f4eb3def2c4f5ea5748ed945ef2a Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 12 Jul 2025 14:23:49 +0200 Subject: [PATCH 2/2] Update comment and rename message Whether the worker exits depends on whether user code is keeping the event loop busy. 'Freed' is a better term. --- lib/fork.js | 2 +- lib/worker/base.js | 8 ++++---- lib/worker/channel.cjs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/fork.js b/lib/fork.js index 6002bca2c..82d12ffc3 100644 --- a/lib/fork.js +++ b/lib/fork.js @@ -105,7 +105,7 @@ export default function loadFork(file, options, execArgv = process.execArgv) { switch (message.ava.type) { case 'worker-finished': { - send({type: 'worker-should-exit'}); + send({type: 'free-worker'}); break; } diff --git a/lib/worker/base.js b/lib/worker/base.js index f31fea8bb..e3d3a3131 100644 --- a/lib/worker/base.js +++ b/lib/worker/base.js @@ -122,11 +122,11 @@ const run = async options => { channel.send({type: 'worker-finished'}); - // As the channel has already been unreferenced - // we need to reference it again to wait for the worker-should-exit message. - // Otherwise the process may exit prematurely, especially on Windows. + // Reference the channel until the worker is freed. This should prevent Node.js from terminating the child process + // prematurely, which has been witnessed on Windows. See discussion at + // . channel.ref(); - await channel.workerShouldExit; + await channel.workerFreed; channel.unref(); nowAndTimers.setImmediate(() => { diff --git a/lib/worker/channel.cjs b/lib/worker/channel.cjs index c9544cb1b..177bba359 100644 --- a/lib/worker/channel.cjs +++ b/lib/worker/channel.cjs @@ -107,7 +107,7 @@ handle.ref(); exports.options = selectAvaMessage(handle.channel, 'options').then(message => message.ava.options); exports.peerFailed = selectAvaMessage(handle.channel, 'peer-failed'); -exports.workerShouldExit = selectAvaMessage(handle.channel, 'worker-should-exit'); +exports.workerFreed = selectAvaMessage(handle.channel, 'free-worker'); exports.send = handle.send.bind(handle); exports.ref = handle.ref.bind(handle); exports.unref = handle.unref.bind(handle);