Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 5 additions & 34 deletions lib/internal/test_runner/harness.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,9 @@ const {
Promise,
PromiseResolve,
PromiseWithResolvers,
SafeMap,
SafePromiseAllReturnVoid,
} = primordials;
const { getCallerLocation } = internalBinding('util');
const {
createHook,
executionAsyncId,
} = require('async_hooks');
const { relative } = require('path');
const {
codes: {
Expand All @@ -26,6 +21,7 @@ const {
parseCommandLine,
reporterScope,
shouldColorizeTestFiles,
testStorage,
setupGlobalSetupTeardownFunctions,
parsePreviousRuns,
} = require('internal/test_runner/utils');
Expand All @@ -35,12 +31,9 @@ const { queueMicrotask } = require('internal/process/task_queues');
const { TIMEOUT_MAX } = require('internal/timers');
const { clearInterval, setImmediate, setInterval } = require('timers');
const { bigint: hrtime } = process.hrtime;
const testResources = new SafeMap();
let globalRoot;
let globalSetupExecuted = false;

testResources.set(reporterScope.asyncId(), reporterScope);

function createTestTree(rootTestOptions, globalOptions) {
const buildPhaseDeferred = PromiseWithResolvers();
const isFilteringByName = globalOptions.testNamePatterns ||
Expand Down Expand Up @@ -124,7 +117,7 @@ function createProcessEventHandler(eventName, rootTest) {
throw err;
}

const test = testResources.get(executionAsyncId());
const test = testStorage.getStore();

// Check if this error is coming from a reporter. If it is, throw it.
if (test === reporterScope) {
Expand Down Expand Up @@ -230,27 +223,6 @@ function setupFailureStateFile(rootTest, globalOptions) {
}

function setupProcessState(root, globalOptions) {
const hook = createHook({
__proto__: null,
init(asyncId, type, triggerAsyncId, resource) {
if (resource instanceof Test) {
testResources.set(asyncId, resource);
return;
}

const parent = testResources.get(triggerAsyncId);

if (parent !== undefined) {
testResources.set(asyncId, parent);
}
},
destroy(asyncId) {
testResources.delete(asyncId);
},
});

hook.enable();

const exceptionHandler =
createProcessEventHandler('uncaughtException', root);
const rejectionHandler =
Expand Down Expand Up @@ -282,7 +254,6 @@ function setupProcessState(root, globalOptions) {
root.harness.globalTeardownFunction = null;
}

hook.disable();
process.removeListener('uncaughtException', exceptionHandler);
process.removeListener('unhandledRejection', rejectionHandler);
process.removeListener('beforeExit', exitHandler);
Expand Down Expand Up @@ -389,7 +360,7 @@ async function startSubtestAfterBootstrap(subtest) {

function runInParentContext(Factory) {
function run(name, options, fn, overrides) {
const parent = testResources.get(executionAsyncId()) || lazyBootstrapRoot();
const parent = testStorage.getStore() || lazyBootstrapRoot();
const subtest = parent.createSubtest(Factory, name, options, fn, overrides);
if (parent instanceof Suite) {
return PromiseResolve();
Expand Down Expand Up @@ -422,7 +393,7 @@ function runInParentContext(Factory) {

function hook(hook) {
return (fn, options) => {
const parent = testResources.get(executionAsyncId()) || lazyBootstrapRoot();
const parent = testStorage.getStore() || lazyBootstrapRoot();
parent.createHook(hook, fn, {
__proto__: null,
...options,
Expand All @@ -434,7 +405,7 @@ function hook(hook) {
}

function getTestContext() {
const test = testResources.get(executionAsyncId());
const test = testStorage.getStore();
// Exclude the reporter sentinel
if (test === undefined || test === reporterScope) {
return undefined;
Expand Down
30 changes: 30 additions & 0 deletions lib/internal/test_runner/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ const { exitCodes: { kGenericUserError } } = internalBinding('errors');
const { addAbortListener } = require('internal/events/abort_listener');
const { queueMicrotask } = require('internal/process/task_queues');
const { AsyncResource } = require('async_hooks');
const AsyncContextFrame = require('internal/async_context_frame');
const {
emitAfter,
emitBefore,
hasAsyncIdStack,
} = require('internal/async_hooks');
const { AbortController } = require('internal/abort_controller');
const {
AbortError,
Expand All @@ -67,6 +73,7 @@ const {
isTestFailureError,
reporterScope,
testChannel,
testStorage,
} = require('internal/test_runner/utils');
const {
kEmptyObject,
Expand Down Expand Up @@ -574,9 +581,12 @@ class Test extends AsyncResource {
outerSignal;
#reportedSubtest;
#tagsArray = null;
#asyncContextFrame;

constructor(options) {
super('Test');
// The context captured by AsyncResource, with this test as the owner.
this.#asyncContextFrame = new AsyncContextFrame(testStorage, this);

let { fn, name, parent } = options;
const { concurrency, entryFile, expectFailure, loc, only, timeout, todo, skip, signal, plan } = options;
Expand Down Expand Up @@ -858,6 +868,26 @@ class Test extends AsyncResource {
return this.#tagsArray;
}

// Same as AsyncResource#runInAsyncScope(), but the restored context also
// makes this test the owner of everything that runs or is scheduled inside
// fn. This is how the harness maps uncaught errors and nested test() or
// hook calls back to the right test. It is inlined rather than wrapping
// super.runInAsyncScope() to avoid adding frames to user stack traces.
runInAsyncScope(fn, thisArg, ...args) {
const asyncId = this.asyncId();
emitBefore(asyncId, this.triggerAsyncId(), this);

const prior = AsyncContextFrame.exchange(this.#asyncContextFrame);
try {
return ReflectApply(fn, thisArg, args);
} finally {
AsyncContextFrame.set(prior);
if (hasAsyncIdStack()) {
emitAfter(asyncId);
}
}
}

applyFilters() {
if (this.error) {
// Never filter out errors.
Expand Down
9 changes: 6 additions & 3 deletions lib/internal/test_runner/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const {
parseTagFilterExpression,
} = require('internal/test_runner/tag_filter');

const { AsyncResource } = require('async_hooks');
const { AsyncLocalStorage, AsyncResource } = require('async_hooks');
const { tracingChannel } = require('diagnostics_channel');
const { relative, sep, resolve } = require('path');
const { createWriteStream, readFileSync } = require('fs');
Expand Down Expand Up @@ -235,6 +235,8 @@ async function getReportersMap(reporters, destinations) {
}

const reporterScope = new AsyncResource('TestReporterScope');
// Tracks the Test (or reporterScope) that owns the current async context.
const testStorage = new AsyncLocalStorage();
const testChannel = tracingChannel('node.test');
let globalTestOptions;

Expand Down Expand Up @@ -397,15 +399,15 @@ function parseCommandLine() {
randomize = true;
}

const setup = reporterScope.bind(async (rootReporter) => {
const setup = reporterScope.bind((rootReporter) => testStorage.run(reporterScope, async () => {
const reportersMap = await getReportersMap(reporters, destinations);
for (let i = 0; i < reportersMap.length; i++) {
const { reporter, destination } = reportersMap[i];
compose(rootReporter, reporter).pipe(destination);
}

reporterScope.reporters = reportersMap;
});
}));

globalTestOptions = {
__proto__: null,
Expand Down Expand Up @@ -734,6 +736,7 @@ module.exports = {
kMaxRandomSeed,
parseCommandLine,
reporterScope,
testStorage,
shouldColorizeTestFiles,
getCoverageReport,
setupGlobalSetupTeardownFunctions,
Expand Down
1 change: 0 additions & 1 deletion test/fixtures/test-runner/output/describe_it.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,6 @@ not ok 50 - callback called twice
code: 'ERR_TEST_FAILURE'
stack: |-
TestContext.<anonymous> (<project-root>/test/fixtures/test-runner/output/describe_it.js:255:3)
<node-internal-frames>
...
# Subtest: callback called twice in different ticks
ok 51 - callback called twice in different ticks
Expand Down
9 changes: 3 additions & 6 deletions test/fixtures/test-runner/output/junit_reporter.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,7 @@ true !== false
<testcase name="+sync throw fail" time="*" classname="subtest sync throw fail" file="*" failure="thrown from subtest sync throw fail">
<failure type="testCodeFailure" message="thrown from subtest sync throw fail">
Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fail
at TestContext.&lt;anonymous> (<project-root>/test/fixtures/test-runner/output/output.js:125:11)
at <node-internal-frames> {
at TestContext.&lt;anonymous> (<project-root>/test/fixtures/test-runner/output/output.js:125:11) {
code: 'ERR_TEST_FAILURE',
failureType: 'testCodeFailure',
cause: Error: thrown from subtest sync throw fail
Expand Down Expand Up @@ -229,8 +228,7 @@ Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fail
<testcase name="callback called twice" time="*" classname="test" file="*" failure="callback invoked multiple times">
<failure type="multipleCallbackInvocations" message="callback invoked multiple times">
Error [ERR_TEST_FAILURE]: callback invoked multiple times
at TestContext.&lt;anonymous> (<project-root>/test/fixtures/test-runner/output/output.js:272:3)
at <node-internal-frames> {
at TestContext.&lt;anonymous> (<project-root>/test/fixtures/test-runner/output/output.js:272:3) {
code: 'ERR_TEST_FAILURE',
failureType: 'multipleCallbackInvocations',
cause: 'callback invoked multiple times'
Expand Down Expand Up @@ -292,8 +290,7 @@ Error [ERR_TEST_FAILURE]: thrown from callback async throw
<testcase name="sync throw fails at first" time="*" classname="subtest sync throw fails" file="*" failure="thrown from subtest sync throw fails at first">
<failure type="testCodeFailure" message="thrown from subtest sync throw fails at first">
Error [ERR_TEST_FAILURE]: thrown from subtest sync throw fails at first
at TestContext.&lt;anonymous> (<project-root>/test/fixtures/test-runner/output/output.js:334:11)
at <node-internal-frames> {
at TestContext.&lt;anonymous> (<project-root>/test/fixtures/test-runner/output/output.js:334:11) {
code: 'ERR_TEST_FAILURE',
failureType: 'testCodeFailure',
cause: Error: thrown from subtest sync throw fails at first
Expand Down
1 change: 0 additions & 1 deletion test/fixtures/test-runner/output/output.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,6 @@ not ok 50 - callback called twice
code: 'ERR_TEST_FAILURE'
stack: |-
TestContext.<anonymous> (<project-root>/test/fixtures/test-runner/output/output.js:272:3)
<node-internal-frames>
...
# Subtest: callback called twice in different ticks
ok 51 - callback called twice in different ticks
Expand Down
1 change: 0 additions & 1 deletion test/fixtures/test-runner/output/output_cli.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,6 @@ not ok 50 - callback called twice
code: 'ERR_TEST_FAILURE'
stack: |-
TestContext.<anonymous> (<project-root>/test/fixtures/test-runner/output/output.js:272:3)
<node-internal-frames>
...
# Subtest: callback called twice in different ticks
ok 51 - callback called twice in different ticks
Expand Down
22 changes: 22 additions & 0 deletions test/parallel/test-runner-no-async-hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Flags: --expose-internals
'use strict';

// The test runner tracks which test owns the current async context with
// AsyncLocalStorage, so it must not install any async_hooks.

const common = require('../common');
const assert = require('assert');
const { setTimeout } = require('timers/promises');
const { enabledHooksExist } = require('internal/async_hooks');
const { test } = require('node:test');

test('no async hooks are enabled', common.mustCall(async (t) => {
assert.strictEqual(enabledHooksExist(), false);

await t.test('subtest', common.mustCall(async () => {
await setTimeout(1);
assert.strictEqual(enabledHooksExist(), false);
}));

assert.strictEqual(enabledHooksExist(), false);
}));
Loading