fix(merge-reports): only change test ids when needed (#31061)

When merging blob reports test ids are patched to make sure there is no
collision when merging reports that might have overlapping test ids.
However, even if you were merging reports that had no overlapping ids,
all test ids will be modified, which is an undesirable side effect.

This PR only modify test ids when the same test id has already been used
in a previous blob report.

----

This change is also part of
https://github.com/microsoft/playwright/pull/30962
This commit is contained in:
Mathias Leppich 2024-05-30 20:29:20 +02:00 committed by GitHub
parent 6067b78f88
commit 5708148496
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 116 additions and 7 deletions

View File

@ -194,12 +194,18 @@ async function mergeEvents(dir: string, shardReportFiles: string[], stringPool:
printStatus(`merging events`);
const reports: ReportData[] = [];
const globalTestIdSet = new Set<string>();
for (let i = 0; i < blobs.length; ++i) {
// Generate unique salt for each blob.
const { parsedEvents, metadata, localPath } = blobs[i];
const eventPatchers = new JsonEventPatchers();
eventPatchers.patchers.push(new IdsPatcher(stringPool, metadata.name, String(i)));
eventPatchers.patchers.push(new IdsPatcher(
stringPool,
metadata.name,
String(i),
globalTestIdSet,
));
// Only patch path separators if we are merging reports with explicit config.
if (rootDirOverride)
eventPatchers.patchers.push(new PathSeparatorPatcher(metadata.pathSeparator));
@ -358,11 +364,20 @@ class IdsPatcher {
private _stringPool: StringInternPool;
private _botName: string | undefined;
private _salt: string;
private _testIdsMap: Map<string, string>;
private _globalTestIdSet: Set<string>;
constructor(stringPool: StringInternPool, botName: string | undefined, salt: string) {
constructor(
stringPool: StringInternPool,
botName: string | undefined,
salt: string,
globalTestIdSet: Set<string>,
) {
this._stringPool = stringPool;
this._botName = botName;
this._salt = salt;
this._testIdsMap = new Map();
this._globalTestIdSet = globalTestIdSet;
}
patchEvent(event: JsonEvent) {
@ -406,7 +421,20 @@ class IdsPatcher {
}
private _mapTestId(testId: string): string {
return this._stringPool.internString(testId + this._salt);
const t1 = this._stringPool.internString(testId);
if (this._testIdsMap.has(t1))
// already mapped
return this._testIdsMap.get(t1)!;
if (this._globalTestIdSet.has(t1)) {
// test id is used in another blob, so we need to salt it.
const t2 = this._stringPool.internString(testId + this._salt);
this._globalTestIdSet.add(t2);
this._testIdsMap.set(t1, t2);
return t2;
}
this._globalTestIdSet.add(t1);
this._testIdsMap.set(t1, t1);
return t1;
}
}
@ -551,4 +579,4 @@ class BlobModernizer {
}
}
const modernizer = new BlobModernizer();
const modernizer = new BlobModernizer();

View File

@ -31,6 +31,7 @@ export { countTimes } from '../config/commonFixtures';
type CliRunResult = {
exitCode: number,
output: string,
outputLines: string[],
};
export type RunResult = {
@ -330,7 +331,12 @@ export const test = base
cwd,
});
const { exitCode } = await testProcess.exited;
return { exitCode, output: testProcess.output.toString() };
const output = testProcess.output.toString();
return {
exitCode,
output,
outputLines: parseOutputLines(output),
};
});
},
@ -416,6 +422,10 @@ export function expectTestHelper(result: RunResult) {
};
}
function parseOutputLines(output: string): string[] {
return output.split('\n').filter(line => line.startsWith('%%')).map(line => line.substring(2).trim());
}
export function parseTestRunnerOutput(output: string) {
const summary = (re: RegExp) => {
let result = 0;
@ -436,7 +446,7 @@ export function parseTestRunnerOutput(output: string) {
const strippedOutput = stripAnsi(output);
return {
output: strippedOutput,
outputLines: strippedOutput.split('\n').filter(line => line.startsWith('%%')).map(line => line.substring(2).trim()),
outputLines: parseOutputLines(strippedOutput),
rawOutput: output,
passed,
failed,

View File

@ -36,7 +36,7 @@ const test = baseTest.extend<{
showReport: async ({ page }, use) => {
let server: HttpServer | undefined;
await use(async (reportFolder?: string) => {
reportFolder ??= test.info().outputPath('playwright-report');
reportFolder ??= test.info().outputPath('playwright-report');
server = startHtmlReportServer(reportFolder) as HttpServer;
await server.start();
await page.goto(server.urlPrefix('precise'));
@ -1813,6 +1813,77 @@ test('merge reports without --config preserves path separators', async ({ runInl
expect(output).toContain(`test title: ${'tests2' + otherSeparator + 'b.test.js'}`);
});
test('merge reports must not change test ids when there is no need to', async ({ runInlineTest, mergeReports }) => {
const files = {
'echo-test-id-reporter.js': `
export default class EchoTestIdReporter {
onTestBegin(test) {
console.log('%%' + test.id);
}
};
`,
'single-run.config.ts': `module.exports = {
reporter: [
['./echo-test-id-reporter.js'],
]
};`,
'shard-1.config.ts': `module.exports = {
fullyParallel: true,
shard: { total: 2, current: 1 },
reporter: [
['./echo-test-id-reporter.js'],
['blob', { outputDir: 'blob-report' }],
]
};`,
'shard-2.config.ts': `module.exports = {
fullyParallel: true,
shard: { total: 2, current: 2 },
reporter: [
['./echo-test-id-reporter.js'],
['blob', { outputDir: 'blob-report' }],
]
};`,
'merge.config.ts': `module.exports = {
reporter: [
['./echo-test-id-reporter.js'],
]
};`,
'a.test.js': `
import { test, expect } from '@playwright/test';
test('test 1', async ({}) => { });
test('test 2', async ({}) => { });
test('test 3', async ({}) => { });
`,
};
let testIdsFromSingleRun: string[];
let testIdsFromShard1: string[];
let testIdsFromShard2: string[];
let testIdsFromMergedReport: string[];
{
const { exitCode, outputLines } = await runInlineTest(files, { workers: 1 }, undefined, { additionalArgs: ['--config', test.info().outputPath('single-run.config.ts')] });
expect(exitCode).toBe(0);
testIdsFromSingleRun = outputLines.sort();
expect(testIdsFromSingleRun.length).toEqual(3);
}
{
const { exitCode, outputLines } = await runInlineTest(files, { workers: 1 }, {}, { additionalArgs: ['--config', test.info().outputPath('shard-1.config.ts')] });
expect(exitCode).toBe(0);
testIdsFromShard1 = outputLines.sort();
}
{
const { exitCode, outputLines } = await runInlineTest(files, { workers: 1 }, { PWTEST_BLOB_DO_NOT_REMOVE: '1' }, { additionalArgs: ['--config', test.info().outputPath('shard-2.config.ts')] });
expect(exitCode).toBe(0);
testIdsFromShard2 = outputLines.sort();
expect([...testIdsFromShard1, ...testIdsFromShard2].sort()).toEqual(testIdsFromSingleRun);
}
{
const { exitCode, outputLines } = await mergeReports(test.info().outputPath('blob-report'), undefined, { additionalArgs: ['--config', test.info().outputPath('merge.config.ts')] });
expect(exitCode).toBe(0);
testIdsFromMergedReport = outputLines.sort();
expect(testIdsFromMergedReport).toEqual(testIdsFromSingleRun);
}
});
test('TestSuite.project() should return owning project', async ({ runInlineTest, mergeReports }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29173' });
const files1 = {