mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: use closures to set current runnable (#27293)
This commit is contained in:
parent
3ea03c9f4c
commit
4e62468aee
@ -21,6 +21,7 @@ import type { TestInfoError, TestInfo, TestStatus, FullProject, FullConfig } fro
|
|||||||
import type { AttachmentPayload, StepBeginPayload, StepEndPayload, WorkerInitParams } from '../common/ipc';
|
import type { AttachmentPayload, StepBeginPayload, StepEndPayload, WorkerInitParams } from '../common/ipc';
|
||||||
import type { TestCase } from '../common/test';
|
import type { TestCase } from '../common/test';
|
||||||
import { TimeoutManager } from './timeoutManager';
|
import { TimeoutManager } from './timeoutManager';
|
||||||
|
import type { RunnableType, TimeSlot, RunnableDescription } from './timeoutManager';
|
||||||
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
|
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
|
||||||
import type { Location } from '../../types/testReporter';
|
import type { Location } from '../../types/testReporter';
|
||||||
import { getContainedPath, normalizeAndSaveAttachment, serializeError, trimLongString } from '../util';
|
import { getContainedPath, normalizeAndSaveAttachment, serializeError, trimLongString } from '../util';
|
||||||
@ -227,6 +228,12 @@ export class TestInfoImpl implements TestInfo {
|
|||||||
this.duration = this._timeoutManager.defaultSlotTimings().elapsed | 0;
|
this.duration = this._timeoutManager.defaultSlotTimings().elapsed | 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _runWithRunnableAndFailOnError(runnable: RunnableDescription, cb: () => Promise<void>): Promise<TestInfoError | undefined> {
|
||||||
|
return await this._timeoutManager.withRunnable(runnable, async () => {
|
||||||
|
return await this._runAndFailOnError(cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async _runAndFailOnError(fn: () => Promise<void>, skips?: 'allowSkips'): Promise<TestInfoError | undefined> {
|
async _runAndFailOnError(fn: () => Promise<void>, skips?: 'allowSkips'): Promise<TestInfoError | undefined> {
|
||||||
try {
|
try {
|
||||||
await fn();
|
await fn();
|
||||||
@ -348,6 +355,21 @@ export class TestInfoImpl implements TestInfo {
|
|||||||
this.errors.push(error);
|
this.errors.push(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _runAsStepWithRunnable<T>(
|
||||||
|
stepInfo: Omit<TestStepInternal, 'complete' | 'wallTime' | 'parentStepId' | 'stepId' | 'steps'> & {
|
||||||
|
wallTime?: number,
|
||||||
|
runnableType: RunnableType;
|
||||||
|
runnableSlot?: TimeSlot;
|
||||||
|
}, cb: (step: TestStepInternal) => Promise<T>): Promise<T> {
|
||||||
|
return await this._timeoutManager.withRunnable({
|
||||||
|
type: stepInfo.runnableType,
|
||||||
|
slot: stepInfo.runnableSlot,
|
||||||
|
location: stepInfo.location,
|
||||||
|
}, async () => {
|
||||||
|
return await this._runAsStep(stepInfo, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async _runAsStep<T>(stepInfo: Omit<TestStepInternal, 'complete' | 'wallTime' | 'parentStepId' | 'stepId' | 'steps'> & { wallTime?: number }, cb: (step: TestStepInternal) => Promise<T>): Promise<T> {
|
async _runAsStep<T>(stepInfo: Omit<TestStepInternal, 'complete' | 'wallTime' | 'parentStepId' | 'stepId' | 'steps'> & { wallTime?: number }, cb: (step: TestStepInternal) => Promise<T>): Promise<T> {
|
||||||
const step = this._addStep({ wallTime: Date.now(), ...stepInfo });
|
const step = this._addStep({ wallTime: Date.now(), ...stepInfo });
|
||||||
return await zones.run('stepZone', step, async () => {
|
return await zones.run('stepZone', step, async () => {
|
||||||
|
@ -24,8 +24,10 @@ export type TimeSlot = {
|
|||||||
elapsed: number;
|
elapsed: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RunnableDescription = {
|
export type RunnableType = 'test' | 'beforeAll' | 'afterAll' | 'beforeEach' | 'afterEach' | 'slow' | 'skip' | 'fail' | 'fixme' | 'teardown';
|
||||||
type: 'test' | 'beforeAll' | 'afterAll' | 'beforeEach' | 'afterEach' | 'slow' | 'skip' | 'fail' | 'fixme' | 'teardown';
|
|
||||||
|
export type RunnableDescription = {
|
||||||
|
type: RunnableType;
|
||||||
location?: Location;
|
location?: Location;
|
||||||
slot?: TimeSlot; // Falls back to test slot.
|
slot?: TimeSlot; // Falls back to test slot.
|
||||||
};
|
};
|
||||||
@ -39,13 +41,15 @@ export type FixtureDescription = {
|
|||||||
|
|
||||||
export class TimeoutManager {
|
export class TimeoutManager {
|
||||||
private _defaultSlot: TimeSlot;
|
private _defaultSlot: TimeSlot;
|
||||||
|
private _defaultRunnable: RunnableDescription;
|
||||||
private _runnable: RunnableDescription;
|
private _runnable: RunnableDescription;
|
||||||
private _fixture: FixtureDescription | undefined;
|
private _fixture: FixtureDescription | undefined;
|
||||||
private _timeoutRunner: TimeoutRunner;
|
private _timeoutRunner: TimeoutRunner;
|
||||||
|
|
||||||
constructor(timeout: number) {
|
constructor(timeout: number) {
|
||||||
this._defaultSlot = { timeout, elapsed: 0 };
|
this._defaultSlot = { timeout, elapsed: 0 };
|
||||||
this._runnable = { type: 'test', slot: this._defaultSlot };
|
this._defaultRunnable = { type: 'test', slot: this._defaultSlot };
|
||||||
|
this._runnable = this._defaultRunnable;
|
||||||
this._timeoutRunner = new TimeoutRunner(timeout);
|
this._timeoutRunner = new TimeoutRunner(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,8 +57,15 @@ export class TimeoutManager {
|
|||||||
this._timeoutRunner.interrupt();
|
this._timeoutRunner.interrupt();
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentRunnable(runnable: RunnableDescription) {
|
async withRunnable<R>(runnable: RunnableDescription, cb: () => Promise<R>): Promise<R> {
|
||||||
this._updateRunnables(runnable, undefined);
|
const existingRunnable = this._runnable;
|
||||||
|
const effectiveRunnable = { ...this._runnable, ...runnable };
|
||||||
|
this._updateRunnables(effectiveRunnable, undefined);
|
||||||
|
try {
|
||||||
|
return await cb();
|
||||||
|
} finally {
|
||||||
|
this._updateRunnables(existingRunnable, undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentFixture(fixture: FixtureDescription | undefined) {
|
setCurrentFixture(fixture: FixtureDescription | undefined) {
|
||||||
|
@ -147,13 +147,14 @@ export class WorkerMain extends ProcessRunner {
|
|||||||
private async _teardownScopes() {
|
private async _teardownScopes() {
|
||||||
// TODO: separate timeout for teardown?
|
// TODO: separate timeout for teardown?
|
||||||
const timeoutManager = new TimeoutManager(this._project.project.timeout);
|
const timeoutManager = new TimeoutManager(this._project.project.timeout);
|
||||||
timeoutManager.setCurrentRunnable({ type: 'teardown' });
|
await timeoutManager.withRunnable({ type: 'teardown' }, async () => {
|
||||||
const timeoutError = await timeoutManager.runWithTimeout(async () => {
|
const timeoutError = await timeoutManager.runWithTimeout(async () => {
|
||||||
await this._fixtureRunner.teardownScope('test', timeoutManager);
|
await this._fixtureRunner.teardownScope('test', timeoutManager);
|
||||||
await this._fixtureRunner.teardownScope('worker', timeoutManager);
|
await this._fixtureRunner.teardownScope('worker', timeoutManager);
|
||||||
|
});
|
||||||
|
if (timeoutError)
|
||||||
|
this._fatalErrors.push(timeoutError);
|
||||||
});
|
});
|
||||||
if (timeoutError)
|
|
||||||
this._fatalErrors.push(timeoutError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unhandledError(error: Error | any) {
|
unhandledError(error: Error | any) {
|
||||||
@ -366,10 +367,9 @@ export class WorkerMain extends ProcessRunner {
|
|||||||
|
|
||||||
// Run "beforeEach" hooks. Once started with "beforeEach", we must run all "afterEach" hooks as well.
|
// Run "beforeEach" hooks. Once started with "beforeEach", we must run all "afterEach" hooks as well.
|
||||||
shouldRunAfterEachHooks = true;
|
shouldRunAfterEachHooks = true;
|
||||||
await this._runEachHooksForSuites(suites, 'beforeEach', testInfo, undefined);
|
await this._runEachHooksForSuites(suites, 'beforeEach', testInfo);
|
||||||
|
|
||||||
// Setup fixtures required by the test.
|
// Setup fixtures required by the test.
|
||||||
testInfo._timeoutManager.setCurrentRunnable({ type: 'test' });
|
|
||||||
testFunctionParams = await this._fixtureRunner.resolveParametersForFunction(test.fn, testInfo, 'test');
|
testFunctionParams = await this._fixtureRunner.resolveParametersForFunction(test.fn, testInfo, 'test');
|
||||||
}, 'allowSkips');
|
}, 'allowSkips');
|
||||||
if (beforeHooksError)
|
if (beforeHooksError)
|
||||||
@ -409,13 +409,9 @@ export class WorkerMain extends ProcessRunner {
|
|||||||
this._skipRemainingTestsInSuite = didFailBeforeAllForSuite;
|
this._skipRemainingTestsInSuite = didFailBeforeAllForSuite;
|
||||||
}
|
}
|
||||||
|
|
||||||
let afterHooksSlot: TimeSlot | undefined;
|
// A timed-out test gets a full additional timeout to run after hooks.
|
||||||
if (testInfo._didTimeout) {
|
const afterHooksSlot = testInfo._didTimeout ? { timeout: this._project.project.timeout, elapsed: 0 } : undefined;
|
||||||
// A timed-out test gets a full additional timeout to run after hooks.
|
await testInfo._runAsStepWithRunnable({ category: 'hook', title: 'After Hooks', runnableType: 'afterEach', runnableSlot: afterHooksSlot }, async step => {
|
||||||
afterHooksSlot = { timeout: this._project.project.timeout, elapsed: 0 };
|
|
||||||
testInfo._timeoutManager.setCurrentRunnable({ type: 'afterEach', slot: afterHooksSlot });
|
|
||||||
}
|
|
||||||
await testInfo._runAsStep({ category: 'hook', title: 'After Hooks' }, async step => {
|
|
||||||
testInfo._afterHooksStep = step;
|
testInfo._afterHooksStep = step;
|
||||||
let firstAfterHooksError: TestInfoError | undefined;
|
let firstAfterHooksError: TestInfoError | undefined;
|
||||||
await testInfo._runWithTimeout(async () => {
|
await testInfo._runWithTimeout(async () => {
|
||||||
@ -430,15 +426,16 @@ export class WorkerMain extends ProcessRunner {
|
|||||||
|
|
||||||
// Run "afterEach" hooks, unless we failed at beforeAll stage.
|
// Run "afterEach" hooks, unless we failed at beforeAll stage.
|
||||||
if (shouldRunAfterEachHooks) {
|
if (shouldRunAfterEachHooks) {
|
||||||
const afterEachError = await testInfo._runAndFailOnError(() => this._runEachHooksForSuites(reversedSuites, 'afterEach', testInfo, afterHooksSlot));
|
const afterEachError = await testInfo._runAndFailOnError(() => this._runEachHooksForSuites(reversedSuites, 'afterEach', testInfo));
|
||||||
firstAfterHooksError = firstAfterHooksError || afterEachError;
|
firstAfterHooksError = firstAfterHooksError || afterEachError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Teardown test-scoped fixtures. Attribute to 'test' so that users understand
|
// Teardown test-scoped fixtures. Attribute to 'test' so that users understand
|
||||||
// they should probably increase the test timeout to fix this issue.
|
// they should probably increase the test timeout to fix this issue.
|
||||||
testInfo._timeoutManager.setCurrentRunnable({ type: 'test', slot: afterHooksSlot });
|
|
||||||
debugTest(`tearing down test scope started`);
|
debugTest(`tearing down test scope started`);
|
||||||
const testScopeError = await testInfo._runAndFailOnError(() => this._fixtureRunner.teardownScope('test', testInfo._timeoutManager));
|
const testScopeError = await testInfo._runWithRunnableAndFailOnError({ type: 'test' }, () => {
|
||||||
|
return this._fixtureRunner.teardownScope('test', testInfo._timeoutManager);
|
||||||
|
});
|
||||||
debugTest(`tearing down test scope finished`);
|
debugTest(`tearing down test scope finished`);
|
||||||
firstAfterHooksError = firstAfterHooksError || testScopeError;
|
firstAfterHooksError = firstAfterHooksError || testScopeError;
|
||||||
|
|
||||||
@ -466,24 +463,28 @@ export class WorkerMain extends ProcessRunner {
|
|||||||
debugTest(`running full cleanup after the failure`);
|
debugTest(`running full cleanup after the failure`);
|
||||||
|
|
||||||
const teardownSlot = { timeout: this._project.project.timeout, elapsed: 0 };
|
const teardownSlot = { timeout: this._project.project.timeout, elapsed: 0 };
|
||||||
// Attribute to 'test' so that users understand they should probably increate the test timeout to fix this issue.
|
await testInfo._timeoutManager.withRunnable({ type: 'test', slot: teardownSlot }, async () => {
|
||||||
testInfo._timeoutManager.setCurrentRunnable({ type: 'test', slot: teardownSlot });
|
// Attribute to 'test' so that users understand they should probably increate the test timeout to fix this issue.
|
||||||
debugTest(`tearing down test scope started`);
|
debugTest(`tearing down test scope started`);
|
||||||
const testScopeError = await testInfo._runAndFailOnError(() => this._fixtureRunner.teardownScope('test', testInfo._timeoutManager));
|
const testScopeError = await testInfo._runWithRunnableAndFailOnError({ type: 'test' }, () => {
|
||||||
debugTest(`tearing down test scope finished`);
|
return this._fixtureRunner.teardownScope('test', testInfo._timeoutManager);
|
||||||
firstAfterHooksError = firstAfterHooksError || testScopeError;
|
});
|
||||||
|
debugTest(`tearing down test scope finished`);
|
||||||
|
firstAfterHooksError = firstAfterHooksError || testScopeError;
|
||||||
|
|
||||||
for (const suite of reversedSuites) {
|
for (const suite of reversedSuites) {
|
||||||
const afterAllError = await this._runAfterAllHooksForSuite(suite, testInfo);
|
const afterAllError = await this._runAfterAllHooksForSuite(suite, testInfo);
|
||||||
firstAfterHooksError = firstAfterHooksError || afterAllError;
|
firstAfterHooksError = firstAfterHooksError || afterAllError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attribute to 'teardown' because worker fixtures are not perceived as a part of a test.
|
// Attribute to 'teardown' because worker fixtures are not perceived as a part of a test.
|
||||||
testInfo._timeoutManager.setCurrentRunnable({ type: 'teardown', slot: teardownSlot });
|
debugTest(`tearing down worker scope started`);
|
||||||
debugTest(`tearing down worker scope started`);
|
const workerScopeError = await testInfo._runWithRunnableAndFailOnError({ type: 'teardown' }, () => {
|
||||||
const workerScopeError = await testInfo._runAndFailOnError(() => this._fixtureRunner.teardownScope('worker', testInfo._timeoutManager));
|
return this._fixtureRunner.teardownScope('worker', testInfo._timeoutManager);
|
||||||
debugTest(`tearing down worker scope finished`);
|
});
|
||||||
firstAfterHooksError = firstAfterHooksError || workerScopeError;
|
debugTest(`tearing down worker scope finished`);
|
||||||
|
firstAfterHooksError = firstAfterHooksError || workerScopeError;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -507,11 +508,12 @@ export class WorkerMain extends ProcessRunner {
|
|||||||
if (actualScope !== scope)
|
if (actualScope !== scope)
|
||||||
continue;
|
continue;
|
||||||
debugTest(`modifier at "${formatLocation(modifier.location)}" started`);
|
debugTest(`modifier at "${formatLocation(modifier.location)}" started`);
|
||||||
testInfo._timeoutManager.setCurrentRunnable({ type: modifier.type, location: modifier.location, slot: timeSlot });
|
const result = await testInfo._runAsStepWithRunnable({
|
||||||
const result = await testInfo._runAsStep({
|
|
||||||
category: 'hook',
|
category: 'hook',
|
||||||
title: `${modifier.type} modifier`,
|
title: `${modifier.type} modifier`,
|
||||||
location: modifier.location,
|
location: modifier.location,
|
||||||
|
runnableType: modifier.type,
|
||||||
|
runnableSlot: timeSlot,
|
||||||
}, () => this._fixtureRunner.resolveParametersAndRunFunction(modifier.fn, testInfo, scope));
|
}, () => this._fixtureRunner.resolveParametersAndRunFunction(modifier.fn, testInfo, scope));
|
||||||
debugTest(`modifier at "${formatLocation(modifier.location)}" finished`);
|
debugTest(`modifier at "${formatLocation(modifier.location)}" finished`);
|
||||||
if (result && extraAnnotations)
|
if (result && extraAnnotations)
|
||||||
@ -532,11 +534,12 @@ export class WorkerMain extends ProcessRunner {
|
|||||||
try {
|
try {
|
||||||
// Separate time slot for each "beforeAll" hook.
|
// Separate time slot for each "beforeAll" hook.
|
||||||
const timeSlot = { timeout: this._project.project.timeout, elapsed: 0 };
|
const timeSlot = { timeout: this._project.project.timeout, elapsed: 0 };
|
||||||
testInfo._timeoutManager.setCurrentRunnable({ type: 'beforeAll', location: hook.location, slot: timeSlot });
|
await testInfo._runAsStepWithRunnable({
|
||||||
await testInfo._runAsStep({
|
|
||||||
category: 'hook',
|
category: 'hook',
|
||||||
title: `${hook.title}`,
|
title: `${hook.title}`,
|
||||||
location: hook.location,
|
location: hook.location,
|
||||||
|
runnableType: 'beforeAll',
|
||||||
|
runnableSlot: timeSlot,
|
||||||
}, async () => {
|
}, async () => {
|
||||||
try {
|
try {
|
||||||
await this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only');
|
await this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only');
|
||||||
@ -568,11 +571,12 @@ export class WorkerMain extends ProcessRunner {
|
|||||||
const afterAllError = await testInfo._runAndFailOnError(async () => {
|
const afterAllError = await testInfo._runAndFailOnError(async () => {
|
||||||
// Separate time slot for each "afterAll" hook.
|
// Separate time slot for each "afterAll" hook.
|
||||||
const timeSlot = { timeout: this._project.project.timeout, elapsed: 0 };
|
const timeSlot = { timeout: this._project.project.timeout, elapsed: 0 };
|
||||||
testInfo._timeoutManager.setCurrentRunnable({ type: 'afterAll', location: hook.location, slot: timeSlot });
|
await testInfo._runAsStepWithRunnable({
|
||||||
await testInfo._runAsStep({
|
|
||||||
category: 'hook',
|
category: 'hook',
|
||||||
title: `${hook.title}`,
|
title: `${hook.title}`,
|
||||||
location: hook.location,
|
location: hook.location,
|
||||||
|
runnableType: 'afterAll',
|
||||||
|
runnableSlot: timeSlot,
|
||||||
}, async () => {
|
}, async () => {
|
||||||
try {
|
try {
|
||||||
await this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only');
|
await this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only');
|
||||||
@ -589,16 +593,16 @@ export class WorkerMain extends ProcessRunner {
|
|||||||
return firstError;
|
return firstError;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _runEachHooksForSuites(suites: Suite[], type: 'beforeEach' | 'afterEach', testInfo: TestInfoImpl, timeSlot: TimeSlot | undefined) {
|
private async _runEachHooksForSuites(suites: Suite[], type: 'beforeEach' | 'afterEach', testInfo: TestInfoImpl) {
|
||||||
const hooks = suites.map(suite => suite._hooks.filter(hook => hook.type === type)).flat();
|
const hooks = suites.map(suite => suite._hooks.filter(hook => hook.type === type)).flat();
|
||||||
let error: Error | undefined;
|
let error: Error | undefined;
|
||||||
for (const hook of hooks) {
|
for (const hook of hooks) {
|
||||||
try {
|
try {
|
||||||
testInfo._timeoutManager.setCurrentRunnable({ type, location: hook.location, slot: timeSlot });
|
await testInfo._runAsStepWithRunnable({
|
||||||
await testInfo._runAsStep({
|
|
||||||
category: 'hook',
|
category: 'hook',
|
||||||
title: `${hook.title}`,
|
title: `${hook.title}`,
|
||||||
location: hook.location,
|
location: hook.location,
|
||||||
|
runnableType: type,
|
||||||
}, () => this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'test'));
|
}, () => this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'test'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Always run all the hooks, and capture the first error.
|
// Always run all the hooks, and capture the first error.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user