chore: move zones into platform (#34786)

This commit is contained in:
Pavel Feldman 2025-02-13 13:06:03 -08:00 committed by GitHub
parent 9ecf2f69ba
commit 90ec838318
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 85 additions and 54 deletions

View File

@ -338,7 +338,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
} }
async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise<void> { async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
this._routes.unshift(new network.RouteHandler(this._options.baseURL, url, handler, options.times)); this._routes.unshift(new network.RouteHandler(this._platform, this._options.baseURL, url, handler, options.times));
await this._updateInterceptionPatterns(); await this._updateInterceptionPatterns();
} }

View File

@ -18,7 +18,6 @@ import { EventEmitter } from './eventEmitter';
import { ValidationError, maybeFindValidator } from '../protocol/validator'; import { ValidationError, maybeFindValidator } from '../protocol/validator';
import { isUnderTest } from '../utils/isomorphic/debug'; import { isUnderTest } from '../utils/isomorphic/debug';
import { captureLibraryStackTrace, stringifyStackFrames } from '../utils/isomorphic/stackTrace'; import { captureLibraryStackTrace, stringifyStackFrames } from '../utils/isomorphic/stackTrace';
import { zones } from '../utils/zones';
import type { ClientInstrumentation } from './clientInstrumentation'; import type { ClientInstrumentation } from './clientInstrumentation';
import type { Connection } from './connection'; import type { Connection } from './connection';
@ -176,7 +175,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal?: boolean): Promise<R> { async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal?: boolean): Promise<R> {
const logger = this._logger; const logger = this._logger;
const existingApiZone = zones.zoneData<ApiZone>('apiZone'); const existingApiZone = this._platform.zones.current().data<ApiZone>();
if (existingApiZone) if (existingApiZone)
return await func(existingApiZone); return await func(existingApiZone);
@ -186,7 +185,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
const apiZone: ApiZone = { apiName: stackTrace.apiName, frames: stackTrace.frames, isInternal, reported: false, userData: undefined, stepId: undefined }; const apiZone: ApiZone = { apiName: stackTrace.apiName, frames: stackTrace.frames, isInternal, reported: false, userData: undefined, stepId: undefined };
try { try {
const result = await zones.run('apiZone', apiZone, async () => await func(apiZone)); const result = await this._platform.zones.current().push(apiZone).run(async () => await func(apiZone));
if (!isInternal) { if (!isInternal) {
logApiCall(this._platform, logger, `<= ${apiZone.apiName} succeeded`); logApiCall(this._platform, logger, `<= ${apiZone.apiName} succeeded`);
this._instrumentation.onApiCallEnd(apiZone); this._instrumentation.onApiCallEnd(apiZone);

View File

@ -43,7 +43,6 @@ import { Worker } from './worker';
import { WritableStream } from './writableStream'; import { WritableStream } from './writableStream';
import { ValidationError, findValidator } from '../protocol/validator'; import { ValidationError, findValidator } from '../protocol/validator';
import { formatCallLog, rewriteErrorMessage } from '../utils/isomorphic/stackTrace'; import { formatCallLog, rewriteErrorMessage } from '../utils/isomorphic/stackTrace';
import { zones } from '../utils/zones';
import type { ClientInstrumentation } from './clientInstrumentation'; import type { ClientInstrumentation } from './clientInstrumentation';
import type { HeadersArray } from './types'; import type { HeadersArray } from './types';
@ -148,7 +147,7 @@ export class Connection extends EventEmitter {
this._localUtils?.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {}); this._localUtils?.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {});
// We need to exit zones before calling into the server, otherwise // We need to exit zones before calling into the server, otherwise
// when we receive events from the server, we would be in an API zone. // when we receive events from the server, we would be in an API zone.
zones.empty().run(() => this.onmessage({ ...message, metadata })); this.platform.zones.empty.run(() => this.onmessage({ ...message, metadata }));
return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, apiName, type, method })); return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, apiName, type, method }));
} }

View File

@ -30,7 +30,6 @@ import { LongStandingScope, ManualPromise } from '../utils/isomorphic/manualProm
import { MultiMap } from '../utils/isomorphic/multimap'; import { MultiMap } from '../utils/isomorphic/multimap';
import { isRegExp, isString } from '../utils/isomorphic/rtti'; import { isRegExp, isString } from '../utils/isomorphic/rtti';
import { rewriteErrorMessage } from '../utils/isomorphic/stackTrace'; import { rewriteErrorMessage } from '../utils/isomorphic/stackTrace';
import { zones } from '../utils/zones';
import { mime } from '../utilsBundle'; import { mime } from '../utilsBundle';
import type { BrowserContext } from './browserContext'; import type { BrowserContext } from './browserContext';
@ -40,8 +39,8 @@ import type { Serializable } from '../../types/structs';
import type * as api from '../../types/types'; import type * as api from '../../types/types';
import type { HeadersArray } from '../common/types'; import type { HeadersArray } from '../common/types';
import type { URLMatch } from '../utils/isomorphic/urlMatch'; import type { URLMatch } from '../utils/isomorphic/urlMatch';
import type { Zone } from '../utils/zones';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import type { Platform, Zone } from '../common/platform';
export type NetworkCookie = { export type NetworkCookie = {
name: string, name: string,
@ -821,14 +820,14 @@ export class RouteHandler {
readonly handler: RouteHandlerCallback; readonly handler: RouteHandlerCallback;
private _ignoreException: boolean = false; private _ignoreException: boolean = false;
private _activeInvocations: Set<{ complete: Promise<void>, route: Route }> = new Set(); private _activeInvocations: Set<{ complete: Promise<void>, route: Route }> = new Set();
private _svedZone: Zone; private _savedZone: Zone;
constructor(baseURL: string | undefined, url: URLMatch, handler: RouteHandlerCallback, times: number = Number.MAX_SAFE_INTEGER) { constructor(platform: Platform, baseURL: string | undefined, url: URLMatch, handler: RouteHandlerCallback, times: number = Number.MAX_SAFE_INTEGER) {
this._baseURL = baseURL; this._baseURL = baseURL;
this._times = times; this._times = times;
this.url = url; this.url = url;
this.handler = handler; this.handler = handler;
this._svedZone = zones.current().without('apiZone'); this._savedZone = platform.zones.current().pop();
} }
static prepareInterceptionPatterns(handlers: RouteHandler[]) { static prepareInterceptionPatterns(handlers: RouteHandler[]) {
@ -852,7 +851,7 @@ export class RouteHandler {
} }
public async handle(route: Route): Promise<boolean> { public async handle(route: Route): Promise<boolean> {
return await this._svedZone.run(async () => this._handleImpl(route)); return await this._savedZone.run(async () => this._handleImpl(route));
} }
private async _handleImpl(route: Route): Promise<boolean> { private async _handleImpl(route: Route): Promise<boolean> {

View File

@ -520,7 +520,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
} }
async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise<void> { async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
this._routes.unshift(new RouteHandler(this._browserContext._options.baseURL, url, handler, options.times)); this._routes.unshift(new RouteHandler(this._platform, this._browserContext._options.baseURL, url, handler, options.times));
await this._updateInterceptionPatterns(); await this._updateInterceptionPatterns();
} }

View File

@ -16,12 +16,11 @@
import { TimeoutError } from './errors'; import { TimeoutError } from './errors';
import { rewriteErrorMessage } from '../utils/isomorphic/stackTrace'; import { rewriteErrorMessage } from '../utils/isomorphic/stackTrace';
import { zones } from '../utils/zones';
import type { ChannelOwner } from './channelOwner'; import type { ChannelOwner } from './channelOwner';
import type { Zone } from '../utils/zones';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import type { EventEmitter } from 'events'; import type { EventEmitter } from 'events';
import type { Zone } from '../common/platform';
export class Waiter { export class Waiter {
private _dispose: (() => void)[]; private _dispose: (() => void)[];
@ -36,7 +35,7 @@ export class Waiter {
constructor(channelOwner: ChannelOwner<channels.EventTargetChannel>, event: string) { constructor(channelOwner: ChannelOwner<channels.EventTargetChannel>, event: string) {
this._waitId = channelOwner._platform.createGuid(); this._waitId = channelOwner._platform.createGuid();
this._channelOwner = channelOwner; this._channelOwner = channelOwner;
this._savedZone = zones.current().without('apiZone'); this._savedZone = channelOwner._platform.zones.current().pop();
this._channelOwner._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'before', event } }).catch(() => {}); this._channelOwner._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'before', event } }).catch(() => {});
this._dispose = [ this._dispose = [

View File

@ -22,6 +22,19 @@ import { webColors, noColors } from '../utils/isomorphic/colors';
import type { Colors } from '../utils/isomorphic/colors'; import type { Colors } from '../utils/isomorphic/colors';
export type Zone = {
push(data: unknown): Zone;
pop(): Zone;
run<R>(func: () => R): R;
data<T>(): T | undefined;
};
const noopZone: Zone = {
push: () => noopZone,
pop: () => noopZone,
run: func => func(),
data: () => undefined,
};
export type Platform = { export type Platform = {
calculateSha1(text: string): Promise<string>; calculateSha1(text: string): Promise<string>;
@ -34,6 +47,7 @@ export type Platform = {
path: () => typeof path; path: () => typeof path;
pathSeparator: string; pathSeparator: string;
ws?: (url: string) => WebSocket; ws?: (url: string) => WebSocket;
zones: { empty: Zone, current: () => Zone; };
}; };
export const webPlatform: Platform = { export const webPlatform: Platform = {
@ -69,6 +83,8 @@ export const webPlatform: Platform = {
pathSeparator: '/', pathSeparator: '/',
ws: (url: string) => new WebSocket(url), ws: (url: string) => new WebSocket(url),
zones: { empty: noopZone, current: () => noopZone },
}; };
export const emptyPlatform: Platform = { export const emptyPlatform: Platform = {
@ -98,5 +114,7 @@ export const emptyPlatform: Platform = {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
pathSeparator: '/' pathSeparator: '/',
zones: { empty: noopZone, current: () => noopZone },
}; };

View File

@ -20,8 +20,39 @@ import * as path from 'path';
import * as util from 'util'; import * as util from 'util';
import { colors } from '../../utilsBundle'; import { colors } from '../../utilsBundle';
import { Platform } from '../../common/platform';
import { debugLogger } from './debugLogger'; import { debugLogger } from './debugLogger';
import { currentZone, emptyZone } from './zones';
import type { Platform, Zone } from '../../common/platform';
import type { Zone as ZoneImpl } from './zones';
class NodeZone implements Zone {
private _zone: ZoneImpl;
constructor(zone: ZoneImpl) {
this._zone = zone;
}
push<T>(data: T) {
return new NodeZone(this._zone.with('apiZone', data));
}
pop() {
return new NodeZone(this._zone.without('apiZone'));
}
run<R>(func: () => R): R {
return this._zone.run(func);
}
runIgnoreCurrent<R>(func: () => R): R {
return emptyZone.run(func);
}
data<T>(): T | undefined {
return this._zone.data('apiZone');
}
}
export const nodePlatform: Platform = { export const nodePlatform: Platform = {
calculateSha1: (text: string) => { calculateSha1: (text: string) => {
@ -48,5 +79,10 @@ export const nodePlatform: Platform = {
path: () => path, path: () => path,
pathSeparator: path.sep pathSeparator: path.sep,
zones: {
current: () => new NodeZone(currentZone()),
empty: new NodeZone(emptyZone),
}
}; };

View File

@ -18,36 +18,13 @@ import { AsyncLocalStorage } from 'async_hooks';
export type ZoneType = 'apiZone' | 'stepZone'; export type ZoneType = 'apiZone' | 'stepZone';
class ZoneManager { const asyncLocalStorage = new AsyncLocalStorage<Zone | undefined>();
private readonly _asyncLocalStorage = new AsyncLocalStorage<Zone | undefined>();
private readonly _emptyZone = Zone.createEmpty(this._asyncLocalStorage);
run<T, R>(type: ZoneType, data: T, func: () => R): R {
return this.current().with(type, data).run(func);
}
zoneData<T>(type: ZoneType): T | undefined {
return this.current().data(type);
}
current(): Zone {
return this._asyncLocalStorage.getStore() ?? this._emptyZone;
}
empty(): Zone {
return this._emptyZone;
}
}
export class Zone { export class Zone {
private readonly _asyncLocalStorage: AsyncLocalStorage<Zone | undefined>; private readonly _asyncLocalStorage: AsyncLocalStorage<Zone | undefined>;
private readonly _data: ReadonlyMap<ZoneType, unknown>; private readonly _data: ReadonlyMap<ZoneType, unknown>;
static createEmpty(asyncLocalStorage: AsyncLocalStorage<Zone | undefined>) { constructor(asyncLocalStorage: AsyncLocalStorage<Zone | undefined>, store: Map<ZoneType, unknown>) {
return new Zone(asyncLocalStorage, new Map());
}
private constructor(asyncLocalStorage: AsyncLocalStorage<Zone | undefined>, store: Map<ZoneType, unknown>) {
this._asyncLocalStorage = asyncLocalStorage; this._asyncLocalStorage = asyncLocalStorage;
this._data = store; this._data = store;
} }
@ -71,4 +48,8 @@ export class Zone {
} }
} }
export const zones = new ZoneManager(); export const emptyZone = new Zone(asyncLocalStorage, new Map());
export function currentZone(): Zone {
return asyncLocalStorage.getStore() ?? emptyZone;
}

View File

@ -30,7 +30,6 @@ export * from './utils/isomorphic/headers';
export * from './utils/isomorphic/semaphore'; export * from './utils/isomorphic/semaphore';
export * from './utils/isomorphic/stackTrace'; export * from './utils/isomorphic/stackTrace';
export * from './utils/zipFile'; export * from './utils/zipFile';
export * from './utils/zones';
export * from './server/utils/ascii'; export * from './server/utils/ascii';
export * from './server/utils/comparators'; export * from './server/utils/comparators';
@ -51,5 +50,6 @@ export * from './server/utils/spawnAsync';
export * from './server/utils/task'; export * from './server/utils/task';
export * from './server/utils/userAgent'; export * from './server/utils/userAgent';
export * from './server/utils/wsServer'; export * from './server/utils/wsServer';
export * from './server/utils/zones';
export { colors } from './utilsBundle'; export { colors } from './utilsBundle';

View File

@ -15,7 +15,7 @@
*/ */
import { errors } from 'playwright-core'; import { errors } from 'playwright-core';
import { getPackageManagerExecCommand, monotonicTime, raceAgainstDeadline, zones } from 'playwright-core/lib/utils'; import { getPackageManagerExecCommand, monotonicTime, raceAgainstDeadline, currentZone } from 'playwright-core/lib/utils';
import { currentTestInfo, currentlyLoadingFileSuite, setCurrentlyLoadingFileSuite } from './globals'; import { currentTestInfo, currentlyLoadingFileSuite, setCurrentlyLoadingFileSuite } from './globals';
import { Suite, TestCase } from './test'; import { Suite, TestCase } from './test';
@ -266,7 +266,7 @@ export class TestTypeImpl {
if (!testInfo) if (!testInfo)
throw new Error(`test.step() can only be called from a test`); throw new Error(`test.step() can only be called from a test`);
const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box }); const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box });
return await zones.run('stepZone', step, async () => { return await currentZone().with('stepZone', step).run(async () => {
try { try {
let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined; let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined;
result = await raceAgainstDeadline(async () => { result = await raceAgainstDeadline(async () => {

View File

@ -18,7 +18,7 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as playwrightLibrary from 'playwright-core'; import * as playwrightLibrary from 'playwright-core';
import { addInternalStackPrefix, asLocator, createGuid, debugMode, isString, jsonStringifyForceASCII, zones } from 'playwright-core/lib/utils'; import { addInternalStackPrefix, asLocator, createGuid, currentZone, debugMode, isString, jsonStringifyForceASCII } from 'playwright-core/lib/utils';
import { currentTestInfo } from './common/globals'; import { currentTestInfo } from './common/globals';
import { rootTestType } from './common/testType'; import { rootTestType } from './common/testType';
@ -260,7 +260,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
// Some special calls do not get into steps. // Some special calls do not get into steps.
if (!testInfo || data.apiName.includes('setTestIdAttribute') || data.apiName === 'tracing.groupEnd') if (!testInfo || data.apiName.includes('setTestIdAttribute') || data.apiName === 'tracing.groupEnd')
return; return;
const zone = zones.zoneData<TestStepInternal>('stepZone'); const zone = currentZone().data<TestStepInternal>('stepZone');
if (zone && zone.category === 'expect') { if (zone && zone.category === 'expect') {
// Display the internal locator._expect call under the name of the enclosing expect call, // Display the internal locator._expect call under the name of the enclosing expect call,
// and connect it to the existing expect step. // and connect it to the existing expect step.

View File

@ -17,9 +17,9 @@
import { import {
captureRawStack, captureRawStack,
createGuid, createGuid,
currentZone,
isString, isString,
pollAgainstDeadline } from 'playwright-core/lib/utils'; pollAgainstDeadline } from 'playwright-core/lib/utils';
import { zones } from 'playwright-core/lib/utils';
import { ExpectError, isJestError } from './matcherHint'; import { ExpectError, isJestError } from './matcherHint';
import { import {
@ -380,7 +380,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
try { try {
setMatcherCallContext({ expectInfo: this._info, testInfo, step: step.info }); setMatcherCallContext({ expectInfo: this._info, testInfo, step: step.info });
const callback = () => matcher.call(target, ...args); const callback = () => matcher.call(target, ...args);
const result = zones.run('stepZone', step, callback); const result = currentZone().with('stepZone', step).run(callback);
if (result instanceof Promise) if (result instanceof Promise)
return result.then(finalizer).catch(reportStepError); return result.then(finalizer).catch(reportStepError);
finalizer(); finalizer();

View File

@ -17,7 +17,7 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, zones } from 'playwright-core/lib/utils'; import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, currentZone } from 'playwright-core/lib/utils';
import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager'; import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager';
import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util'; import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util';
@ -245,7 +245,7 @@ export class TestInfoImpl implements TestInfo {
} }
private _parentStep() { private _parentStep() {
return zones.zoneData<TestStepInternal>('stepZone') return currentZone().data<TestStepInternal>('stepZone')
?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent. ?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent.
} }