diff --git a/packages/playwright-test/src/worker.ts b/packages/playwright-test/src/worker.ts index 7928e0966f..0f34a16075 100644 --- a/packages/playwright-test/src/worker.ts +++ b/packages/playwright-test/src/worker.ts @@ -103,8 +103,11 @@ async function gracefullyCloseAndExit() { if (workerIndex !== undefined) await stopProfiling(workerIndex); } catch (e) { - const payload: TeardownErrorsPayload = { fatalErrors: [serializeError(e)] }; - process.send!({ method: 'teardownErrors', params: payload }); + try { + const payload: TeardownErrorsPayload = { fatalErrors: [serializeError(e)] }; + process.send!({ method: 'teardownErrors', params: payload }); + } catch { + } } process.exit(0); } diff --git a/packages/playwright-test/src/workerRunner.ts b/packages/playwright-test/src/workerRunner.ts index 6556d0f351..4a3e0777dc 100644 --- a/packages/playwright-test/src/workerRunner.ts +++ b/packages/playwright-test/src/workerRunner.ts @@ -14,10 +14,11 @@ * limitations under the License. */ +import colors from 'colors/safe'; import rimraf from 'rimraf'; import util from 'util'; import { EventEmitter } from 'events'; -import { serializeError } from './util'; +import { relativeFilePath, serializeError } from './util'; import { TestBeginPayload, TestEndPayload, RunPayload, DonePayload, WorkerInitParams, StepBeginPayload, StepEndPayload, TeardownErrorsPayload } from './ipc'; import { setCurrentTestInfo } from './globals'; import { Loader } from './loader'; @@ -49,6 +50,8 @@ export class WorkerRunner extends EventEmitter { // This promise resolves once the single "run test group" call finishes. private _runFinished = new ManualPromise(); _currentTest: TestInfoImpl | null = null; + private _lastRunningTests: TestInfoImpl[] = []; + private _totalRunningTests = 0; // Dynamic annotations originated by modifiers with a callback, e.g. `test.skip(() => true)`. private _extraSuiteAnnotations = new Map(); // Suites that had their beforeAll hooks, but not afterAll hooks executed. @@ -84,11 +87,28 @@ export class WorkerRunner extends EventEmitter { await this._loadIfNeeded(); await this._teardownScopes(); if (this._fatalErrors.length) { + const diagnostics = this._createWorkerTeardownDiagnostics(); + if (diagnostics) + this._fatalErrors.unshift(diagnostics); const payload: TeardownErrorsPayload = { fatalErrors: this._fatalErrors }; this.emit('teardownErrors', payload); } } + private _createWorkerTeardownDiagnostics(): TestError | undefined { + if (!this._lastRunningTests.length) + return; + const count = this._totalRunningTests === 1 ? '1 test' : `${this._totalRunningTests} tests`; + let lastMessage = ''; + if (this._lastRunningTests.length < this._totalRunningTests) + lastMessage = `, last ${this._lastRunningTests.length} tests were`; + const message = [ + colors.red(`Worker teardown error. This worker ran ${count}${lastMessage}:`), + ...this._lastRunningTests.map(testInfo => formatTestTitle(testInfo._test, testInfo.project.name)), + ].join('\n'); + return { message }; + } + private async _teardownScopes() { // TODO: separate timeout for teardown? const timeoutManager = new TimeoutManager(this._project.config.timeout); @@ -268,6 +288,10 @@ export class WorkerRunner extends EventEmitter { return; } + this._totalRunningTests++; + this._lastRunningTests.push(testInfo); + if (this._lastRunningTests.length > 10) + this._lastRunningTests.shift(); let didFailBeforeAllForSuite: Suite | undefined; let shouldRunAfterEachHooks = false; @@ -539,3 +563,11 @@ function getSuites(test: TestCase | undefined): Suite[] { suites.reverse(); // Put root suite first. return suites; } + +function formatTestTitle(test: TestCase, projectName: string) { + // file, ...describes, test + const [, ...titles] = test.titlePath(); + const location = `${relativeFilePath(test.location.file)}:${test.location.line}:${test.location.column}`; + const projectTitle = projectName ? `[${projectName}] › ` : ''; + return `${projectTitle}${location} › ${titles.join(' › ')}`; +} diff --git a/tests/playwright-test/fixture-errors.spec.ts b/tests/playwright-test/fixture-errors.spec.ts index b6a57f5999..6004f2b67e 100644 --- a/tests/playwright-test/fixture-errors.spec.ts +++ b/tests/playwright-test/fixture-errors.spec.ts @@ -500,3 +500,35 @@ test('should handle fixture teardown error after test timeout and continue', asy expect(result.output).toContain('Timeout of 100ms exceeded'); expect(result.output).toContain('Error: Oh my error'); }); + +test('should report worker fixture teardown with debug info', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + const test = pwt.test.extend({ + fixture: [ async ({ }, use) => { + await use(); + await new Promise(() => {}); + }, { scope: 'worker' } ], + }); + for (let i = 0; i < 20; i++) + test('good' + i, async ({ fixture }) => {}); + `, + }, { reporter: 'list', timeout: 1000 }); + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(20); + expect(stripAnsi(result.output)).toContain([ + 'Worker teardown error. This worker ran 20 tests, last 10 tests were:', + 'a.spec.ts:12:9 › good10', + 'a.spec.ts:12:9 › good11', + 'a.spec.ts:12:9 › good12', + 'a.spec.ts:12:9 › good13', + 'a.spec.ts:12:9 › good14', + 'a.spec.ts:12:9 › good15', + 'a.spec.ts:12:9 › good16', + 'a.spec.ts:12:9 › good17', + 'a.spec.ts:12:9 › good18', + 'a.spec.ts:12:9 › good19', + '', + 'Timeout of 1000ms exceeded in fixtures teardown.', + ].join('\n')); +});