feat: merge config properties (#23071)
This commit is contained in:
parent
37b2853b7b
commit
ed19e5403b
|
|
@ -24,13 +24,12 @@ export type JsonLocation = Location;
|
||||||
export type JsonError = string;
|
export type JsonError = string;
|
||||||
export type JsonStackFrame = { file: string, line: number, column: number };
|
export type JsonStackFrame = { file: string, line: number, column: number };
|
||||||
|
|
||||||
export type JsonConfig = {
|
export type JsonConfig = Pick<FullConfig, 'configFile' | 'globalTimeout' | 'maxFailures' | 'metadata' | 'rootDir' | 'version' | 'workers'> & {
|
||||||
rootDir: string;
|
|
||||||
configFile: string | undefined;
|
|
||||||
listOnly: boolean;
|
listOnly: boolean;
|
||||||
workers: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MergeReporterConfig = Pick<FullConfig, 'configFile' | 'reportSlowTests' | 'quiet' >;
|
||||||
|
|
||||||
export type JsonPattern = {
|
export type JsonPattern = {
|
||||||
s?: string;
|
s?: string;
|
||||||
r?: { source: string, flags: string };
|
r?: { source: string, flags: string };
|
||||||
|
|
@ -122,11 +121,13 @@ export class TeleReporterReceiver {
|
||||||
private _tests = new Map<string, TeleTestCase>();
|
private _tests = new Map<string, TeleTestCase>();
|
||||||
private _rootDir!: string;
|
private _rootDir!: string;
|
||||||
private _clearPreviousResultsWhenTestBegins: boolean = false;
|
private _clearPreviousResultsWhenTestBegins: boolean = false;
|
||||||
|
private _reportConfig: MergeReporterConfig | undefined;
|
||||||
|
|
||||||
constructor(pathSeparator: string, reporter: Reporter) {
|
constructor(pathSeparator: string, reporter: Reporter, reportConfig?: MergeReporterConfig) {
|
||||||
this._rootSuite = new TeleSuite('', 'root');
|
this._rootSuite = new TeleSuite('', 'root');
|
||||||
this._pathSeparator = pathSeparator;
|
this._pathSeparator = pathSeparator;
|
||||||
this._reporter = reporter;
|
this._reporter = reporter;
|
||||||
|
this._reportConfig = reportConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(message: JsonEvent): Promise<void> | undefined {
|
dispatch(message: JsonEvent): Promise<void> | undefined {
|
||||||
|
|
@ -282,11 +283,13 @@ export class TeleReporterReceiver {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _parseConfig(config: JsonConfig): FullConfig {
|
private _parseConfig(config: JsonConfig): FullConfig {
|
||||||
const fullConfig = baseFullConfig;
|
const result = { ...baseFullConfig, ...config };
|
||||||
fullConfig.rootDir = config.rootDir;
|
if (this._reportConfig) {
|
||||||
fullConfig.configFile = config.configFile;
|
result.configFile = this._reportConfig.configFile;
|
||||||
fullConfig.workers = config.workers;
|
result.reportSlowTests = this._reportConfig.reportSlowTests;
|
||||||
return fullConfig;
|
result.quiet = this._reportConfig.quiet;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _parseProject(project: JsonProject): TeleFullProject {
|
private _parseProject(project: JsonProject): TeleFullProject {
|
||||||
|
|
|
||||||
|
|
@ -16,21 +16,22 @@
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { ReporterDescription } from '../../types/test';
|
import type { FullConfig, ReporterDescription } from '../../types/test';
|
||||||
import type { FullResult } from '../../types/testReporter';
|
import type { FullResult } from '../../types/testReporter';
|
||||||
import type { FullConfigInternal } from '../common/config';
|
import type { FullConfigInternal } from '../common/config';
|
||||||
import { TeleReporterReceiver, type JsonEvent, type JsonProject, type JsonSuite, type JsonTestResultEnd } from '../isomorphic/teleReceiver';
|
import { TeleReporterReceiver, type JsonEvent, type JsonProject, type JsonSuite, type JsonTestResultEnd, type JsonConfig } from '../isomorphic/teleReceiver';
|
||||||
import { createReporters } from '../runner/reporters';
|
import { createReporters } from '../runner/reporters';
|
||||||
import { Multiplexer } from './multiplexer';
|
import { Multiplexer } from './multiplexer';
|
||||||
|
|
||||||
export async function createMergedReport(config: FullConfigInternal, dir: string, reporterDescriptions: ReporterDescription[], resolvePaths: boolean) {
|
export async function createMergedReport(config: FullConfigInternal, dir: string, reporterDescriptions: ReporterDescription[], resolvePaths: boolean) {
|
||||||
const shardFiles = await sortedShardFiles(dir);
|
const shardFiles = await sortedShardFiles(dir);
|
||||||
const events = await mergeEvents(dir, shardFiles);
|
const events = await mergeEvents(dir, shardFiles, config.config);
|
||||||
if (resolvePaths)
|
if (resolvePaths)
|
||||||
patchAttachmentPaths(events, dir);
|
patchAttachmentPaths(events, dir);
|
||||||
|
|
||||||
const reporters = await createReporters(config, 'merge', reporterDescriptions);
|
const reporters = await createReporters(config, 'merge', reporterDescriptions);
|
||||||
const receiver = new TeleReporterReceiver(path.sep, new Multiplexer(reporters));
|
const receiver = new TeleReporterReceiver(path.sep, new Multiplexer(reporters), config.config);
|
||||||
|
|
||||||
for (const event of events)
|
for (const event of events)
|
||||||
await receiver.dispatch(event);
|
await receiver.dispatch(event);
|
||||||
}
|
}
|
||||||
|
|
@ -52,7 +53,7 @@ function parseEvents(reportJsonl: string): JsonEvent[] {
|
||||||
return reportJsonl.toString().split('\n').filter(line => line.length).map(line => JSON.parse(line)) as JsonEvent[];
|
return reportJsonl.toString().split('\n').filter(line => line.length).map(line => JSON.parse(line)) as JsonEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mergeEvents(dir: string, shardReportFiles: string[]) {
|
async function mergeEvents(dir: string, shardReportFiles: string[], reportConfig: FullConfig) {
|
||||||
const events: JsonEvent[] = [];
|
const events: JsonEvent[] = [];
|
||||||
const beginEvents: JsonEvent[] = [];
|
const beginEvents: JsonEvent[] = [];
|
||||||
const endEvents: JsonEvent[] = [];
|
const endEvents: JsonEvent[] = [];
|
||||||
|
|
@ -68,16 +69,25 @@ async function mergeEvents(dir: string, shardReportFiles: string[]) {
|
||||||
events.push(event);
|
events.push(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [mergeBeginEvents(beginEvents), ...events, mergeEndEvents(endEvents), { method: 'onExit', params: undefined }];
|
return [mergeBeginEvents(beginEvents, reportConfig), ...events, mergeEndEvents(endEvents), { method: 'onExit', params: undefined }];
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeBeginEvents(beginEvents: JsonEvent[]): JsonEvent {
|
function mergeBeginEvents(beginEvents: JsonEvent[], reportConfig: FullConfig): JsonEvent {
|
||||||
if (!beginEvents.length)
|
if (!beginEvents.length)
|
||||||
throw new Error('No begin events found');
|
throw new Error('No begin events found');
|
||||||
const projects: JsonProject[] = [];
|
const projects: JsonProject[] = [];
|
||||||
let totalWorkers = 0;
|
let config: JsonConfig = {
|
||||||
|
configFile: undefined,
|
||||||
|
globalTimeout: 0,
|
||||||
|
maxFailures: 0,
|
||||||
|
metadata: {},
|
||||||
|
rootDir: '',
|
||||||
|
version: '',
|
||||||
|
workers: 0,
|
||||||
|
listOnly: false
|
||||||
|
};
|
||||||
for (const event of beginEvents) {
|
for (const event of beginEvents) {
|
||||||
totalWorkers += event.params.config.workers;
|
config = mergeConfigs(config, event.params.config);
|
||||||
const shardProjects: JsonProject[] = event.params.projects;
|
const shardProjects: JsonProject[] = event.params.projects;
|
||||||
for (const shardProject of shardProjects) {
|
for (const shardProject of shardProjects) {
|
||||||
const mergedProject = projects.find(p => p.id === shardProject.id);
|
const mergedProject = projects.find(p => p.id === shardProject.id);
|
||||||
|
|
@ -87,11 +97,6 @@ function mergeBeginEvents(beginEvents: JsonEvent[]): JsonEvent {
|
||||||
mergeJsonSuites(shardProject.suites, mergedProject);
|
mergeJsonSuites(shardProject.suites, mergedProject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const config = {
|
|
||||||
...beginEvents[0].params.config,
|
|
||||||
workers: totalWorkers,
|
|
||||||
shard: undefined
|
|
||||||
};
|
|
||||||
return {
|
return {
|
||||||
method: 'onBegin',
|
method: 'onBegin',
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -101,6 +106,18 @@ function mergeBeginEvents(beginEvents: JsonEvent[]): JsonEvent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeConfigs(to: JsonConfig, from: JsonConfig): JsonConfig {
|
||||||
|
return {
|
||||||
|
...to,
|
||||||
|
...from,
|
||||||
|
metadata: {
|
||||||
|
...to.metadata,
|
||||||
|
...from.metadata,
|
||||||
|
},
|
||||||
|
workers: to.workers + from.workers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function mergeJsonSuites(jsonSuites: JsonSuite[], parent: JsonSuite | JsonProject) {
|
function mergeJsonSuites(jsonSuites: JsonSuite[], parent: JsonSuite | JsonProject) {
|
||||||
for (const jsonSuite of jsonSuites) {
|
for (const jsonSuite of jsonSuites) {
|
||||||
const existingSuite = parent.suites.find(s => s.title === jsonSuite.title);
|
const existingSuite = parent.suites.find(s => s.title === jsonSuite.title);
|
||||||
|
|
|
||||||
|
|
@ -127,10 +127,15 @@ export class TeleReporterEmitter implements Reporter {
|
||||||
|
|
||||||
private _serializeConfig(config: FullConfig): JsonConfig {
|
private _serializeConfig(config: FullConfig): JsonConfig {
|
||||||
return {
|
return {
|
||||||
rootDir: config.rootDir,
|
|
||||||
configFile: this._relativePath(config.configFile),
|
configFile: this._relativePath(config.configFile),
|
||||||
listOnly: FullConfigInternal.from(config)?.cliListOnly,
|
globalTimeout: config.globalTimeout,
|
||||||
|
maxFailures: config.maxFailures,
|
||||||
|
metadata: config.metadata,
|
||||||
|
rootDir: config.rootDir,
|
||||||
|
version: config.version,
|
||||||
workers: config.workers,
|
workers: config.workers,
|
||||||
|
|
||||||
|
listOnly: FullConfigInternal.from(config)?.cliListOnly,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import type { HttpServer } from '../../packages/playwright-core/src/utils';
|
||||||
import { startHtmlReportServer } from '../../packages/playwright-test/lib/reporters/html';
|
import { startHtmlReportServer } from '../../packages/playwright-test/lib/reporters/html';
|
||||||
import { type CliRunResult, type RunOptions, stripAnsi } from './playwright-test-fixtures';
|
import { type CliRunResult, type RunOptions, stripAnsi } from './playwright-test-fixtures';
|
||||||
import { cleanEnv, cliEntrypoint, expect, test as baseTest } from './playwright-test-fixtures';
|
import { cleanEnv, cliEntrypoint, expect, test as baseTest } from './playwright-test-fixtures';
|
||||||
|
import type { PlaywrightTestConfig } from 'packages/playwright-test';
|
||||||
|
|
||||||
const DOES_NOT_SUPPORT_UTF8_IN_TERMINAL = process.platform === 'win32' && process.env.TERM_PROGRAM !== 'vscode' && !process.env.WT_SESSION;
|
const DOES_NOT_SUPPORT_UTF8_IN_TERMINAL = process.platform === 'win32' && process.env.TERM_PROGRAM !== 'vscode' && !process.env.WT_SESSION;
|
||||||
const POSITIVE_STATUS_MARK = DOES_NOT_SUPPORT_UTF8_IN_TERMINAL ? 'ok' : '✓ ';
|
const POSITIVE_STATUS_MARK = DOES_NOT_SUPPORT_UTF8_IN_TERMINAL ? 'ok' : '✓ ';
|
||||||
|
|
@ -759,3 +760,90 @@ test('onError in the report', async ({ runInlineTest, mergeReports, showReport,
|
||||||
await expect(page.locator('.subnav-item:has-text("Flaky") .counter')).toHaveText('0');
|
await expect(page.locator('.subnav-item:has-text("Flaky") .counter')).toHaveText('0');
|
||||||
await expect(page.locator('.subnav-item:has-text("Skipped") .counter')).toHaveText('1');
|
await expect(page.locator('.subnav-item:has-text("Skipped") .counter')).toHaveText('1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('preserve config fields', async ({ runInlineTest, mergeReports }) => {
|
||||||
|
test.slow();
|
||||||
|
const reportDir = test.info().outputPath('blob-report');
|
||||||
|
const config: PlaywrightTestConfig = {
|
||||||
|
// Runner options:
|
||||||
|
globalTimeout: 202300,
|
||||||
|
maxFailures: 3,
|
||||||
|
metadata: {
|
||||||
|
'a': 'b',
|
||||||
|
'b': 100,
|
||||||
|
},
|
||||||
|
workers: 1,
|
||||||
|
retries: 1,
|
||||||
|
reporter: [['blob', { outputDir: `${reportDir.replace(/\\/g, '/')}` }]],
|
||||||
|
// Reporter options:
|
||||||
|
reportSlowTests: {
|
||||||
|
max: 7,
|
||||||
|
threshold: 15_000,
|
||||||
|
},
|
||||||
|
quiet: true
|
||||||
|
};
|
||||||
|
const files = {
|
||||||
|
'echo-reporter.js': `
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
class EchoReporter {
|
||||||
|
onBegin(config, suite) {
|
||||||
|
fs.writeFileSync('config.json', JSON.stringify(config));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = EchoReporter;
|
||||||
|
`,
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = ${JSON.stringify(config, null, 2)};
|
||||||
|
`,
|
||||||
|
'a.test.js': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('math 1', async ({}) => {
|
||||||
|
expect(1 + 1).toBe(2);
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
'b.test.js': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('math 2', async ({}) => {
|
||||||
|
expect(1 + 1).toBe(2);
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
'c.test.js': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('math 3', async ({}) => {
|
||||||
|
expect(1 + 1).toBe(2);
|
||||||
|
});
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
await runInlineTest(files, { shard: `1/3` });
|
||||||
|
await runInlineTest(files, { shard: `3/3` });
|
||||||
|
|
||||||
|
const mergeConfig = {
|
||||||
|
reportSlowTests: {
|
||||||
|
max: 2,
|
||||||
|
threshold: 1_000,
|
||||||
|
},
|
||||||
|
quiet: false
|
||||||
|
};
|
||||||
|
await fs.promises.writeFile(test.info().outputPath('merge.config.ts'), `module.exports = ${JSON.stringify(mergeConfig, null, 2)};`);
|
||||||
|
|
||||||
|
const reportFiles = await fs.promises.readdir(reportDir);
|
||||||
|
reportFiles.sort();
|
||||||
|
expect(reportFiles).toEqual([expect.stringMatching(/report-1-of-3.*.jsonl/), expect.stringMatching(/report-3-of-3.*.jsonl/), 'resources']);
|
||||||
|
const { exitCode } = await mergeReports(reportDir, {}, { additionalArgs: ['--reporter', test.info().outputPath('echo-reporter.js'), '-c', test.info().outputPath('merge.config.ts')] });
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
const json = JSON.parse(fs.readFileSync(test.info().outputPath('config.json')).toString());
|
||||||
|
// Test shard parameters.
|
||||||
|
expect(json.rootDir).toBe(test.info().outputDir);
|
||||||
|
expect(json.globalTimeout).toBe(config.globalTimeout);
|
||||||
|
expect(json.maxFailures).toBe(config.maxFailures);
|
||||||
|
expect(json.metadata).toEqual(config.metadata);
|
||||||
|
expect(json.workers).toBe(2);
|
||||||
|
expect(json.version).toBeTruthy();
|
||||||
|
expect(json.version).not.toEqual(test.info().config.version);
|
||||||
|
// Reporter config parameters.
|
||||||
|
expect(json.reportSlowTests).toEqual(mergeConfig.reportSlowTests);
|
||||||
|
expect(json.configFile).toEqual(test.info().outputPath('merge.config.ts'));
|
||||||
|
expect(json.quiet).toEqual(mergeConfig.quiet);
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue