diff --git a/packages/playwright-core/src/utils/fileUtils.ts b/packages/playwright-core/src/utils/fileUtils.ts index 473e5d333e..6a67649a25 100644 --- a/packages/playwright-core/src/utils/fileUtils.ts +++ b/packages/playwright-core/src/utils/fileUtils.ts @@ -49,3 +49,7 @@ export async function copyFileAndMakeWritable(from: string, to: string) { await fs.promises.copyFile(from, to); await fs.promises.chmod(to, 0o664); } + +export function sanitizeForFilePath(s: string) { + return s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-'); +} diff --git a/packages/playwright-test/src/matchers/toMatchSnapshot.ts b/packages/playwright-test/src/matchers/toMatchSnapshot.ts index 0cbe9b03c8..1390a03457 100644 --- a/packages/playwright-test/src/matchers/toMatchSnapshot.ts +++ b/packages/playwright-test/src/matchers/toMatchSnapshot.ts @@ -19,10 +19,10 @@ import type { Page as PageEx } from 'playwright-core/lib/client/page'; import type { Locator as LocatorEx } from 'playwright-core/lib/client/locator'; import { currentTestInfo, currentExpectTimeout } from '../common/globals'; import type { ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils'; -import { getComparator } from 'playwright-core/lib/utils'; +import { getComparator, sanitizeForFilePath } from 'playwright-core/lib/utils'; import type { PageScreenshotOptions } from 'playwright-core/types/types'; import { - addSuffixToFilePath, serializeError, sanitizeForFilePath, + addSuffixToFilePath, serializeError, trimLongString, callLogText, expectTypes } from '../util'; import { colors } from 'playwright-core/lib/utilsBundle'; diff --git a/packages/playwright-test/src/reporters/blob.ts b/packages/playwright-test/src/reporters/blob.ts index 76cdfa3507..1c665ca818 100644 --- a/packages/playwright-test/src/reporters/blob.ts +++ b/packages/playwright-test/src/reporters/blob.ts @@ -16,7 +16,7 @@ import fs from 'fs'; import path from 'path'; -import { ManualPromise, calculateSha1, createGuid, removeFolders } from 'playwright-core/lib/utils'; +import { ManualPromise, calculateSha1, createGuid, removeFolders, sanitizeForFilePath } from 'playwright-core/lib/utils'; import { mime } from 'playwright-core/lib/utilsBundle'; import { Readable } from 'stream'; import type { EventEmitter } from 'events'; @@ -44,6 +44,7 @@ export class BlobReporter extends TeleReporterEmitter { private readonly _attachments: { originalPath: string, zipEntryPath: string }[] = []; private readonly _options: BlobReporterOptions; private readonly _salt: string; + private _reportName!: string; constructor(options: BlobReporterOptions) { super(message => this._messages.push(message), false); @@ -62,6 +63,7 @@ export class BlobReporter extends TeleReporterEmitter { params: metadata }); + this._reportName = this._computeReportName(config); super.onConfigure(config); } @@ -73,16 +75,14 @@ export class BlobReporter extends TeleReporterEmitter { await removeFolders([outputDir]); await fs.promises.mkdir(outputDir, { recursive: true }); - const reportName = `report-${createGuid()}`; - const zipFile = new yazl.ZipFile(); const zipFinishPromise = new ManualPromise(); const finishPromise = zipFinishPromise.catch(e => { - throw new Error(`Failed to write report ${reportName + '.zip'}: ` + e.message); + throw new Error(`Failed to write report ${this._reportName + '.zip'}: ` + e.message); }); (zipFile as any as EventEmitter).on('error', error => zipFinishPromise.reject(error)); - const zipFileName = path.join(outputDir, reportName + '.zip'); + const zipFileName = path.join(outputDir, this._reportName + '.zip'); zipFile.outputStream.pipe(fs.createWriteStream(zipFileName)).on('close', () => { zipFinishPromise.resolve(undefined); }).on('error', error => zipFinishPromise.reject(error)); @@ -95,12 +95,23 @@ export class BlobReporter extends TeleReporterEmitter { const lines = this._messages.map(m => JSON.stringify(m) + '\n'); const content = Readable.from(lines); - zipFile.addReadStream(content, reportName + '.jsonl'); + zipFile.addReadStream(content, this._reportName + '.jsonl'); zipFile.end(); await finishPromise; } + private _computeReportName(config: FullConfig) { + let reportName = 'report'; + if (process.env.PWTEST_BLOB_SUFFIX) + reportName += sanitizeForFilePath(process.env.PWTEST_BLOB_SUFFIX); + if (config.shard) { + const paddedNumber = `${config.shard.current}`.padStart(`${config.shard.total}`.length, '0'); + reportName += `-${paddedNumber}`; + } + return reportName; + } + override _serializeAttachments(attachments: TestResult['attachments']): JsonAttachment[] { return super._serializeAttachments(attachments).map(attachment => { if (!attachment.path) diff --git a/packages/playwright-test/src/reporters/html.ts b/packages/playwright-test/src/reporters/html.ts index f40cabc8fa..381ff22f81 100644 --- a/packages/playwright-test/src/reporters/html.ts +++ b/packages/playwright-test/src/reporters/html.ts @@ -20,11 +20,11 @@ import path from 'path'; import type { TransformCallback } from 'stream'; import { Transform } from 'stream'; import type { FullConfig, Suite } from '../../types/testReporter'; -import { HttpServer, assert, calculateSha1, copyFileAndMakeWritable, gracefullyProcessExitDoNotHang, removeFolders } from 'playwright-core/lib/utils'; +import { HttpServer, assert, calculateSha1, copyFileAndMakeWritable, gracefullyProcessExitDoNotHang, removeFolders, sanitizeForFilePath } from 'playwright-core/lib/utils'; import type { JsonAttachment, JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep } from './raw'; import RawReporter from './raw'; import { stripAnsiEscapes } from './base'; -import { resolveReporterOutputPath, sanitizeForFilePath } from '../util'; +import { resolveReporterOutputPath } from '../util'; import type { Metadata } from '../../types/test'; import type { ZipFile } from 'playwright-core/lib/zipBundle'; import { yazl } from 'playwright-core/lib/zipBundle'; diff --git a/packages/playwright-test/src/reporters/merge.ts b/packages/playwright-test/src/reporters/merge.ts index 791e68d6d9..b6167099c0 100644 --- a/packages/playwright-test/src/reporters/merge.ts +++ b/packages/playwright-test/src/reporters/merge.ts @@ -229,7 +229,7 @@ function mergeEndEvents(endEvents: JsonEvent[]): JsonEvent { async function sortedShardFiles(dir: string) { const files = await fs.promises.readdir(dir); - return files.filter(file => file.startsWith('report-') && file.endsWith('.zip')).sort(); + return files.filter(file => file.startsWith('report') && file.endsWith('.zip')).sort(); } function printStatusToStdout(message: string) { diff --git a/packages/playwright-test/src/reporters/raw.ts b/packages/playwright-test/src/reporters/raw.ts index 3b8bb8e282..eb08788c7a 100644 --- a/packages/playwright-test/src/reporters/raw.ts +++ b/packages/playwright-test/src/reporters/raw.ts @@ -17,8 +17,7 @@ import fs from 'fs'; import path from 'path'; import type { FullConfig, Location, Suite, TestCase, TestResult, TestStatus, TestStep } from '../../types/testReporter'; -import { assert } from 'playwright-core/lib/utils'; -import { sanitizeForFilePath } from '../util'; +import { assert, sanitizeForFilePath } from 'playwright-core/lib/utils'; import { formatResultFailure } from './base'; import { toPosixPath, serializePatterns } from './json'; import { MultiMap } from 'playwright-core/lib/utils'; diff --git a/packages/playwright-test/src/util.ts b/packages/playwright-test/src/util.ts index 77db4a7514..fab927c2da 100644 --- a/packages/playwright-test/src/util.ts +++ b/packages/playwright-test/src/util.ts @@ -23,7 +23,7 @@ import url from 'url'; import { colors, debug, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle'; import type { TestInfoError } from './../types/test'; import type { Location } from './../types/testReporter'; -import { calculateSha1, isRegExp, isString } from 'playwright-core/lib/utils'; +import { calculateSha1, isRegExp, isString, sanitizeForFilePath } from 'playwright-core/lib/utils'; import type { RawStack } from 'playwright-core/lib/utils'; const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..'); @@ -195,10 +195,6 @@ export function expectTypes(receiver: any, types: string[], matcherName: string) } } -export function sanitizeForFilePath(s: string) { - return s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-'); -} - export function trimLongString(s: string, length = 100) { if (s.length <= length) return s; diff --git a/packages/playwright-test/src/worker/testInfo.ts b/packages/playwright-test/src/worker/testInfo.ts index 5813a51a78..e93ec5bc2b 100644 --- a/packages/playwright-test/src/worker/testInfo.ts +++ b/packages/playwright-test/src/worker/testInfo.ts @@ -16,14 +16,14 @@ import fs from 'fs'; import path from 'path'; -import { MaxTime, captureRawStack, createAfterActionTraceEventForStep, createBeforeActionTraceEventForStep, monotonicTime, zones } from 'playwright-core/lib/utils'; +import { MaxTime, captureRawStack, createAfterActionTraceEventForStep, createBeforeActionTraceEventForStep, monotonicTime, zones, sanitizeForFilePath } from 'playwright-core/lib/utils'; import type { TestInfoError, TestInfo, TestStatus, FullProject, FullConfig } from '../../types/test'; import type { AttachmentPayload, StepBeginPayload, StepEndPayload, WorkerInitParams } from '../common/ipc'; import type { TestCase } from '../common/test'; import { TimeoutManager } from './timeoutManager'; import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config'; import type { Location } from '../../types/testReporter'; -import { getContainedPath, normalizeAndSaveAttachment, sanitizeForFilePath, serializeError, trimLongString } from '../util'; +import { getContainedPath, normalizeAndSaveAttachment, serializeError, trimLongString } from '../util'; import type * as trace from '@trace/trace'; export interface TestStepInternal { diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts index 035e8b706e..a6fe8b6064 100644 --- a/tests/playwright-test/reporter-blob.spec.ts +++ b/tests/playwright-test/reporter-blob.spec.ts @@ -381,7 +381,7 @@ test('merge into list report by default', async ({ runInlineTest, mergeReports } await runInlineTest(files, { shard: `${i + 1}/${totalShards}` }, { PWTEST_BLOB_DO_NOT_REMOVE: '1' }); const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); - expect(reportFiles).toEqual([expect.stringMatching(/report-.*.zip/), expect.stringMatching(/report-.*.zip/), expect.stringMatching(/report-.*.zip/)]); + expect(reportFiles).toEqual(['report-1.zip', 'report-2.zip', 'report-3.zip']); 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); @@ -460,7 +460,7 @@ test('should print progress', async ({ runInlineTest, mergeReports }) => { await runInlineTest(files, { shard: `2/2` }, { PWTEST_BLOB_DO_NOT_REMOVE: '1' }); const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); - expect(reportFiles).toEqual([expect.stringMatching(/report-.*.zip/), expect.stringMatching(/report-.*.zip/)]); + expect(reportFiles).toEqual(['report-1.zip', 'report-2.zip']); const { exitCode, output } = await mergeReports(reportDir, { PW_TEST_HTML_REPORT_OPEN: 'never' }, { additionalArgs: ['--reporter', 'html'] }); expect(exitCode).toBe(0); @@ -511,7 +511,7 @@ test('preserve attachments', async ({ runInlineTest, mergeReports, showReport, p const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); - expect(reportFiles).toEqual([expect.stringMatching(/report-.*.zip/)]); + expect(reportFiles).toEqual(['report-1.zip']); const { exitCode } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); expect(exitCode).toBe(0); @@ -574,7 +574,7 @@ test('generate html with attachment urls', async ({ runInlineTest, mergeReports, const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); - expect(reportFiles).toEqual([expect.stringMatching(/report-.*.zip/)]); + expect(reportFiles).toEqual(['report-1.zip']); const { exitCode } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] }); expect(exitCode).toBe(0); @@ -648,7 +648,7 @@ test('resource names should not clash between runs', async ({ runInlineTest, sho const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); - expect(reportFiles).toEqual([expect.stringMatching(/report-.*.zip/), expect.stringMatching(/report-.*.zip/)]); + expect(reportFiles).toEqual(['report-1.zip', 'report-2.zip']); const { exitCode } = await mergeReports(reportDir, {}, { additionalArgs: ['--reporter', 'html'] }); expect(exitCode).toBe(0); @@ -723,7 +723,7 @@ test('multiple output reports', async ({ runInlineTest, mergeReports, showReport const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); - expect(reportFiles).toEqual([expect.stringMatching(/report-.*.zip/)]); + expect(reportFiles).toEqual(['report-1.zip']); const { exitCode, output } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html,line'] }); expect(exitCode).toBe(0); @@ -784,7 +784,7 @@ test('multiple output reports based on config', async ({ runInlineTest, mergeRep const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); - expect(reportFiles).toEqual([expect.stringMatching(/report-.*.zip/), expect.stringMatching(/report-.*.zip/)]); + expect(reportFiles).toEqual(['report-1.zip', 'report-2.zip']); const { exitCode, output } = await mergeReports(reportDir, undefined, { additionalArgs: ['--config', test.info().outputPath('merged/playwright.config.ts')] }); expect(exitCode).toBe(0); @@ -799,7 +799,7 @@ test('multiple output reports based on config', async ({ runInlineTest, mergeRep // Check report presence. const mergedBlobReportFiles = await fs.promises.readdir(test.info().outputPath('merged/merged-blob')); - expect(mergedBlobReportFiles).toEqual([expect.stringMatching(/report.*.zip/)]); + expect(mergedBlobReportFiles).toEqual(['report.zip']); }); test('onError in the report', async ({ runInlineTest, mergeReports, showReport, page }) => { @@ -930,7 +930,7 @@ test('preserve config fields', async ({ runInlineTest, mergeReports }) => { const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); - expect(reportFiles).toEqual([expect.stringMatching(/report-.*.zip/), expect.stringMatching(/report-.*.zip/)]); + expect(reportFiles).toEqual(['report-1.zip', 'report-3.zip']); const { exitCode } = await mergeReports(reportDir, {}, { additionalArgs: ['--reporter', test.info().outputPath('echo-reporter.js'), '-c', test.info().outputPath('merge.config.ts')] }); expect(exitCode).toBe(0); const json = JSON.parse(fs.readFileSync(test.info().outputPath('config.json')).toString()); @@ -1087,7 +1087,7 @@ test('preserve steps in html report', async ({ runInlineTest, mergeReports, show await runInlineTest(files); const reportFiles = await fs.promises.readdir(reportDir); reportFiles.sort(); - expect(reportFiles).toEqual([expect.stringMatching(/report-.*.zip/)]); + expect(reportFiles).toEqual(['report.zip']); // Run merger in a different directory to make sure relative paths will not be resolved // relative to the current directory. const mergeCwd = test.info().outputPath('foo'); @@ -1146,7 +1146,12 @@ test('custom project suffix', async ({ runInlineTest, mergeReports }) => { `, }; - await runInlineTest(files, undefined, { PWTEST_BLOB_SUFFIX: '-suffix' }); + await runInlineTest(files, { shard: `1/2` }, { PWTEST_BLOB_SUFFIX: '-suffix', PWTEST_BLOB_DO_NOT_REMOVE: '1' }); + await runInlineTest(files, { shard: `2/2` }, { PWTEST_BLOB_SUFFIX: '-suffix', PWTEST_BLOB_DO_NOT_REMOVE: '1' }); + + const reportFiles = await fs.promises.readdir(reportDir); + reportFiles.sort(); + expect(reportFiles).toEqual(['report-suffix-1.zip', 'report-suffix-2.zip']); const { exitCode, output } = await mergeReports(reportDir, {}, { additionalArgs: ['--reporter', test.info().outputPath('echo-reporter.js')] }); expect(exitCode).toBe(0); @@ -1235,7 +1240,7 @@ test('blob-report should include version', async ({ runInlineTest }) => { }; await runInlineTest(files); const reportFiles = await fs.promises.readdir(reportDir); - expect(reportFiles).toEqual([expect.stringMatching(/report-.*.zip/)]); + expect(reportFiles).toEqual(['report.zip']); await extractZip(test.info().outputPath('blob-report', reportFiles[0]), { dir: test.info().outputPath('blob-report') }); const reportFile = test.info().outputPath('blob-report', reportFiles[0].replace(/\.zip$/, '.jsonl'));