chore: write uncompressed blob report (#22945)

This commit is contained in:
Yury Semikhatsky 2023-05-10 15:08:53 -07:00 committed by GitHub
parent 759d14b881
commit c9dad439cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 42 additions and 69 deletions

View File

@ -14,12 +14,10 @@
* limitations under the License.
*/
import type { EventEmitter } from 'events';
import fs from 'fs';
import path from 'path';
import { ManualPromise, calculateSha1 } from 'playwright-core/lib/utils';
import { calculateSha1, createGuid } from 'playwright-core/lib/utils';
import { mime } from 'playwright-core/lib/utilsBundle';
import { yazl } from 'playwright-core/lib/zipBundle';
import { Readable } from 'stream';
import type { FullConfig, FullResult, TestResult } from '../../types/testReporter';
import type { Suite } from '../common/test';
@ -34,9 +32,10 @@ type BlobReporterOptions = {
export class BlobReporter extends TeleReporterEmitter {
private _messages: any[] = [];
private _options: BlobReporterOptions;
private _copyFilePromises = new Set<Promise<void>>();
private readonly _zipFile = new yazl.ZipFile();
private readonly _zipFinishPromise = new ManualPromise<undefined>();
private _outputDir!: string;
private _reportFile!: string;
constructor(options: BlobReporterOptions) {
super(message => this._messages.push(message));
@ -44,17 +43,21 @@ export class BlobReporter extends TeleReporterEmitter {
}
override onBegin(config: FullConfig<{}, {}>, suite: Suite): void {
this._outputDir = path.resolve(this._options.configDir, this._options.outputDir || 'blob-report');
fs.mkdirSync(path.join(this._outputDir, 'resources'), { recursive: true });
this._reportFile = this._computeOutputFileName(config);
super.onBegin(config, suite);
this._initializeZipFile(config);
}
override async onEnd(result: FullResult): Promise<void> {
await super.onEnd(result);
const lines = this._messages.map(m => JSON.stringify(m) + '\n');
const content = Readable.from(lines);
this._zipFile.addReadStream(content, 'report.jsonl');
this._zipFile.end();
await this._zipFinishPromise;
await Promise.all([
...this._copyFilePromises,
// Requires Node v14.18.0+
fs.promises.writeFile(this._reportFile, content as any).catch(e => console.error(`Failed to write report ${this._reportFile}: ${e}`))
]);
}
override _serializeAttachments(attachments: TestResult['attachments']): TestResult['attachments'] {
@ -64,7 +67,7 @@ export class BlobReporter extends TeleReporterEmitter {
const sha1 = calculateSha1(attachment.path);
const extension = mime.getExtension(attachment.contentType) || 'dat';
const newPath = `resources/${sha1}.${extension}`;
this._zipFile.addFile(attachment.path, newPath);
this._startCopyingFile(attachment.path, path.join(this._outputDir, newPath));
return {
...attachment,
path: newPath,
@ -72,22 +75,19 @@ export class BlobReporter extends TeleReporterEmitter {
});
}
private _initializeZipFile(config: FullConfig) {
(this._zipFile as any as EventEmitter).on('error', error => this._zipFinishPromise.reject(error));
const zipFileName = this._computeOutputFileName(config);
fs.mkdirSync(path.dirname(zipFileName), { recursive: true });
this._zipFile.outputStream.pipe(fs.createWriteStream(zipFileName)).on('close', () => {
this._zipFinishPromise.resolve(undefined);
});
}
private _computeOutputFileName(config: FullConfig) {
const outputDir = path.resolve(this._options.configDir, this._options.outputDir || '');
let shardSuffix = '';
if (config.shard) {
const paddedNumber = `${config.shard.current}`.padStart(`${config.shard.total}`.length, '0');
shardSuffix = `-${paddedNumber}-of-${config.shard.total}`;
shardSuffix = `${paddedNumber}-of-${config.shard.total}-`;
}
return path.join(outputDir, `report${shardSuffix}.zip`);
return path.join(this._outputDir, `report-${shardSuffix}${createGuid()}.jsonl`);
}
private _startCopyingFile(from: string, to: string) {
const copyPromise: Promise<void> = fs.promises.copyFile(from, to)
.catch(e => { console.error(`Failed to copy file from "${from}" to "${to}": ${e}`); })
.then(() => { this._copyFilePromises.delete(copyPromise); });
this._copyFilePromises.add(copyPromise);
}
}

View File

@ -15,9 +15,7 @@
*/
import fs from 'fs';
import os from 'os';
import path from 'path';
import { ZipFile, removeFolders } from 'playwright-core/lib/utils';
import type { ReporterDescription } from '../../types/test';
import type { FullResult } from '../../types/testReporter';
import type { FullConfigInternal } from '../common/config';
@ -27,39 +25,13 @@ import { Multiplexer } from './multiplexer';
export async function createMergedReport(config: FullConfigInternal, dir: string, reporterDescriptions: ReporterDescription[]) {
const shardFiles = await sortedShardFiles(dir);
const resourceDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-report-'));
await fs.promises.mkdir(resourceDir, { recursive: true });
try {
const shardReports = await extractReports(dir, shardFiles, resourceDir);
const events = mergeEvents(shardReports);
patchAttachmentPaths(events, resourceDir);
const events = await mergeEvents(dir, shardFiles);
patchAttachmentPaths(events, dir);
const reporters = await createReporters(config, 'merge', reporterDescriptions);
const receiver = new TeleReporterReceiver(path.sep, new Multiplexer(reporters));
for (const event of events)
await receiver.dispatch(event);
} finally {
await removeFolders([resourceDir]);
}
}
async function extractReports(dir: string, shardFiles: string[], resourceDir: string): Promise<string[]> {
const reports = [];
for (const file of shardFiles) {
const zipFile = new ZipFile(path.join(dir, file));
const entryNames = await zipFile.entries();
for (const entryName of entryNames) {
const content = await zipFile.read(entryName);
if (entryName.endsWith('report.jsonl')) {
reports.push(content.toString());
} else {
const fileName = path.join(resourceDir, entryName);
await fs.promises.mkdir(path.dirname(fileName), { recursive: true });
await fs.promises.writeFile(fileName, content);
}
}
}
return reports;
const reporters = await createReporters(config, 'merge', reporterDescriptions);
const receiver = new TeleReporterReceiver(path.sep, new Multiplexer(reporters));
for (const event of events)
await receiver.dispatch(event);
}
function patchAttachmentPaths(events: JsonEvent[], resourceDir: string) {
@ -79,11 +51,12 @@ function parseEvents(reportJsonl: string): JsonEvent[] {
return reportJsonl.toString().split('\n').filter(line => line.length).map(line => JSON.parse(line)) as JsonEvent[];
}
function mergeEvents(shardReports: string[]) {
async function mergeEvents(dir: string, shardReportFiles: string[]) {
const events: JsonEvent[] = [];
const beginEvents: JsonEvent[] = [];
const endEvents: JsonEvent[] = [];
for (const reportJsonl of shardReports) {
for (const reportFile of shardReportFiles) {
const reportJsonl = await fs.promises.readFile(path.join(dir, reportFile), 'utf8');
const parsedEvents = parseEvents(reportJsonl);
for (const event of parsedEvents) {
if (event.method === 'onBegin')
@ -160,5 +133,5 @@ function mergeEndEvents(endEvents: JsonEvent[]): JsonEvent {
async function sortedShardFiles(dir: string) {
const files = await fs.promises.readdir(dir);
return files.filter(file => file.endsWith('.zip')).sort();
return files.filter(file => file.endsWith('.jsonl')).sort();
}

View File

@ -91,7 +91,7 @@ class EchoReporter {
module.exports = EchoReporter;
`;
test('should call methods in right order', async ({ runInlineTest, mergeReports, showReport, page }) => {
test('should call methods in right order', async ({ runInlineTest, mergeReports }) => {
test.slow();
const reportDir = test.info().outputPath('blob-report');
const files = {
@ -140,7 +140,7 @@ test('should call methods in right order', async ({ runInlineTest, mergeReports,
await runInlineTest(files, { shard: `3/3` });
const reportFiles = await fs.promises.readdir(reportDir);
reportFiles.sort();
expect(reportFiles).toEqual(['report-1-of-3.zip', 'report-3-of-3.zip']);
expect(reportFiles).toEqual([expect.stringMatching(/report-1-of-3.*.jsonl/), expect.stringMatching(/report-3-of-3.*.jsonl/), 'resources']);
const { exitCode, output } = await mergeReports(reportDir, {}, { additionalArgs: ['--reporter', test.info().outputPath('echo-reporter.js')] });
expect(exitCode).toBe(0);
const lines = output.split('\n').filter(l => l.trim().length);
@ -205,7 +205,7 @@ test('should merge into html', async ({ runInlineTest, mergeReports, showReport,
await runInlineTest(files, { shard: `${i + 1}/${totalShards}` });
const reportFiles = await fs.promises.readdir(reportDir);
reportFiles.sort();
expect(reportFiles).toEqual(['report-1-of-3.zip', 'report-2-of-3.zip', 'report-3-of-3.zip']);
expect(reportFiles).toEqual([expect.stringMatching(/report-1-of-3.*.jsonl/), expect.stringMatching(/report-2-of-3.*.jsonl/), expect.stringMatching(/report-3-of-3.*.jsonl/), 'resources']);
const { exitCode, output } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] });
expect(exitCode).toBe(0);
@ -266,7 +266,7 @@ test('be able to merge incomplete shards', async ({ runInlineTest, mergeReports,
const reportFiles = await fs.promises.readdir(reportDir);
reportFiles.sort();
expect(reportFiles).toEqual(['report-1-of-3.zip', 'report-3-of-3.zip']);
expect(reportFiles).toEqual([expect.stringMatching(/report-1-of-3.*.jsonl/), expect.stringMatching(/report-3-of-3.*.jsonl/), 'resources']);
const { exitCode } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] });
expect(exitCode).toBe(0);
@ -328,7 +328,7 @@ test('merge into list report by default', async ({ runInlineTest, mergeReports }
await runInlineTest(files, { shard: `${i + 1}/${totalShards}` });
const reportFiles = await fs.promises.readdir(reportDir);
reportFiles.sort();
expect(reportFiles).toEqual(['report-1-of-3.zip', 'report-2-of-3.zip', 'report-3-of-3.zip']);
expect(reportFiles).toEqual([expect.stringMatching(/report-1-of-3.*.jsonl/), expect.stringMatching(/report-2-of-3.*.jsonl/), expect.stringMatching(/report-3-of-3.*.jsonl/), 'resources']);
const { exitCode, output } = await mergeReports(reportDir, { PW_TEST_DEBUG_REPORTERS: '1', PW_TEST_DEBUG_REPORTERS_PRINT_STEPS: '1', PWTEST_TTY_WIDTH: '80' }, { additionalArgs: ['--reporter', 'list'] });
expect(exitCode).toBe(0);
@ -400,7 +400,7 @@ test('preserve attachments', async ({ runInlineTest, mergeReports, showReport, p
const reportFiles = await fs.promises.readdir(reportDir);
reportFiles.sort();
expect(reportFiles).toEqual(['report-1-of-2.zip']);
expect(reportFiles).toEqual([expect.stringMatching(/report-1-of-2.*.jsonl/), 'resources']);
const { exitCode } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] });
expect(exitCode).toBe(0);
@ -454,7 +454,7 @@ test('multiple output reports', async ({ runInlineTest, mergeReports, showReport
const reportFiles = await fs.promises.readdir(reportDir);
reportFiles.sort();
expect(reportFiles).toEqual(['report-1-of-2.zip']);
expect(reportFiles).toEqual([expect.stringMatching(/report-1-of-2.*.jsonl/), 'resources']);
const { exitCode, output } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never', 'PW_TEST_DEBUG_REPORTERS': '1' }, { additionalArgs: ['--reporter', 'html,line'] });
expect(exitCode).toBe(0);
@ -516,7 +516,7 @@ test('multiple output reports based on config', async ({ runInlineTest, mergeRep
const reportFiles = await fs.promises.readdir(reportDir);
reportFiles.sort();
expect(reportFiles).toEqual(['report-1-of-2.zip', 'report-2-of-2.zip']);
expect(reportFiles).toEqual([expect.stringMatching(/report-1-of-2.*.jsonl/), expect.stringMatching(/report-2-of-2.*.jsonl/), 'resources']);
const { exitCode, output } = await mergeReports(reportDir, { 'PW_TEST_DEBUG_REPORTERS': '1' }, { additionalArgs: ['--config', test.info().outputPath('merged/playwright.config.ts')] });
expect(exitCode).toBe(0);
@ -530,8 +530,8 @@ test('multiple output reports based on config', async ({ runInlineTest, mergeRep
expect((await fs.promises.stat(test.info().outputPath('merged/html/index.html'))).isFile).toBeTruthy();
// Check report presence.
expect((await fs.promises.stat(test.info().outputPath('merged/merged-blob/report.zip'))).isFile).toBeTruthy();
const mergedBlobReportFiles = await fs.promises.readdir(test.info().outputPath('merged/merged-blob'));
expect(mergedBlobReportFiles).toEqual([expect.stringMatching(/report.*.jsonl/), 'resources']);
});
test('onError in the report', async ({ runInlineTest, mergeReports, showReport, page }) => {