Skip to content

Commit

Permalink
fix(merge): normalize path separators when merging across platforms (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
yury-s authored Nov 27, 2023
1 parent 854ae4e commit dc8ecc3
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 67 deletions.
2 changes: 2 additions & 0 deletions packages/playwright/src/reporters/blob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type BlobReportMetadata = {
userAgent: string;
name?: string;
shard?: { total: number, current: number };
pathSeparator?: string;
};

export class BlobReporter extends TeleReporterEmitter {
Expand All @@ -59,6 +60,7 @@ export class BlobReporter extends TeleReporterEmitter {
userAgent: getUserAgent(),
name: process.env.PWTEST_BLOB_REPORT_NAME,
shard: config.shard ?? undefined,
pathSeparator: path.sep,
};
this._messages.push({
method: 'onBlobReportMetadata',
Expand Down
166 changes: 125 additions & 41 deletions packages/playwright/src/reporters/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, JsonProject, JsonSuite, JsonTestResultEnd } from '../isomorphic/teleReceiver';
import type { JsonConfig, JsonEvent, JsonFullResult, JsonLocation, JsonProject, JsonSuite, JsonTestResultEnd, JsonTestStepStart } from '../isomorphic/teleReceiver';
import { TeleReporterReceiver } from '../isomorphic/teleReceiver';
import { JsonStringInternalizer, StringInternPool } from '../isomorphic/stringInternPool';
import { createReporters } from '../runner/reporters';
Expand All @@ -30,14 +30,13 @@ import { relativeFilePath } from '../util';
type StatusCallback = (message: string) => void;

type ReportData = {
idsPatcher: IdsPatcher;
eventPatchers: JsonEventPatchers;
reportFile: string;
};

export async function createMergedReport(config: FullConfigInternal, dir: string, reporterDescriptions: ReporterDescription[], rootDirOverride: string | undefined) {
const reporters = await createReporters(config, 'merge', reporterDescriptions);
const multiplexer = new Multiplexer(reporters);
const receiver = new TeleReporterReceiver(path.sep, multiplexer, false, config.config);
const stringPool = new StringInternPool();

let printStatus: StatusCallback = () => {};
Expand All @@ -50,6 +49,9 @@ export async function createMergedReport(config: FullConfigInternal, dir: string
if (shardFiles.length === 0)
throw new Error(`No report files found in ${dir}`);
const eventData = await mergeEvents(dir, shardFiles, stringPool, printStatus, rootDirOverride);
// If expicit config is provided, use platform path separator, otherwise use the one from the report (if any).
const pathSep = rootDirOverride ? path.sep : (eventData.pathSeparatorFromMetadata ?? path.sep);
const receiver = new TeleReporterReceiver(pathSep, multiplexer, false, config.config);
printStatus(`processing test events`);

const dispatchEvents = async (events: JsonEvent[]) => {
Expand All @@ -63,30 +65,17 @@ export async function createMergedReport(config: FullConfigInternal, dir: string
};

await dispatchEvents(eventData.prologue);
for (const { reportFile, idsPatcher } of eventData.reports) {
for (const { reportFile, eventPatchers } of eventData.reports) {
const reportJsonl = await fs.promises.readFile(reportFile);
const events = parseTestEvents(reportJsonl);
new JsonStringInternalizer(stringPool).traverse(events);
idsPatcher.patchEvents(events);
patchAttachmentPaths(events, dir);
eventPatchers.patchers.push(new AttachmentPathPatcher(dir));
eventPatchers.patchEvents(events);
await dispatchEvents(events);
}
await dispatchEvents(eventData.epilogue);
}

function patchAttachmentPaths(events: JsonEvent[], resourceDir: string) {
for (const event of events) {
if (event.method !== 'onTestEnd')
continue;
for (const attachment of (event.params.result as JsonTestResultEnd).attachments) {
if (!attachment.path)
continue;

attachment.path = path.join(resourceDir, attachment.path);
}
}
}

const commonEventNames = ['onBlobReportMetadata', 'onConfigure', 'onProject', 'onBegin', 'onEnd'];
const commonEvents = new Set(commonEventNames);
const commonEventRegex = new RegExp(`${commonEventNames.join('|')}`);
Expand Down Expand Up @@ -186,8 +175,12 @@ async function mergeEvents(dir: string, shardReportFiles: string[], stringPool:
salt = sha1 + '-' + i;
saltSet.add(salt);

const idsPatcher = new IdsPatcher(stringPool, metadata.name, salt);
idsPatcher.patchEvents(parsedEvents);
const eventPatchers = new JsonEventPatchers();
eventPatchers.patchers.push(new IdsPatcher(stringPool, metadata.name, salt));
// Only patch path separators if we are merging reports with explicit config.
if (rootDirOverride)
eventPatchers.patchers.push(new PathSeparatorPatcher(metadata.pathSeparator));
eventPatchers.patchEvents(parsedEvents);

for (const event of parsedEvents) {
if (event.method === 'onConfigure')
Expand All @@ -200,7 +193,7 @@ async function mergeEvents(dir: string, shardReportFiles: string[], stringPool:

// Save information about the reports to stream their test events later.
reports.push({
idsPatcher,
eventPatchers,
reportFile: localPath,
});
}
Expand All @@ -215,7 +208,8 @@ async function mergeEvents(dir: string, shardReportFiles: string[], stringPool:
epilogue: [
mergeEndEvents(endEvents),
{ method: 'onExit', params: undefined },
]
],
pathSeparatorFromMetadata: blobs[0]?.metadata.pathSeparator,
};
}

Expand Down Expand Up @@ -338,26 +332,27 @@ class UniqueFileNameGenerator {
}

class IdsPatcher {
constructor(private _stringPool: StringInternPool, private _reportName: string | undefined, private _salt: string) {
constructor(
private _stringPool: StringInternPool,
private _reportName: string | undefined,
private _salt: string) {
}

patchEvents(events: JsonEvent[]) {
for (const event of events) {
const { method, params } = event;
switch (method) {
case 'onProject':
this._onProject(params.project);
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;
}
patchEvent(event: JsonEvent) {
const { method, params } = event;
switch (method) {
case 'onProject':
this._onProject(params.project);
return;
case 'onTestBegin':
case 'onStepBegin':
case 'onStepEnd':
case 'onStdIO':
params.testId = this._mapTestId(params.testId);
return;
case 'onTestEnd':
params.test.testId = this._mapTestId(params.test.testId);
return;
}
}

Expand All @@ -377,3 +372,92 @@ class IdsPatcher {
return this._stringPool.internString(testId + this._salt);
}
}

class AttachmentPathPatcher {
constructor(private _resourceDir: string) {
}

patchEvent(event: JsonEvent) {
if (event.method !== 'onTestEnd')
return;
for (const attachment of (event.params.result as JsonTestResultEnd).attachments) {
if (!attachment.path)
continue;

attachment.path = path.join(this._resourceDir, attachment.path);
}
}
}

class PathSeparatorPatcher {
private _from: string;
private _to: string;
constructor(from?: string) {
this._from = from ?? (path.sep === '/' ? '\\' : '/');
this._to = path.sep;
}

patchEvent(jsonEvent: JsonEvent) {
if (this._from === this._to)
return;
if (jsonEvent.method === 'onProject') {
this._updateProject(jsonEvent.params.project as JsonProject);
return;
}
if (jsonEvent.method === 'onTestEnd') {
const testResult = jsonEvent.params.result as JsonTestResultEnd;
testResult.errors.forEach(error => this._updateLocation(error.location));
testResult.attachments.forEach(attachment => {
if (attachment.path)
attachment.path = this._updatePath(attachment.path);
});
return;
}
if (jsonEvent.method === 'onStepBegin') {
const step = jsonEvent.params.step as JsonTestStepStart;
this._updateLocation(step.location);
return;
}
}

private _updateProject(project: JsonProject) {
project.outputDir = this._updatePath(project.outputDir);
project.testDir = this._updatePath(project.testDir);
project.snapshotDir = this._updatePath(project.snapshotDir);
project.suites.forEach(suite => this._updateSuite(suite, true));
}

private _updateSuite(suite: JsonSuite, isFileSuite: boolean = false) {
this._updateLocation(suite.location);
if (isFileSuite)
suite.title = this._updatePath(suite.title);
for (const child of suite.suites)
this._updateSuite(child);
for (const test of suite.tests)
this._updateLocation(test.location);
}

private _updateLocation(location?: JsonLocation) {
if (location)
location.file = this._updatePath(location.file);
}

private _updatePath(text: string): string {
return text.split(this._from).join(this._to);
}
}

interface JsonEventPatcher {
patchEvent(event: JsonEvent): void;
}

class JsonEventPatchers {
readonly patchers: JsonEventPatcher[] = [];

patchEvents(events: JsonEvent[]) {
for (const event of events) {
for (const patcher of this.patchers)
patcher.patchEvent(event);
}
}
}
Loading

0 comments on commit dc8ecc3

Please sign in to comment.