Skip to content

Commit d613b81

Browse files
authored
fix(reporters): --merge-reports to show each total run times (#7877)
1 parent b42117b commit d613b81

File tree

6 files changed

+127
-30
lines changed

6 files changed

+127
-30
lines changed

packages/vitest/src/node/core.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,8 @@ export class Vitest {
499499
throw new Error('Cannot merge reports when `--reporter=blob` is used. Remove blob reporter from the config first.')
500500
}
501501

502-
const { files, errors, coverages } = await readBlobs(this.version, directory || this.config.mergeReports, this.projects)
502+
const { files, errors, coverages, executionTimes } = await readBlobs(this.version, directory || this.config.mergeReports, this.projects)
503+
this.state.blobs = { files, errors, coverages, executionTimes }
503504

504505
await this.report('onInit', this)
505506
await this.report('onPathsCollected', files.flatMap(f => f.filepath))

packages/vitest/src/node/reporters/base.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,11 @@ export abstract class BaseReporter implements Reporter {
489489
this.log(padSummaryTitle('Duration'), formatTime(collectTime + testsTime + setupTime))
490490
}
491491
else {
492-
const executionTime = this.end - this.start
492+
const blobs = this.ctx.state.blobs
493+
494+
// Execution time is either sum of all runs of `--merge-reports` or the current run's time
495+
const executionTime = blobs?.executionTimes ? sum(blobs.executionTimes, time => time) : this.end - this.start
496+
493497
const environmentTime = sum(files, file => file.environmentLoad)
494498
const prepareTime = sum(files, file => file.prepareDuration)
495499
const transformTime = sum(this.ctx.projects, project => project.vitenode.getTotalDuration())
@@ -506,6 +510,10 @@ export abstract class BaseReporter implements Reporter {
506510
].filter(Boolean).join(', ')
507511

508512
this.log(padSummaryTitle('Duration'), formatTime(executionTime) + c.dim(` (${timers})`))
513+
514+
if (blobs?.executionTimes) {
515+
this.log(padSummaryTitle('Per blob') + blobs.executionTimes.map(time => ` ${formatTime(time)}`).join(''))
516+
}
509517
}
510518

511519
this.log()

packages/vitest/src/node/reporters/blob.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface BlobOptions {
1313
}
1414

1515
export class BlobReporter implements Reporter {
16+
start = 0
1617
ctx!: Vitest
1718
options: BlobOptions
1819

@@ -26,13 +27,16 @@ export class BlobReporter implements Reporter {
2627
}
2728

2829
this.ctx = ctx
30+
this.start = performance.now()
2931
}
3032

3133
async onFinished(
3234
files: File[] = [],
3335
errors: unknown[] = [],
3436
coverage: unknown,
3537
): Promise<void> {
38+
const executionTime = performance.now() - this.start
39+
3640
let outputFile
3741
= this.options.outputFile ?? getOutputFile(this.ctx.config, 'blob')
3842
if (!outputFile) {
@@ -56,31 +60,38 @@ export class BlobReporter implements Reporter {
5660
},
5761
)
5862

59-
const report = stringify([
63+
const report = [
6064
this.ctx.version,
6165
files,
6266
errors,
6367
modules,
6468
coverage,
65-
] satisfies MergeReport)
69+
executionTime,
70+
] satisfies MergeReport
6671

6772
const reportFile = resolve(this.ctx.config.root, outputFile)
73+
await writeBlob(report, reportFile)
6874

69-
const dir = dirname(reportFile)
70-
if (!existsSync(dir)) {
71-
await mkdir(dir, { recursive: true })
72-
}
73-
74-
await writeFile(reportFile, report, 'utf-8')
7575
this.ctx.logger.log('blob report written to', reportFile)
7676
}
7777
}
7878

79+
export async function writeBlob(content: MergeReport, filename: string): Promise<void> {
80+
const report = stringify(content)
81+
82+
const dir = dirname(filename)
83+
if (!existsSync(dir)) {
84+
await mkdir(dir, { recursive: true })
85+
}
86+
87+
await writeFile(filename, report, 'utf-8')
88+
}
89+
7990
export async function readBlobs(
8091
currentVersion: string,
8192
blobsDirectory: string,
8293
projectsArray: TestProject[],
83-
): Promise<{ files: File[]; errors: unknown[]; coverages: unknown[] }> {
94+
): Promise<MergedBlobs> {
8495
// using process.cwd() because --merge-reports can only be used in CLI
8596
const resolvedDir = resolve(process.cwd(), blobsDirectory)
8697
const blobsFiles = await readdir(resolvedDir)
@@ -93,15 +104,15 @@ export async function readBlobs(
93104
)
94105
}
95106
const content = await readFile(fullPath, 'utf-8')
96-
const [version, files, errors, moduleKeys, coverage] = parse(
107+
const [version, files, errors, moduleKeys, coverage, executionTime] = parse(
97108
content,
98109
) as MergeReport
99110
if (!version) {
100111
throw new TypeError(
101112
`vitest.mergeReports() expects all paths in "${blobsDirectory}" to be files generated by the blob reporter, but "${filename}" is not a valid blob file`,
102113
)
103114
}
104-
return { version, files, errors, moduleKeys, coverage, file: filename }
115+
return { version, files, errors, moduleKeys, coverage, file: filename, executionTime }
105116
})
106117
const blobs = await Promise.all(promises)
107118

@@ -153,20 +164,30 @@ export async function readBlobs(
153164
})
154165
const errors = blobs.flatMap(blob => blob.errors)
155166
const coverages = blobs.map(blob => blob.coverage)
167+
const executionTimes = blobs.map(blob => blob.executionTime)
156168

157169
return {
158170
files,
159171
errors,
160172
coverages,
173+
executionTimes,
161174
}
162175
}
163176

177+
export interface MergedBlobs {
178+
files: File[]
179+
errors: unknown[]
180+
coverages: unknown[]
181+
executionTimes: number[]
182+
}
183+
164184
type MergeReport = [
165185
vitestVersion: string,
166186
files: File[],
167187
errors: unknown[],
168188
modules: MergeReportModuleKeys[],
169189
coverage: unknown,
190+
executionTime: number,
170191
]
171192

172193
type SerializedModuleNode = [

packages/vitest/src/node/state.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { File, Task, TaskResultPack } from '@vitest/runner'
22
import type { UserConsoleLog } from '../types/general'
33
import type { TestProject } from './project'
4+
import type { MergedBlobs } from './reporters/blob'
45
import { createFileTask } from '@vitest/runner/utils'
56
import { TestCase, TestModule, TestSuite } from './reporters/reported-tasks'
67

@@ -20,6 +21,7 @@ export class StateManager {
2021
errorsSet: Set<unknown> = new Set()
2122
processTimeoutCauses: Set<string> = new Set()
2223
reportedTasksMap: WeakMap<Task, TestModule | TestCase | TestSuite> = new WeakMap()
24+
blobs?: MergedBlobs
2325

2426
catchError(err: unknown, type: string): void {
2527
if (isAggregateError(err)) {

test/reporters/tests/merge-reports.test.ts

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,42 @@
1+
import type { File, Test } from '@vitest/runner/types'
2+
import { rmSync } from 'node:fs'
13
import { resolve } from 'node:path'
2-
import { expect, test } from 'vitest'
4+
import { createFileTask } from '@vitest/runner/utils'
5+
import { beforeEach, expect, test } from 'vitest'
6+
import { version } from 'vitest/package.json'
7+
import { writeBlob } from 'vitest/src/node/reporters/blob.js'
38
import { runVitest } from '../../test-utils'
49

10+
// always relative to CWD because it's used only from the CLI,
11+
// so we need to correctly resolve it here
12+
const reportsDir = resolve('./fixtures/merge-reports/.vitest-reports')
13+
14+
beforeEach(() => {
15+
rmSync(reportsDir, { force: true, recursive: true })
16+
})
17+
518
test('merge reports', async () => {
619
await runVitest({
720
root: './fixtures/merge-reports',
821
include: ['first.test.ts'],
922
reporters: [['blob', { outputFile: './.vitest-reports/first-run.json' }]],
1023
})
24+
1125
await runVitest({
1226
root: './fixtures/merge-reports',
1327
include: ['second.test.ts'],
1428
reporters: [['blob', { outputFile: './.vitest-reports/second-run.json' }]],
1529
})
1630

17-
// always relative to CWD because it's used only from the CLI,
18-
// so we need to correctly resolve it here
19-
const mergeReports = resolve('./fixtures/merge-reports/.vitest-reports')
20-
2131
const { stdout: reporterDefault, stderr: stderrDefault, exitCode } = await runVitest({
2232
root: './fixtures/merge-reports',
23-
mergeReports,
33+
mergeReports: reportsDir,
2434
reporters: [['default', { isTTY: false }]],
2535
})
2636

2737
expect(exitCode).toBe(1)
2838

29-
// remove "RUN v{} path" and "Duration" because it's not stable
30-
const stdoutCheck = reporterDefault
31-
.split('\n')
32-
.slice(2, -3)
33-
.join('\n')
34-
.replace(/Start at [\w\s:]+/, 'Start at <time>')
39+
const stdoutCheck = trimReporterOutput(reporterDefault)
3540
const stderrArr = stderrDefault.split('\n')
3641
const stderrCheck = [
3742
...stderrArr.slice(4, 19),
@@ -76,9 +81,8 @@ test('merge reports', async () => {
7681
"
7782
`)
7883

79-
expect(stdoutCheck.replace(/\d+ms/g, '<time>')).toMatchInlineSnapshot(`
80-
"
81-
stdout | first.test.ts
84+
expect(stdoutCheck).toMatchInlineSnapshot(`
85+
"stdout | first.test.ts
8286
global scope
8387
8488
stdout | first.test.ts > test 1-1
@@ -105,12 +109,13 @@ test('merge reports', async () => {
105109
106110
Test Files 2 failed (2)
107111
Tests 2 failed | 3 passed (5)
108-
Start at <time>"
112+
Duration <time> (transform <time>, setup <time>, collect <time>, tests <time>, environment <time>, prepare <time>)
113+
Per blob <time> <time>"
109114
`)
110115

111116
const { stdout: reporterJson } = await runVitest({
112117
root: './fixtures/merge-reports',
113-
mergeReports,
118+
mergeReports: reportsDir,
114119
reporters: [['json', { outputFile: /** so it outputs into stdout */ null }]],
115120
})
116121

@@ -234,3 +239,62 @@ test('merge reports', async () => {
234239
}
235240
`)
236241
})
242+
243+
test('total and merged execution times are shown', async () => {
244+
for (const [_index, name] of ['first.test.ts', 'second.test.ts'].entries()) {
245+
const index = 1 + _index
246+
const file = createFileTask(
247+
resolve('./fixtures/merge-reports', name),
248+
resolve('./fixtures/merge-reports'),
249+
'',
250+
)
251+
file.tasks.push(createTest('some test', file))
252+
253+
await writeBlob(
254+
[version, [file], [], [], undefined, 1500 * index],
255+
resolve(`./fixtures/merge-reports/.vitest-reports/blob-${index}-2.json`),
256+
)
257+
}
258+
259+
const { stdout } = await runVitest({
260+
root: resolve('./fixtures/merge-reports'),
261+
mergeReports: resolve('./fixtures/merge-reports/.vitest-reports'),
262+
reporters: [['default', { isTTY: false }]],
263+
})
264+
265+
expect(stdout).toContain('✓ first.test.ts (1 test)')
266+
expect(stdout).toContain('✓ second.test.ts (1 test)')
267+
268+
expect(stdout).toContain('Duration 4.50s')
269+
expect(stdout).toContain('Per blob 1.50s 3.00s')
270+
})
271+
272+
function trimReporterOutput(report: string) {
273+
const rows = report
274+
.replace(/\d+ms/g, '<time>')
275+
.replace(/\d+\.\d+s/g, '<time>')
276+
.split('\n')
277+
278+
// Trim start and end, capture just rendered tree
279+
rows.splice(0, 1 + rows.findIndex(row => row.includes('RUN v')))
280+
rows.splice(rows.findIndex(row => row.includes('Start at')), 1)
281+
282+
return rows.join('\n').trim()
283+
}
284+
285+
function createTest(name: string, file: File): Test {
286+
file.result = { state: 'pass' }
287+
288+
return {
289+
type: 'test',
290+
name,
291+
id: `${file.id}_0`,
292+
mode: 'run',
293+
file,
294+
suite: file,
295+
timeout: 0,
296+
result: { state: 'pass' },
297+
meta: {},
298+
context: {} as any,
299+
}
300+
}

test/reporters/vitest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { defineConfig } from 'vitest/config'
22

33
export default defineConfig({
44
test: {
5+
watch: false,
56
exclude: ['node_modules', 'fixtures', 'dist'],
67
reporters: ['verbose'],
78
testTimeout: 100000,

0 commit comments

Comments
 (0)