Skip to content

Commit f68189b

Browse files
MoLowaduh95
authored andcommitted
test_runner: add testId to test events
Signed-off-by: Moshe Atlow <moshe@atlow.co.il> PR-URL: #62772 Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent 5c27704 commit f68189b

5 files changed

Lines changed: 146 additions & 13 deletions

File tree

doc/api/test.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3423,6 +3423,9 @@ Emitted when code coverage is enabled and all tests have completed.
34233423
`undefined` if the test was run through the REPL.
34243424
* `name` {string} The test name.
34253425
* `nesting` {number} The nesting level of the test.
3426+
* `testId` {number} A numeric identifier for this test instance, unique
3427+
within the test file's process. Consistent across all events for the same
3428+
test instance, enabling reliable correlation in custom reporters.
34263429
* `testNumber` {number} The ordinal number of the test.
34273430
* `todo` {string|boolean|undefined} Present if [`context.todo`][] is called
34283431
* `skip` {string|boolean|undefined} Present if [`context.skip`][] is called
@@ -3443,6 +3446,9 @@ The corresponding declaration ordered events are `'test:pass'` and `'test:fail'`
34433446
`undefined` if the test was run through the REPL.
34443447
* `name` {string} The test name.
34453448
* `nesting` {number} The nesting level of the test.
3449+
* `testId` {number} A numeric identifier for this test instance, unique
3450+
within the test file's process. Consistent across all events for the same
3451+
test instance, enabling reliable correlation in custom reporters.
34463452
* `type` {string} The test type. Either `'suite'` or `'test'`.
34473453

34483454
Emitted when a test is dequeued, right before it is executed.
@@ -3481,6 +3487,9 @@ defined.
34813487
`undefined` if the test was run through the REPL.
34823488
* `name` {string} The test name.
34833489
* `nesting` {number} The nesting level of the test.
3490+
* `testId` {number} A numeric identifier for this test instance, unique
3491+
within the test file's process. Consistent across all events for the same
3492+
test instance, enabling reliable correlation in custom reporters.
34843493
* `type` {string} The test type. Either `'suite'` or `'test'`.
34853494

34863495
Emitted when a test is enqueued for execution.
@@ -3504,6 +3513,9 @@ Emitted when a test is enqueued for execution.
35043513
`undefined` if the test was run through the REPL.
35053514
* `name` {string} The test name.
35063515
* `nesting` {number} The nesting level of the test.
3516+
* `testId` {number} A numeric identifier for this test instance, unique
3517+
within the test file's process. Consistent across all events for the same
3518+
test instance, enabling reliable correlation in custom reporters.
35073519
* `testNumber` {number} The ordinal number of the test.
35083520
* `todo` {string|boolean|undefined} Present if [`context.todo`][] is called
35093521
* `skip` {string|boolean|undefined} Present if [`context.skip`][] is called
@@ -3558,6 +3570,9 @@ since the parent runner only knows about file-level tests. When using
35583570
`undefined` if the test was run through the REPL.
35593571
* `name` {string} The test name.
35603572
* `nesting` {number} The nesting level of the test.
3573+
* `testId` {number} A numeric identifier for this test instance, unique
3574+
within the test file's process. Consistent across all events for the same
3575+
test instance, enabling reliable correlation in custom reporters.
35613576
* `testNumber` {number} The ordinal number of the test.
35623577
* `todo` {string|boolean|undefined} Present if [`context.todo`][] is called
35633578
* `skip` {string|boolean|undefined} Present if [`context.skip`][] is called
@@ -3594,6 +3609,9 @@ defined.
35943609
`undefined` if the test was run through the REPL.
35953610
* `name` {string} The test name.
35963611
* `nesting` {number} The nesting level of the test.
3612+
* `testId` {number} A numeric identifier for this test instance, unique
3613+
within the test file's process. Consistent across all events for the same
3614+
test instance, enabling reliable correlation in custom reporters.
35973615

35983616
Emitted when a test starts reporting its own and its subtests status.
35993617
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();
@@ -890,7 +893,7 @@ class Test extends AsyncResource {
890893
const deferred = this.dequeuePendingSubtest();
891894
const test = deferred.test;
892895
this.assignReportOrder(test);
893-
test.reporter.dequeue(test.nesting, test.loc, test.name, this.reportedType);
896+
test.reporter.dequeue(test.nesting, test.loc, test.name, this.reportedType, test.testId);
894897
await test.run();
895898
deferred.resolve();
896899
}
@@ -1147,7 +1150,7 @@ class Test extends AsyncResource {
11471150
// it. Otherwise, return a Promise to the caller and mark the test as
11481151
// pending for later execution.
11491152
this.parent.unfinishedSubtests.add(this);
1150-
this.reporter.enqueue(this.nesting, this.loc, this.name, this.reportedType);
1153+
this.reporter.enqueue(this.nesting, this.loc, this.name, this.reportedType, this.testId);
11511154
if (this.root.harness.buildPromise || !this.parent.hasConcurrency()) {
11521155
const deferred = PromiseWithResolvers();
11531156

@@ -1170,7 +1173,7 @@ class Test extends AsyncResource {
11701173
}
11711174

11721175
this.parent.assignReportOrder(this);
1173-
this.reporter.dequeue(this.nesting, this.loc, this.name, this.reportedType);
1176+
this.reporter.dequeue(this.nesting, this.loc, this.name, this.reportedType, this.testId);
11741177
return this.run();
11751178
}
11761179

@@ -1432,7 +1435,10 @@ class Test extends AsyncResource {
14321435
const report = this.getReportDetails();
14331436
report.details.passed = this.passed;
14341437
this.testNumber ||= ++this.parent.outputSubtestCount;
1435-
this.reporter.complete(this.nesting, this.loc, this.testNumber, this.name, report.details, report.directive);
1438+
this.reporter.complete(
1439+
this.nesting, this.loc, this.testNumber, this.name,
1440+
report.details, report.directive, this.testId,
1441+
);
14361442
this.parent.activeSubtests--;
14371443
}
14381444

@@ -1585,9 +1591,15 @@ class Test extends AsyncResource {
15851591
const report = this.getReportDetails();
15861592

15871593
if (this.passed) {
1588-
this.reporter.ok(this.nesting, this.loc, this.testNumber, this.name, report.details, report.directive);
1594+
this.reporter.ok(
1595+
this.nesting, this.loc, this.testNumber, this.name,
1596+
report.details, report.directive, this.testId,
1597+
);
15891598
} else {
1590-
this.reporter.fail(this.nesting, this.loc, this.testNumber, this.name, report.details, report.directive);
1599+
this.reporter.fail(
1600+
this.nesting, this.loc, this.testNumber, this.name,
1601+
report.details, report.directive, this.testId,
1602+
);
15911603
}
15921604

15931605
for (let i = 0; i < this.diagnostics.length; i++) {
@@ -1601,7 +1613,7 @@ class Test extends AsyncResource {
16011613
}
16021614
this.#reportedSubtest = true;
16031615
this.parent.reportStarted();
1604-
this.reporter.start(this.nesting, this.loc, this.name);
1616+
this.reporter.start(this.nesting, this.loc, this.name, this.testId);
16051617
}
16061618

16071619
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)