2023-01-25 15:38:23 -08:00
|
|
|
/**
|
|
|
|
* 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 { debug } from 'playwright-core/lib/utilsBundle';
|
|
|
|
import { ManualPromise, monotonicTime } from 'playwright-core/lib/utils';
|
2023-04-04 10:50:40 -07:00
|
|
|
import type { FullResult, TestError } from '../../reporter';
|
2023-01-25 15:38:23 -08:00
|
|
|
import { SigIntWatcher } from './sigIntWatcher';
|
2023-01-26 17:26:47 -08:00
|
|
|
import { serializeError } from '../util';
|
2023-04-04 10:50:40 -07:00
|
|
|
import type { Multiplexer } from '../reporters/multiplexer';
|
2023-01-25 15:38:23 -08:00
|
|
|
|
|
|
|
type TaskTeardown = () => Promise<any> | undefined;
|
2023-01-26 13:20:05 -08:00
|
|
|
export type Task<Context> = (context: Context, errors: TestError[]) => Promise<TaskTeardown | void> | undefined;
|
2023-01-25 15:38:23 -08:00
|
|
|
|
2023-01-26 13:20:05 -08:00
|
|
|
export class TaskRunner<Context> {
|
|
|
|
private _tasks: { name: string, task: Task<Context> }[] = [];
|
2023-04-04 10:50:40 -07:00
|
|
|
private _reporter: Multiplexer;
|
2023-01-25 15:38:23 -08:00
|
|
|
private _hasErrors = false;
|
|
|
|
private _interrupted = false;
|
|
|
|
private _isTearDown = false;
|
|
|
|
private _globalTimeoutForError: number;
|
|
|
|
|
2023-04-04 10:50:40 -07:00
|
|
|
constructor(reporter: Multiplexer, globalTimeoutForError: number) {
|
2023-01-25 15:38:23 -08:00
|
|
|
this._reporter = reporter;
|
|
|
|
this._globalTimeoutForError = globalTimeoutForError;
|
|
|
|
}
|
|
|
|
|
2023-01-26 13:20:05 -08:00
|
|
|
addTask(name: string, task: Task<Context>) {
|
2023-01-25 15:38:23 -08:00
|
|
|
this._tasks.push({ name, task });
|
|
|
|
}
|
|
|
|
|
2023-03-04 15:05:41 -08:00
|
|
|
async run(context: Context, deadline: number, cancelPromise?: ManualPromise<void>): Promise<FullResult['status']> {
|
|
|
|
const { status, cleanup } = await this.runDeferCleanup(context, deadline, cancelPromise);
|
2023-02-08 12:44:51 -08:00
|
|
|
const teardownStatus = await cleanup();
|
|
|
|
return status === 'passed' ? teardownStatus : status;
|
|
|
|
}
|
|
|
|
|
2023-03-04 15:05:41 -08:00
|
|
|
async runDeferCleanup(context: Context, deadline: number, cancelPromise = new ManualPromise<void>()): Promise<{ status: FullResult['status'], cleanup: () => Promise<FullResult['status']> }> {
|
2023-01-25 15:38:23 -08:00
|
|
|
const sigintWatcher = new SigIntWatcher();
|
|
|
|
const timeoutWatcher = new TimeoutWatcher(deadline);
|
|
|
|
const teardownRunner = new TaskRunner(this._reporter, this._globalTimeoutForError);
|
|
|
|
teardownRunner._isTearDown = true;
|
2023-02-08 12:44:51 -08:00
|
|
|
|
|
|
|
let currentTaskName: string | undefined;
|
|
|
|
|
|
|
|
const taskLoop = async () => {
|
|
|
|
for (const { name, task } of this._tasks) {
|
|
|
|
currentTaskName = name;
|
|
|
|
if (this._interrupted)
|
|
|
|
break;
|
|
|
|
debug('pw:test:task')(`"${name}" started`);
|
|
|
|
const errors: TestError[] = [];
|
|
|
|
try {
|
|
|
|
const teardown = await task(context, errors);
|
|
|
|
if (teardown)
|
|
|
|
teardownRunner._tasks.unshift({ name: `teardown for ${name}`, task: teardown });
|
|
|
|
} catch (e) {
|
|
|
|
debug('pw:test:task')(`error in "${name}": `, e);
|
|
|
|
errors.push(serializeError(e));
|
|
|
|
} finally {
|
|
|
|
for (const error of errors)
|
|
|
|
this._reporter.onError?.(error);
|
|
|
|
if (errors.length) {
|
|
|
|
if (!this._isTearDown)
|
|
|
|
this._interrupted = true;
|
|
|
|
this._hasErrors = true;
|
2023-01-25 15:38:23 -08:00
|
|
|
}
|
|
|
|
}
|
2023-02-08 12:44:51 -08:00
|
|
|
debug('pw:test:task')(`"${name}" finished`);
|
2023-01-25 15:38:23 -08:00
|
|
|
}
|
2023-02-08 12:44:51 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
await Promise.race([
|
|
|
|
taskLoop(),
|
2023-03-04 15:05:41 -08:00
|
|
|
cancelPromise,
|
2023-02-08 12:44:51 -08:00
|
|
|
sigintWatcher.promise(),
|
|
|
|
timeoutWatcher.promise,
|
|
|
|
]);
|
2023-01-25 15:38:23 -08:00
|
|
|
|
2023-02-08 12:44:51 -08:00
|
|
|
sigintWatcher.disarm();
|
|
|
|
timeoutWatcher.disarm();
|
|
|
|
|
|
|
|
// Prevent subsequent tasks from running.
|
|
|
|
this._interrupted = true;
|
|
|
|
|
|
|
|
let status: FullResult['status'] = 'passed';
|
2023-03-04 15:05:41 -08:00
|
|
|
if (sigintWatcher.hadSignal() || cancelPromise?.isDone()) {
|
2023-02-08 12:44:51 -08:00
|
|
|
status = 'interrupted';
|
|
|
|
} else if (timeoutWatcher.timedOut()) {
|
|
|
|
this._reporter.onError?.({ message: `Timed out waiting ${this._globalTimeoutForError / 1000}s for the ${currentTaskName} to run` });
|
|
|
|
status = 'timedout';
|
|
|
|
} else if (this._hasErrors) {
|
|
|
|
status = 'failed';
|
2023-01-25 15:38:23 -08:00
|
|
|
}
|
2023-03-04 15:05:41 -08:00
|
|
|
cancelPromise?.resolve();
|
2023-02-08 12:44:51 -08:00
|
|
|
const cleanup = () => teardownRunner.runDeferCleanup(context, deadline).then(r => r.status);
|
|
|
|
return { status, cleanup };
|
2023-01-25 15:38:23 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class TimeoutWatcher {
|
|
|
|
private _timedOut = false;
|
|
|
|
readonly promise = new ManualPromise();
|
|
|
|
private _timer: NodeJS.Timeout | undefined;
|
|
|
|
|
|
|
|
constructor(deadline: number) {
|
|
|
|
if (!deadline)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (deadline - monotonicTime() <= 0) {
|
|
|
|
this._timedOut = true;
|
|
|
|
this.promise.resolve();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this._timer = setTimeout(() => {
|
|
|
|
this._timedOut = true;
|
|
|
|
this.promise.resolve();
|
|
|
|
}, deadline - monotonicTime());
|
|
|
|
}
|
|
|
|
|
|
|
|
timedOut(): boolean {
|
|
|
|
return this._timedOut;
|
|
|
|
}
|
|
|
|
|
|
|
|
disarm() {
|
|
|
|
clearTimeout(this._timer);
|
|
|
|
}
|
|
|
|
}
|