Skip to content

Commit 4556212

Browse files
committed
test_runner: add testId to test events
Signed-off-by: Moshe Atlow <moshe@atlow.co.il>
1 parent 2071c44 commit 4556212

File tree

5 files changed

+146
-13
lines changed

5 files changed

+146
-13
lines changed

doc/api/test.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3431,6 +3431,9 @@ Emitted when code coverage is enabled and all tests have completed.
34313431
`undefined` if the test was run through the REPL.
34323432
* `name` {string} The test name.
34333433
* `nesting` {number} The nesting level of the test.
3434+
* `testId` {number} A numeric identifier for this test instance, unique
3435+
within the test file's process. Consistent across all events for the same
3436+
test instance, enabling reliable correlation in custom reporters.
34343437
* `testNumber` {number} The ordinal number of the test.
34353438
* `todo` {string|boolean|undefined} Present if [`context.todo`][] is called
34363439
* `skip` {string|boolean|undefined} Present if [`context.skip`][] is called
@@ -3451,6 +3454,9 @@ The corresponding declaration ordered events are `'test:pass'` and `'test:fail'`
34513454
`undefined` if the test was run through the REPL.
34523455
* `name` {string} The test name.
34533456
* `nesting` {number} The nesting level of the test.
3457+
* `testId` {number} A numeric identifier for this test instance, unique
3458+
within the test file's process. Consistent across all events for the same
3459+
test instance, enabling reliable correlation in custom reporters.
34543460
* `type` {string} The test type. Either `'suite'` or `'test'`.
34553461

34563462
Emitted when a test is dequeued, right before it is executed.
@@ -3489,6 +3495,9 @@ defined.
34893495
`undefined` if the test was run through the REPL.
34903496
* `name` {string} The test name.
34913497
* `nesting` {number} The nesting level of the test.
3498+
* `testId` {number} A numeric identifier for this test instance, unique
3499+
within the test file's process. Consistent across all events for the same
3500+
test instance, enabling reliable correlation in custom reporters.
34923501
* `type` {string} The test type. Either `'suite'` or `'test'`.
34933502

34943503
Emitted when a test is enqueued for execution.
@@ -3512,6 +3521,9 @@ Emitted when a test is enqueued for execution.
35123521
`undefined` if the test was run through the REPL.
35133522
* `name` {string} The test name.
35143523
* `nesting` {number} The nesting level of the test.
3524+
* `testId` {number} A numeric identifier for this test instance, unique
3525+
within the test file's process. Consistent across all events for the same
3526+
test instance, enabling reliable correlation in custom reporters.
35153527
* `testNumber` {number} The ordinal number of the test.
35163528
* `todo` {string|boolean|undefined} Present if [`context.todo`][] is called
35173529
* `skip` {string|boolean|undefined} Present if [`context.skip`][] is called
@@ -3568,6 +3580,9 @@ since the parent runner only knows about file-level tests. When using
35683580
`undefined` if the test was run through the REPL.
35693581
* `name` {string} The test name.
35703582
* `nesting` {number} The nesting level of the test.
3583+
* `testId` {number} A numeric identifier for this test instance, unique
3584+
within the test file's process. Consistent across all events for the same
3585+
test instance, enabling reliable correlation in custom reporters.
35713586
* `testNumber` {number} The ordinal number of the test.
35723587
* `todo` {string|boolean|undefined} Present if [`context.todo`][] is called
35733588
* `skip` {string|boolean|undefined} Present if [`context.skip`][] is called
@@ -3604,6 +3619,9 @@ defined.
36043619
`undefined` if the test was run through the REPL.
36053620
* `name` {string} The test name.
36063621
* `nesting` {number} The nesting level of the test.
3622+
* `testId` {number} A numeric identifier for this test instance, unique
3623+
within the test file's process. Consistent across all events for the same
3624+
test instance, enabling reliable correlation in custom reporters.
36073625

36083626
Emitted when a test starts reporting its own and its subtests status.
36093627
This event is guaranteed to be emitted in the same order as the tests are

lib/internal/test_runner/test.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,8 @@ class Test extends AsyncResource {
590590
this.timeout = kDefaultTimeout;
591591
this.entryFile = entryFile;
592592
this.testDisambiguator = new SafeMap();
593+
this.nextTestId = 1;
594+
this.testId = 0;
593595
} else {
594596
const nesting = parent.parent === null ? parent.nesting :
595597
parent.nesting + 1;
@@ -606,6 +608,7 @@ class Test extends AsyncResource {
606608
this.childNumber = parent.subtests.length + 1;
607609
this.timeout = parent.timeout;
608610
this.entryFile = parent.entryFile;
611+
this.testId = this.root.nextTestId++;
609612

610613
if (isFilteringByName) {
611614
this.filteredByName = this.willBeFilteredByName();
@@ -884,7 +887,7 @@ class Test extends AsyncResource {
884887
const deferred = this.dequeuePendingSubtest();
885888
const test = deferred.test;
886889
this.assignReportOrder(test);
887-
test.reporter.dequeue(test.nesting, test.loc, test.name, this.reportedType);
890+
test.reporter.dequeue(test.nesting, test.loc, test.name, this.reportedType, test.testId);
888891
await test.run();
889892
deferred.resolve();
890893
}
@@ -1141,7 +1144,7 @@ class Test extends AsyncResource {
11411144
// it. Otherwise, return a Promise to the caller and mark the test as
11421145
// pending for later execution.
11431146
this.parent.unfinishedSubtests.add(this);
1144-
this.reporter.enqueue(this.nesting, this.loc, this.name, this.reportedType);
1147+
this.reporter.enqueue(this.nesting, this.loc, this.name, this.reportedType, this.testId);
11451148
if (this.root.harness.buildPromise || !this.parent.hasConcurrency()) {
11461149
const deferred = PromiseWithResolvers();
11471150

@@ -1164,7 +1167,7 @@ class Test extends AsyncResource {
11641167
}
11651168

11661169
this.parent.assignReportOrder(this);
1167-
this.reporter.dequeue(this.nesting, this.loc, this.name, this.reportedType);
1170+
this.reporter.dequeue(this.nesting, this.loc, this.name, this.reportedType, this.testId);
11681171
return this.run();
11691172
}
11701173

@@ -1426,7 +1429,10 @@ class Test extends AsyncResource {
14261429
const report = this.getReportDetails();
14271430
report.details.passed = this.passed;
14281431
this.testNumber ||= ++this.parent.outputSubtestCount;
1429-
this.reporter.complete(this.nesting, this.loc, this.testNumber, this.name, report.details, report.directive);
1432+
this.reporter.complete(
1433+
this.nesting, this.loc, this.testNumber, this.name,
1434+
report.details, report.directive, this.testId,
1435+
);
14301436
this.parent.activeSubtests--;
14311437
}
14321438

@@ -1579,9 +1585,15 @@ class Test extends AsyncResource {
15791585
const report = this.getReportDetails();
15801586

15811587
if (this.passed) {
1582-
this.reporter.ok(this.nesting, this.loc, this.testNumber, this.name, report.details, report.directive);
1588+
this.reporter.ok(
1589+
this.nesting, this.loc, this.testNumber, this.name,
1590+
report.details, report.directive, this.testId,
1591+
);
15831592
} else {
1584-
this.reporter.fail(this.nesting, this.loc, this.testNumber, this.name, report.details, report.directive);
1593+
this.reporter.fail(
1594+
this.nesting, this.loc, this.testNumber, this.name,
1595+
report.details, report.directive, this.testId,
1596+
);
15851597
}
15861598

15871599
for (let i = 0; i < this.diagnostics.length; i++) {
@@ -1595,7 +1607,7 @@ class Test extends AsyncResource {
15951607
}
15961608
this.#reportedSubtest = true;
15971609
this.parent.reportStarted();
1598-
this.reporter.start(this.nesting, this.loc, this.name);
1610+
this.reporter.start(this.nesting, this.loc, this.name, this.testId);
15991611
}
16001612

16011613
clearExecutionTime() {

lib/internal/test_runner/tests_stream.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,36 +34,39 @@ class TestsStream extends Readable {
3434
}
3535
}
3636

37-
fail(nesting, loc, testNumber, name, details, directive) {
37+
fail(nesting, loc, testNumber, name, details, directive, testId) {
3838
this[kEmitMessage]('test:fail', {
3939
__proto__: null,
4040
name,
4141
nesting,
4242
testNumber,
43+
testId,
4344
details,
4445
...loc,
4546
...directive,
4647
});
4748
}
4849

49-
ok(nesting, loc, testNumber, name, details, directive) {
50+
ok(nesting, loc, testNumber, name, details, directive, testId) {
5051
this[kEmitMessage]('test:pass', {
5152
__proto__: null,
5253
name,
5354
nesting,
5455
testNumber,
56+
testId,
5557
details,
5658
...loc,
5759
...directive,
5860
});
5961
}
6062

61-
complete(nesting, loc, testNumber, name, details, directive) {
63+
complete(nesting, loc, testNumber, name, details, directive, testId) {
6264
this[kEmitMessage]('test:complete', {
6365
__proto__: null,
6466
name,
6567
nesting,
6668
testNumber,
69+
testId,
6770
details,
6871
...loc,
6972
...directive,
@@ -91,31 +94,34 @@ class TestsStream extends Readable {
9194
return { __proto__: null, expectFailure: expectation ?? true };
9295
}
9396

94-
enqueue(nesting, loc, name, type) {
97+
enqueue(nesting, loc, name, type, testId) {
9598
this[kEmitMessage]('test:enqueue', {
9699
__proto__: null,
97100
nesting,
98101
name,
99102
type,
103+
testId,
100104
...loc,
101105
});
102106
}
103107

104-
dequeue(nesting, loc, name, type) {
108+
dequeue(nesting, loc, name, type, testId) {
105109
this[kEmitMessage]('test:dequeue', {
106110
__proto__: null,
107111
nesting,
108112
name,
109113
type,
114+
testId,
110115
...loc,
111116
});
112117
}
113118

114-
start(nesting, loc, name) {
119+
start(nesting, loc, name, testId) {
115120
this[kEmitMessage]('test:start', {
116121
__proto__: null,
117122
nesting,
118123
name,
124+
testId,
119125
...loc,
120126
});
121127
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict';
2+
const { describe, it } = require('node:test');
3+
const assert = require('node:assert');
4+
5+
// Factory that creates subtests at the SAME source location.
6+
// Multiple concurrent `it` blocks calling this will have subtests
7+
// sharing file:line:column — but each should get a distinct testId.
8+
function makeSubtest(shouldFail) {
9+
return async function(t) {
10+
await t.test('e2e', async () => {
11+
if (shouldFail) assert.fail('intentional');
12+
});
13+
};
14+
}
15+
16+
describe('suite', { concurrency: 10_000 }, () => {
17+
it('test-A (passes)', makeSubtest(false));
18+
it('test-B (passes)', makeSubtest(false));
19+
it('test-C (fails)', makeSubtest(true));
20+
it('test-D (passes)', makeSubtest(false));
21+
});
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
'use strict';
2+
require('../common');
3+
const assert = require('node:assert');
4+
const { run } = require('node:test');
5+
const fixtures = require('../common/fixtures');
6+
7+
async function collectEvents() {
8+
const events = [];
9+
const stream = run({
10+
files: [fixtures.path('test-runner/test-id-fixture.js')],
11+
isolation: 'none',
12+
});
13+
for await (const event of stream) {
14+
events.push(event);
15+
}
16+
return events;
17+
}
18+
19+
async function main() {
20+
const events = await collectEvents();
21+
22+
// 1. Every per-test event should have a numeric testId.
23+
const perTestTypes = new Set([
24+
'test:start', 'test:complete', 'test:fail',
25+
'test:pass', 'test:enqueue', 'test:dequeue',
26+
]);
27+
for (const event of events) {
28+
if (perTestTypes.has(event.type)) {
29+
assert.strictEqual(typeof event.data.testId, 'number',
30+
`${event.type} for "${event.data.name}" should have numeric testId`);
31+
}
32+
}
33+
34+
// 2. test:start and test:fail for the same instance should share testId.
35+
const failEvent = events.find(
36+
(e) => e.type === 'test:fail' && e.data.name === 'e2e',
37+
);
38+
assert.ok(failEvent, 'should have a test:fail for "e2e"');
39+
40+
const startEvent = events.find(
41+
(e) => e.type === 'test:start' &&
42+
e.data.testId === failEvent.data.testId,
43+
);
44+
assert.ok(startEvent, 'should have a test:start with matching testId');
45+
assert.strictEqual(startEvent.data.name, 'e2e');
46+
47+
// 3. Concurrent instances at the same source location get distinct testIds.
48+
const e2eStarts = events.filter(
49+
(e) => e.type === 'test:start' && e.data.name === 'e2e',
50+
);
51+
assert.strictEqual(e2eStarts.length, 4);
52+
53+
const testIds = e2eStarts.map((e) => e.data.testId);
54+
const uniqueIds = new Set(testIds);
55+
assert.strictEqual(uniqueIds.size, 4,
56+
`all 4 "e2e" instances should have distinct testIds, got: ${testIds}`);
57+
58+
// 4. test:complete for the same instance shares testId with test:start.
59+
const completeEvents = events.filter(
60+
(e) => e.type === 'test:complete' && e.data.name === 'e2e',
61+
);
62+
for (const complete of completeEvents) {
63+
const matchingStart = e2eStarts.find(
64+
(s) => s.data.testId === complete.data.testId,
65+
);
66+
assert.ok(matchingStart,
67+
`test:complete (testId=${complete.data.testId}) should match a test:start`);
68+
}
69+
70+
console.log('All testId assertions passed');
71+
}
72+
73+
main().catch((err) => {
74+
console.error(err);
75+
process.exit(1);
76+
});

0 commit comments

Comments
 (0)