node/test/parallel/test-runner-diagnostics-channel.js
Moshe Atlow cebe4248af
test_runner: fix suite diagnostic chanel end
Signed-off-by: Moshe Atlow <moshe@atlow.co.il>
PR-URL: https://github.com/nodejs/node/pull/63533
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: James M Snell <jasnell@gmail.com>
2026-05-26 13:41:29 +00:00

175 lines
7.3 KiB
JavaScript

'use strict';
require('../common');
const assert = require('node:assert');
const { AsyncLocalStorage } = require('node:async_hooks');
const dc = require('node:diagnostics_channel');
const { describe, it, test } = require('node:test');
const { spawnSync } = require('child_process');
const { join } = require('path');
const events = [];
dc.subscribe('tracing:node.test:start', (data) => events.push({ event: 'start', name: data.name, type: data.type }));
dc.subscribe('tracing:node.test:end', (data) => events.push({ event: 'end', name: data.name, type: data.type }));
dc.subscribe('tracing:node.test:error', (data) => events.push({ event: 'error', name: data.name, type: data.type }));
describe('suite end ordering', () => {
it('child a', async () => { await new Promise((r) => setTimeout(r, 5)); });
it('child b', () => {});
});
test('passing test fires start and end', async () => {});
// Validate events were emitted (check after all tests via process.on('exit'))
process.on('exit', () => {
// Check passing test
const testName1 = 'passing test fires start and end';
const startEvents = events.filter((e) => e.event === 'start' && e.name === testName1);
const endEvents = events.filter((e) => e.event === 'end' && e.name === testName1);
assert.strictEqual(startEvents.length, 1);
assert.strictEqual(endEvents.length, 1);
// Check nested tests fire events
const nested1Start = events.filter((e) => e.event === 'start' && e.name === 'nested test 1');
const nested1End = events.filter((e) => e.event === 'end' && e.name === 'nested test 1');
const nested2Start = events.filter((e) => e.event === 'start' && e.name === 'nested test 2');
const nested2End = events.filter((e) => e.event === 'end' && e.name === 'nested test 2');
assert.strictEqual(nested1Start.length, 1);
assert.strictEqual(nested1End.length, 1);
assert.strictEqual(nested2Start.length, 1);
assert.strictEqual(nested2End.length, 1);
// Check describe block tests fire events
const describeStart = events.filter((e) => e.event === 'start' && e.name === 'test inside describe');
const describeEnd = events.filter((e) => e.event === 'end' && e.name === 'test inside describe');
const describeStart2 = events.filter(
(e) => e.event === 'start' && e.name === 'another test inside describe',
);
const describeEnd2 = events.filter(
(e) => e.event === 'end' && e.name === 'another test inside describe',
);
assert.strictEqual(describeStart.length, 1);
assert.strictEqual(describeEnd.length, 1);
assert.strictEqual(describeStart2.length, 1);
assert.strictEqual(describeEnd2.length, 1);
// Check async operations test fires events
const asyncTestName = 'context is available in async operations within test';
const asyncStart = events.filter((e) => e.event === 'start' && e.name === asyncTestName);
const asyncEnd = events.filter((e) => e.event === 'end' && e.name === asyncTestName);
assert.strictEqual(asyncStart.length, 1);
assert.strictEqual(asyncEnd.length, 1);
const suiteNames = new Set(['suite end ordering', 'child a', 'child b']);
const suiteSequence = events
.filter((e) => suiteNames.has(e.name))
.map((e) => `${e.event}:${e.name}`);
assert.deepStrictEqual(suiteSequence, [
'start:suite end ordering',
'start:child a',
'end:child a',
'start:child b',
'end:child b',
'end:suite end ordering',
]);
});
// Test bindStore context propagation
const testStorage = new AsyncLocalStorage();
// bindStore on the start channel: whenever a test fn runs, set testStorage to the test name
dc.channel('tracing:node.test:start').bindStore(testStorage, (data) => data.name);
const expectedName = 'bindStore propagates into test body via start channel';
test(expectedName, async () => {
const storedValueDuringTest = testStorage.getStore();
assert.strictEqual(storedValueDuringTest, expectedName);
// Propagates into async operations inside the test
const valueInSetImmediate = await new Promise((resolve) => {
setImmediate(() => resolve(testStorage.getStore()));
});
assert.strictEqual(valueInSetImmediate, expectedName);
});
test('bindStore value is isolated between tests', async () => {
assert.strictEqual(testStorage.getStore(), 'bindStore value is isolated between tests');
});
test('nested tests fire events with correct names', async (t) => {
await t.test('nested test 1', async () => {
const stored = testStorage.getStore();
assert.strictEqual(stored, 'nested test 1');
});
await t.test('nested test 2', async () => {
const stored = testStorage.getStore();
assert.strictEqual(stored, 'nested test 2');
});
});
describe('describe block with tests', () => {
it('test inside describe', async () => {
const stored = testStorage.getStore();
assert.strictEqual(stored, 'test inside describe');
});
it('another test inside describe', async () => {
const stored = testStorage.getStore();
assert.strictEqual(stored, 'another test inside describe');
});
});
test('context is available in async operations within test', async () => {
const testName = 'context is available in async operations within test';
assert.strictEqual(testStorage.getStore(), testName);
// Verify context is available in setImmediate
const valueInImmediate = await new Promise((resolve) => {
setImmediate(() => resolve(testStorage.getStore()));
});
assert.strictEqual(valueInImmediate, testName);
// Verify context is available in setTimeout
const valueInTimeout = await new Promise((resolve) => {
setTimeout(() => resolve(testStorage.getStore()), 0);
});
assert.strictEqual(valueInTimeout, testName);
});
test('bindStore propagates store to end and error subscribers', async () => {
// Spawn a fixture that records `als.getStore()` at end/error publish time so
// we can assert subscribers see the bound store, not undefined.
const fixturePath = join(__dirname, '../fixtures/test-runner/diagnostics-channel-bindstore-end.js');
const result = spawnSync(process.execPath, [fixturePath], { encoding: 'utf8' });
// The fixture contains an intentionally failing test, so exit is non-zero.
assert.notStrictEqual(result.status, 0);
const line = result.stdout.split('\n').find((l) => l.includes('storeAtEnd'));
assert.ok(line, `expected storeAtEnd line in stdout:\n${result.stdout}`);
const { storeAtEnd, storeAtError } = JSON.parse(line);
assert.deepStrictEqual(storeAtEnd, {
'<root>': '<root>',
'passing test': 'passing test',
'failing test': 'failing test',
});
assert.deepStrictEqual(storeAtError, {
'failing test': 'failing test',
});
});
test('error events fire for failing tests in fixture', async () => {
// Run the fixture test that intentionally fails
const fixturePath = join(__dirname, '../fixtures/test-runner/diagnostics-channel-error-test.js');
const result = spawnSync(process.execPath, [fixturePath], { encoding: 'utf8' });
// The fixture test intentionally fails, so exit code should be non-zero
assert.notStrictEqual(result.status, 0);
// Extract and verify error events from fixture output
// The fixture outputs JSON with errorEvents array on exit
const lines = result.stdout.split('\n');
const eventLine = lines.find((line) => line.includes('errorEvents'));
assert.ok(eventLine, 'Expected errorEvents line in fixture output');
const { errorEvents } = JSON.parse(eventLine);
assert.strictEqual(errorEvents.includes('test that intentionally fails'), true);
});