fix(merger): total time is sum of shard total times (#23534)
This commit is contained in:
		
							parent
							
								
									ceaa29cec1
								
							
						
					
					
						commit
						874f4525b4
					
				|  | @ -20,7 +20,7 @@ import path from 'path'; | ||||||
| import type { TransformCallback } from 'stream'; | import type { TransformCallback } from 'stream'; | ||||||
| import { Transform } from 'stream'; | import { Transform } from 'stream'; | ||||||
| import type { FullConfig, Reporter, Suite } from '../../types/testReporter'; | import type { FullConfig, Reporter, Suite } from '../../types/testReporter'; | ||||||
| import { HttpServer, assert, calculateSha1, monotonicTime, copyFileAndMakeWritable, removeFolders } from 'playwright-core/lib/utils'; | import { HttpServer, assert, calculateSha1, copyFileAndMakeWritable, removeFolders } from 'playwright-core/lib/utils'; | ||||||
| import type { JsonAttachment, JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep } from './raw'; | import type { JsonAttachment, JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep } from './raw'; | ||||||
| import RawReporter from './raw'; | import RawReporter from './raw'; | ||||||
| import { stripAnsiEscapes } from './base'; | import { stripAnsiEscapes } from './base'; | ||||||
|  | @ -49,7 +49,6 @@ type HtmlReporterOptions = { | ||||||
| class HtmlReporter implements Reporter { | class HtmlReporter implements Reporter { | ||||||
|   private config!: FullConfig; |   private config!: FullConfig; | ||||||
|   private suite!: Suite; |   private suite!: Suite; | ||||||
|   private _montonicStartTime: number = 0; |  | ||||||
|   private _options: HtmlReporterOptions; |   private _options: HtmlReporterOptions; | ||||||
|   private _outputFolder!: string; |   private _outputFolder!: string; | ||||||
|   private _attachmentsBaseURL!: string; |   private _attachmentsBaseURL!: string; | ||||||
|  | @ -65,7 +64,6 @@ class HtmlReporter implements Reporter { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onBegin(config: FullConfig, suite: Suite) { |   onBegin(config: FullConfig, suite: Suite) { | ||||||
|     this._montonicStartTime = monotonicTime(); |  | ||||||
|     this.config = config; |     this.config = config; | ||||||
|     const { outputFolder, open, attachmentsBaseURL } = this._resolveOptions(); |     const { outputFolder, open, attachmentsBaseURL } = this._resolveOptions(); | ||||||
|     this._outputFolder = outputFolder; |     this._outputFolder = outputFolder; | ||||||
|  | @ -102,7 +100,6 @@ class HtmlReporter implements Reporter { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async onEnd() { |   async onEnd() { | ||||||
|     const duration = monotonicTime() - this._montonicStartTime; |  | ||||||
|     const projectSuites = this.suite.suites; |     const projectSuites = this.suite.suites; | ||||||
|     const reports = projectSuites.map(suite => { |     const reports = projectSuites.map(suite => { | ||||||
|       const rawReporter = new RawReporter(); |       const rawReporter = new RawReporter(); | ||||||
|  | @ -111,7 +108,7 @@ class HtmlReporter implements Reporter { | ||||||
|     }); |     }); | ||||||
|     await removeFolders([this._outputFolder]); |     await removeFolders([this._outputFolder]); | ||||||
|     const builder = new HtmlBuilder(this._outputFolder, this._attachmentsBaseURL); |     const builder = new HtmlBuilder(this._outputFolder, this._attachmentsBaseURL); | ||||||
|     this._buildResult = await builder.build({ ...this.config.metadata, duration }, reports); |     this._buildResult = await builder.build(this.config.metadata, reports); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async onExit() { |   async onExit() { | ||||||
|  | @ -208,7 +205,7 @@ class HtmlBuilder { | ||||||
|     this._attachmentsBaseURL = attachmentsBaseURL; |     this._attachmentsBaseURL = attachmentsBaseURL; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async build(metadata: Metadata & { duration: number }, rawReports: JsonReport[]): Promise<{ ok: boolean, singleTestId: string | undefined }> { |   async build(metadata: Metadata, rawReports: JsonReport[]): Promise<{ ok: boolean, singleTestId: string | undefined }> { | ||||||
| 
 | 
 | ||||||
|     const data = new Map<string, { testFile: TestFile, testFileSummary: TestFileSummary }>(); |     const data = new Map<string, { testFile: TestFile, testFileSummary: TestFileSummary }>(); | ||||||
|     for (const projectJson of rawReports) { |     for (const projectJson of rawReports) { | ||||||
|  | @ -265,7 +262,7 @@ class HtmlBuilder { | ||||||
|       metadata, |       metadata, | ||||||
|       files: [...data.values()].map(e => e.testFileSummary), |       files: [...data.values()].map(e => e.testFileSummary), | ||||||
|       projectNames: rawReports.map(r => r.project.name), |       projectNames: rawReports.map(r => r.project.name), | ||||||
|       stats: { ...[...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats()), duration: metadata.duration } |       stats: { ...[...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats()), duration: metadata.totalTime } | ||||||
|     }; |     }; | ||||||
|     htmlReport.files.sort((f1, f2) => { |     htmlReport.files.sort((f1, f2) => { | ||||||
|       const w1 = f1.stats.unexpected * 1000 + f1.stats.flaky; |       const w1 = f1.stats.unexpected * 1000 + f1.stats.flaky; | ||||||
|  |  | ||||||
|  | @ -22,6 +22,7 @@ import { Suite } from '../common/test'; | ||||||
| import type { FullConfigInternal } from '../common/config'; | import type { FullConfigInternal } from '../common/config'; | ||||||
| import { Multiplexer } from './multiplexer'; | import { Multiplexer } from './multiplexer'; | ||||||
| import { prepareErrorStack, relativeFilePath } from './base'; | import { prepareErrorStack, relativeFilePath } from './base'; | ||||||
|  | import { monotonicTime } from 'playwright-core/lib/utils'; | ||||||
| 
 | 
 | ||||||
| type StdIOChunk = { | type StdIOChunk = { | ||||||
|   chunk: string | Buffer; |   chunk: string | Buffer; | ||||||
|  | @ -33,6 +34,7 @@ export class InternalReporter { | ||||||
|   private _multiplexer: Multiplexer; |   private _multiplexer: Multiplexer; | ||||||
|   private _deferred: { error?: TestError, stdout?: StdIOChunk, stderr?: StdIOChunk }[] | null = []; |   private _deferred: { error?: TestError, stdout?: StdIOChunk, stderr?: StdIOChunk }[] | null = []; | ||||||
|   private _config!: FullConfigInternal; |   private _config!: FullConfigInternal; | ||||||
|  |   private _montonicStartTime: number = 0; | ||||||
| 
 | 
 | ||||||
|   constructor(reporters: Reporter[]) { |   constructor(reporters: Reporter[]) { | ||||||
|     this._multiplexer = new Multiplexer(reporters); |     this._multiplexer = new Multiplexer(reporters); | ||||||
|  | @ -43,6 +45,7 @@ export class InternalReporter { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onBegin(config: FullConfig, suite: Suite) { |   onBegin(config: FullConfig, suite: Suite) { | ||||||
|  |     this._montonicStartTime = monotonicTime(); | ||||||
|     this._multiplexer.onBegin(config, suite); |     this._multiplexer.onBegin(config, suite); | ||||||
| 
 | 
 | ||||||
|     const deferred = this._deferred!; |     const deferred = this._deferred!; | ||||||
|  | @ -83,7 +86,9 @@ export class InternalReporter { | ||||||
|     this._multiplexer.onTestEnd(test, result); |     this._multiplexer.onTestEnd(test, result); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async onEnd() { } |   async onEnd() { | ||||||
|  |     this._config.config.metadata.totalTime = monotonicTime() - this._montonicStartTime; | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   async onExit(result: FullResult) { |   async onExit(result: FullResult) { | ||||||
|     if (this._deferred) { |     if (this._deferred) { | ||||||
|  |  | ||||||
|  | @ -85,7 +85,9 @@ function mergeBeginEvents(beginEvents: JsonEvent[]): JsonEvent { | ||||||
|     configFile: undefined, |     configFile: undefined, | ||||||
|     globalTimeout: 0, |     globalTimeout: 0, | ||||||
|     maxFailures: 0, |     maxFailures: 0, | ||||||
|     metadata: {}, |     metadata: { | ||||||
|  |       totalTime: 0, | ||||||
|  |     }, | ||||||
|     rootDir: '', |     rootDir: '', | ||||||
|     version: '', |     version: '', | ||||||
|     workers: 0, |     workers: 0, | ||||||
|  | @ -118,6 +120,7 @@ function mergeConfigs(to: JsonConfig, from: JsonConfig): JsonConfig { | ||||||
|     metadata: { |     metadata: { | ||||||
|       ...to.metadata, |       ...to.metadata, | ||||||
|       ...from.metadata, |       ...from.metadata, | ||||||
|  |       totalTime: to.metadata.totalTime + from.metadata.totalTime, | ||||||
|     }, |     }, | ||||||
|     workers: to.workers + from.workers, |     workers: to.workers + from.workers, | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  | @ -283,6 +283,51 @@ test('be able to merge incomplete shards', async ({ runInlineTest, mergeReports, | ||||||
|   await expect(page.locator('.subnav-item:has-text("Skipped") .counter')).toHaveText('2'); |   await expect(page.locator('.subnav-item:has-text("Skipped") .counter')).toHaveText('2'); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | test('total time is from test run not from merge', async ({ runInlineTest, mergeReports, showReport, page }) => { | ||||||
|  |   const reportDir = test.info().outputPath('blob-report'); | ||||||
|  |   const files = { | ||||||
|  |     'playwright.config.ts': ` | ||||||
|  |       module.exports = { | ||||||
|  |         retries: 1, | ||||||
|  |         reporter: [['blob', { outputDir: '${reportDir.replace(/\\/g, '/')}' }]] | ||||||
|  |       }; | ||||||
|  |     `,
 | ||||||
|  |     'a.test.js': ` | ||||||
|  |       import { test, expect } from '@playwright/test'; | ||||||
|  |       test('slow 1', async ({}) => { | ||||||
|  |         await new Promise(f => setTimeout(f, 2000)); | ||||||
|  |         expect(1 + 1).toBe(2); | ||||||
|  |       }); | ||||||
|  |     `,
 | ||||||
|  |     'b.test.js': ` | ||||||
|  |       import { test, expect } from '@playwright/test'; | ||||||
|  |       test('slow 1', async ({}) => { | ||||||
|  |         await new Promise(f => setTimeout(f, 1000)); | ||||||
|  |         expect(1 + 1).toBe(2); | ||||||
|  |       }); | ||||||
|  |     `,
 | ||||||
|  |   }; | ||||||
|  |   await runInlineTest(files, { shard: `1/2` }); | ||||||
|  |   await runInlineTest(files, { shard: `2/2` }); | ||||||
|  | 
 | ||||||
|  |   const { exitCode, output } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); | ||||||
|  |   expect(exitCode).toBe(0); | ||||||
|  | 
 | ||||||
|  |   expect(output).toContain('To open last HTML report run:'); | ||||||
|  |   // console.log(output);
 | ||||||
|  | 
 | ||||||
|  |   await showReport(); | ||||||
|  | 
 | ||||||
|  |   await expect(page.locator('.subnav-item:has-text("All") .counter')).toHaveText('2'); | ||||||
|  |   await expect(page.locator('.subnav-item:has-text("Passed") .counter')).toHaveText('2'); | ||||||
|  | 
 | ||||||
|  |   const durationText = await page.getByTestId('overall-duration').textContent(); | ||||||
|  |   // "Total time: 2.1s"
 | ||||||
|  |   const time = /Total time: (\d+)(\.\d+)?s/.exec(durationText); | ||||||
|  |   expect(time).toBeTruthy(); | ||||||
|  |   expect(parseInt(time[1], 10)).toBeGreaterThan(2); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| test('merge into list report by default', async ({ runInlineTest, mergeReports }) => { | test('merge into list report by default', async ({ runInlineTest, mergeReports }) => { | ||||||
|   const reportDir = test.info().outputPath('blob-report'); |   const reportDir = test.info().outputPath('blob-report'); | ||||||
|   const files = { |   const files = { | ||||||
|  | @ -830,7 +875,8 @@ test('preserve config fields', async ({ runInlineTest, mergeReports }) => { | ||||||
|   expect(json.rootDir).toBe(test.info().outputDir); |   expect(json.rootDir).toBe(test.info().outputDir); | ||||||
|   expect(json.globalTimeout).toBe(config.globalTimeout); |   expect(json.globalTimeout).toBe(config.globalTimeout); | ||||||
|   expect(json.maxFailures).toBe(config.maxFailures); |   expect(json.maxFailures).toBe(config.maxFailures); | ||||||
|   expect(json.metadata).toEqual(config.metadata); |   expect(json.metadata).toEqual(expect.objectContaining(config.metadata)); | ||||||
|  |   expect(json.metadata.totalTime).toBeTruthy(); | ||||||
|   expect(json.workers).toBe(2); |   expect(json.workers).toBe(2); | ||||||
|   expect(json.version).toBeTruthy(); |   expect(json.version).toBeTruthy(); | ||||||
|   expect(json.version).not.toEqual(test.info().config.version); |   expect(json.version).not.toEqual(test.info().config.version); | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue