Skip to content

Commit a8b475d

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

File tree

5 files changed

+106
-18
lines changed

5 files changed

+106
-18
lines changed

packages/coverage-istanbul/src/provider.ts

+42-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { existsSync, promises as fs, writeFileSync } from 'node:fs'
2+
import { basename, dirname } from 'node:path'
23
import { resolve } from 'pathe'
34
import type { AfterSuiteRunMeta, CoverageIstanbulOptions, CoverageProvider, ReportContext, ResolvedCoverageOptions, Vitest } from 'vitest'
45
import { coverageConfigDefaults, defaultExclude, defaultInclude } from 'vitest/config'
@@ -202,6 +203,18 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
202203
coverageMap.merge(await transformCoverage(uncoveredCoverage))
203204
}
204205

206+
await this.generateReports(coverageMap, allTestsRun)
207+
208+
// In watch mode we need to preserve the previous results if cleanOnRerun is disabled
209+
const keepResults = !this.options.cleanOnRerun && this.ctx.config.watch
210+
211+
if (!keepResults) {
212+
this.coverageFiles = new Map()
213+
await fs.rm(this.coverageFilesDirectory, { recursive: true })
214+
}
215+
}
216+
217+
async generateReports(coverageMap: CoverageMap, allTestsRun: boolean | undefined) {
205218
const context = libReport.createContext({
206219
dir: this.options.reportsDirectory,
207220
coverageMap,
@@ -248,17 +261,40 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
248261
})
249262
}
250263
}
264+
}
251265

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
266+
async mergeReports(path: string) {
267+
const directory = resolve(path, 'coverage')
268+
const files = await fs.readdir(directory)
269+
const coverageMap = libCoverage.createCoverageMap({})
254270

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

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

packages/coverage-v8/src/provider.ts

+46-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { existsSync, promises as fs, writeFileSync } from 'node:fs'
22
import type { Profiler } from 'node:inspector'
33
import { fileURLToPath, pathToFileURL } from 'node:url'
4+
import { basename, dirname } from 'node:path'
45
import v8ToIstanbul from 'v8-to-istanbul'
56
import { mergeProcessCovs } from '@bcoe/v8-coverage'
67
import libReport from 'istanbul-lib-report'
@@ -193,6 +194,18 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
193194
coverageMap.merge(await transformCoverage(converted))
194195
}
195196

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

205218
for (const reporter of this.options.reporter) {
219+
if (reporter[0] === 'blob') {
220+
await this.createBlobReport(coverageMap, reporter[1])
221+
continue
222+
}
223+
206224
// Type assertion required for custom reporters
207225
reports.create(reporter[0] as Parameters<typeof reports.create>[0], {
208226
skipFull: this.options.skipFull,
@@ -239,14 +257,37 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
239257
})
240258
}
241259
}
260+
}
261+
262+
async mergeReports(path: string) {
263+
const directory = resolve(path, 'coverage')
264+
const files = await fs.readdir(directory)
265+
const coverageMap = libCoverage.createCoverageMap({})
266+
267+
for (const file of files) {
268+
const report = await fs.readFile(resolve(directory, file), 'utf8')
269+
coverageMap.merge(JSON.parse(report))
270+
}
242271

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
272+
await this.generateReports(coverageMap, true)
273+
}
245274

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

252293
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-6
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,14 +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)
69-
70-
if (!blobs.length)
71-
throw new Error(`vitest.mergeReports() requires at least one blob file paths in the config`)
73+
const results = await Promise.all(promises)
74+
const blobs = results.filter((result): result is NonNullable<typeof result> => result != null)
7275

7376
// fake module graph - it is used to check if module is imported, but we don't use values inside
7477
const projects = Object.fromEntries(projectsArray.map(p => [p.getName(), p]))

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: { outputFile?: string }
59+
}
60+
5561
export type CoverageReporter = keyof ReportOptions | (string & {})
5662

5763
type CoverageReporterWithOptions<ReporterName extends CoverageReporter = CoverageReporter> =

0 commit comments

Comments
 (0)