Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions lib/internal/test_runner/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { test } from 'node:test';
import assert from 'node:assert';

test('fast-fail', () => {
assert.fail('fast');
});
9 changes: 9 additions & 0 deletions test/fixtures/test-runner/execution-ordered-bypass/slow.mjs
Original file line number Diff line number Diff line change
@@ -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);
});
59 changes: 59 additions & 0 deletions test/parallel/test-runner-execution-ordered-bypass.mjs
Original file line number Diff line number Diff line change
@@ -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(', ')}`,
);
});
Loading