feat(test runner): expose FullConfig.actualWorkers

This property is used by reporters, so stop plumbing it through metadata.
This commit is contained in:
Dmitry Gozman 2024-07-09 08:42:19 +01:00
parent ce2b138eeb
commit f474eab1e4
13 changed files with 103 additions and 15 deletions

View file

@ -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. 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 ## property: FullConfig.configFile
* since: v1.20 * since: v1.20
- type: ?<[string]> - type: ?<[string]>

View file

@ -70,6 +70,7 @@ export class FullConfigInternal {
this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p })); this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p }));
this.config = { this.config = {
actualWorkers: 0,
configFile: resolvedConfigFile, configFile: resolvedConfigFile,
rootDir: pathResolve(configDir, userConfig.testDir) || configDir, rootDir: pathResolve(configDir, userConfig.testDir) || configDir,
forbidOnly: takeFirst(configCLIOverrides.forbidOnly, userConfig.forbidOnly, false), forbidOnly: takeFirst(configCLIOverrides.forbidOnly, userConfig.forbidOnly, false),
@ -134,6 +135,10 @@ export class FullConfigInternal {
this.config.projects = this.projects.map(p => p.project); this.config.projects = this.projects.map(p => p.project);
} }
setActualWorkers(actualWorkers: number) {
this.config.actualWorkers = actualWorkers;
}
private _assignUniqueProjectIds(projects: FullProjectInternal[]) { private _assignUniqueProjectIds(projects: FullProjectInternal[]) {
const usedNames = new Set(); const usedNames = new Set();
for (const p of projects) { for (const p of projects) {

View file

@ -89,6 +89,7 @@ export async function deserializeConfig(data: SerializedConfig): Promise<FullCon
addToCompilationCache(data.compilationCache); addToCompilationCache(data.compilationCache);
const config = await loadConfig(data.location, data.configCLIOverrides); const config = await loadConfig(data.location, data.configCLIOverrides);
config.setActualWorkers(data.actualWorkers);
await initializeEsmLoader(); await initializeEsmLoader();
return config; return config;
} }

View file

@ -43,6 +43,8 @@ export type ConfigCLIOverrides = {
export type SerializedConfig = { export type SerializedConfig = {
location: ConfigLocation; location: ConfigLocation;
configCLIOverrides: ConfigCLIOverrides; configCLIOverrides: ConfigCLIOverrides;
// TODO: create a struct to hold this and more properties like cliProjectFilter.
actualWorkers: number;
compilationCache?: SerializedCompilationCache; compilationCache?: SerializedCompilationCache;
}; };
@ -131,6 +133,7 @@ export function serializeConfig(config: FullConfigInternal, passCompilationCache
const result: SerializedConfig = { const result: SerializedConfig = {
location: { configDir: config.configDir, resolvedConfigFile: config.config.configFile }, location: { configDir: config.configDir, resolvedConfigFile: config.config.configFile },
configCLIOverrides: config.configCLIOverrides, configCLIOverrides: config.configCLIOverrides,
actualWorkers: config.config.actualWorkers,
compilationCache: passCompilationCache ? serializeCompilationCache() : undefined, compilationCache: passCompilationCache ? serializeCompilationCache() : undefined,
}; };
return result; return result;

View file

@ -29,6 +29,7 @@ export type JsonStackFrame = { file: string, line: number, column: number };
export type JsonStdIOType = 'stdout' | 'stderr'; export type JsonStdIOType = 'stdout' | 'stderr';
export type JsonConfig = Pick<reporterTypes.FullConfig, 'configFile' | 'globalTimeout' | 'maxFailures' | 'metadata' | 'rootDir' | 'version' | 'workers'>; export type JsonConfig = Pick<reporterTypes.FullConfig, 'configFile' | 'globalTimeout' | 'maxFailures' | 'metadata' | 'rootDir' | 'version' | 'workers'>;
export type JsonBeginParams = { actualWorkers: number };
export type JsonPattern = { export type JsonPattern = {
s?: string; s?: string;
@ -162,7 +163,7 @@ export class TeleReporterReceiver {
return; return;
} }
if (method === 'onBegin') { if (method === 'onBegin') {
this._onBegin(); this._onBegin(params);
return; return;
} }
if (method === 'onTestBegin') { if (method === 'onTestBegin') {
@ -213,7 +214,8 @@ export class TeleReporterReceiver {
this._mergeSuiteInto(suite, projectSuite); this._mergeSuiteInto(suite, projectSuite);
} }
private _onBegin() { private _onBegin(params: JsonBeginParams) {
this._config.actualWorkers = params.actualWorkers;
this._reporter.onBegin?.(this._rootSuite); this._reporter.onBegin?.(this._rootSuite);
} }
@ -578,6 +580,7 @@ export class TeleTestResult implements reporterTypes.TestResult {
export type TeleFullProject = reporterTypes.FullProject; export type TeleFullProject = reporterTypes.FullProject;
export const baseFullConfig: reporterTypes.FullConfig = { export const baseFullConfig: reporterTypes.FullConfig = {
actualWorkers: 0,
forbidOnly: false, forbidOnly: false,
fullyParallel: false, fullyParallel: false,
globalSetup: null, globalSetup: null,

View file

@ -168,7 +168,7 @@ export class BaseReporter implements ReporterV2 {
} }
protected generateStartingMessage() { 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}` : ''; const shardDetails = this.config.shard ? `, shard ${this.config.shard.current} of ${this.config.shard.total}` : '';
if (!this.totalTestCount) if (!this.totalTestCount)
return ''; return '';

View file

@ -34,7 +34,7 @@ type BlobReporterOptions = {
_commandHash: string; _commandHash: string;
}; };
export const currentBlobReportVersion = 2; export const currentBlobReportVersion = 3;
export type BlobReportMetadata = { export type BlobReportMetadata = {
version: number; version: number;
@ -49,7 +49,6 @@ export class BlobReporter extends TeleReporterEmitter {
private readonly _attachments: { originalPath: string, zipEntryPath: string }[] = []; private readonly _attachments: { originalPath: string, zipEntryPath: string }[] = [];
private readonly _options: BlobReporterOptions; private readonly _options: BlobReporterOptions;
private readonly _salt: string; private readonly _salt: string;
private _config!: FullConfig;
constructor(options: BlobReporterOptions) { constructor(options: BlobReporterOptions) {
super(message => this._messages.push(message)); super(message => this._messages.push(message));
@ -72,7 +71,6 @@ export class BlobReporter extends TeleReporterEmitter {
params: metadata params: metadata
}); });
this._config = config;
super.onConfigure(config); super.onConfigure(config);
} }

View file

@ -18,7 +18,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import type { ReporterDescription } from '../../types/test'; import type { ReporterDescription } from '../../types/test';
import type { FullConfigInternal } from '../common/config'; 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 { TeleReporterReceiver } from '../isomorphic/teleReceiver';
import { JsonStringInternalizer, StringInternPool } from '../isomorphic/stringInternPool'; import { JsonStringInternalizer, StringInternPool } from '../isomorphic/stringInternPool';
import { createReporters } from '../runner/reporters'; import { createReporters } from '../runner/reporters';
@ -172,6 +172,7 @@ async function mergeEvents(dir: string, shardReportFiles: string[], stringPool:
const configureEvents: JsonEvent[] = []; const configureEvents: JsonEvent[] = [];
const projectEvents: JsonEvent[] = []; const projectEvents: JsonEvent[] = [];
const beginEvents: JsonEvent[] = [];
const endEvents: JsonEvent[] = []; const endEvents: JsonEvent[] = [];
const blobs = await extractAndParseReports(dir, shardReportFiles, internalizer, printStatus); 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) { for (const event of parsedEvents) {
if (event.method === 'onConfigure') if (event.method === 'onConfigure')
configureEvents.push(event); configureEvents.push(event);
else if (event.method === 'onBegin')
beginEvents.push(event);
else if (event.method === 'onProject') else if (event.method === 'onProject')
projectEvents.push(event); projectEvents.push(event);
else if (event.method === 'onEnd') else if (event.method === 'onEnd')
@ -232,7 +235,7 @@ async function mergeEvents(dir: string, shardReportFiles: string[], stringPool:
prologue: [ prologue: [
mergeConfigureEvents(configureEvents, rootDirOverride), mergeConfigureEvents(configureEvents, rootDirOverride),
...projectEvents, ...projectEvents,
{ method: 'onBegin', params: undefined }, mergeBeginEvents(beginEvents),
], ],
reports, reports,
epilogue: [ 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 { function mergeConfigs(to: JsonConfig, from: JsonConfig): JsonConfig {
return { return {
...to, ...to,
@ -295,7 +312,6 @@ function mergeConfigs(to: JsonConfig, from: JsonConfig): JsonConfig {
metadata: { metadata: {
...to.metadata, ...to.metadata,
...from.metadata, ...from.metadata,
actualWorkers: (to.metadata.actualWorkers || 0) + (from.metadata.actualWorkers || 0),
}, },
workers: to.workers + from.workers, workers: to.workers + from.workers,
}; };
@ -548,6 +564,8 @@ class JsonEventPatchers {
} }
class BlobModernizer { class BlobModernizer {
private _2to3actualWorkers?: number;
modernize(fromVersion: number, events: JsonEvent[]): JsonEvent[] { modernize(fromVersion: number, events: JsonEvent[]): JsonEvent[] {
const result = []; const result = [];
for (const event of events) for (const event of events)
@ -577,6 +595,23 @@ class BlobModernizer {
return event; 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(); const modernizer = new BlobModernizer();

View file

@ -30,7 +30,7 @@ export type TeleReporterEmitterOptions = {
export class TeleReporterEmitter implements ReporterV2 { export class TeleReporterEmitter implements ReporterV2 {
private _messageSink: (message: teleReceiver.JsonEvent) => void; private _messageSink: (message: teleReceiver.JsonEvent) => void;
private _rootDir!: string; protected _config!: reporterTypes.FullConfig;
private _emitterOptions: TeleReporterEmitterOptions; private _emitterOptions: TeleReporterEmitterOptions;
// In case there is blob reporter and UI mode, make sure one does override // In case there is blob reporter and UI mode, make sure one does override
// the id assigned by the other. // the id assigned by the other.
@ -46,7 +46,7 @@ export class TeleReporterEmitter implements ReporterV2 {
} }
onConfigure(config: reporterTypes.FullConfig) { onConfigure(config: reporterTypes.FullConfig) {
this._rootDir = config.rootDir; this._config = config;
this._messageSink({ method: 'onConfigure', params: { config: this._serializeConfig(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)); const projects = suite.suites.map(projectSuite => this._serializeProject(projectSuite));
for (const project of projects) for (const project of projects)
this._messageSink({ method: 'onProject', params: { project } }); 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 { onTestBegin(test: reporterTypes.TestCase, result: reporterTypes.TestResult): void {
@ -280,6 +281,6 @@ export class TeleReporterEmitter implements ReporterV2 {
private _relativePath(absolutePath?: string): string | undefined { private _relativePath(absolutePath?: string): string | undefined {
if (!absolutePath) if (!absolutePath)
return absolutePath; return absolutePath;
return path.relative(this._rootDir, absolutePath); return path.relative(this._config.rootDir, absolutePath);
} }
} }

View file

@ -291,7 +291,7 @@ function createPhasesTask(): Task<TestRun> {
} }
} }
testRun.config.config.metadata.actualWorkers = Math.min(testRun.config.config.workers, maxConcurrentTestGroups); testRun.config.setActualWorkers(Math.min(testRun.config.config.workers, maxConcurrentTestGroups));
}, },
}; };
} }

View file

@ -1707,6 +1707,15 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
* See [testConfig.webServer](https://playwright.dev/docs/api/class-testconfig#test-config-web-server). * See [testConfig.webServer](https://playwright.dev/docs/api/class-testconfig#test-config-web-server).
*/ */
webServer: TestConfigWebServer | null; 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. * Path to the configuration file used to run the tests. The value is an empty string if no config file was used.
*/ */

View file

@ -661,3 +661,28 @@ test('should merge ct configs', async ({ runInlineTest }) => {
}); });
expect(result.exitCode).toBe(0); 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');
});

View file

@ -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 events = await extractReport(test.info().outputPath('blob-report', 'report.zip'), test.info().outputPath('tmp'));
const metadataEvent = events.find(e => e.method === 'onBlobReportMetadata'); 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()); expect(metadataEvent.params.userAgent).toBe(getUserAgent());
}); });