2023-04-07 22:47:52 +02:00
|
|
|
/**
|
|
|
|
|
* Copyright (c) Microsoft Corporation.
|
|
|
|
|
*
|
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
|
*
|
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
*
|
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
|
* limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import fs from 'fs';
|
2023-04-26 20:48:19 +02:00
|
|
|
import path from 'path';
|
2023-07-29 00:49:31 +02:00
|
|
|
import { ManualPromise, calculateSha1, createGuid, removeFolders } from 'playwright-core/lib/utils';
|
2023-04-25 02:34:09 +02:00
|
|
|
import { mime } from 'playwright-core/lib/utilsBundle';
|
2023-04-07 22:47:52 +02:00
|
|
|
import { Readable } from 'stream';
|
2023-06-15 02:10:39 +02:00
|
|
|
import type { EventEmitter } from 'events';
|
2023-04-26 20:48:19 +02:00
|
|
|
import type { FullConfig, FullResult, TestResult } from '../../types/testReporter';
|
2023-06-09 20:52:18 +02:00
|
|
|
import type { JsonAttachment, JsonEvent } from '../isomorphic/teleReceiver';
|
2023-04-07 22:47:52 +02:00
|
|
|
import { TeleReporterEmitter } from './teleEmitter';
|
2023-06-15 02:10:39 +02:00
|
|
|
import { yazl } from 'playwright-core/lib/zipBundle';
|
2023-07-29 00:49:31 +02:00
|
|
|
import { resolveReporterOutputPath } from '../util';
|
2023-04-07 22:47:52 +02:00
|
|
|
|
|
|
|
|
type BlobReporterOptions = {
|
|
|
|
|
configDir: string;
|
|
|
|
|
outputDir?: string;
|
|
|
|
|
};
|
|
|
|
|
|
2023-05-26 08:02:23 +02:00
|
|
|
export type BlobReportMetadata = {
|
|
|
|
|
projectSuffix?: string;
|
2023-07-18 18:29:25 +02:00
|
|
|
shard?: { total: number, current: number };
|
2023-05-26 08:02:23 +02:00
|
|
|
};
|
|
|
|
|
|
2023-04-07 22:47:52 +02:00
|
|
|
export class BlobReporter extends TeleReporterEmitter {
|
2023-05-26 08:02:23 +02:00
|
|
|
private _messages: JsonEvent[] = [];
|
2023-04-07 22:47:52 +02:00
|
|
|
private _options: BlobReporterOptions;
|
2023-05-13 03:21:43 +02:00
|
|
|
private _salt: string;
|
2023-05-11 00:08:53 +02:00
|
|
|
private _copyFilePromises = new Set<Promise<void>>();
|
2023-04-25 02:34:09 +02:00
|
|
|
|
2023-07-29 00:49:31 +02:00
|
|
|
private _outputDir!: Promise<string>;
|
2023-06-15 02:10:39 +02:00
|
|
|
private _reportName!: string;
|
2023-04-07 22:47:52 +02:00
|
|
|
|
|
|
|
|
constructor(options: BlobReporterOptions) {
|
2023-06-09 20:52:18 +02:00
|
|
|
super(message => this._messages.push(message), false);
|
2023-04-07 22:47:52 +02:00
|
|
|
this._options = options;
|
2023-05-13 03:21:43 +02:00
|
|
|
this._salt = createGuid();
|
2023-04-07 22:47:52 +02:00
|
|
|
}
|
|
|
|
|
|
2023-06-30 22:36:50 +02:00
|
|
|
override onConfigure(config: FullConfig) {
|
2023-07-29 00:49:31 +02:00
|
|
|
const outputDir = resolveReporterOutputPath('blob-report', this._options.configDir, this._options.outputDir);
|
|
|
|
|
const removePromise = process.env.PWTEST_BLOB_DO_NOT_REMOVE ? Promise.resolve() : removeFolders([outputDir]);
|
|
|
|
|
this._outputDir = removePromise.then(() => fs.promises.mkdir(path.join(outputDir, 'resources'), { recursive: true })).then(() => outputDir);
|
2023-07-18 18:29:25 +02:00
|
|
|
this._reportName = `report-${createGuid()}`;
|
|
|
|
|
const metadata: BlobReportMetadata = {
|
|
|
|
|
projectSuffix: process.env.PWTEST_BLOB_SUFFIX,
|
|
|
|
|
shard: config.shard ? config.shard : undefined,
|
|
|
|
|
};
|
|
|
|
|
this._messages.push({
|
|
|
|
|
method: 'onBlobReportMetadata',
|
|
|
|
|
params: metadata
|
|
|
|
|
});
|
2023-06-30 22:36:50 +02:00
|
|
|
super.onConfigure(config);
|
2023-05-15 21:46:17 +02:00
|
|
|
}
|
|
|
|
|
|
2023-04-07 22:47:52 +02:00
|
|
|
override async onEnd(result: FullResult): Promise<void> {
|
|
|
|
|
await super.onEnd(result);
|
2023-07-29 00:49:31 +02:00
|
|
|
const outputDir = await this._outputDir;
|
2023-04-07 22:47:52 +02:00
|
|
|
const lines = this._messages.map(m => JSON.stringify(m) + '\n');
|
2023-04-25 02:34:09 +02:00
|
|
|
const content = Readable.from(lines);
|
2023-06-15 02:10:39 +02:00
|
|
|
|
|
|
|
|
const zipFile = new yazl.ZipFile();
|
|
|
|
|
const zipFinishPromise = new ManualPromise<undefined>();
|
|
|
|
|
(zipFile as any as EventEmitter).on('error', error => zipFinishPromise.reject(error));
|
2023-07-29 00:49:31 +02:00
|
|
|
const zipFileName = path.join(outputDir, this._reportName + '.zip');
|
2023-06-15 02:10:39 +02:00
|
|
|
zipFile.outputStream.pipe(fs.createWriteStream(zipFileName)).on('close', () => {
|
|
|
|
|
zipFinishPromise.resolve(undefined);
|
2023-06-30 22:36:50 +02:00
|
|
|
}).on('error', error => zipFinishPromise.reject(error));
|
2023-06-15 02:10:39 +02:00
|
|
|
zipFile.addReadStream(content, this._reportName + '.jsonl');
|
|
|
|
|
zipFile.end();
|
|
|
|
|
|
2023-05-11 00:08:53 +02:00
|
|
|
await Promise.all([
|
|
|
|
|
...this._copyFilePromises,
|
|
|
|
|
// Requires Node v14.18.0+
|
2023-06-30 22:36:50 +02:00
|
|
|
zipFinishPromise.catch(e => {
|
|
|
|
|
throw new Error(`Failed to write report ${zipFileName}: ` + e.message);
|
|
|
|
|
}),
|
2023-05-11 00:08:53 +02:00
|
|
|
]);
|
2023-04-25 02:34:09 +02:00
|
|
|
}
|
|
|
|
|
|
2023-06-09 20:52:18 +02:00
|
|
|
override _serializeAttachments(attachments: TestResult['attachments']): JsonAttachment[] {
|
|
|
|
|
return super._serializeAttachments(attachments).map(attachment => {
|
2023-06-17 06:30:55 +02:00
|
|
|
if (!attachment.path || !fs.statSync(attachment.path, { throwIfNoEntry: false })?.isFile())
|
2023-04-25 02:34:09 +02:00
|
|
|
return attachment;
|
2023-05-13 03:21:43 +02:00
|
|
|
// Add run guid to avoid clashes between shards.
|
|
|
|
|
const sha1 = calculateSha1(attachment.path + this._salt);
|
2023-04-25 02:34:09 +02:00
|
|
|
const extension = mime.getExtension(attachment.contentType) || 'dat';
|
|
|
|
|
const newPath = `resources/${sha1}.${extension}`;
|
2023-07-29 00:49:31 +02:00
|
|
|
this._startCopyingFile(attachment.path, newPath);
|
2023-04-25 02:34:09 +02:00
|
|
|
return {
|
|
|
|
|
...attachment,
|
|
|
|
|
path: newPath,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-11 00:08:53 +02:00
|
|
|
private _startCopyingFile(from: string, to: string) {
|
2023-07-29 00:49:31 +02:00
|
|
|
const copyPromise: Promise<void> = this._outputDir
|
|
|
|
|
.then(dir => fs.promises.copyFile(from, path.join(dir, to)))
|
2023-05-11 00:08:53 +02:00
|
|
|
.catch(e => { console.error(`Failed to copy file from "${from}" to "${to}": ${e}`); })
|
|
|
|
|
.then(() => { this._copyFilePromises.delete(copyPromise); });
|
|
|
|
|
this._copyFilePromises.add(copyPromise);
|
2023-04-07 22:47:52 +02:00
|
|
|
}
|
|
|
|
|
}
|