Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/playwright/src/common/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export type DonePayload = {
fatalErrors: TestInfoErrorImpl[];
skipTestsDueToSetupFailure: string[]; // test ids
fatalUnknownTestIds?: string[];
missingProjectById?: string;
};

export type TestOutputPayload = {
Expand Down
9 changes: 8 additions & 1 deletion packages/playwright/src/runner/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,11 +412,18 @@ class JobDispatcher {
// - there are no remaining
// - we are here not because something failed
// - no unrecoverable worker error
if (!this._remainingByTestId.size && !this._failedTests.size && !params.fatalErrors.length && !params.skipTestsDueToSetupFailure.length && !params.fatalUnknownTestIds && !params.unexpectedExitError) {
if (!this._remainingByTestId.size && !this._failedTests.size && !params.fatalErrors.length && !params.skipTestsDueToSetupFailure.length && !params.fatalUnknownTestIds && !params.missingProjectById && !params.unexpectedExitError) {
this._finished({ didFail: false });
return;
}

if (params.missingProjectById) {
this._failureTracker.onWorkerError();
this._reporter.onError?.({ message: `Project with name ${params.missingProjectById} not found. Make sure project name does not change.` });
this._finished({ didFail: true, newJob: undefined });
return;
}

for (const testId of params.fatalUnknownTestIds || []) {
const test = this._remainingByTestId.get(testId);
if (test) {
Expand Down
45 changes: 34 additions & 11 deletions packages/playwright/src/worker/workerMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export class WorkerMain extends ProcessRunner {
private _didRunFullCleanup = false;
// Whether the worker was requested to stop.
private _isStopped = false;
// Whether a fatal error was caused by a missing project.
private _isMissingProject = false;
// This promise resolves once the single "run test group" call finishes.
private _runFinished = new ManualPromise<void>();
private _currentTest: TestInfoImpl | null = null;
Expand Down Expand Up @@ -103,13 +105,18 @@ export class WorkerMain extends ProcessRunner {
}

override async gracefullyClose() {
if (this._isMissingProject) {
// Never set anything up and we can crash on attempting cleanup
return;
}

try {
await this._stop();
// Ignore top-level errors, they are already inside TestInfo.errors.
const fakeTestInfo = new TestInfoImpl(this._config, this._project, this._params, undefined, 0, () => {}, () => {}, () => {});
const runnable = { type: 'teardown' } as const;
// We have to load the project to get the right deadline below.
await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => this._loadIfNeeded()).catch(() => {});
await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => this._load()).catch(() => {});
await this._fixtureRunner.teardownScope('test', fakeTestInfo, runnable).catch(() => {});
await this._fixtureRunner.teardownScope('worker', fakeTestInfo, runnable).catch(() => {});
// Close any other browsers launched in this process. This includes anything launched
Expand Down Expand Up @@ -186,28 +193,43 @@ export class WorkerMain extends ProcessRunner {
void this._stop();
}

private async _loadIfNeeded() {
private async _load(): Promise<{
config: FullConfigInternal,
project: FullProjectInternal,
poolBuilder: PoolBuilder,
} | undefined> {
if (this._config)
return;
return { config: this._config, project: this._project, poolBuilder: this._poolBuilder };

this._config = await deserializeConfig(this._params.config);
this._project = this._config.projects.find(p => p.id === this._params.projectId)!;
const project = this._config.projects.find(p => p.id === this._params.projectId);
if (!project)
return undefined;
this._project = project;
this._poolBuilder = PoolBuilder.createForWorker(this._project);

return { config: this._config, project: this._project, poolBuilder: this._poolBuilder };
}

async runTestGroup(runPayload: RunPayload) {
this._runFinished = new ManualPromise<void>();
const entries = new Map(runPayload.entries.map(e => [e.testId, e]));
let fatalUnknownTestIds;
let fatalUnknownTestIds: string[] | undefined;
try {
await this._loadIfNeeded();
const fileSuite = await loadTestFile(runPayload.file, this._config.config.rootDir);
const suite = bindFileSuiteToProject(this._project, fileSuite);
const workerState = await this._load();
if (!workerState) {
this._isMissingProject = true;
void this._stop();
return;
}
const { config, project, poolBuilder } = workerState;
const fileSuite = await loadTestFile(runPayload.file, config.config.rootDir);
const suite = bindFileSuiteToProject(project, fileSuite);
if (this._params.repeatEachIndex)
applyRepeatEachIndex(this._project, suite, this._params.repeatEachIndex);
applyRepeatEachIndex(project, suite, this._params.repeatEachIndex);
const hasEntries = filterTestsRemoveEmptySuites(suite, test => entries.has(test.id));
if (hasEntries) {
this._poolBuilder.buildPools(suite);
poolBuilder.buildPools(suite);
this._activeSuites = new Map();
this._didRunFullCleanup = false;
const tests = suite.allTests();
Expand Down Expand Up @@ -235,7 +257,8 @@ export class WorkerMain extends ProcessRunner {
const donePayload: DonePayload = {
fatalErrors: this._fatalErrors,
skipTestsDueToSetupFailure: [],
fatalUnknownTestIds
fatalUnknownTestIds,
missingProjectById: this._isMissingProject ? this._params.projectId : undefined
};
for (const test of this._skipRemainingTestsInSuite?.allTests() || []) {
if (entries.has(test.id))
Expand Down
Loading