chore: refactor timeout manager to use scopes (1) (#24315)

This commit is contained in:
Pavel Feldman 2023-07-20 17:21:21 -07:00 committed by GitHub
parent 767addec8c
commit f5df0940c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 223 additions and 217 deletions

View file

@ -439,9 +439,10 @@ function formatStackFrame(frame: StackFrame) {
} }
function hookType(testInfo: TestInfoImpl): 'beforeAll' | 'afterAll' | undefined { function hookType(testInfo: TestInfoImpl): 'beforeAll' | 'afterAll' | undefined {
const type = testInfo._timeoutManager.currentRunnableType(); if (testInfo._timeoutManager.hasRunnableType('beforeAll'))
if (type === 'beforeAll' || type === 'afterAll') return 'beforeAll';
return type; if (testInfo._timeoutManager.hasRunnableType('afterAll'))
return 'afterAll';
} }
type StackFrame = { type StackFrame = {

View file

@ -17,7 +17,7 @@
import { formatLocation, debugTest, filterStackFile, serializeError } from '../util'; import { formatLocation, debugTest, filterStackFile, serializeError } from '../util';
import { ManualPromise, zones } from 'playwright-core/lib/utils'; import { ManualPromise, zones } from 'playwright-core/lib/utils';
import type { TestInfoImpl, TestStepInternal } from './testInfo'; import type { TestInfoImpl, TestStepInternal } from './testInfo';
import type { FixtureDescription, TimeoutManager } from './timeoutManager'; import type { RunnableDescription, TimeoutManager } from './timeoutManager';
import { fixtureParameterNames, type FixturePool, type FixtureRegistration, type FixtureScope } from '../common/fixtures'; import { fixtureParameterNames, type FixturePool, type FixtureRegistration, type FixtureScope } from '../common/fixtures';
import type { WorkerInfo } from '../../types/test'; import type { WorkerInfo } from '../../types/test';
import type { Location } from '../../types/testReporter'; import type { Location } from '../../types/testReporter';
@ -31,7 +31,7 @@ class Fixture {
_useFuncFinished: ManualPromise<void> | undefined; _useFuncFinished: ManualPromise<void> | undefined;
_selfTeardownComplete: Promise<void> | undefined; _selfTeardownComplete: Promise<void> | undefined;
_teardownWithDepsComplete: Promise<void> | undefined; _teardownWithDepsComplete: Promise<void> | undefined;
_runnableDescription: FixtureDescription; _runnableDescription: RunnableDescription;
_deps = new Set<Fixture>(); _deps = new Set<Fixture>();
_usages = new Set<Fixture>(); _usages = new Set<Fixture>();
@ -42,6 +42,7 @@ class Fixture {
const title = this.registration.customTitle || this.registration.name; const title = this.registration.customTitle || this.registration.name;
this._runnableDescription = { this._runnableDescription = {
title, title,
type: 'fixture',
phase: 'setup', phase: 'setup',
location: registration.location, location: registration.location,
slot: this.registration.timeout === undefined ? undefined : { slot: this.registration.timeout === undefined ? undefined : {
@ -106,7 +107,6 @@ class Fixture {
const workerInfo: WorkerInfo = { config: testInfo.config, parallelIndex: testInfo.parallelIndex, workerIndex: testInfo.workerIndex, project: testInfo.project }; const workerInfo: WorkerInfo = { config: testInfo.config, parallelIndex: testInfo.parallelIndex, workerIndex: testInfo.workerIndex, project: testInfo.project };
const info = this.registration.scope === 'worker' ? workerInfo : testInfo; const info = this.registration.scope === 'worker' ? workerInfo : testInfo;
testInfo._timeoutManager.setCurrentFixture(this._runnableDescription);
const handleError = (e: any) => { const handleError = (e: any) => {
this.failed = true; this.failed = true;
@ -115,38 +115,40 @@ class Fixture {
else else
throw e; throw e;
}; };
try {
const result = zones.preserve(async () => {
if (!shouldGenerateStep)
return await this.registration.fn(params, useFunc, info);
await testInfo._runAsStep({ await testInfo._timeoutManager.runRunnable(this._runnableDescription, async () => {
title: `fixture: ${this.registration.name}`, try {
category: 'fixture', const result = zones.preserve(async () => {
location: isInternalFixture ? this.registration.location : undefined, if (!shouldGenerateStep)
}, async step => { return await this.registration.fn(params, useFunc, info);
mutableStepOnStack = step;
return await this.registration.fn(params, useFunc, info); await testInfo._runAsStep({
title: `fixture: ${this.registration.name}`,
category: 'fixture',
location: isInternalFixture ? this.registration.location : undefined,
}, async step => {
mutableStepOnStack = step;
return await this.registration.fn(params, useFunc, info);
});
}); });
});
if (result instanceof Promise) if (result instanceof Promise)
this._selfTeardownComplete = result.catch(handleError); this._selfTeardownComplete = result.catch(handleError);
else else
this._selfTeardownComplete = Promise.resolve(); this._selfTeardownComplete = Promise.resolve();
} catch (e) { } catch (e) {
handleError(e); handleError(e);
} }
await useFuncStarted; await useFuncStarted;
if (shouldGenerateStep) { if (shouldGenerateStep) {
mutableStepOnStack?.complete({}); mutableStepOnStack?.complete({});
this._selfTeardownComplete?.then(() => { this._selfTeardownComplete?.then(() => {
afterStep?.complete({}); afterStep?.complete({});
}).catch(e => { }).catch(e => {
afterStep?.complete({ error: serializeError(e) }); afterStep?.complete({ error: serializeError(e) });
}); });
} }
testInfo._timeoutManager.setCurrentFixture(undefined); });
} }
async teardown(timeoutManager: TimeoutManager) { async teardown(timeoutManager: TimeoutManager) {
@ -154,20 +156,16 @@ class Fixture {
// When we are waiting for the teardown for the second time, // When we are waiting for the teardown for the second time,
// most likely after the first time did timeout, annotate current fixture // most likely after the first time did timeout, annotate current fixture
// for better error messages. // for better error messages.
this._setTeardownDescription(timeoutManager); this._runnableDescription.phase = 'teardown';
await this._teardownWithDepsComplete; await timeoutManager.runRunnable(this._runnableDescription, async () => {
timeoutManager.setCurrentFixture(undefined); await this._teardownWithDepsComplete;
});
return; return;
} }
this._teardownWithDepsComplete = this._teardownInternal(timeoutManager); this._teardownWithDepsComplete = this._teardownInternal(timeoutManager);
await this._teardownWithDepsComplete; await this._teardownWithDepsComplete;
} }
private _setTeardownDescription(timeoutManager: TimeoutManager) {
this._runnableDescription.phase = 'teardown';
timeoutManager.setCurrentFixture(this._runnableDescription);
}
private async _teardownInternal(timeoutManager: TimeoutManager) { private async _teardownInternal(timeoutManager: TimeoutManager) {
if (typeof this.registration.fn !== 'function') if (typeof this.registration.fn !== 'function')
return; return;
@ -181,10 +179,11 @@ class Fixture {
} }
if (this._useFuncFinished) { if (this._useFuncFinished) {
debugTest(`teardown ${this.registration.name}`); debugTest(`teardown ${this.registration.name}`);
this._setTeardownDescription(timeoutManager); this._runnableDescription.phase = 'teardown';
this._useFuncFinished.resolve(); await timeoutManager.runRunnable(this._runnableDescription, async () => {
await this._selfTeardownComplete; this._useFuncFinished!.resolve();
timeoutManager.setCurrentFixture(undefined); await this._selfTeardownComplete;
});
} }
} finally { } finally {
for (const dep of this._deps) for (const dep of this._deps)

View file

@ -24,28 +24,22 @@ export type TimeSlot = {
elapsed: number; elapsed: number;
}; };
type RunnableDescription = { export type RunnableDescription = {
type: 'test' | 'beforeAll' | 'afterAll' | 'beforeEach' | 'afterEach' | 'slow' | 'skip' | 'fail' | 'fixme' | 'teardown'; type: 'test' | 'beforeAll' | 'afterAll' | 'beforeEach' | 'afterEach' | 'slow' | 'skip' | 'fail' | 'fixme' | 'teardown' | 'fixture';
phase?: 'setup' | 'teardown';
title?: string;
location?: Location; location?: Location;
slot?: TimeSlot; // Falls back to test slot. slot?: TimeSlot; // Falls back to test slot.
}; };
export type FixtureDescription = {
title: string;
phase: 'setup' | 'teardown';
location?: Location;
slot?: TimeSlot; // Falls back to current runnable slot.
};
export class TimeoutManager { export class TimeoutManager {
private _defaultSlot: TimeSlot; private _defaultSlot: TimeSlot;
private _runnable: RunnableDescription; private _runnables: RunnableDescription[] = [];
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._runnables = [{ type: 'test', slot: this._defaultSlot }];
this._timeoutRunner = new TimeoutRunner(timeout); this._timeoutRunner = new TimeoutRunner(timeout);
} }
@ -53,12 +47,22 @@ export class TimeoutManager {
this._timeoutRunner.interrupt(); this._timeoutRunner.interrupt();
} }
setCurrentRunnable(runnable: RunnableDescription) { async runRunnable<T>(runnable: RunnableDescription, cb: () => Promise<T>): Promise<T> {
this._updateRunnables(runnable, undefined); let slot = this._currentSlot();
} slot.elapsed = this._timeoutRunner.elapsed();
this._runnables.unshift(runnable);
slot = this._currentSlot();
this._timeoutRunner.updateTimeout(slot.timeout, slot.elapsed);
setCurrentFixture(fixture: FixtureDescription | undefined) { try {
this._updateRunnables(this._runnable, fixture); return await cb();
} finally {
let slot = this._currentSlot();
slot.elapsed = this._timeoutRunner.elapsed();
this._runnables.splice(this._runnables.indexOf(runnable), 1);
slot = this._currentSlot();
this._timeoutRunner.updateTimeout(slot.timeout, slot.elapsed);
}
} }
defaultSlotTimings() { defaultSlotTimings() {
@ -91,8 +95,12 @@ export class TimeoutManager {
this._timeoutRunner.updateTimeout(timeout); this._timeoutRunner.updateTimeout(timeout);
} }
currentRunnableType() { hasRunnableType(type: RunnableDescription['type']) {
return this._runnable.type; return this._runnables.some(r => r.type === type);
}
private _runnable(): RunnableDescription {
return this._runnables[0]!;
} }
currentSlotDeadline() { currentSlotDeadline() {
@ -100,66 +108,59 @@ export class TimeoutManager {
} }
private _currentSlot() { private _currentSlot() {
return this._fixture?.slot || this._runnable.slot || this._defaultSlot; for (const runnable of this._runnables) {
} if (runnable.slot)
return runnable.slot;
private _updateRunnables(runnable: RunnableDescription, fixture: FixtureDescription | undefined) { }
let slot = this._currentSlot(); return this._defaultSlot;
slot.elapsed = this._timeoutRunner.elapsed();
this._runnable = runnable;
this._fixture = fixture;
slot = this._currentSlot();
this._timeoutRunner.updateTimeout(slot.timeout, slot.elapsed);
} }
private _createTimeoutError(): TestInfoError { private _createTimeoutError(): TestInfoError {
let message = ''; let message = '';
const timeout = this._currentSlot().timeout; const timeout = this._currentSlot().timeout;
switch (this._runnable.type) { const runnable = this._runnable();
switch (runnable.type) {
case 'test': { case 'test': {
if (this._fixture) { message = `Test timeout of ${timeout}ms exceeded.`;
if (this._fixture.phase === 'setup') { break;
message = `Test timeout of ${timeout}ms exceeded while setting up "${this._fixture.title}".`; }
} else { case 'fixture': {
message = [ if (this._runnables.some(r => r.type === 'teardown')) {
`Test finished within timeout of ${timeout}ms, but tearing down "${this._fixture.title}" ran out of time.`, message = `Worker teardown timeout of ${timeout}ms exceeded while ${runnable.phase === 'setup' ? 'setting up' : 'tearing down'} "${runnable.title}".`;
`Please allow more time for the test, since teardown is attributed towards the test timeout budget.`, } else if (runnable.phase === 'setup') {
].join('\n'); message = `Test timeout of ${timeout}ms exceeded while setting up "${runnable.title}".`;
}
} else { } else {
message = `Test timeout of ${timeout}ms exceeded.`; message = [
`Test finished within timeout of ${timeout}ms, but tearing down "${runnable.title}" ran out of time.`,
`Please allow more time for the test, since teardown is attributed towards the test timeout budget.`,
].join('\n');
} }
break; break;
} }
case 'afterEach': case 'afterEach':
case 'beforeEach': case 'beforeEach':
message = `Test timeout of ${timeout}ms exceeded while running "${this._runnable.type}" hook.`; message = `Test timeout of ${timeout}ms exceeded while running "${runnable.type}" hook.`;
break; break;
case 'beforeAll': case 'beforeAll':
case 'afterAll': case 'afterAll':
message = `"${this._runnable.type}" hook timeout of ${timeout}ms exceeded.`; message = `"${runnable.type}" hook timeout of ${timeout}ms exceeded.`;
break; break;
case 'teardown': { case 'teardown': {
if (this._fixture) message = `Worker teardown timeout of ${timeout}ms exceeded.`;
message = `Worker teardown timeout of ${timeout}ms exceeded while ${this._fixture.phase === 'setup' ? 'setting up' : 'tearing down'} "${this._fixture.title}".`;
else
message = `Worker teardown timeout of ${timeout}ms exceeded.`;
break; break;
} }
case 'skip': case 'skip':
case 'slow': case 'slow':
case 'fixme': case 'fixme':
case 'fail': case 'fail':
message = `"${this._runnable.type}" modifier timeout of ${timeout}ms exceeded.`; message = `"${runnable.type}" modifier timeout of ${timeout}ms exceeded.`;
break; break;
} }
const fixtureWithSlot = this._fixture?.slot ? this._fixture : undefined; const fixtureWithSlot = runnable.type === 'fixture' && runnable.slot ? runnable : undefined;
if (fixtureWithSlot) if (fixtureWithSlot)
message = `Fixture "${fixtureWithSlot.title}" timeout of ${timeout}ms exceeded during ${fixtureWithSlot.phase}.`; message = `Fixture "${fixtureWithSlot.title}" timeout of ${timeout}ms exceeded during ${fixtureWithSlot.phase}.`;
message = colors.red(message); message = colors.red(message);
const location = (fixtureWithSlot || this._runnable).location; const location = (fixtureWithSlot || runnable).location;
return { return {
message, message,
// Include location for hooks, modifiers and fixtures to distinguish between them. // Include location for hooks, modifiers and fixtures to distinguish between them.

View file

@ -150,10 +150,11 @@ 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' });
const timeoutError = await timeoutManager.runWithTimeout(async () => { const timeoutError = await timeoutManager.runWithTimeout(async () => {
await this._fixtureRunner.teardownScope('test', timeoutManager); await timeoutManager.runRunnable({ type: 'teardown' }, async () => {
await this._fixtureRunner.teardownScope('worker', timeoutManager); await this._fixtureRunner.teardownScope('test', timeoutManager);
await this._fixtureRunner.teardownScope('worker', timeoutManager);
});
}); });
if (timeoutError) if (timeoutError)
this._fatalErrors.push(timeoutError); this._fatalErrors.push(timeoutError);
@ -359,7 +360,6 @@ export class WorkerMain extends ProcessRunner {
await this._runEachHooksForSuites(suites, 'beforeEach', testInfo, undefined); await this._runEachHooksForSuites(suites, 'beforeEach', testInfo, undefined);
// 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)
@ -403,82 +403,84 @@ export class WorkerMain extends ProcessRunner {
if (testInfo._didTimeout) { if (testInfo._didTimeout) {
// A timed-out test gets a full additional timeout to run after hooks. // A timed-out test gets a full additional timeout to run after hooks.
afterHooksSlot = { timeout: this._project.project.timeout, elapsed: 0 }; 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 => { await testInfo._timeoutManager.runRunnable({ type: 'afterEach', slot: afterHooksSlot }, async () => {
testInfo._afterHooksStep = step; await testInfo._runAsStep({ category: 'hook', title: 'After Hooks' }, async step => {
let firstAfterHooksError: TestInfoError | undefined; testInfo._afterHooksStep = step;
await testInfo._runWithTimeout(async () => { let firstAfterHooksError: TestInfoError | undefined;
// Note: do not wrap all teardown steps together, because failure in any of them
// does not prevent further teardown steps from running.
// Run "immediately upon test function finish" callback.
debugTest(`on-test-function-finish callback started`);
const didFinishTestFunctionError = await testInfo._runAndFailOnError(async () => testInfo._onDidFinishTestFunction?.());
firstAfterHooksError = firstAfterHooksError || didFinishTestFunctionError;
debugTest(`on-test-function-finish callback finished`);
// Run "afterEach" hooks, unless we failed at beforeAll stage.
if (shouldRunAfterEachHooks) {
const afterEachError = await testInfo._runAndFailOnError(() => this._runEachHooksForSuites(reversedSuites, 'afterEach', testInfo, afterHooksSlot));
firstAfterHooksError = firstAfterHooksError || afterEachError;
}
// Teardown test-scoped fixtures. Attribute to 'test' so that users understand
// they should probably increase the test timeout to fix this issue.
testInfo._timeoutManager.setCurrentRunnable({ type: 'test', slot: afterHooksSlot });
debugTest(`tearing down test scope started`);
const testScopeError = await testInfo._runAndFailOnError(() => this._fixtureRunner.teardownScope('test', testInfo._timeoutManager));
debugTest(`tearing down test scope finished`);
firstAfterHooksError = firstAfterHooksError || testScopeError;
// Run "afterAll" hooks for suites that are not shared with the next test.
// In case of failure the worker will be stopped and we have to make sure that afterAll
// hooks run before worker fixtures teardown.
for (const suite of reversedSuites) {
if (!nextSuites.has(suite) || testInfo._isFailure()) {
const afterAllError = await this._runAfterAllHooksForSuite(suite, testInfo);
firstAfterHooksError = firstAfterHooksError || afterAllError;
}
}
});
if (testInfo._isFailure())
this._isStopped = true;
if (this._isStopped) {
// Run all remaining "afterAll" hooks and teardown all fixtures when worker is shutting down.
// Mark as "cleaned up" early to avoid running cleanup twice.
this._didRunFullCleanup = true;
// Give it more time for the full cleanup.
await testInfo._runWithTimeout(async () => { await testInfo._runWithTimeout(async () => {
debugTest(`running full cleanup after the failure`); // Note: do not wrap all teardown steps together, because failure in any of them
// does not prevent further teardown steps from running.
const teardownSlot = { timeout: this._project.project.timeout, elapsed: 0 }; // Run "immediately upon test function finish" callback.
// Attribute to 'test' so that users understand they should probably increate the test timeout to fix this issue. debugTest(`on-test-function-finish callback started`);
testInfo._timeoutManager.setCurrentRunnable({ type: 'test', slot: teardownSlot }); const didFinishTestFunctionError = await testInfo._runAndFailOnError(async () => testInfo._onDidFinishTestFunction?.());
debugTest(`tearing down test scope started`); firstAfterHooksError = firstAfterHooksError || didFinishTestFunctionError;
const testScopeError = await testInfo._runAndFailOnError(() => this._fixtureRunner.teardownScope('test', testInfo._timeoutManager)); debugTest(`on-test-function-finish callback finished`);
debugTest(`tearing down test scope finished`);
firstAfterHooksError = firstAfterHooksError || testScopeError;
for (const suite of reversedSuites) { // Run "afterEach" hooks, unless we failed at beforeAll stage.
const afterAllError = await this._runAfterAllHooksForSuite(suite, testInfo); if (shouldRunAfterEachHooks) {
firstAfterHooksError = firstAfterHooksError || afterAllError; const afterEachError = await testInfo._runAndFailOnError(() => this._runEachHooksForSuites(reversedSuites, 'afterEach', testInfo, afterHooksSlot));
firstAfterHooksError = firstAfterHooksError || afterEachError;
} }
// Attribute to 'teardown' because worker fixtures are not perceived as a part of a test. // Teardown test-scoped fixtures. Attribute to 'test' so that users understand
testInfo._timeoutManager.setCurrentRunnable({ type: 'teardown', slot: teardownSlot }); // they should probably increase the test timeout to fix this issue.
debugTest(`tearing down worker scope started`); await testInfo._timeoutManager.runRunnable({ type: 'test', slot: afterHooksSlot }, async () => {
const workerScopeError = await testInfo._runAndFailOnError(() => this._fixtureRunner.teardownScope('worker', testInfo._timeoutManager)); debugTest(`tearing down test scope started`);
debugTest(`tearing down worker scope finished`); const testScopeError = await testInfo._runAndFailOnError(() => this._fixtureRunner.teardownScope('test', testInfo._timeoutManager));
firstAfterHooksError = firstAfterHooksError || workerScopeError; debugTest(`tearing down test scope finished`);
}); firstAfterHooksError = firstAfterHooksError || testScopeError;
}
if (firstAfterHooksError) // Run "afterAll" hooks for suites that are not shared with the next test.
step.complete({ error: firstAfterHooksError }); // In case of failure the worker will be stopped and we have to make sure that afterAll
// hooks run before worker fixtures teardown.
for (const suite of reversedSuites) {
if (!nextSuites.has(suite) || testInfo._isFailure()) {
const afterAllError = await this._runAfterAllHooksForSuite(suite, testInfo);
firstAfterHooksError = firstAfterHooksError || afterAllError;
}
}
});
});
if (testInfo._isFailure())
this._isStopped = true;
if (this._isStopped) {
// Run all remaining "afterAll" hooks and teardown all fixtures when worker is shutting down.
// Mark as "cleaned up" early to avoid running cleanup twice.
this._didRunFullCleanup = true;
// Give it more time for the full cleanup.
await testInfo._runWithTimeout(async () => {
debugTest(`running full cleanup after the failure`);
const teardownSlot = { timeout: this._project.project.timeout, elapsed: 0 };
// Attribute to 'test' so that users understand they should probably increase the test timeout to fix this issue.
await testInfo._timeoutManager.runRunnable({ type: 'test', slot: teardownSlot }, async () => {
debugTest(`tearing down test scope started`);
const testScopeError = await testInfo._runAndFailOnError(() => this._fixtureRunner.teardownScope('test', testInfo._timeoutManager));
debugTest(`tearing down test scope finished`);
firstAfterHooksError = firstAfterHooksError || testScopeError;
for (const suite of reversedSuites) {
const afterAllError = await this._runAfterAllHooksForSuite(suite, testInfo);
firstAfterHooksError = firstAfterHooksError || afterAllError;
}
debugTest(`tearing down worker scope started`);
const workerScopeError = await testInfo._runAndFailOnError(() => this._fixtureRunner.teardownScope('worker', testInfo._timeoutManager));
debugTest(`tearing down worker scope finished`);
firstAfterHooksError = firstAfterHooksError || workerScopeError;
});
});
}
if (firstAfterHooksError)
step.complete({ error: firstAfterHooksError });
});
}); });
this._currentTest = null; this._currentTest = null;
@ -496,17 +498,18 @@ export class WorkerMain extends ProcessRunner {
const actualScope = this._fixtureRunner.dependsOnWorkerFixturesOnly(modifier.fn, modifier.location) ? 'worker' : 'test'; const actualScope = this._fixtureRunner.dependsOnWorkerFixturesOnly(modifier.fn, modifier.location) ? 'worker' : 'test';
if (actualScope !== scope) if (actualScope !== scope)
continue; continue;
debugTest(`modifier at "${formatLocation(modifier.location)}" started`); await testInfo._timeoutManager.runRunnable({ type: modifier.type, location: modifier.location, slot: timeSlot }, async () => {
testInfo._timeoutManager.setCurrentRunnable({ type: modifier.type, location: modifier.location, slot: timeSlot }); debugTest(`modifier at "${formatLocation(modifier.location)}" started`);
const result = await testInfo._runAsStep({ const result = await testInfo._runAsStep({
category: 'hook', category: 'hook',
title: `${modifier.type} modifier`, title: `${modifier.type} modifier`,
location: modifier.location, location: modifier.location,
}, () => 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)
extraAnnotations.push({ type: modifier.type, description: modifier.description }); extraAnnotations.push({ type: modifier.type, description: modifier.description });
testInfo[modifier.type](!!result, modifier.description); testInfo[modifier.type](!!result, modifier.description);
});
} }
} }
@ -522,19 +525,20 @@ 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._timeoutManager.runRunnable({ type: 'beforeAll', location: hook.location, slot: timeSlot }, async () => {
await testInfo._runAsStep({ await testInfo._runAsStep({
category: 'hook', category: 'hook',
title: `${hook.type} hook`, title: `${hook.type} hook`,
location: hook.location, location: hook.location,
}, async () => { }, async () => {
try { try {
await this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only'); await this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only');
} finally { } finally {
// Each beforeAll hook has its own scope for test fixtures. Attribute to the same runnable and timeSlot. // Each beforeAll hook has its own scope for test fixtures. Attribute to the same runnable and timeSlot.
// Note: we must teardown even after beforeAll fails, because we'll run more beforeAlls. // Note: we must teardown even after beforeAll fails, because we'll run more beforeAlls.
await this._fixtureRunner.teardownScope('test', testInfo._timeoutManager); await this._fixtureRunner.teardownScope('test', testInfo._timeoutManager);
} }
});
}); });
} catch (e) { } catch (e) {
// Always run all the hooks, and capture the first error. // Always run all the hooks, and capture the first error.
@ -558,19 +562,20 @@ 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._timeoutManager.runRunnable({ type: 'afterAll', location: hook.location, slot: timeSlot }, async () => {
await testInfo._runAsStep({ await testInfo._runAsStep({
category: 'hook', category: 'hook',
title: `${hook.type} hook`, title: `${hook.type} hook`,
location: hook.location, location: hook.location,
}, async () => { }, async () => {
try { try {
await this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only'); await this._fixtureRunner.resolveParametersAndRunFunction(hook.fn, testInfo, 'all-hooks-only');
} finally { } finally {
// Each afterAll hook has its own scope for test fixtures. Attribute to the same runnable and timeSlot. // Each afterAll hook has its own scope for test fixtures. Attribute to the same runnable and timeSlot.
// Note: we must teardown even after afterAll fails, because we'll run more afterAlls. // Note: we must teardown even after afterAll fails, because we'll run more afterAlls.
await this._fixtureRunner.teardownScope('test', testInfo._timeoutManager); await this._fixtureRunner.teardownScope('test', testInfo._timeoutManager);
} }
});
}); });
}); });
firstError = firstError || afterAllError; firstError = firstError || afterAllError;
@ -584,12 +589,13 @@ export class WorkerMain extends ProcessRunner {
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._timeoutManager.runRunnable({ type, location: hook.location, slot: timeSlot }, async () => {
await testInfo._runAsStep({ await testInfo._runAsStep({
category: 'hook', category: 'hook',
title: `${hook.type} hook`, title: `${hook.type} hook`,
location: hook.location, location: hook.location,
}, () => 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.
error = error || e; error = error || e;

View file

@ -477,7 +477,7 @@ test('should not report fixture teardown error twice', async ({ runInlineTest })
expect(countTimes(result.output, 'Oh my error')).toBe(2); expect(countTimes(result.output, 'Oh my error')).toBe(2);
}); });
test('should not report fixture teardown timeout twice', async ({ runInlineTest }) => { test('should report fixture teardown extend', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'a.spec.ts': ` 'a.spec.ts': `
import { test as base, expect } from '@playwright/test'; import { test as base, expect } from '@playwright/test';
@ -494,8 +494,7 @@ test('should not report fixture teardown timeout twice', async ({ runInlineTest
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1); expect(result.failed).toBe(1);
expect(result.output).toContain('Test finished within timeout of 1000ms, but tearing down "fixture" ran out of time.'); expect(result.output).toContain('Test finished within timeout of 1000ms, but tearing down "fixture" ran out of time.');
expect(result.output).not.toContain('base.extend'); // Should not point to the location. expect(result.output).toContain('base.extend');
// TODO: this should be "not.toContain" actually.
expect(result.output).toContain('Worker teardown timeout of 1000ms exceeded while tearing down "fixture".'); expect(result.output).toContain('Worker teardown timeout of 1000ms exceeded while tearing down "fixture".');
}); });