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 { formatLocation, debugTest, filterStackFile } from '../util';
import { ManualPromise } from 'playwright-core/lib/utils'; import { ManualPromise } 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 { FixtureDescription } 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';
@ -136,21 +136,21 @@ class Fixture {
testInfo._timeoutManager.setCurrentFixture(undefined); testInfo._timeoutManager.setCurrentFixture(undefined);
} }
async teardown(timeoutManager: TimeoutManager) { async teardown(testInfo: TestInfoImpl) {
if (this._teardownWithDepsComplete) { if (this._teardownWithDepsComplete) {
// 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._setTeardownDescription(testInfo);
await this._teardownWithDepsComplete; await this._teardownWithDepsComplete;
timeoutManager.setCurrentFixture(undefined); testInfo._timeoutManager.setCurrentFixture(undefined);
return; return;
} }
this._teardownWithDepsComplete = this._teardownInternal(timeoutManager); this._teardownWithDepsComplete = this._teardownInternal(testInfo);
await this._teardownWithDepsComplete; await this._teardownWithDepsComplete;
} }
private async _teardownInternal(timeoutManager: TimeoutManager) { private async _teardownInternal(testInfo: TestInfoImpl) {
if (typeof this.registration.fn !== 'function') if (typeof this.registration.fn !== 'function')
return; return;
try { try {
@ -161,10 +161,10 @@ class Fixture {
} }
if (this._useFuncFinished) { if (this._useFuncFinished) {
debugTest(`teardown ${this.registration.name}`); debugTest(`teardown ${this.registration.name}`);
this._setTeardownDescription(timeoutManager); this._setTeardownDescription(testInfo);
this._useFuncFinished.resolve(); this._useFuncFinished.resolve();
await this._selfTeardownComplete; await this._selfTeardownComplete;
timeoutManager.setCurrentFixture(undefined); testInfo._timeoutManager.setCurrentFixture(undefined);
} }
} finally { } finally {
for (const dep of this._deps) for (const dep of this._deps)
@ -173,9 +173,9 @@ class Fixture {
} }
} }
private _setTeardownDescription(timeoutManager: TimeoutManager) { private _setTeardownDescription(testInfo: TestInfoImpl) {
this._runnableDescription.phase = 'teardown'; this._runnableDescription.phase = 'teardown';
timeoutManager.setCurrentFixture(this._runnableDescription); testInfo._timeoutManager.setCurrentFixture(this._runnableDescription);
} }
_collectFixturesInTeardownOrder(scope: FixtureScope, collector: Set<Fixture>) { _collectFixturesInTeardownOrder(scope: FixtureScope, collector: Set<Fixture>) {
@ -206,14 +206,14 @@ export class FixtureRunner {
this.pool = pool; 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. // Teardown fixtures in the reverse order.
const fixtures = Array.from(this.instanceForId.values()).reverse(); const fixtures = Array.from(this.instanceForId.values()).reverse();
const collector = new Set<Fixture>(); const collector = new Set<Fixture>();
for (const fixture of fixtures) for (const fixture of fixtures)
fixture._collectFixturesInTeardownOrder(scope, collector); fixture._collectFixturesInTeardownOrder(scope, collector);
for (const fixture of collector) for (const fixture of collector)
await fixture.teardown(timeoutManager).catch(onFixtureError); await fixture.teardown(testInfo).catch(onFixtureError);
if (scope === 'test') if (scope === 'test')
this.testScopeClean = true; this.testScopeClean = true;
} }

View File

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

View File

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

View File

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