diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 92b963cd72bcf3..d150943783e975 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -128,6 +128,11 @@ const kDiagnosticsFilterArgs = ['tests', 'suites', 'pass', 'fail', 'cancelled', const kCanceledTests = new SafeSet() .add(kCancelledByParent).add(kAborted).add(kTestTimeoutFailure); +// Execution-ordered events are forwarded immediately, bypassing the +// per-file declaration-order buffer. +const kExecutionOrderedEvents = new SafeSet() + .add('test:enqueue').add('test:dequeue').add('test:complete'); + let kResistStopPropagation; // Worker ID pool management for concurrent test execution @@ -331,6 +336,10 @@ class FileTest extends Test { } } addToReport(item) { + if (kExecutionOrderedEvents.has(item.type)) { + this.#handleReportItem(item); + return; + } this.#accumulateReportItem(item); if (!this.isClearToSend()) { ArrayPrototypePush(this.#reportBuffer, item); diff --git a/test/fixtures/test-runner/execution-ordered-bypass/fast-fail.mjs b/test/fixtures/test-runner/execution-ordered-bypass/fast-fail.mjs new file mode 100644 index 00000000000000..74b77682b6821d --- /dev/null +++ b/test/fixtures/test-runner/execution-ordered-bypass/fast-fail.mjs @@ -0,0 +1,6 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; + +test('fast-fail', () => { + assert.fail('fast'); +}); diff --git a/test/fixtures/test-runner/execution-ordered-bypass/slow.mjs b/test/fixtures/test-runner/execution-ordered-bypass/slow.mjs new file mode 100644 index 00000000000000..4ee60ffe8537e7 --- /dev/null +++ b/test/fixtures/test-runner/execution-ordered-bypass/slow.mjs @@ -0,0 +1,9 @@ +import { test } from 'node:test'; +import { setTimeout as sleep } from 'node:timers/promises'; + +test('slow', async () => { + // Long enough that fast-fail's process can spawn, run, and round-trip its + // bypassed test:complete to the host on slow CI, but short enough that the + // test does not waste much time when the bypass is working. + await sleep(30_000); +}); diff --git a/test/parallel/test-runner-execution-ordered-bypass.mjs b/test/parallel/test-runner-execution-ordered-bypass.mjs new file mode 100644 index 00000000000000..85b468cae2a7e1 --- /dev/null +++ b/test/parallel/test-runner-execution-ordered-bypass.mjs @@ -0,0 +1,59 @@ +// Flags: --no-warnings + +import '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import assert from 'node:assert'; +import { test, run } from 'node:test'; + +const files = [ + fixtures.path('test-runner', 'execution-ordered-bypass', 'slow.mjs'), + fixtures.path('test-runner', 'execution-ordered-bypass', 'fast-fail.mjs'), +]; + +test('execution-ordered events bypass FileTest declaration-order buffer', async () => { + // Concurrency must be a number so the runner does not collapse it to 1 on + // single-core CI runners (where `concurrency: true` resolves to + // `availableParallelism() - 1`). Without two slots the runner spawns the + // files sequentially and fast-fail never starts while slow is sleeping. + const stream = run({ + files, + isolation: 'process', + concurrency: 2, + }); + + const events = []; + + stream.on('test:complete', (data) => { + if (data.name === 'slow' || data.name === 'fast-fail') { + events.push(`complete:${data.name}`); + } + }); + + stream.on('test:fail', (data) => { + if (data.name === 'fast-fail') { + events.push(`fail:${data.name}`); + } + }); + + // eslint-disable-next-line no-unused-vars + for await (const _ of stream); + + const completeFast = events.indexOf('complete:fast-fail'); + const completeSlow = events.indexOf('complete:slow'); + const failFast = events.indexOf('fail:fast-fail'); + + assert.notStrictEqual(completeFast, -1); + assert.notStrictEqual(completeSlow, -1); + assert.notStrictEqual(failFast, -1); + + assert.ok( + completeFast < completeSlow, + `test:complete for fast-fail should arrive before slow; events=${events.join(', ')}`, + ); + + // test:fail is declaration-ordered, so the bypass must not affect it. + assert.ok( + failFast > completeSlow, + `test:fail for fast-fail should arrive after test:complete for slow; events=${events.join(', ')}`, + ); +});