fix: patch project names and ids when merging reports (#23295)
Project name is used in testId calculation, so patching it in the reporter is too late. Instead we now save project suffix to the blob report file and patch all project and test ids as well as project names in the report merger
This commit is contained in:
parent
6a2d07401e
commit
0ef8832f56
|
|
@ -21,6 +21,7 @@ import { mime } from 'playwright-core/lib/utilsBundle';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import type { FullConfig, FullResult, TestResult } from '../../types/testReporter';
|
import type { FullConfig, FullResult, TestResult } from '../../types/testReporter';
|
||||||
import type { Suite } from '../common/test';
|
import type { Suite } from '../common/test';
|
||||||
|
import type { JsonEvent } from '../isomorphic/teleReceiver';
|
||||||
import { TeleReporterEmitter } from './teleEmitter';
|
import { TeleReporterEmitter } from './teleEmitter';
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -29,8 +30,12 @@ type BlobReporterOptions = {
|
||||||
outputDir?: string;
|
outputDir?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type BlobReportMetadata = {
|
||||||
|
projectSuffix?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export class BlobReporter extends TeleReporterEmitter {
|
export class BlobReporter extends TeleReporterEmitter {
|
||||||
private _messages: any[] = [];
|
private _messages: JsonEvent[] = [];
|
||||||
private _options: BlobReporterOptions;
|
private _options: BlobReporterOptions;
|
||||||
private _salt: string;
|
private _salt: string;
|
||||||
private _copyFilePromises = new Set<Promise<void>>();
|
private _copyFilePromises = new Set<Promise<void>>();
|
||||||
|
|
@ -42,6 +47,13 @@ export class BlobReporter extends TeleReporterEmitter {
|
||||||
super(message => this._messages.push(message));
|
super(message => this._messages.push(message));
|
||||||
this._options = options;
|
this._options = options;
|
||||||
this._salt = createGuid();
|
this._salt = createGuid();
|
||||||
|
|
||||||
|
this._messages.push({
|
||||||
|
method: 'onBlobReportMetadata',
|
||||||
|
params: {
|
||||||
|
projectSuffix: process.env.PWTEST_BLOB_SUFFIX,
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
printsToStdio() {
|
printsToStdio() {
|
||||||
|
|
@ -66,11 +78,6 @@ export class BlobReporter extends TeleReporterEmitter {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
override _serializeProjectName(name: string): string {
|
|
||||||
const suffix = process.env.PWTEST_BLOB_SUFFIX;
|
|
||||||
return name + (suffix ? suffix : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
override _serializeAttachments(attachments: TestResult['attachments']): TestResult['attachments'] {
|
override _serializeAttachments(attachments: TestResult['attachments']): TestResult['attachments'] {
|
||||||
return attachments.map(attachment => {
|
return attachments.map(attachment => {
|
||||||
if (!attachment.path || !fs.statSync(attachment.path).isFile())
|
if (!attachment.path || !fs.statSync(attachment.path).isFile())
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ import path from 'path';
|
||||||
import type { ReporterDescription } from '../../types/test';
|
import type { 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, type JsonConfig } from '../isomorphic/teleReceiver';
|
import type { JsonConfig, JsonEvent, JsonProject, JsonSuite, JsonTestResultEnd } from '../isomorphic/teleReceiver';
|
||||||
|
import { TeleReporterReceiver } from '../isomorphic/teleReceiver';
|
||||||
import { createReporters } from '../runner/reporters';
|
import { createReporters } from '../runner/reporters';
|
||||||
import { Multiplexer } from './multiplexer';
|
import { Multiplexer } from './multiplexer';
|
||||||
|
|
||||||
|
|
@ -65,6 +66,8 @@ async function mergeEvents(dir: string, shardReportFiles: string[]) {
|
||||||
beginEvents.push(event);
|
beginEvents.push(event);
|
||||||
else if (event.method === 'onEnd')
|
else if (event.method === 'onEnd')
|
||||||
endEvents.push(event);
|
endEvents.push(event);
|
||||||
|
else if (event.method === 'onBlobReportMetadata')
|
||||||
|
new ProjectNamePatcher(event.params.projectSuffix).patchEvents(parsedEvents);
|
||||||
else
|
else
|
||||||
events.push(event);
|
events.push(event);
|
||||||
}
|
}
|
||||||
|
|
@ -153,3 +156,61 @@ async function sortedShardFiles(dir: string) {
|
||||||
const files = await fs.promises.readdir(dir);
|
const files = await fs.promises.readdir(dir);
|
||||||
return files.filter(file => file.endsWith('.jsonl')).sort();
|
return files.filter(file => file.endsWith('.jsonl')).sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ProjectNamePatcher {
|
||||||
|
constructor(private _projectNameSuffix: string) {
|
||||||
|
}
|
||||||
|
|
||||||
|
patchEvents(events: JsonEvent[]) {
|
||||||
|
if (!this._projectNameSuffix)
|
||||||
|
return;
|
||||||
|
for (const event of events) {
|
||||||
|
const { method, params } = event;
|
||||||
|
switch (method) {
|
||||||
|
case 'onBegin':
|
||||||
|
this._onBegin(params.config, params.projects);
|
||||||
|
continue;
|
||||||
|
case 'onTestBegin':
|
||||||
|
case 'onStepBegin':
|
||||||
|
case 'onStepEnd':
|
||||||
|
case 'onStdIO':
|
||||||
|
params.testId = this._mapTestId(params.testId);
|
||||||
|
continue;
|
||||||
|
case 'onTestEnd':
|
||||||
|
params.test.testId = this._mapTestId(params.test.testId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onBegin(config: JsonConfig, projects: JsonProject[]) {
|
||||||
|
for (const project of projects)
|
||||||
|
project.name += this._projectNameSuffix;
|
||||||
|
this._updateProjectIds(projects);
|
||||||
|
for (const project of projects)
|
||||||
|
project.suites.forEach(suite => this._updateTestIds(suite));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateProjectIds(projects: JsonProject[]) {
|
||||||
|
const usedNames = new Set<string>();
|
||||||
|
for (const p of projects) {
|
||||||
|
for (let i = 0; i < projects.length; ++i) {
|
||||||
|
const candidate = p.name + (i ? i : '');
|
||||||
|
if (usedNames.has(candidate))
|
||||||
|
continue;
|
||||||
|
p.id = candidate;
|
||||||
|
usedNames.add(candidate);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateTestIds(suite: JsonSuite) {
|
||||||
|
suite.tests.forEach(test => test.testId = this._mapTestId(test.testId));
|
||||||
|
suite.suites.forEach(suite => this._updateTestIds(suite));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _mapTestId(testId: string): string {
|
||||||
|
return testId + '-' + this._projectNameSuffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,32 +14,26 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { FullConfig, FullResult, Reporter, TestError, TestResult, TestStep, Location } from '../../types/testReporter';
|
|
||||||
import type { Suite, TestCase } from '../common/test';
|
|
||||||
import type { JsonConfig, JsonProject, JsonSuite, JsonTestCase, JsonTestEnd, JsonTestResultEnd, JsonTestResultStart, JsonTestStepEnd, JsonTestStepStart } from '../isomorphic/teleReceiver';
|
|
||||||
import type { SuitePrivate } from '../../types/reporterPrivate';
|
|
||||||
import { FullConfigInternal } from '../common/config';
|
|
||||||
import { createGuid } from 'playwright-core/lib/utils';
|
|
||||||
import { serializeRegexPatterns } from '../isomorphic/teleReceiver';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { FullProject } from '../../types/test';
|
import { createGuid } from 'playwright-core/lib/utils';
|
||||||
|
import type { SuitePrivate } from '../../types/reporterPrivate';
|
||||||
|
import type { FullConfig, FullResult, Location, Reporter, TestError, TestResult, TestStep } from '../../types/testReporter';
|
||||||
|
import { FullConfigInternal, FullProjectInternal } from '../common/config';
|
||||||
|
import type { Suite, TestCase } from '../common/test';
|
||||||
|
import type { JsonConfig, JsonEvent, JsonProject, JsonSuite, JsonTestCase, JsonTestEnd, JsonTestResultEnd, JsonTestResultStart, JsonTestStepEnd, JsonTestStepStart } from '../isomorphic/teleReceiver';
|
||||||
|
import { serializeRegexPatterns } from '../isomorphic/teleReceiver';
|
||||||
|
|
||||||
export class TeleReporterEmitter implements Reporter {
|
export class TeleReporterEmitter implements Reporter {
|
||||||
private _messageSink: (message: any) => void;
|
private _messageSink: (message: JsonEvent) => void;
|
||||||
private _rootDir!: string;
|
private _rootDir!: string;
|
||||||
|
|
||||||
constructor(messageSink: (message: any) => void) {
|
constructor(messageSink: (message: JsonEvent) => void) {
|
||||||
this._messageSink = messageSink;
|
this._messageSink = messageSink;
|
||||||
}
|
}
|
||||||
|
|
||||||
onBegin(config: FullConfig, suite: Suite) {
|
onBegin(config: FullConfig, suite: Suite) {
|
||||||
this._rootDir = config.rootDir;
|
this._rootDir = config.rootDir;
|
||||||
const projects: any[] = [];
|
const projects = suite.suites.map(projectSuite => this._serializeProject(projectSuite));
|
||||||
const projectIds = this._uniqueProjectIds(config.projects);
|
|
||||||
for (const projectSuite of suite.suites) {
|
|
||||||
const report = this._serializeProject(projectSuite, projectIds);
|
|
||||||
projects.push(report);
|
|
||||||
}
|
|
||||||
this._messageSink({ method: 'onBegin', params: { config: this._serializeConfig(config), projects } });
|
this._messageSink({ method: 'onBegin', params: { config: this._serializeConfig(config), projects } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,29 +132,12 @@ export class TeleReporterEmitter implements Reporter {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _uniqueProjectIds(projects: FullProject[]): Map<FullProject, string> {
|
private _serializeProject(suite: Suite): JsonProject {
|
||||||
const usedNames = new Set<string>();
|
|
||||||
const result = new Map<FullProject, string>();
|
|
||||||
for (const p of projects) {
|
|
||||||
const name = this._serializeProjectName(p.name);
|
|
||||||
for (let i = 0; i < projects.length; ++i) {
|
|
||||||
const candidate = name + (i ? i : '');
|
|
||||||
if (usedNames.has(candidate))
|
|
||||||
continue;
|
|
||||||
result.set(p, candidate);
|
|
||||||
usedNames.add(candidate);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _serializeProject(suite: Suite, projectIds: Map<FullProject, string>): JsonProject {
|
|
||||||
const project = suite.project()!;
|
const project = suite.project()!;
|
||||||
const report: JsonProject = {
|
const report: JsonProject = {
|
||||||
id: projectIds.get(project)!,
|
id: FullProjectInternal.from(project).id,
|
||||||
metadata: project.metadata,
|
metadata: project.metadata,
|
||||||
name: this._serializeProjectName(project.name),
|
name: project.name,
|
||||||
outputDir: this._relativePath(project.outputDir),
|
outputDir: this._relativePath(project.outputDir),
|
||||||
repeatEach: project.repeatEach,
|
repeatEach: project.repeatEach,
|
||||||
retries: project.retries,
|
retries: project.retries,
|
||||||
|
|
@ -180,10 +157,6 @@ export class TeleReporterEmitter implements Reporter {
|
||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
|
|
||||||
_serializeProjectName(name: string): string {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _serializeSuite(suite: Suite): JsonSuite {
|
private _serializeSuite(suite: Suite): JsonSuite {
|
||||||
const result = {
|
const result = {
|
||||||
type: suite._type,
|
type: suite._type,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue