From fa0a640e6d76d67de96fb6a5b68579c3a6638ebb Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Wed, 13 May 2026 12:52:59 +0300 Subject: [PATCH] test_runner: fix diagnostics channel context tracking Signed-off-by: Moshe Atlow --- lib/internal/test_runner/test.js | 31 ++++++++++++------- .../diagnostics-channel-bindstore-end.js | 27 ++++++++++++++++ .../test-runner-diagnostics-channel.js | 20 ++++++++++++ 3 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 test/fixtures/test-runner/diagnostics-channel-bindstore-end.js diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 013ba85087f1a0..1dcf2bda1e56dc 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -1308,6 +1308,9 @@ class Test extends AsyncResource { let stopPromise; + let publishEnd = () => testChannel.end.publish(channelContext); + let publishError = (err) => testChannel.error.publish({ __proto__: null, ...channelContext, error: err }); + try { if (this.parent?.hooks.before.length > 0) { // This hook usually runs immediately, we need to wait for it to finish @@ -1326,9 +1329,11 @@ class Test extends AsyncResource { // not the runInAsyncScope call itself, to maintain AsyncLocalStorage bindings. let testFn = this.fn; if (channelContext !== null && testChannel.start.hasSubscribers) { - testFn = (...fnArgs) => testChannel.start.runStores(channelContext, - () => ReflectApply(this.fn, this, fnArgs), - ); + testFn = (...fnArgs) => testChannel.start.runStores(channelContext, () => { + publishEnd = AsyncResource.bind(publishEnd); + publishError = AsyncResource.bind(publishError); + return ReflectApply(this.fn, this, fnArgs); + }); } ArrayPrototypeUnshift(runArgs, testFn, ctx); @@ -1380,9 +1385,8 @@ class Test extends AsyncResource { await afterEach(); await after(); } catch (err) { - // Publish diagnostics_channel error event if the channel has subscribers if (channelContext !== null && testChannel.error.hasSubscribers) { - testChannel.error.publish({ __proto__: null, ...channelContext, error: err }); + publishError(err); } if (isTestFailureError(err)) { if (err.failureType === kTestTimeoutFailure) { @@ -1406,7 +1410,7 @@ class Test extends AsyncResource { // Publish diagnostics_channel end event if the channel has subscribers (in both success and error cases) if (channelContext !== null && testChannel.end.hasSubscribers) { - testChannel.end.publish(channelContext); + publishEnd(); } } @@ -1751,6 +1755,9 @@ class Suite extends Test { file: this.entryFile, type: this.reportedType, }; + let publishEnd = () => testChannel.end.publish(channelContext); + let publishError = (err) => testChannel.error.publish({ __proto__: null, ...channelContext, error: err }); + try { const { ctx, args } = this.getRunArgs(); @@ -1762,9 +1769,11 @@ class Suite extends Test { let suiteFn = this.fn; if (testChannel.start.hasSubscribers) { const baseFn = this.fn; - suiteFn = (...fnArgs) => testChannel.start.runStores(channelContext, - () => ReflectApply(baseFn, this, fnArgs), - ); + suiteFn = (...fnArgs) => testChannel.start.runStores(channelContext, () => { + publishEnd = AsyncResource.bind(publishEnd); + publishError = AsyncResource.bind(publishError); + return ReflectApply(baseFn, this, fnArgs); + }); } const runArgs = [suiteFn, ctx]; @@ -1773,12 +1782,12 @@ class Suite extends Test { await ReflectApply(this.runInAsyncScope, this, runArgs); } catch (err) { if (testChannel.error.hasSubscribers) { - testChannel.error.publish({ __proto__: null, ...channelContext, error: err }); + publishError(err); } this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)); } finally { if (testChannel.end.hasSubscribers) { - testChannel.end.publish(channelContext); + publishEnd(); } } diff --git a/test/fixtures/test-runner/diagnostics-channel-bindstore-end.js b/test/fixtures/test-runner/diagnostics-channel-bindstore-end.js new file mode 100644 index 00000000000000..0b2d9faea4f998 --- /dev/null +++ b/test/fixtures/test-runner/diagnostics-channel-bindstore-end.js @@ -0,0 +1,27 @@ +'use strict'; +const dc = require('node:diagnostics_channel'); +const { AsyncLocalStorage } = require('node:async_hooks'); +const { test } = require('node:test'); + +const als = new AsyncLocalStorage(); +const ch = dc.tracingChannel('node.test'); + +ch.start.bindStore(als, (data) => data.name); + +const storeAtEnd = {}; +const storeAtError = {}; + +ch.end.subscribe((data) => { + storeAtEnd[data.name] = als.getStore(); +}); + +ch.error.subscribe((data) => { + storeAtError[data.name] = als.getStore(); +}); + +test('passing test', () => {}); +test('failing test', () => { throw new Error('boom'); }); + +process.on('exit', () => { + console.log(JSON.stringify({ storeAtEnd, storeAtError })); +}); diff --git a/test/parallel/test-runner-diagnostics-channel.js b/test/parallel/test-runner-diagnostics-channel.js index b3e6532a1a6318..8f2cdd59a2af93 100644 --- a/test/parallel/test-runner-diagnostics-channel.js +++ b/test/parallel/test-runner-diagnostics-channel.js @@ -119,6 +119,26 @@ test('context is available in async operations within test', async () => { 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, { + '': '', + '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');