From 1c169289b2131d281535d1d99fca888c8034cb56 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 31 Aug 2021 14:44:08 -0700 Subject: [PATCH] chore: move async utils into a separate file (#8595) --- src/client/browserType.ts | 3 +- src/client/network.ts | 3 +- src/server/artifact.ts | 3 +- src/server/firefox/ffPage.ts | 3 +- src/server/frames.ts | 3 +- src/server/network.ts | 3 +- src/server/page.ts | 3 +- src/server/webkit/wkInterceptableRequest.ts | 3 +- src/server/webkit/wkPage.ts | 3 +- src/test/runner.ts | 5 +- src/test/util.ts | 50 ---------- src/test/webServer.ts | 3 +- src/test/workerRunner.ts | 13 +-- src/utils/async.ts | 103 ++++++++++++++++++++ src/utils/utils.ts | 40 -------- 15 files changed, 133 insertions(+), 108 deletions(-) create mode 100644 src/utils/async.ts diff --git a/src/client/browserType.ts b/src/client/browserType.ts index f484d64489..80710db6b2 100644 --- a/src/client/browserType.ts +++ b/src/client/browserType.ts @@ -23,9 +23,10 @@ import { Connection } from './connection'; import { Events } from './events'; import { ChildProcess } from 'child_process'; import { envObjectToArray } from './clientHelper'; -import { assert, headersObjectToArray, getUserAgent, ManualPromise } from '../utils/utils'; +import { assert, headersObjectToArray, getUserAgent } from '../utils/utils'; import * as api from '../../types/types'; import { kBrowserClosedError } from '../utils/errors'; +import { ManualPromise } from '../utils/async'; export interface BrowserServerLauncher { launchServer(options?: LaunchServerOptions): Promise; diff --git a/src/client/network.ts b/src/client/network.ts index 900714a8b9..800f6d3c54 100644 --- a/src/client/network.ts +++ b/src/client/network.ts @@ -21,7 +21,8 @@ import { Frame } from './frame'; import { Headers, RemoteAddr, SecurityDetails, WaitForEventOptions } from './types'; import fs from 'fs'; import * as mime from 'mime'; -import { isString, headersObjectToArray, headersArrayToObject, ManualPromise } from '../utils/utils'; +import { isString, headersObjectToArray, headersArrayToObject } from '../utils/utils'; +import { ManualPromise } from '../utils/async'; import { Events } from './events'; import { Page } from './page'; import { Waiter } from './waiter'; diff --git a/src/server/artifact.ts b/src/server/artifact.ts index 231e5745e2..0868ef19a8 100644 --- a/src/server/artifact.ts +++ b/src/server/artifact.ts @@ -15,7 +15,8 @@ */ import fs from 'fs'; -import { assert, ManualPromise } from '../utils/utils'; +import { assert } from '../utils/utils'; +import { ManualPromise } from '../utils/async'; import { SdkObject } from './instrumentation'; type SaveCallback = (localPath: string, error?: string) => Promise; diff --git a/src/server/firefox/ffPage.ts b/src/server/firefox/ffPage.ts index 49e64fb31e..4f69a6adf1 100644 --- a/src/server/firefox/ffPage.ts +++ b/src/server/firefox/ffPage.ts @@ -19,7 +19,7 @@ import * as dialog from '../dialog'; import * as dom from '../dom'; import * as frames from '../frames'; import { eventsHelper, RegisteredListener } from '../../utils/eventsHelper'; -import { assert, ManualPromise } from '../../utils/utils'; +import { assert } from '../../utils/utils'; import { Page, PageBinding, PageDelegate, Worker } from '../page'; import * as types from '../types'; import { getAccessibilityTree } from './ffAccessibility'; @@ -32,6 +32,7 @@ import { Protocol } from './protocol'; import { Progress } from '../progress'; import { splitErrorMessage } from '../../utils/stackTrace'; import { debugLogger } from '../../utils/debugLogger'; +import { ManualPromise } from '../../utils/async'; export const UTILITY_WORLD_NAME = '__playwright_utility_world__'; diff --git a/src/server/frames.ts b/src/server/frames.ts index 6d6cfaac86..09cb3aaa14 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -26,7 +26,8 @@ import { Page } from './page'; import * as types from './types'; import { BrowserContext } from './browserContext'; import { Progress, ProgressController } from './progress'; -import { assert, constructURLBasedOnBaseURL, makeWaitForNextTask, ManualPromise } from '../utils/utils'; +import { assert, constructURLBasedOnBaseURL, makeWaitForNextTask } from '../utils/utils'; +import { ManualPromise } from '../utils/async'; import { debugLogger } from '../utils/debugLogger'; import { CallMetadata, internalCallMetadata, SdkObject } from './instrumentation'; import { ElementStateWithoutStable } from './injected/injectedScript'; diff --git a/src/server/network.ts b/src/server/network.ts index dbfed29520..e965613879 100644 --- a/src/server/network.ts +++ b/src/server/network.ts @@ -16,7 +16,8 @@ import * as frames from './frames'; import * as types from './types'; -import { assert, ManualPromise } from '../utils/utils'; +import { assert } from '../utils/utils'; +import { ManualPromise } from '../utils/async'; import { SdkObject } from './instrumentation'; export function filterCookies(cookies: types.NetworkCookie[], urls: string[]): types.NetworkCookie[] { diff --git a/src/server/page.ts b/src/server/page.ts index bc68850687..77b61ac9c4 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -28,7 +28,8 @@ import { ConsoleMessage } from './console'; import * as accessibility from './accessibility'; import { FileChooser } from './fileChooser'; import { Progress, ProgressController } from './progress'; -import { assert, isError, ManualPromise } from '../utils/utils'; +import { assert, isError } from '../utils/utils'; +import { ManualPromise } from '../utils/async'; import { debugLogger } from '../utils/debugLogger'; import { SelectorInfo, Selectors } from './selectors'; import { CallMetadata, SdkObject } from './instrumentation'; diff --git a/src/server/webkit/wkInterceptableRequest.ts b/src/server/webkit/wkInterceptableRequest.ts index 863d6b3996..729670be82 100644 --- a/src/server/webkit/wkInterceptableRequest.ts +++ b/src/server/webkit/wkInterceptableRequest.ts @@ -20,9 +20,10 @@ import * as network from '../network'; import * as types from '../types'; import { Protocol } from './protocol'; import { WKSession } from './wkConnection'; -import { assert, headersObjectToArray, headersArrayToObject, ManualPromise } from '../../utils/utils'; +import { assert, headersObjectToArray, headersArrayToObject } from '../../utils/utils'; import { InterceptedResponse } from '../network'; import { WKPage } from './wkPage'; +import { ManualPromise } from '../../utils/async'; const errorReasons: { [reason: string]: Protocol.Network.ResourceErrorType } = { 'aborted': 'Cancellation', diff --git a/src/server/webkit/wkPage.ts b/src/server/webkit/wkPage.ts index 73aac533bc..f348b7b141 100644 --- a/src/server/webkit/wkPage.ts +++ b/src/server/webkit/wkPage.ts @@ -19,7 +19,7 @@ import * as jpeg from 'jpeg-js'; import path from 'path'; import * as png from 'pngjs'; import { splitErrorMessage } from '../../utils/stackTrace'; -import { assert, createGuid, debugAssert, headersArrayToObject, headersObjectToArray, hostPlatform, ManualPromise } from '../../utils/utils'; +import { assert, createGuid, debugAssert, headersArrayToObject, headersObjectToArray, hostPlatform } from '../../utils/utils'; import * as accessibility from '../accessibility'; import * as dialog from '../dialog'; import * as dom from '../dom'; @@ -41,6 +41,7 @@ import { WKInterceptableRequest, WKRouteImpl } from './wkInterceptableRequest'; import { WKProvisionalPage } from './wkProvisionalPage'; import { WKWorkers } from './wkWorkers'; import { debugLogger } from '../../utils/debugLogger'; +import { ManualPromise } from '../../utils/async'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const BINDING_CALL_MESSAGE = '__playwright_binding_call__'; diff --git a/src/test/runner.ts b/src/test/runner.ts index b221180311..5dc74e87f4 100644 --- a/src/test/runner.ts +++ b/src/test/runner.ts @@ -21,7 +21,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { promisify } from 'util'; import { Dispatcher, TestGroup } from './dispatcher'; -import { createMatcher, FilePatternFilter, monotonicTime, raceAgainstDeadline } from './util'; +import { createMatcher, FilePatternFilter, monotonicTime } from './util'; import { TestCase, Suite } from './test'; import { Loader } from './loader'; import { Reporter } from '../../types/testReporter'; @@ -36,6 +36,7 @@ import { ProjectImpl } from './project'; import { Minimatch } from 'minimatch'; import { Config, FullConfig } from './types'; import { WebServer } from './webServer'; +import { raceAgainstDeadline } from '../utils/async'; const removeFolderAsync = promisify(rimraf); const readDirAsync = promisify(fs.readdir); @@ -95,7 +96,7 @@ export class Runner { async run(list: boolean, filePatternFilters: FilePatternFilter[], projectName?: string): Promise { this._reporter = await this._createReporter(list); const config = this._loader.fullConfig(); - const globalDeadline = config.globalTimeout ? config.globalTimeout + monotonicTime() : undefined; + const globalDeadline = config.globalTimeout ? config.globalTimeout + monotonicTime() : 0; const { result, timedOut } = await raceAgainstDeadline(this._run(list, filePatternFilters, projectName), globalDeadline); if (timedOut) { if (!this._didBegin) diff --git a/src/test/util.ts b/src/test/util.ts index 4f5523e3e1..1b95349b43 100644 --- a/src/test/util.ts +++ b/src/test/util.ts @@ -21,56 +21,6 @@ import type { TestError, Location } from './types'; import { default as minimatch } from 'minimatch'; import { errors } from '../..'; -export class DeadlineRunner { - private _timer: NodeJS.Timer | undefined; - private _done = false; - private _fulfill!: (t: { result?: T, timedOut?: boolean }) => void; - private _reject!: (error: any) => void; - - readonly result: Promise<{ result?: T, timedOut?: boolean }>; - - constructor(promise: Promise, deadline: number | undefined) { - this.result = new Promise((f, r) => { - this._fulfill = f; - this._reject = r; - }); - promise.then(result => { - this._finish({ result }); - }).catch(e => { - this._finish(undefined, e); - }); - this.setDeadline(deadline); - } - - private _finish(success?: { result?: T, timedOut?: boolean }, error?: any) { - if (this._done) - return; - this.setDeadline(undefined); - if (success) - this._fulfill(success); - else - this._reject(error); - } - - setDeadline(deadline: number | undefined) { - if (this._timer) { - clearTimeout(this._timer); - this._timer = undefined; - } - if (deadline === undefined) - return; - const timeout = deadline - monotonicTime(); - if (timeout <= 0) - this._finish({ timedOut: true }); - else - this._timer = setTimeout(() => this._finish({ timedOut: true }), timeout); - } -} - -export async function raceAgainstDeadline(promise: Promise, deadline: number | undefined): Promise<{ result?: T, timedOut?: boolean }> { - return (new DeadlineRunner(promise, deadline)).result; -} - export async function pollUntilDeadline(testInfo: TestInfoImpl, func: (remainingTime: number) => Promise, pollTime: number | undefined, deadlinePromise: Promise): Promise { let defaultExpectTimeout = testInfo.project.expect?.timeout; if (typeof defaultExpectTimeout === 'undefined') diff --git a/src/test/webServer.ts b/src/test/webServer.ts index adf691899d..31226c56f1 100644 --- a/src/test/webServer.ts +++ b/src/test/webServer.ts @@ -17,7 +17,8 @@ import net from 'net'; import os from 'os'; import stream from 'stream'; -import { monotonicTime, raceAgainstDeadline } from './util'; +import { monotonicTime } from './util'; +import { raceAgainstDeadline } from '../utils/async'; import { WebServerConfig } from '../../types/test'; import { launchProcess } from '../utils/processLauncher'; diff --git a/src/test/workerRunner.ts b/src/test/workerRunner.ts index 54df37d327..75c258c176 100644 --- a/src/test/workerRunner.ts +++ b/src/test/workerRunner.ts @@ -20,7 +20,7 @@ import rimraf from 'rimraf'; import util from 'util'; import colors from 'colors/safe'; import { EventEmitter } from 'events'; -import { monotonicTime, DeadlineRunner, raceAgainstDeadline, serializeError, sanitizeForFilePath } from './util'; +import { monotonicTime, serializeError, sanitizeForFilePath } from './util'; import { TestBeginPayload, TestEndPayload, RunPayload, TestEntry, DonePayload, WorkerInitParams, StepBeginPayload, StepEndPayload } from './ipc'; import { setCurrentTestInfo } from './globals'; import { Loader } from './loader'; @@ -28,6 +28,7 @@ import { Modifier, Suite, TestCase } from './test'; import { Annotations, CompleteStepCallback, TestError, TestInfo, TestInfoImpl, WorkerInfo } from './types'; import { ProjectImpl } from './project'; import { FixturePool, FixtureRunner } from './fixtures'; +import { DeadlineRunner, raceAgainstDeadline } from '../utils/async'; const removeFolderAsync = util.promisify(rimraf); @@ -59,7 +60,7 @@ export class WorkerRunner extends EventEmitter { this._isStopped = true; // Interrupt current action. - this._currentDeadlineRunner?.setDeadline(0); + this._currentDeadlineRunner?.interrupt(); // TODO: mark test as 'interrupted' instead. if (this._currentTest && this._currentTest.testInfo.status === 'passed') @@ -97,7 +98,7 @@ export class WorkerRunner extends EventEmitter { } private _deadline() { - return this._project.config.timeout ? monotonicTime() + this._project.config.timeout : undefined; + return this._project.config.timeout ? monotonicTime() + this._project.config.timeout : 0; } private async _loadIfNeeded() { @@ -263,7 +264,7 @@ export class WorkerRunner extends EventEmitter { setTimeout: (timeout: number) => { testInfo.timeout = timeout; if (deadlineRunner) - deadlineRunner.setDeadline(deadline()); + deadlineRunner.updateDeadline(deadline()); }, _testFinished: new Promise(f => testFinishedCallback = f), _addStep: (category: string, title: string) => { @@ -326,7 +327,7 @@ export class WorkerRunner extends EventEmitter { setCurrentTestInfo(testInfo); const deadline = () => { - return testInfo.timeout ? startTime + testInfo.timeout : undefined; + return testInfo.timeout ? startTime + testInfo.timeout : 0; }; if (reportEvents) @@ -351,7 +352,7 @@ export class WorkerRunner extends EventEmitter { if (!result.timedOut) { this._currentDeadlineRunner = deadlineRunner = new DeadlineRunner(this._runAfterHooks(test, testInfo), deadline()); - deadlineRunner.setDeadline(deadline()); + deadlineRunner.updateDeadline(deadline()); const hooksResult = await deadlineRunner.result; // Do not overwrite test failure upon hook timeout. if (hooksResult.timedOut && testInfo.status === 'passed') diff --git a/src/utils/async.ts b/src/utils/async.ts new file mode 100644 index 0000000000..718899288c --- /dev/null +++ b/src/utils/async.ts @@ -0,0 +1,103 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { monotonicTime } from './utils'; + +export class DeadlineRunner { + private _timer: NodeJS.Timer | undefined; + readonly result = new ManualPromise<{ result?: T, timedOut?: boolean }>(); + + constructor(promise: Promise, deadline: number) { + promise.then(result => { + this._finish({ result }); + }).catch(e => { + this._finish(undefined, e); + }); + this.updateDeadline(deadline); + } + + private _finish(success?: { result?: T, timedOut?: boolean }, error?: any) { + if (this.result.isDone()) + return; + this.updateDeadline(0); + if (success) + this.result.resolve(success); + else + this.result.reject(error); + } + + interrupt() { + this.updateDeadline(-1); + } + + updateDeadline(deadline: number) { + if (this._timer) { + clearTimeout(this._timer); + this._timer = undefined; + } + if (deadline === 0) + return; + const timeout = deadline - monotonicTime(); + if (timeout <= 0) + this._finish({ timedOut: true }); + else + this._timer = setTimeout(() => this._finish({ timedOut: true }), timeout); + } +} + +export async function raceAgainstDeadline(promise: Promise, deadline: number): Promise<{ result?: T, timedOut?: boolean }> { + return (new DeadlineRunner(promise, deadline)).result; +} + +export class ManualPromise extends Promise { + private _resolve!: (t: T) => void; + private _reject!: (e: Error) => void; + private _isDone: boolean; + + constructor() { + let resolve: (t: T) => void; + let reject: (e: Error) => void; + super((f, r) => { + resolve = f; + reject = r; + }); + this._isDone = false; + this._resolve = resolve!; + this._reject = reject!; + } + + isDone() { + return this._isDone; + } + + resolve(t: T) { + this._isDone = true; + this._resolve(t); + } + + reject(e: Error) { + this._isDone = true; + this._reject(e); + } + + static override get [Symbol.species]() { + return Promise; + } + + override get [Symbol.toStringTag]() { + return 'ManualPromise'; + } +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 1a45db48cd..302ca4f454 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -392,43 +392,3 @@ export function wrapInASCIIBox(text: string, padding = 0): string { '╚' + '═'.repeat(maxLength + padding * 2) + '╝', ].join('\n'); } - -export class ManualPromise extends Promise { - private _resolve!: (t: T) => void; - private _reject!: (e: Error) => void; - private _isDone: boolean; - - constructor() { - let resolve: (t: T) => void; - let reject: (e: Error) => void; - super((f, r) => { - resolve = f; - reject = r; - }); - this._isDone = false; - this._resolve = resolve!; - this._reject = reject!; - } - - isDone() { - return this._isDone; - } - - resolve(t: T) { - this._isDone = true; - this._resolve(t); - } - - reject(e: Error) { - this._isDone = true; - this._reject(e); - } - - static override get [Symbol.species]() { - return Promise; - } - - override get [Symbol.toStringTag]() { - return 'ManualPromise'; - } -}