test_runner: publish to TracingChannel for OTel instrumentation · nodejs/node@5c27704
1+'use strict';
2+require('../common');
3+const assert = require('node:assert');
4+const { AsyncLocalStorage } = require('node:async_hooks');
5+const dc = require('node:diagnostics_channel');
6+const { describe, it, test } = require('node:test');
7+const { spawnSync } = require('child_process');
8+const { join } = require('path');
9+10+const events = [];
11+12+dc.subscribe('tracing:node.test:start', (data) => events.push({ event: 'start', name: data.name }));
13+dc.subscribe('tracing:node.test:end', (data) => events.push({ event: 'end', name: data.name }));
14+dc.subscribe('tracing:node.test:error', (data) => events.push({ event: 'error', name: data.name }));
15+16+test('passing test fires start and end', async () => {});
17+18+// Validate events were emitted (check after all tests via process.on('exit'))
19+process.on('exit', () => {
20+// Check passing test
21+const testName1 = 'passing test fires start and end';
22+const startEvents = events.filter((e) => e.event === 'start' && e.name === testName1);
23+const endEvents = events.filter((e) => e.event === 'end' && e.name === testName1);
24+assert.strictEqual(startEvents.length, 1);
25+assert.strictEqual(endEvents.length, 1);
26+27+// Check nested tests fire events
28+const nested1Start = events.filter((e) => e.event === 'start' && e.name === 'nested test 1');
29+const nested1End = events.filter((e) => e.event === 'end' && e.name === 'nested test 1');
30+const nested2Start = events.filter((e) => e.event === 'start' && e.name === 'nested test 2');
31+const nested2End = events.filter((e) => e.event === 'end' && e.name === 'nested test 2');
32+assert.strictEqual(nested1Start.length, 1);
33+assert.strictEqual(nested1End.length, 1);
34+assert.strictEqual(nested2Start.length, 1);
35+assert.strictEqual(nested2End.length, 1);
36+37+// Check describe block tests fire events
38+const describeStart = events.filter((e) => e.event === 'start' && e.name === 'test inside describe');
39+const describeEnd = events.filter((e) => e.event === 'end' && e.name === 'test inside describe');
40+const describeStart2 = events.filter(
41+(e) => e.event === 'start' && e.name === 'another test inside describe',
42+);
43+const describeEnd2 = events.filter(
44+(e) => e.event === 'end' && e.name === 'another test inside describe',
45+);
46+assert.strictEqual(describeStart.length, 1);
47+assert.strictEqual(describeEnd.length, 1);
48+assert.strictEqual(describeStart2.length, 1);
49+assert.strictEqual(describeEnd2.length, 1);
50+51+// Check async operations test fires events
52+const asyncTestName = 'context is available in async operations within test';
53+const asyncStart = events.filter((e) => e.event === 'start' && e.name === asyncTestName);
54+const asyncEnd = events.filter((e) => e.event === 'end' && e.name === asyncTestName);
55+assert.strictEqual(asyncStart.length, 1);
56+assert.strictEqual(asyncEnd.length, 1);
57+});
58+59+// Test bindStore context propagation
60+const testStorage = new AsyncLocalStorage();
61+62+// bindStore on the start channel: whenever a test fn runs, set testStorage to the test name
63+dc.channel('tracing:node.test:start').bindStore(testStorage, (data) => data.name);
64+65+const expectedName = 'bindStore propagates into test body via start channel';
66+test(expectedName, async () => {
67+const storedValueDuringTest = testStorage.getStore();
68+assert.strictEqual(storedValueDuringTest, expectedName);
69+70+// Propagates into async operations inside the test
71+const valueInSetImmediate = await new Promise((resolve) => {
72+setImmediate(() => resolve(testStorage.getStore()));
73+});
74+assert.strictEqual(valueInSetImmediate, expectedName);
75+});
76+77+test('bindStore value is isolated between tests', async () => {
78+assert.strictEqual(testStorage.getStore(), 'bindStore value is isolated between tests');
79+});
80+81+test('nested tests fire events with correct names', async (t) => {
82+await t.test('nested test 1', async () => {
83+const stored = testStorage.getStore();
84+assert.strictEqual(stored, 'nested test 1');
85+});
86+87+await t.test('nested test 2', async () => {
88+const stored = testStorage.getStore();
89+assert.strictEqual(stored, 'nested test 2');
90+});
91+});
92+93+describe('describe block with tests', () => {
94+it('test inside describe', async () => {
95+const stored = testStorage.getStore();
96+assert.strictEqual(stored, 'test inside describe');
97+});
98+99+it('another test inside describe', async () => {
100+const stored = testStorage.getStore();
101+assert.strictEqual(stored, 'another test inside describe');
102+});
103+});
104+105+test('context is available in async operations within test', async () => {
106+const testName = 'context is available in async operations within test';
107+assert.strictEqual(testStorage.getStore(), testName);
108+109+// Verify context is available in setImmediate
110+const valueInImmediate = await new Promise((resolve) => {
111+setImmediate(() => resolve(testStorage.getStore()));
112+});
113+assert.strictEqual(valueInImmediate, testName);
114+115+// Verify context is available in setTimeout
116+const valueInTimeout = await new Promise((resolve) => {
117+setTimeout(() => resolve(testStorage.getStore()), 0);
118+});
119+assert.strictEqual(valueInTimeout, testName);
120+});
121+122+test('error events fire for failing tests in fixture', async () => {
123+// Run the fixture test that intentionally fails
124+const fixturePath = join(__dirname, '../fixtures/test-runner/diagnostics-channel-error-test.js');
125+const result = spawnSync(process.execPath, [fixturePath], { encoding: 'utf8' });
126+127+// The fixture test intentionally fails, so exit code should be non-zero
128+assert.notStrictEqual(result.status, 0);
129+130+// Extract and verify error events from fixture output
131+// The fixture outputs JSON with errorEvents array on exit
132+const lines = result.stdout.split('\n');
133+const eventLine = lines.find((line) => line.includes('errorEvents'));
134+assert.ok(eventLine, 'Expected errorEvents line in fixture output');
135+const { errorEvents } = JSON.parse(eventLine);
136+assert.strictEqual(errorEvents.includes('test that intentionally fails'), true);
137+});