fix(merge): make sure testId from different blobs are unique (#24475)

Fixes a scenario where each shard runs the same setup project.

References #24451.
This commit is contained in:
Dmitry Gozman 2023-07-27 18:54:00 -07:00 committed by GitHub
parent 0c253dafc7
commit 9c70a75d48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 45 additions and 12 deletions

View File

@ -96,6 +96,7 @@ async function mergeEvents(dir: string, shardReportFiles: string[]) {
const shardB = b.metadata.shard?.current ?? 0; const shardB = b.metadata.shard?.current ?? 0;
return shardA - shardB; return shardA - shardB;
}); });
const allTestIds = new Set<string>();
for (const { parsedEvents } of shardEvents) { for (const { parsedEvents } of shardEvents) {
for (const event of parsedEvents) { for (const event of parsedEvents) {
if (event.method === 'onConfigure') if (event.method === 'onConfigure')
@ -105,7 +106,7 @@ async function mergeEvents(dir: string, shardReportFiles: string[]) {
else if (event.method === 'onEnd') else if (event.method === 'onEnd')
endEvents.push(event); endEvents.push(event);
else if (event.method === 'onBlobReportMetadata') else if (event.method === 'onBlobReportMetadata')
new ProjectNamePatcher(event.params.projectSuffix).patchEvents(parsedEvents); new ProjectNamePatcher(allTestIds, event.params.projectSuffix || '').patchEvents(parsedEvents);
else else
events.push(event); events.push(event);
} }
@ -211,12 +212,12 @@ async function sortedShardFiles(dir: string) {
} }
class ProjectNamePatcher { class ProjectNamePatcher {
constructor(private _projectNameSuffix: string) { private _testIds = new Set<string>();
constructor(private _allTestIds: Set<string>, private _projectNameSuffix: string) {
} }
patchEvents(events: JsonEvent[]) { patchEvents(events: JsonEvent[]) {
if (!this._projectNameSuffix)
return;
for (const event of events) { for (const event of events) {
const { method, params } = event; const { method, params } = event;
switch (method) { switch (method) {
@ -234,6 +235,8 @@ class ProjectNamePatcher {
continue; continue;
} }
} }
for (const testId of this._testIds)
this._allTestIds.add(testId);
} }
private _onBegin(config: JsonConfig, projects: JsonProject[]) { private _onBegin(config: JsonConfig, projects: JsonProject[]) {
@ -259,11 +262,21 @@ class ProjectNamePatcher {
} }
private _updateTestIds(suite: JsonSuite) { private _updateTestIds(suite: JsonSuite) {
suite.tests.forEach(test => test.testId = this._mapTestId(test.testId)); suite.tests.forEach(test => {
test.testId = this._mapTestId(test.testId);
this._testIds.add(test.testId);
});
suite.suites.forEach(suite => this._updateTestIds(suite)); suite.suites.forEach(suite => this._updateTestIds(suite));
} }
private _mapTestId(testId: string): string { private _mapTestId(testId: string): string {
return testId + '-' + this._projectNameSuffix; testId = testId + this._projectNameSuffix;
// Consider a setup project running on each shard. In this case we'll have
// the same testId (from setup project) in multiple blob reports.
// To avoid reporters being confused by clashing test ids, we automatically
// make them unique and produce a separate test from each blob.
while (this._allTestIds.has(testId))
testId = testId + '1';
return testId;
} }
} }

View File

@ -146,15 +146,25 @@ test('should call methods in right order', async ({ runInlineTest, mergeReports
expect(lines.filter(l => l === 'onExit').length).toBe(1); expect(lines.filter(l => l === 'onExit').length).toBe(1);
}); });
test('should merge into html', async ({ runInlineTest, mergeReports, showReport, page }) => { test('should merge into html with dependencies', async ({ runInlineTest, mergeReports, showReport, page }) => {
const reportDir = test.info().outputPath('blob-report'); const reportDir = test.info().outputPath('blob-report');
const files = { const files = {
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { module.exports = {
retries: 1, retries: 1,
reporter: [['blob', { outputDir: '${reportDir.replace(/\\/g, '/')}' }]] reporter: [['blob', { outputDir: '${reportDir.replace(/\\/g, '/')}' }]],
projects: [
{ name: 'test', dependencies: ['setup'] },
{ name: 'setup', testMatch: /.*setup.js/ },
]
}; };
`, `,
'setup.js': `
import { test as setup } from '@playwright/test';
setup('login once', async ({}) => {
await setup.step('login step', async () => {});
});
`,
'a.test.js': ` 'a.test.js': `
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
test('math 1', async ({}) => { test('math 1', async ({}) => {
@ -202,14 +212,24 @@ test('should merge into html', async ({ runInlineTest, mergeReports, showReport,
await showReport(); await showReport();
await expect(page.locator('.subnav-item:has-text("All") .counter')).toHaveText('10'); await expect(page.locator('.subnav-item:has-text("All") .counter')).toHaveText('13');
await expect(page.locator('.subnav-item:has-text("Passed") .counter')).toHaveText('3'); await expect(page.locator('.subnav-item:has-text("Passed") .counter')).toHaveText('6');
await expect(page.locator('.subnav-item:has-text("Failed") .counter')).toHaveText('2'); await expect(page.locator('.subnav-item:has-text("Failed") .counter')).toHaveText('2');
await expect(page.locator('.subnav-item:has-text("Flaky") .counter')).toHaveText('2'); await expect(page.locator('.subnav-item:has-text("Flaky") .counter')).toHaveText('2');
await expect(page.locator('.subnav-item:has-text("Skipped") .counter')).toHaveText('3'); await expect(page.locator('.subnav-item:has-text("Skipped") .counter')).toHaveText('3');
await expect(page.locator('.test-file-test .test-file-title')).toHaveText( await expect(page.locator('.test-file-test .test-file-title')).toHaveText([
['failing 1', 'flaky 1', 'math 1', 'skipped 1', 'failing 2', 'math 2', 'skipped 2', 'flaky 2', 'math 3', 'skipped 3']); 'failing 1', 'flaky 1', 'math 1', 'skipped 1',
'failing 2', 'math 2', 'skipped 2',
'flaky 2', 'math 3', 'skipped 3',
'login once', 'login once', 'login once',
]);
for (let i = 0; i < 3; i++) {
await page.getByText('login once').nth(i).click();
await expect(page.getByText('login step')).toBeVisible();
await page.goBack();
}
}); });
test('be able to merge incomplete shards', async ({ runInlineTest, mergeReports, showReport, page }) => { test('be able to merge incomplete shards', async ({ runInlineTest, mergeReports, showReport, page }) => {