diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 4647caf34..e9d4aeb02 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -499,7 +499,8 @@ export class Vitest { throw new Error('Cannot merge reports when `--reporter=blob` is used. Remove blob reporter from the config first.') } - const { files, errors, coverages } = await readBlobs(this.version, directory || this.config.mergeReports, this.projects) + const { files, errors, coverages, executionTimes } = await readBlobs(this.version, directory || this.config.mergeReports, this.projects) + this.state.blobs = { files, errors, coverages, executionTimes } await this.report('onInit', this) await this.report('onPathsCollected', files.flatMap(f => f.filepath)) diff --git a/packages/vitest/src/node/reporters/base.ts b/packages/vitest/src/node/reporters/base.ts index ae3b6d9fc..5591b0eb6 100644 --- a/packages/vitest/src/node/reporters/base.ts +++ b/packages/vitest/src/node/reporters/base.ts @@ -489,7 +489,11 @@ export abstract class BaseReporter implements Reporter { this.log(padSummaryTitle('Duration'), formatTime(collectTime + testsTime + setupTime)) } else { - const executionTime = this.end - this.start + const blobs = this.ctx.state.blobs + + // Execution time is either sum of all runs of `--merge-reports` or the current run's time + const executionTime = blobs?.executionTimes ? sum(blobs.executionTimes, time => time) : this.end - this.start + const environmentTime = sum(files, file => file.environmentLoad) const prepareTime = sum(files, file => file.prepareDuration) const transformTime = sum(this.ctx.projects, project => project.vitenode.getTotalDuration()) @@ -506,6 +510,10 @@ export abstract class BaseReporter implements Reporter { ].filter(Boolean).join(', ') this.log(padSummaryTitle('Duration'), formatTime(executionTime) + c.dim(` (${timers})`)) + + if (blobs?.executionTimes) { + this.log(padSummaryTitle('Per blob') + blobs.executionTimes.map(time => ` ${formatTime(time)}`).join('')) + } } this.log() diff --git a/packages/vitest/src/node/reporters/blob.ts b/packages/vitest/src/node/reporters/blob.ts index 6136e54c6..6a2a54ac6 100644 --- a/packages/vitest/src/node/reporters/blob.ts +++ b/packages/vitest/src/node/reporters/blob.ts @@ -13,6 +13,7 @@ export interface BlobOptions { } export class BlobReporter implements Reporter { + start = 0 ctx!: Vitest options: BlobOptions @@ -26,6 +27,7 @@ export class BlobReporter implements Reporter { } this.ctx = ctx + this.start = performance.now() } async onFinished( @@ -33,6 +35,8 @@ export class BlobReporter implements Reporter { errors: unknown[] = [], coverage: unknown, ): Promise { + const executionTime = performance.now() - this.start + let outputFile = this.options.outputFile ?? getOutputFile(this.ctx.config, 'blob') if (!outputFile) { @@ -56,31 +60,38 @@ export class BlobReporter implements Reporter { }, ) - const report = stringify([ + const report = [ this.ctx.version, files, errors, modules, coverage, - ] satisfies MergeReport) + executionTime, + ] satisfies MergeReport const reportFile = resolve(this.ctx.config.root, outputFile) + await writeBlob(report, reportFile) - const dir = dirname(reportFile) - if (!existsSync(dir)) { - await mkdir(dir, { recursive: true }) - } - - await writeFile(reportFile, report, 'utf-8') this.ctx.logger.log('blob report written to', reportFile) } } +export async function writeBlob(content: MergeReport, filename: string): Promise { + const report = stringify(content) + + const dir = dirname(filename) + if (!existsSync(dir)) { + await mkdir(dir, { recursive: true }) + } + + await writeFile(filename, report, 'utf-8') +} + export async function readBlobs( currentVersion: string, blobsDirectory: string, projectsArray: TestProject[], -): Promise<{ files: File[]; errors: unknown[]; coverages: unknown[] }> { +): Promise { // using process.cwd() because --merge-reports can only be used in CLI const resolvedDir = resolve(process.cwd(), blobsDirectory) const blobsFiles = await readdir(resolvedDir) @@ -93,7 +104,7 @@ export async function readBlobs( ) } const content = await readFile(fullPath, 'utf-8') - const [version, files, errors, moduleKeys, coverage] = parse( + const [version, files, errors, moduleKeys, coverage, executionTime] = parse( content, ) as MergeReport if (!version) { @@ -101,7 +112,7 @@ export async function readBlobs( `vitest.mergeReports() expects all paths in "${blobsDirectory}" to be files generated by the blob reporter, but "${filename}" is not a valid blob file`, ) } - return { version, files, errors, moduleKeys, coverage, file: filename } + return { version, files, errors, moduleKeys, coverage, file: filename, executionTime } }) const blobs = await Promise.all(promises) @@ -153,20 +164,30 @@ export async function readBlobs( }) const errors = blobs.flatMap(blob => blob.errors) const coverages = blobs.map(blob => blob.coverage) + const executionTimes = blobs.map(blob => blob.executionTime) return { files, errors, coverages, + executionTimes, } } +export interface MergedBlobs { + files: File[] + errors: unknown[] + coverages: unknown[] + executionTimes: number[] +} + type MergeReport = [ vitestVersion: string, files: File[], errors: unknown[], modules: MergeReportModuleKeys[], coverage: unknown, + executionTime: number, ] type SerializedModuleNode = [ diff --git a/packages/vitest/src/node/state.ts b/packages/vitest/src/node/state.ts index aba7b0359..6121cc682 100644 --- a/packages/vitest/src/node/state.ts +++ b/packages/vitest/src/node/state.ts @@ -1,6 +1,7 @@ import type { File, Task, TaskResultPack } from '@vitest/runner' import type { UserConsoleLog } from '../types/general' import type { TestProject } from './project' +import type { MergedBlobs } from './reporters/blob' import { createFileTask } from '@vitest/runner/utils' import { TestCase, TestModule, TestSuite } from './reporters/reported-tasks' @@ -20,6 +21,7 @@ export class StateManager { errorsSet: Set = new Set() processTimeoutCauses: Set = new Set() reportedTasksMap: WeakMap = new WeakMap() + blobs?: MergedBlobs catchError(err: unknown, type: string): void { if (isAggregateError(err)) { diff --git a/test/reporters/tests/merge-reports.test.ts b/test/reporters/tests/merge-reports.test.ts index bd0fab502..809d25175 100644 --- a/test/reporters/tests/merge-reports.test.ts +++ b/test/reporters/tests/merge-reports.test.ts @@ -1,37 +1,42 @@ +import type { File, Test } from '@vitest/runner/types' +import { rmSync } from 'node:fs' import { resolve } from 'node:path' -import { expect, test } from 'vitest' +import { createFileTask } from '@vitest/runner/utils' +import { beforeEach, expect, test } from 'vitest' +import { version } from 'vitest/package.json' +import { writeBlob } from 'vitest/src/node/reporters/blob.js' import { runVitest } from '../../test-utils' +// always relative to CWD because it's used only from the CLI, +// so we need to correctly resolve it here +const reportsDir = resolve('./fixtures/merge-reports/.vitest-reports') + +beforeEach(() => { + rmSync(reportsDir, { force: true, recursive: true }) +}) + test('merge reports', async () => { await runVitest({ root: './fixtures/merge-reports', include: ['first.test.ts'], reporters: [['blob', { outputFile: './.vitest-reports/first-run.json' }]], }) + await runVitest({ root: './fixtures/merge-reports', include: ['second.test.ts'], reporters: [['blob', { outputFile: './.vitest-reports/second-run.json' }]], }) - // always relative to CWD because it's used only from the CLI, - // so we need to correctly resolve it here - const mergeReports = resolve('./fixtures/merge-reports/.vitest-reports') - const { stdout: reporterDefault, stderr: stderrDefault, exitCode } = await runVitest({ root: './fixtures/merge-reports', - mergeReports, + mergeReports: reportsDir, reporters: [['default', { isTTY: false }]], }) expect(exitCode).toBe(1) - // remove "RUN v{} path" and "Duration" because it's not stable - const stdoutCheck = reporterDefault - .split('\n') - .slice(2, -3) - .join('\n') - .replace(/Start at [\w\s:]+/, 'Start at