chore(test runner): allow TestInfoImpl without a TestCase (#29534)

This will be useful to run `beforeAll`/`afterAll` hooks with a separate
`TestInfo` instance, as well as run use helpers like
`_runAndFailOnError()` during scope teardown.
This commit is contained in:
Dmitry Gozman 2024-02-16 12:43:13 -08:00 committed by GitHub
parent dbf0b25146
commit 269a293ba1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 42 additions and 49 deletions

View File

@ -17,7 +17,7 @@
import { formatLocation, debugTest, filterStackFile } from '../util';
import { ManualPromise } from 'playwright-core/lib/utils';
import type { TestInfoImpl, TestStepInternal } from './testInfo';
import type { FixtureDescription, TimeoutManager } from './timeoutManager';
import type { FixtureDescription } from './timeoutManager';
import { fixtureParameterNames, type FixturePool, type FixtureRegistration, type FixtureScope } from '../common/fixtures';
import type { WorkerInfo } from '../../types/test';
import type { Location } from '../../types/testReporter';
@ -136,21 +136,21 @@ class Fixture {
testInfo._timeoutManager.setCurrentFixture(undefined);
}
async teardown(timeoutManager: TimeoutManager) {
async teardown(testInfo: TestInfoImpl) {
if (this._teardownWithDepsComplete) {
// When we are waiting for the teardown for the second time,
// most likely after the first time did timeout, annotate current fixture
// for better error messages.
this._setTeardownDescription(timeoutManager);
this._setTeardownDescription(testInfo);
await this._teardownWithDepsComplete;
timeoutManager.setCurrentFixture(undefined);
testInfo._timeoutManager.setCurrentFixture(undefined);
return;
}
this._teardownWithDepsComplete = this._teardownInternal(timeoutManager);
this._teardownWithDepsComplete = this._teardownInternal(testInfo);
await this._teardownWithDepsComplete;
}
private async _teardownInternal(timeoutManager: TimeoutManager) {
private async _teardownInternal(testInfo: TestInfoImpl) {
if (typeof this.registration.fn !== 'function')
return;
try {
@ -161,10 +161,10 @@ class Fixture {
}
if (this._useFuncFinished) {
debugTest(`teardown ${this.registration.name}`);
this._setTeardownDescription(timeoutManager);
this._setTeardownDescription(testInfo);
this._useFuncFinished.resolve();
await this._selfTeardownComplete;
timeoutManager.setCurrentFixture(undefined);
testInfo._timeoutManager.setCurrentFixture(undefined);
}
} finally {
for (const dep of this._deps)
@ -173,9 +173,9 @@ class Fixture {
}
}
private _setTeardownDescription(timeoutManager: TimeoutManager) {
private _setTeardownDescription(testInfo: TestInfoImpl) {
this._runnableDescription.phase = 'teardown';
timeoutManager.setCurrentFixture(this._runnableDescription);
testInfo._timeoutManager.setCurrentFixture(this._runnableDescription);
}
_collectFixturesInTeardownOrder(scope: FixtureScope, collector: Set<Fixture>) {
@ -206,14 +206,14 @@ export class FixtureRunner {
this.pool = pool;
}
async teardownScope(scope: FixtureScope, timeoutManager: TimeoutManager, onFixtureError: (error: Error) => void) {
async teardownScope(scope: FixtureScope, testInfo: TestInfoImpl, onFixtureError: (error: Error) => void) {
// Teardown fixtures in the reverse order.
const fixtures = Array.from(this.instanceForId.values()).reverse();
const collector = new Set<Fixture>();
for (const fixture of fixtures)
fixture._collectFixturesInTeardownOrder(scope, collector);
for (const fixture of collector)
await fixture.teardown(timeoutManager).catch(onFixtureError);
await fixture.teardown(testInfo).catch(onFixtureError);
if (scope === 'test')
this.testScopeClean = true;
}

View File

@ -52,7 +52,6 @@ export class TestInfoImpl implements TestInfo {
private _onStepBegin: (payload: StepBeginPayload) => void;
private _onStepEnd: (payload: StepEndPayload) => void;
private _onAttach: (payload: AttachmentPayload) => void;
readonly _test: TestCase;
readonly _timeoutManager: TimeoutManager;
readonly _startTime: number;
readonly _startWallTime: number;
@ -62,6 +61,7 @@ export class TestInfoImpl implements TestInfo {
_didTimeout = false;
_wasInterrupted = false;
_lastStepId = 0;
private readonly _requireFile: string;
readonly _projectInternal: FullProjectInternal;
readonly _configInternal: FullConfigInternal;
readonly _steps: TestStepInternal[] = [];
@ -129,19 +129,19 @@ export class TestInfoImpl implements TestInfo {
configInternal: FullConfigInternal,
projectInternal: FullProjectInternal,
workerParams: WorkerInitParams,
test: TestCase,
test: TestCase | undefined,
retry: number,
onStepBegin: (payload: StepBeginPayload) => void,
onStepEnd: (payload: StepEndPayload) => void,
onAttach: (payload: AttachmentPayload) => void,
) {
this._test = test;
this.testId = test.id;
this.testId = test?.id ?? '';
this._onStepBegin = onStepBegin;
this._onStepEnd = onStepEnd;
this._onAttach = onAttach;
this._startTime = monotonicTime();
this._startWallTime = Date.now();
this._requireFile = test?._requireFile ?? '';
this.repeatEachIndex = workerParams.repeatEachIndex;
this.retry = retry;
@ -151,20 +151,20 @@ export class TestInfoImpl implements TestInfo {
this.project = projectInternal.project;
this._configInternal = configInternal;
this.config = configInternal.config;
this.title = test.title;
this.titlePath = test.titlePath();
this.file = test.location.file;
this.line = test.location.line;
this.column = test.location.column;
this.fn = test.fn;
this.expectedStatus = test.expectedStatus;
this.title = test?.title ?? '';
this.titlePath = test?.titlePath() ?? [];
this.file = test?.location.file ?? '';
this.line = test?.location.line ?? 0;
this.column = test?.location.column ?? 0;
this.fn = test?.fn ?? (() => {});
this.expectedStatus = test?.expectedStatus ?? 'skipped';
this._timeoutManager = new TimeoutManager(this.project.timeout);
this.outputDir = (() => {
const relativeTestFilePath = path.relative(this.project.testDir, test._requireFile.replace(/\.(spec|test)\.(js|ts|mjs)$/, ''));
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile.replace(/\.(spec|test)\.(js|ts|mjs)$/, ''));
const sanitizedRelativePath = relativeTestFilePath.replace(process.platform === 'win32' ? new RegExp('\\\\', 'g') : new RegExp('/', 'g'), '-');
const fullTitleWithoutSpec = test.titlePath().slice(1).join(' ');
const fullTitleWithoutSpec = this.titlePath.slice(1).join(' ');
let testOutputDir = trimLongString(sanitizedRelativePath + '-' + sanitizeForFilePath(fullTitleWithoutSpec));
if (projectInternal.id)
@ -177,7 +177,7 @@ export class TestInfoImpl implements TestInfo {
})();
this.snapshotDir = (() => {
const relativeTestFilePath = path.relative(this.project.testDir, test._requireFile);
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile);
return path.join(this.project.snapshotDir, relativeTestFilePath + '-snapshots');
})();
@ -328,7 +328,7 @@ export class TestInfoImpl implements TestInfo {
}
const payload: StepEndPayload = {
testId: this._test.id,
testId: this.testId,
stepId,
wallTime: step.endWallTime,
error: step.error,
@ -344,7 +344,7 @@ export class TestInfoImpl implements TestInfo {
const parentStepList = parentStep ? parentStep.steps : this._steps;
parentStepList.push(step);
const payload: StepBeginPayload = {
testId: this._test.id,
testId: this.testId,
stepId,
parentStepId: parentStep ? parentStep.stepId : undefined,
title: data.title,
@ -434,7 +434,7 @@ export class TestInfoImpl implements TestInfo {
});
this._attachmentsPush(attachment);
this._onAttach({
testId: this._test.id,
testId: this.testId,
name: attachment.name,
contentType: attachment.contentType,
path: attachment.path,
@ -465,7 +465,7 @@ export class TestInfoImpl implements TestInfo {
snapshotPath(...pathSegments: string[]) {
const subPath = path.join(...pathSegments);
const parsedSubPath = path.parse(subPath);
const relativeTestFilePath = path.relative(this.project.testDir, this._test._requireFile);
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile);
const parsedRelativeTestFilePath = path.parse(relativeTestFilePath);
const projectNamePathSegment = sanitizeForFilePath(this.project.name);

View File

@ -24,7 +24,6 @@ import type { Annotation, FullConfigInternal, FullProjectInternal } from '../com
import { FixtureRunner } from './fixtureRunner';
import { ManualPromise, gracefullyCloseAll, removeFolders } from 'playwright-core/lib/utils';
import { TestInfoImpl } from './testInfo';
import { TimeoutManager } from './timeoutManager';
import { ProcessRunner } from '../common/process';
import { loadTestFile } from '../common/testLoader';
import { applyRepeatEachIndex, bindFileSuiteToProject, filterTestsRemoveEmptySuites } from '../common/suiteUtils';
@ -53,7 +52,7 @@ export class WorkerMain extends ProcessRunner {
// This promise resolves once the single "run test group" call finishes.
private _runFinished = new ManualPromise<void>();
private _currentTest: TestInfoImpl | null = null;
private _lastRunningTests: TestInfoImpl[] = [];
private _lastRunningTests: TestCase[] = [];
private _totalRunningTests = 0;
// Suites that had their beforeAll hooks, but not afterAll hooks executed.
// These suites still need afterAll hooks to be executed for the proper cleanup.
@ -129,7 +128,7 @@ export class WorkerMain extends ProcessRunner {
'',
'',
colors.red(`Failed worker ran ${count}${lastMessage}:`),
...this._lastRunningTests.map(testInfo => formatTestTitle(testInfo._test, testInfo.project.name)),
...this._lastRunningTests.map(test => formatTestTitle(test, this._project.project.name)),
].join('\n');
if (error.message) {
if (error.stack) {
@ -153,7 +152,7 @@ export class WorkerMain extends ProcessRunner {
private async _teardownScopeAndReturnFirstError(scope: FixtureScope, testInfo: TestInfoImpl): Promise<Error | undefined> {
let error: Error | undefined;
await this._fixtureRunner.teardownScope(scope, testInfo._timeoutManager, e => {
await this._fixtureRunner.teardownScope(scope, testInfo, e => {
testInfo._failWithError(e, true, false);
if (error === undefined)
error = e;
@ -163,15 +162,14 @@ export class WorkerMain extends ProcessRunner {
private async _teardownScopes() {
// TODO: separate timeout for teardown?
const timeoutManager = new TimeoutManager(this._project.project.timeout);
await timeoutManager.withRunnable({ type: 'teardown' }, async () => {
const timeoutError = await timeoutManager.runWithTimeout(async () => {
await this._fixtureRunner.teardownScope('test', timeoutManager, e => this._fatalErrors.push(serializeError(e)));
await this._fixtureRunner.teardownScope('worker', timeoutManager, e => this._fatalErrors.push(serializeError(e)));
const fakeTestInfo = new TestInfoImpl(this._config, this._project, this._params, undefined, 0, () => {}, () => {}, () => {});
await fakeTestInfo._timeoutManager.withRunnable({ type: 'teardown' }, async () => {
await fakeTestInfo._runWithTimeout(async () => {
await this._teardownScopeAndReturnFirstError('test', fakeTestInfo);
await this._teardownScopeAndReturnFirstError('worker', fakeTestInfo);
});
if (timeoutError)
this._fatalErrors.push(serializeError(timeoutError));
});
this._fatalErrors.push(...fakeTestInfo.errors);
}
unhandledError(error: Error | any) {
@ -322,7 +320,7 @@ export class WorkerMain extends ProcessRunner {
}
this._totalRunningTests++;
this._lastRunningTests.push(testInfo);
this._lastRunningTests.push(test);
if (this._lastRunningTests.length > 10)
this._lastRunningTests.shift();
let didFailBeforeAllForSuite: Suite | undefined;
@ -603,14 +601,14 @@ export class WorkerMain extends ProcessRunner {
function buildTestBeginPayload(testInfo: TestInfoImpl): TestBeginPayload {
return {
testId: testInfo._test.id,
testId: testInfo.testId,
startWallTime: testInfo._startWallTime,
};
}
function buildTestEndPayload(testInfo: TestInfoImpl): TestEndPayload {
return {
testId: testInfo._test.id,
testId: testInfo.testId,
duration: testInfo.duration,
status: testInfo.status!,
errors: testInfo.errors,

View File

@ -42,13 +42,10 @@ test('should have correct tags', async ({ runInlineTest }) => {
'stdio.spec.js': `
import { test, expect } from '@playwright/test';
test('no-tags', () => {
expect(test.info()._test.tags).toEqual([]);
});
test('foo-tag @inline', { tag: '@foo' }, () => {
expect(test.info()._test.tags).toEqual(['@inline', '@foo']);
});
test('foo-bar-tags', { tag: ['@foo', '@bar'] }, () => {
expect(test.info()._test.tags).toEqual(['@foo', '@bar']);
});
test.skip('skip-foo-tag', { tag: '@foo' }, () => {
});
@ -59,11 +56,9 @@ test('should have correct tags', async ({ runInlineTest }) => {
});
test.describe('suite @inline', { tag: '@foo' }, () => {
test('foo-suite', () => {
expect(test.info()._test.tags).toEqual(['@inline', '@foo']);
});
test.describe('inner', { tag: '@bar' }, () => {
test('foo-bar-suite', () => {
expect(test.info()._test.tags).toEqual(['@inline', '@foo', '@bar']);
});
});
});