From ac4f79ac9d421b2cb2d30f4106edc44d5a2e41c3 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 31 Jan 2025 11:07:16 -0800 Subject: [PATCH] fix: display error when project is not found --- packages/playwright/src/common/ipc.ts | 1 + packages/playwright/src/runner/dispatcher.ts | 9 +++- packages/playwright/src/worker/workerMain.ts | 45 +++++++++++++++----- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/packages/playwright/src/common/ipc.ts b/packages/playwright/src/common/ipc.ts index 76ee996216..35c20bece9 100644 --- a/packages/playwright/src/common/ipc.ts +++ b/packages/playwright/src/common/ipc.ts @@ -125,6 +125,7 @@ export type DonePayload = { fatalErrors: TestInfoErrorImpl[]; skipTestsDueToSetupFailure: string[]; // test ids fatalUnknownTestIds?: string[]; + missingProjectById?: string; }; export type TestOutputPayload = { diff --git a/packages/playwright/src/runner/dispatcher.ts b/packages/playwright/src/runner/dispatcher.ts index 534fe7eb4a..67140d8e5a 100644 --- a/packages/playwright/src/runner/dispatcher.ts +++ b/packages/playwright/src/runner/dispatcher.ts @@ -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) { diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index b4878ff659..4d4d8db58c 100644 --- a/packages/playwright/src/worker/workerMain.ts +++ b/packages/playwright/src/worker/workerMain.ts @@ -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(); private _currentTest: TestInfoImpl | null = null; @@ -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 @@ -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(); 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(); @@ -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))