Skip to content
Open
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
42 changes: 42 additions & 0 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -3440,6 +3440,10 @@ added:
- v18.9.0
- v16.19.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/64309
description: Added `entryFile` to events forwarded from child processes
when tests run with process isolation.
- version: v26.3.0
pr-url: https://github.com/nodejs/node/pull/63435
description: Added `parentId` to test events that carry a `testId`.
Expand Down Expand Up @@ -3524,6 +3528,10 @@ Emitted when code coverage is enabled and all tests have completed.
* `cause` {Error} The actual error thrown by the test.
* `type` {string|undefined} The type of the test, used to denote whether
this is a suite.
* `entryFile` {string|undefined} The path of the test file that was
executed as the entry point of the child process that emitted this event.
Only present when tests run with process isolation. May differ from
`file` when the test is defined in a module imported by the entry file.
* `file` {string|undefined} The path of the test file,
`undefined` if test was run through the REPL.
* `line` {number|undefined} The line number where the test is defined, or
Expand Down Expand Up @@ -3553,6 +3561,10 @@ The corresponding declaration ordered events are `'test:pass'` and `'test:fail'`
* `data` {Object}
* `column` {number|undefined} The column number where the test is defined, or
`undefined` if the test was run through the REPL.
* `entryFile` {string|undefined} The path of the test file that was
executed as the entry point of the child process that emitted this event.
Only present when tests run with process isolation. May differ from
`file` when the test is defined in a module imported by the entry file.
* `file` {string|undefined} The path of the test file,
`undefined` if test was run through the REPL.
* `line` {number|undefined} The line number where the test is defined, or
Expand All @@ -3579,6 +3591,10 @@ defined. The corresponding declaration ordered event is `'test:start'`.
* `data` {Object}
* `column` {number|undefined} The column number where the test is defined, or
`undefined` if the test was run through the REPL.
* `entryFile` {string|undefined} The path of the test file that was
executed as the entry point of the child process that emitted this event.
Only present when tests run with process isolation. May differ from
`file` when the test is defined in a module imported by the entry file.
* `file` {string|undefined} The path of the test file,
`undefined` if test was run through the REPL.
* `line` {number|undefined} The line number where the test is defined, or
Expand All @@ -3600,6 +3616,10 @@ defined.
* `data` {Object}
* `column` {number|undefined} The column number where the test is defined, or
`undefined` if the test was run through the REPL.
* `entryFile` {string|undefined} The path of the test file that was
executed as the entry point of the child process that emitted this event.
Only present when tests run with process isolation. May differ from
`file` when the test is defined in a module imported by the entry file.
* `file` {string|undefined} The path of the test file,
`undefined` if test was run through the REPL.
* `line` {number|undefined} The line number where the test is defined, or
Expand Down Expand Up @@ -3632,6 +3652,10 @@ Emitted when a test is enqueued for execution.
this is a suite.
* `attempt` {number|undefined} The attempt number of the test run,
present only when using the [`--test-rerun-failures`][] flag.
* `entryFile` {string|undefined} The path of the test file that was
executed as the entry point of the child process that emitted this event.
Only present when tests run with process isolation. May differ from
`file` when the test is defined in a module imported by the entry file.
* `file` {string|undefined} The path of the test file,
`undefined` if test was run through the REPL.
* `line` {number|undefined} The line number where the test is defined, or
Expand Down Expand Up @@ -3697,6 +3721,10 @@ since the parent runner only knows about file-level tests. When using
present only when using the [`--test-rerun-failures`][] flag.
* `passed_on_attempt` {number|undefined} The attempt number the test passed on,
present only when using the [`--test-rerun-failures`][] flag.
* `entryFile` {string|undefined} The path of the test file that was
executed as the entry point of the child process that emitted this event.
Only present when tests run with process isolation. May differ from
`file` when the test is defined in a module imported by the entry file.
* `file` {string|undefined} The path of the test file,
`undefined` if test was run through the REPL.
* `line` {number|undefined} The line number where the test is defined, or
Expand Down Expand Up @@ -3726,6 +3754,10 @@ The corresponding execution ordered event is `'test:complete'`.
* `data` {Object}
* `column` {number|undefined} The column number where the test is defined, or
`undefined` if the test was run through the REPL.
* `entryFile` {string|undefined} The path of the test file that was
executed as the entry point of the child process that emitted this event.
Only present when tests run with process isolation. May differ from
`file` when the test is defined in a module imported by the entry file.
* `file` {string|undefined} The path of the test file,
`undefined` if test was run through the REPL.
* `line` {number|undefined} The line number where the test is defined, or
Expand All @@ -3742,6 +3774,10 @@ defined.
* `data` {Object}
* `column` {number|undefined} The column number where the test is defined, or
`undefined` if the test was run through the REPL.
* `entryFile` {string|undefined} The path of the test file that was
executed as the entry point of the child process that emitted this event.
Only present when tests run with process isolation. May differ from
`file` when the test is defined in a module imported by the entry file.
* `file` {string|undefined} The path of the test file,
`undefined` if test was run through the REPL.
* `line` {number|undefined} The line number where the test is defined, or
Expand All @@ -3766,6 +3802,9 @@ The corresponding execution ordered event is `'test:dequeue'`.
### Event: `'test:stderr'`

* `data` {Object}
* `entryFile` {string|undefined} The path of the test file that was
executed as the entry point of the child process that emitted this event.
Only present when tests run with process isolation.
* `file` {string} The path of the test file.
* `message` {string} The message written to `stderr`.

Expand All @@ -3777,6 +3816,9 @@ defined.
### Event: `'test:stdout'`

* `data` {Object}
* `entryFile` {string|undefined} The path of the test file that was
executed as the entry point of the child process that emitted this event.
Only present when tests run with process isolation.
* `file` {string} The path of the test file.
* `message` {string} The message written to `stdout`.

Expand Down
4 changes: 4 additions & 0 deletions lib/internal/test_runner/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,10 @@ class FileTest extends Test {
ArrayPrototypeIncludes(kDiagnosticsFilterArgs, StringPrototypeSlice(comment, 0, firstSpaceIndex));
}
#handleReportItem(item) {
// The name is empty when a single child process runs all test files.
if (this.name !== '') {
item.data.entryFile = this.loc.file;
}
const isTopLevel = item.data.nesting === 0;
if (isTopLevel) {
if (item.type === 'test:plan' && this.#skipReporting()) {
Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/test-runner/entry-file/a.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { test } from 'node:test';
import { runShared } from './helper.mjs';
test('backup A', async (t) => { await runShared(t, 'A'); });
3 changes: 3 additions & 0 deletions test/fixtures/test-runner/entry-file/b.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { test } from 'node:test';
import { runShared } from './helper.mjs';
test('backup B', async (t) => { await runShared(t, 'B'); });
3 changes: 3 additions & 0 deletions test/fixtures/test-runner/entry-file/helper.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function runShared(t, target) {
await t.test(`restore ${target}`, async () => {});
}
59 changes: 59 additions & 0 deletions test/parallel/test-runner-entry-file.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { describe, it, run } from 'node:test';
import assert from 'node:assert';

const aPath = fixtures.path('test-runner', 'entry-file', 'a.test.mjs');
const bPath = fixtures.path('test-runner', 'entry-file', 'b.test.mjs');
const helperPath = fixtures.path('test-runner', 'entry-file', 'helper.mjs');

async function collectEvents(options) {
const events = [];
const stream = run({ files: [aPath, bPath], ...options });
stream.on('test:fail', () => {});
for await (const event of stream) {
events.push(event);
}
return events;
}

describe('entryFile attribution in reporter events', { concurrency: false }, () => {
it('stamps entryFile on events forwarded from child processes', async () => {
const events = await collectEvents({ isolation: 'process' });
const checked = { __proto__: null, A: 0, B: 0 };

for (const { type, data } of events) {
if (data?.name === 'restore A' || data?.name === 'restore B') {
const target = data.name === 'restore A' ? 'A' : 'B';
const expectedEntry = target === 'A' ? aPath : bPath;
assert.strictEqual(data.file, helperPath,
`${type} file should be the definition site`);
assert.strictEqual(data.entryFile, expectedEntry,
`${type} entryFile should be the entry file`);
checked[target]++;
}
}

// Each subtest emits at least enqueue/dequeue/start/pass/complete.
assert.ok(checked.A >= 4, `expected events for restore A, got ${checked.A}`);
assert.ok(checked.B >= 4, `expected events for restore B, got ${checked.B}`);
});

it('stamps entryFile on top-level tests forwarded from child processes', async () => {
const events = await collectEvents({ isolation: 'process' });
const pass = events.filter(({ type }) => type === 'test:pass');
const backupA = pass.find(({ data }) => data.name === 'backup A');
const backupB = pass.find(({ data }) => data.name === 'backup B');
assert.strictEqual(backupA.data.entryFile, aPath);
assert.strictEqual(backupB.data.entryFile, bPath);
});

it('does not stamp entryFile with isolation none', async () => {
const events = await collectEvents({ isolation: 'none' });
for (const { data } of events) {
if (data?.name === 'restore A' || data?.name === 'restore B') {
assert.strictEqual(data.entryFile, undefined);
}
}
});
});
28 changes: 17 additions & 11 deletions test/parallel/test-runner-v8-deserializer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert';
import { finished } from 'node:stream/promises';
import { DefaultSerializer } from 'node:v8';
import { resolve } from 'node:path';
import serializer from 'internal/test_runner/reporter/v8-serializer';
import runner from 'internal/test_runner/runner';

Expand All @@ -14,10 +15,15 @@ async function toArray(chunks) {
return arr;
}

const entryFile = resolve('filetest');
const diagnosticEvent = {
type: 'test:diagnostic',
data: { nesting: 0, details: {}, message: 'diagnostic' },
};
const reportedDiagnosticEvent = {
type: 'test:diagnostic',
data: { ...diagnosticEvent.data, entryFile },
};
const chunks = await toArray(serializer([diagnosticEvent]));
const defaultSerializer = new DefaultSerializer();
defaultSerializer.writeHeader();
Expand Down Expand Up @@ -67,28 +73,28 @@ describe('v8 deserializer', common.mustCall(() => {
it('should deserialize a chunk with no serialization', async () => {
const reported = await collectReported([Buffer.from('unknown')]);
assert.deepStrictEqual(reported, [
{ data: { __proto__: null, file: 'filetest', message: 'unknown' }, type: 'test:stdout' },
{ data: { __proto__: null, entryFile, file: 'filetest', message: 'unknown' }, type: 'test:stdout' },
]);
});

it('should deserialize a serialized chunk', async () => {
const reported = await collectReported(chunks);
assert.deepStrictEqual(reported, [diagnosticEvent]);
assert.deepStrictEqual(reported, [reportedDiagnosticEvent]);
});

it('should deserialize a serialized chunk after non-serialized chunk', async () => {
const reported = await collectReported([Buffer.concat([Buffer.from('unknown'), ...chunks])]);
assert.deepStrictEqual(reported, [
{ data: { __proto__: null, file: 'filetest', message: 'unknown' }, type: 'test:stdout' },
diagnosticEvent,
{ data: { __proto__: null, entryFile, file: 'filetest', message: 'unknown' }, type: 'test:stdout' },
reportedDiagnosticEvent,
]);
});

it('should deserialize a serialized chunk before non-serialized output', async () => {
const reported = await collectReported([Buffer.concat([ ...chunks, Buffer.from('unknown')])]);
assert.deepStrictEqual(reported, [
diagnosticEvent,
{ data: { __proto__: null, file: 'filetest', message: 'unknown' }, type: 'test:stdout' },
reportedDiagnosticEvent,
{ data: { __proto__: null, entryFile, file: 'filetest', message: 'unknown' }, type: 'test:stdout' },
]);
});

Expand Down Expand Up @@ -131,7 +137,7 @@ describe('v8 deserializer', common.mustCall(() => {
oversizedLengthHeader,
...chunks,
]);
assert.deepStrictEqual(reported.at(-1), diagnosticEvent);
assert.deepStrictEqual(reported.at(-1), reportedDiagnosticEvent);
assert.strictEqual(reported.filter((event) => event.type === 'test:diagnostic').length, 1);
assert.strictEqual(collectStdout(reported), oversizedLengthStdout);
});
Expand All @@ -152,7 +158,7 @@ describe('v8 deserializer', common.mustCall(() => {
const data = chunks[0];
const reported = await collectReported([data.subarray(0, i), data.subarray(i)]);
assert.deepStrictEqual(reported, [
diagnosticEvent,
reportedDiagnosticEvent,
]);
});

Expand All @@ -163,9 +169,9 @@ describe('v8 deserializer', common.mustCall(() => {
Buffer.concat([data.subarray(i), Buffer.from('unknown')]),
]);
assert.deepStrictEqual(reported, [
{ data: { __proto__: null, file: 'filetest', message: 'unknown' }, type: 'test:stdout' },
diagnosticEvent,
{ data: { __proto__: null, file: 'filetest', message: 'unknown' }, type: 'test:stdout' },
{ data: { __proto__: null, entryFile, file: 'filetest', message: 'unknown' }, type: 'test:stdout' },
reportedDiagnosticEvent,
{ data: { __proto__: null, entryFile, file: 'filetest', message: 'unknown' }, type: 'test:stdout' },
]);
}
);
Expand Down
Loading