From f474eab1e4fa7ecd26db0fc55c3906a5157764d7 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 9 Jul 2024 08:42:19 +0100 Subject: [PATCH] feat(test runner): expose FullConfig.actualWorkers This property is used by reporters, so stop plumbing it through metadata. --- docs/src/test-api/class-fullconfig.md | 8 ++++ packages/playwright/src/common/config.ts | 5 +++ .../playwright/src/common/configLoader.ts | 1 + packages/playwright/src/common/ipc.ts | 3 ++ .../playwright/src/isomorphic/teleReceiver.ts | 7 +++- packages/playwright/src/reporters/base.ts | 2 +- packages/playwright/src/reporters/blob.ts | 4 +- packages/playwright/src/reporters/merge.ts | 41 +++++++++++++++++-- .../playwright/src/reporters/teleEmitter.ts | 9 ++-- packages/playwright/src/runner/tasks.ts | 2 +- packages/playwright/types/test.d.ts | 9 ++++ tests/playwright-test/config.spec.ts | 25 +++++++++++ tests/playwright-test/reporter-blob.spec.ts | 2 +- 13 files changed, 103 insertions(+), 15 deletions(-) diff --git a/docs/src/test-api/class-fullconfig.md b/docs/src/test-api/class-fullconfig.md index e6437ab314..582bd27ce6 100644 --- a/docs/src/test-api/class-fullconfig.md +++ b/docs/src/test-api/class-fullconfig.md @@ -4,6 +4,14 @@ Resolved configuration which is accessible via [`property: TestInfo.config`] and is passed to the test reporters. To see the format of Playwright configuration file, please see [TestConfig] instead. +## property: FullConfig.actualWorkers +* since: v1.46 +- type: <[int]> + +The actual number of worker processes used for running tests. This number depends on the maximum number of workers specified in [`property: TestConfig.workers`], the list of running tests and their mode of parallel or sequential execution. + +This property is only available after tests have started running. + ## property: FullConfig.configFile * since: v1.20 - type: ?<[string]> diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index 32d00d2751..4c71ae7fbe 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -70,6 +70,7 @@ export class FullConfigInternal { this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p })); this.config = { + actualWorkers: 0, configFile: resolvedConfigFile, rootDir: pathResolve(configDir, userConfig.testDir) || configDir, forbidOnly: takeFirst(configCLIOverrides.forbidOnly, userConfig.forbidOnly, false), @@ -134,6 +135,10 @@ export class FullConfigInternal { this.config.projects = this.projects.map(p => p.project); } + setActualWorkers(actualWorkers: number) { + this.config.actualWorkers = actualWorkers; + } + private _assignUniqueProjectIds(projects: FullProjectInternal[]) { const usedNames = new Set(); for (const p of projects) { diff --git a/packages/playwright/src/common/configLoader.ts b/packages/playwright/src/common/configLoader.ts index 81ea74e8e3..607995fa70 100644 --- a/packages/playwright/src/common/configLoader.ts +++ b/packages/playwright/src/common/configLoader.ts @@ -89,6 +89,7 @@ export async function deserializeConfig(data: SerializedConfig): Promise; +export type JsonBeginParams = { actualWorkers: number }; export type JsonPattern = { s?: string; @@ -162,7 +163,7 @@ export class TeleReporterReceiver { return; } if (method === 'onBegin') { - this._onBegin(); + this._onBegin(params); return; } if (method === 'onTestBegin') { @@ -213,7 +214,8 @@ export class TeleReporterReceiver { this._mergeSuiteInto(suite, projectSuite); } - private _onBegin() { + private _onBegin(params: JsonBeginParams) { + this._config.actualWorkers = params.actualWorkers; this._reporter.onBegin?.(this._rootSuite); } @@ -578,6 +580,7 @@ export class TeleTestResult implements reporterTypes.TestResult { export type TeleFullProject = reporterTypes.FullProject; export const baseFullConfig: reporterTypes.FullConfig = { + actualWorkers: 0, forbidOnly: false, fullyParallel: false, globalSetup: null, diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index e4575a8627..2f80b106e3 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -168,7 +168,7 @@ export class BaseReporter implements ReporterV2 { } protected generateStartingMessage() { - const jobs = this.config.metadata.actualWorkers ?? this.config.workers; + const jobs = this.config.actualWorkers ?? this.config.workers; const shardDetails = this.config.shard ? `, shard ${this.config.shard.current} of ${this.config.shard.total}` : ''; if (!this.totalTestCount) return ''; diff --git a/packages/playwright/src/reporters/blob.ts b/packages/playwright/src/reporters/blob.ts index 837fe55be0..3c200a26ed 100644 --- a/packages/playwright/src/reporters/blob.ts +++ b/packages/playwright/src/reporters/blob.ts @@ -34,7 +34,7 @@ type BlobReporterOptions = { _commandHash: string; }; -export const currentBlobReportVersion = 2; +export const currentBlobReportVersion = 3; export type BlobReportMetadata = { version: number; @@ -49,7 +49,6 @@ export class BlobReporter extends TeleReporterEmitter { private readonly _attachments: { originalPath: string, zipEntryPath: string }[] = []; private readonly _options: BlobReporterOptions; private readonly _salt: string; - private _config!: FullConfig; constructor(options: BlobReporterOptions) { super(message => this._messages.push(message)); @@ -72,7 +71,6 @@ export class BlobReporter extends TeleReporterEmitter { params: metadata }); - this._config = config; super.onConfigure(config); } diff --git a/packages/playwright/src/reporters/merge.ts b/packages/playwright/src/reporters/merge.ts index 5eedcac136..e2cc962cd1 100644 --- a/packages/playwright/src/reporters/merge.ts +++ b/packages/playwright/src/reporters/merge.ts @@ -18,7 +18,7 @@ import fs from 'fs'; import path from 'path'; import type { ReporterDescription } from '../../types/test'; import type { FullConfigInternal } from '../common/config'; -import type { JsonConfig, JsonEvent, JsonFullResult, JsonLocation, JsonProject, JsonSuite, JsonTestCase, JsonTestResultEnd, JsonTestStepStart } from '../isomorphic/teleReceiver'; +import type { JsonBeginParams, JsonConfig, JsonEvent, JsonFullResult, JsonLocation, JsonProject, JsonSuite, JsonTestCase, JsonTestResultEnd, JsonTestStepStart } from '../isomorphic/teleReceiver'; import { TeleReporterReceiver } from '../isomorphic/teleReceiver'; import { JsonStringInternalizer, StringInternPool } from '../isomorphic/stringInternPool'; import { createReporters } from '../runner/reporters'; @@ -172,6 +172,7 @@ async function mergeEvents(dir: string, shardReportFiles: string[], stringPool: const configureEvents: JsonEvent[] = []; const projectEvents: JsonEvent[] = []; + const beginEvents: JsonEvent[] = []; const endEvents: JsonEvent[] = []; const blobs = await extractAndParseReports(dir, shardReportFiles, internalizer, printStatus); @@ -214,6 +215,8 @@ async function mergeEvents(dir: string, shardReportFiles: string[], stringPool: for (const event of parsedEvents) { if (event.method === 'onConfigure') configureEvents.push(event); + else if (event.method === 'onBegin') + beginEvents.push(event); else if (event.method === 'onProject') projectEvents.push(event); else if (event.method === 'onEnd') @@ -232,7 +235,7 @@ async function mergeEvents(dir: string, shardReportFiles: string[], stringPool: prologue: [ mergeConfigureEvents(configureEvents, rootDirOverride), ...projectEvents, - { method: 'onBegin', params: undefined }, + mergeBeginEvents(beginEvents), ], reports, epilogue: [ @@ -288,6 +291,20 @@ function mergeConfigureEvents(configureEvents: JsonEvent[], rootDirOverride: str }; } +function mergeBeginEvents(beginEvents: JsonEvent[]): JsonEvent { + let actualWorkers = 0; + for (const event of beginEvents) { + const params = event.params as JsonBeginParams; + actualWorkers += params.actualWorkers; + } + return { + method: 'onBegin', + params: { + actualWorkers, + } + }; +} + function mergeConfigs(to: JsonConfig, from: JsonConfig): JsonConfig { return { ...to, @@ -295,7 +312,6 @@ function mergeConfigs(to: JsonConfig, from: JsonConfig): JsonConfig { metadata: { ...to.metadata, ...from.metadata, - actualWorkers: (to.metadata.actualWorkers || 0) + (from.metadata.actualWorkers || 0), }, workers: to.workers + from.workers, }; @@ -548,6 +564,8 @@ class JsonEventPatchers { } class BlobModernizer { + private _2to3actualWorkers?: number; + modernize(fromVersion: number, events: JsonEvent[]): JsonEvent[] { const result = []; for (const event of events) @@ -577,6 +595,23 @@ class BlobModernizer { return event; }); } + + _modernize_2_to_3(events: JsonEvent[]): JsonEvent[] { + // Migrate from onConfigure.config.metadata.actualWorkers to onBegin.actualWorkers. + return events.map(event => { + if (event.method === 'onConfigure') { + const config = event.params.config as JsonConfig; + this._2to3actualWorkers = config.metadata.actualWorkers; + delete config.metadata.actualWorkers; + } + if (event.method === 'onBegin') { + event.params = event.params || {}; + if (event.params.actualWorkers === undefined && this._2to3actualWorkers !== undefined) + (event.params as JsonBeginParams).actualWorkers = this._2to3actualWorkers; + } + return event; + }); + } } const modernizer = new BlobModernizer(); diff --git a/packages/playwright/src/reporters/teleEmitter.ts b/packages/playwright/src/reporters/teleEmitter.ts index df77370dd2..19588f17cd 100644 --- a/packages/playwright/src/reporters/teleEmitter.ts +++ b/packages/playwright/src/reporters/teleEmitter.ts @@ -30,7 +30,7 @@ export type TeleReporterEmitterOptions = { export class TeleReporterEmitter implements ReporterV2 { private _messageSink: (message: teleReceiver.JsonEvent) => void; - private _rootDir!: string; + protected _config!: reporterTypes.FullConfig; private _emitterOptions: TeleReporterEmitterOptions; // In case there is blob reporter and UI mode, make sure one does override // the id assigned by the other. @@ -46,7 +46,7 @@ export class TeleReporterEmitter implements ReporterV2 { } onConfigure(config: reporterTypes.FullConfig) { - this._rootDir = config.rootDir; + this._config = config; this._messageSink({ method: 'onConfigure', params: { config: this._serializeConfig(config) } }); } @@ -54,7 +54,8 @@ export class TeleReporterEmitter implements ReporterV2 { const projects = suite.suites.map(projectSuite => this._serializeProject(projectSuite)); for (const project of projects) this._messageSink({ method: 'onProject', params: { project } }); - this._messageSink({ method: 'onBegin', params: undefined }); + const beginParams: teleReceiver.JsonBeginParams = { actualWorkers: this._config.actualWorkers }; + this._messageSink({ method: 'onBegin', params: beginParams }); } onTestBegin(test: reporterTypes.TestCase, result: reporterTypes.TestResult): void { @@ -280,6 +281,6 @@ export class TeleReporterEmitter implements ReporterV2 { private _relativePath(absolutePath?: string): string | undefined { if (!absolutePath) return absolutePath; - return path.relative(this._rootDir, absolutePath); + return path.relative(this._config.rootDir, absolutePath); } } diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts index 5a69b94e8b..634d65c833 100644 --- a/packages/playwright/src/runner/tasks.ts +++ b/packages/playwright/src/runner/tasks.ts @@ -291,7 +291,7 @@ function createPhasesTask(): Task { } } - testRun.config.config.metadata.actualWorkers = Math.min(testRun.config.config.workers, maxConcurrentTestGroups); + testRun.config.setActualWorkers(Math.min(testRun.config.config.workers, maxConcurrentTestGroups)); }, }; } diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index d9d740f000..f5d939ad68 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -1707,6 +1707,15 @@ export interface FullConfig { * See [testConfig.webServer](https://playwright.dev/docs/api/class-testconfig#test-config-web-server). */ webServer: TestConfigWebServer | null; + /** + * The actual number of worker processes used for running tests. This number depends on the maximum number of workers + * specified in [testConfig.workers](https://playwright.dev/docs/api/class-testconfig#test-config-workers), the list + * of running tests and their mode of parallel or sequential execution. + * + * This property is only available after tests have started running. + */ + actualWorkers: number; + /** * Path to the configuration file used to run the tests. The value is an empty string if no config file was used. */ diff --git a/tests/playwright-test/config.spec.ts b/tests/playwright-test/config.spec.ts index 5550d84253..c5890b32ec 100644 --- a/tests/playwright-test/config.spec.ts +++ b/tests/playwright-test/config.spec.ts @@ -661,3 +661,28 @@ test('should merge ct configs', async ({ runInlineTest }) => { }); expect(result.exitCode).toBe(0); }); + +test('should expose actualWorkers', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { workers: 42 }; + `, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + + test('pass', async ({}) => { + expect(test.info().config.actualWorkers).toBe(2); + }); + `, + 'b.test.ts': ` + import { test, expect } from '@playwright/test'; + + test('pass', async ({}) => { + expect(test.info().config.actualWorkers).toBe(2); + }); + `, + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); + expect(result.output).toContain('Running 2 tests using 2 workers'); +}); diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts index 99882777a8..fba0ae737d 100644 --- a/tests/playwright-test/reporter-blob.spec.ts +++ b/tests/playwright-test/reporter-blob.spec.ts @@ -1421,7 +1421,7 @@ test('blob report should include version', async ({ runInlineTest }) => { const events = await extractReport(test.info().outputPath('blob-report', 'report.zip'), test.info().outputPath('tmp')); const metadataEvent = events.find(e => e.method === 'onBlobReportMetadata'); - expect(metadataEvent.params.version).toBe(2); + expect(metadataEvent.params.version).toBe(3); expect(metadataEvent.params.userAgent).toBe(getUserAgent()); });