Skip to content

Commit e910458

Browse files
committed
feat: --merge-reports to support coverage
1 parent 1ec61ce commit e910458

File tree

5 files changed

+102
-15
lines changed

5 files changed

+102
-15
lines changed

packages/coverage-istanbul/src/provider.ts

+40-6
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,18 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
202202
coverageMap.merge(await transformCoverage(uncoveredCoverage))
203203
}
204204

205+
await this.generateReports(coverageMap, allTestsRun)
206+
207+
// In watch mode we need to preserve the previous results if cleanOnRerun is disabled
208+
const keepResults = !this.options.cleanOnRerun && this.ctx.config.watch
209+
210+
if (!keepResults) {
211+
this.coverageFiles = new Map()
212+
await fs.rm(this.coverageFilesDirectory, { recursive: true })
213+
}
214+
}
215+
216+
async generateReports(coverageMap: CoverageMap, allTestsRun: boolean | undefined) {
205217
const context = libReport.createContext({
206218
dir: this.options.reportsDirectory,
207219
coverageMap,
@@ -248,17 +260,39 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
248260
})
249261
}
250262
}
263+
}
251264

252-
// In watch mode we need to preserve the previous results if cleanOnRerun is disabled
253-
const keepResults = !this.options.cleanOnRerun && this.ctx.config.watch
265+
async mergeReports(path: string) {
266+
const directory = resolve(path, 'coverage')
267+
const files = await fs.readdir(directory)
268+
const coverageMap = libCoverage.createCoverageMap({})
254269

255-
if (!keepResults) {
256-
this.coverageFiles = new Map()
257-
await fs.rm(this.coverageFilesDirectory, { recursive: true })
270+
for (const file of files) {
271+
const report = await fs.readFile(resolve(directory, file), 'utf8')
272+
coverageMap.merge(JSON.parse(report))
273+
}
274+
275+
await this.generateReports(coverageMap, true)
276+
}
277+
278+
async createBlobReport(coverageMap: CoverageMap, options: Options['reporter'][number][1]) {
279+
const dir = this.ctx.config.coverage.reportsDirectory || '.vitest-reports/coverage/'
280+
let file = 'file' in options && options.file as string
281+
282+
if (!file) {
283+
const shard = this.ctx.config.shard
284+
file = shard
285+
? `.coverage-blob-${shard.index}-${shard.count}.json`
286+
: '.coverage-blob.json'
258287
}
288+
289+
const context = libReport.createContext({ dir, coverageMap })
290+
reports.create('json', { file }).execute(context)
291+
292+
this.ctx.logger.log(`coverage blob report written to ${resolve(dir, file)}`)
259293
}
260294

261-
async getCoverageMapForUncoveredFiles(coveredFiles: string[]) {
295+
private async getCoverageMapForUncoveredFiles(coveredFiles: string[]) {
262296
const allFiles = await this.testExclude.glob(this.ctx.config.root)
263297
let includedFiles = allFiles.map(file => resolve(this.ctx.config.root, file))
264298

packages/coverage-v8/src/provider.ts

+44-5
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,18 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
193193
coverageMap.merge(await transformCoverage(converted))
194194
}
195195

196+
await this.generateReports(coverageMap, allTestsRun)
197+
198+
// In watch mode we need to preserve the previous results if cleanOnRerun is disabled
199+
const keepResults = !this.options.cleanOnRerun && this.ctx.config.watch
200+
201+
if (!keepResults) {
202+
this.coverageFiles = new Map()
203+
await fs.rm(this.coverageFilesDirectory, { recursive: true })
204+
}
205+
}
206+
207+
async generateReports(coverageMap: CoverageMap, allTestsRun: boolean | undefined) {
196208
const context = libReport.createContext({
197209
dir: this.options.reportsDirectory,
198210
coverageMap,
@@ -203,6 +215,11 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
203215
this.ctx.logger.log(c.blue(' % ') + c.dim('Coverage report from ') + c.yellow(this.name))
204216

205217
for (const reporter of this.options.reporter) {
218+
if (reporter[0] === 'blob') {
219+
await this.createBlobReport(coverageMap, reporter[1])
220+
continue
221+
}
222+
206223
// Type assertion required for custom reporters
207224
reports.create(reporter[0] as Parameters<typeof reports.create>[0], {
208225
skipFull: this.options.skipFull,
@@ -239,14 +256,36 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
239256
})
240257
}
241258
}
259+
}
260+
261+
async mergeReports(path: string) {
262+
const directory = resolve(path, 'coverage')
263+
const files = await fs.readdir(directory)
264+
const coverageMap = libCoverage.createCoverageMap({})
265+
266+
for (const file of files) {
267+
const report = await fs.readFile(resolve(directory, file), 'utf8')
268+
coverageMap.merge(JSON.parse(report))
269+
}
242270

243-
// In watch mode we need to preserve the previous results if cleanOnRerun is disabled
244-
const keepResults = !this.options.cleanOnRerun && this.ctx.config.watch
271+
await this.generateReports(coverageMap, true)
272+
}
245273

246-
if (!keepResults) {
247-
this.coverageFiles = new Map()
248-
await fs.rm(this.coverageFilesDirectory, { recursive: true })
274+
async createBlobReport(coverageMap: CoverageMap, options: Options['reporter'][number][1]) {
275+
const dir = this.ctx.config.coverage.reportsDirectory || '.vitest-reports/coverage/'
276+
let file = 'file' in options && options.file as string
277+
278+
if (!file) {
279+
const shard = this.ctx.config.shard
280+
file = shard
281+
? `.coverage-blob-${shard.index}-${shard.count}.json`
282+
: '.coverage-blob.json'
249283
}
284+
285+
const context = libReport.createContext({ dir, coverageMap })
286+
reports.create('json', { file }).execute(context)
287+
288+
this.ctx.logger.log(`coverage blob report written to ${resolve(dir, file)}`)
250289
}
251290

252291
private async getUntestedFiles(testedFiles: string[]): Promise<RawCoverage> {

packages/vitest/src/node/core.ts

+2
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,8 @@ export class Vitest {
439439
process.exitCode = 1
440440

441441
await this.report('onFinished', files, errors)
442+
await this.initCoverageProvider()
443+
await this.coverageProvider?.mergeReports?.(this.config.mergeReports)
442444
}
443445

444446
async start(filters?: string[]) {

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

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
1+
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises'
22
import { existsSync } from 'node:fs'
33
import { parse, stringify } from 'flatted'
44
import { dirname, resolve } from 'pathe'
@@ -61,11 +61,17 @@ export async function readBlobs(blobsDirectory: string, projectsArray: Workspace
6161
const resolvedDir = resolve(process.cwd(), blobsDirectory)
6262
const blobsFiles = await readdir(resolvedDir)
6363
const promises = blobsFiles.map(async (file) => {
64-
const content = await readFile(resolve(resolvedDir, file), 'utf-8')
64+
const filename = resolve(resolvedDir, file)
65+
const stats = await stat(filename)
66+
if (!stats.isFile())
67+
return null
68+
69+
const content = await readFile(filename, 'utf-8')
6570
const [version, files, errors, moduleKeys] = parse(content) as MergeReport
6671
return { version, files, errors, moduleKeys }
6772
})
68-
const blobs = await Promise.all(promises)
73+
const results = await Promise.all(promises)
74+
const blobs = results.filter((result): result is NonNullable<typeof result> => result != null)
6975

7076
if (!blobs.length)
7177
throw new Error(`vitest.mergeReports() requires at least one blob file paths in the config`)

packages/vitest/src/types/coverage.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { TransformResult as ViteTransformResult } from 'vite'
2-
import type { ReportOptions } from 'istanbul-reports'
2+
import type { ReportOptions as IstanbulReportOptions } from 'istanbul-reports'
33
import type { Vitest } from '../node'
44
import type { Arrayable } from './general'
55
import type { AfterSuiteRunMeta } from './worker'
@@ -17,6 +17,8 @@ export interface CoverageProvider {
1717

1818
reportCoverage: (reportContext?: ReportContext) => void | Promise<void>
1919

20+
mergeReports?: (path: string) => void | Promise<void>
21+
2022
onFileTransform?: (
2123
sourceCode: string,
2224
id: string,
@@ -52,6 +54,10 @@ export interface CoverageProviderModule {
5254
stopCoverage?: () => unknown | Promise<unknown>
5355
}
5456

57+
interface ReportOptions extends IstanbulReportOptions {
58+
blob: { file?: string }
59+
}
60+
5561
export type CoverageReporter = keyof ReportOptions | (string & {})
5662

5763
type CoverageReporterWithOptions<ReporterName extends CoverageReporter = CoverageReporter> =

0 commit comments

Comments
 (0)