Files
node/test/parallel/test-runner-execution-ordered-bypass.mjs
T
Moshe Atlow 892976d17a test_runner: add context.log() and test:log event
Add a log(message[, data]) method to TestContext and SuiteContext that
emits a new test:log event. Unlike test:diagnostic, which is buffered
so it is emitted in the order tests are defined, test:log is emitted
immediately, in the order tests execute, including under process
isolation where it bypasses the per-file declaration order buffer.
This gives reporters that render the test tree unbuffered a live,
attributed logging channel that captured stdout cannot provide under
concurrency.

The event carries the message, an optional opaque structured payload
that the runner passes through untouched, and the emitting test's
name, testId, parentId, nesting, and location. Built-in reporters
render it the same way they render test:diagnostic.

Signed-off-by: Moses Atlow <moshe@atlow.co.il>
PR-URL: https://github.com/nodejs/node/pull/64389
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
2026-07-12 11:45:05 +00:00

72 lines
2.2 KiB
JavaScript

// 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}`);
}
});
stream.on('test:log', (data) => {
events.push(`log:${data.message}`);
});
// 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(', ')}`,
);
// test:log is execution-ordered, so it must bypass the buffer too.
const logFast = events.indexOf('log:live');
assert.notStrictEqual(logFast, -1);
assert.ok(
logFast < completeSlow,
`test:log for fast-fail should arrive before slow's test:complete; events=${events.join(', ')}`,
);
});