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 a new issue