@@ -5,9 +5,10 @@ import { coverageConfigDefaults, defaultExclude, defaultInclude } from 'vitest/c
55import { BaseCoverageProvider } from 'vitest/coverage'
66import c from 'picocolors'
77import { parseModule } from 'magicast'
8+ import createDebug from 'debug'
89import libReport from 'istanbul-lib-report'
910import reports from 'istanbul-reports'
10- import type { CoverageMap , CoverageMapData } from 'istanbul-lib-coverage'
11+ import type { CoverageMap } from 'istanbul-lib-coverage'
1112import libCoverage from 'istanbul-lib-coverage'
1213import libSourceMaps from 'istanbul-lib-source-maps'
1314import { type Instrumenter , createInstrumenter } from 'istanbul-lib-instrument'
@@ -17,7 +18,8 @@ import _TestExclude from 'test-exclude'
1718import { COVERAGE_STORE_KEY } from './constants'
1819
1920type Options = ResolvedCoverageOptions < 'istanbul' >
20- type CoverageByTransformMode = Record < AfterSuiteRunMeta [ 'transformMode' ] , CoverageMapData [ ] >
21+ type Filename = string
22+ type CoverageFilesByTransformMode = Record < AfterSuiteRunMeta [ 'transformMode' ] , Filename [ ] >
2123type ProjectName = NonNullable < AfterSuiteRunMeta [ 'projectName' ] > | typeof DEFAULT_PROJECT
2224
2325interface TestExclude {
@@ -35,6 +37,8 @@ interface TestExclude {
3537}
3638
3739const DEFAULT_PROJECT = Symbol . for ( 'default-project' )
40+ const debug = createDebug ( 'vitest:coverage' )
41+ let uniqueId = 0
3842
3943export class IstanbulCoverageProvider extends BaseCoverageProvider implements CoverageProvider {
4044 name = 'istanbul'
@@ -44,13 +48,9 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
4448 instrumenter ! : Instrumenter
4549 testExclude ! : InstanceType < TestExclude >
4650
47- /**
48- * Coverage objects collected from workers.
49- * Some istanbul utilizers write these into file system instead of storing in memory.
50- * If storing in memory causes issues, we can simply write these into fs in `onAfterSuiteRun`
51- * and read them back when merging coverage objects in `onAfterAllFilesRun`.
52- */
53- coverages = new Map < ProjectName , CoverageByTransformMode > ( )
51+ coverageFiles = new Map < ProjectName , CoverageFilesByTransformMode > ( )
52+ coverageFilesDirectory ! : string
53+ pendingPromises : Promise < void > [ ] = [ ]
5454
5555 initialize ( ctx : Vitest ) {
5656 const config : CoverageIstanbulOptions = ctx . config . coverage
@@ -96,6 +96,8 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
9696 extension : this . options . extension ,
9797 relativePath : ! this . options . allowExternal ,
9898 } )
99+
100+ this . coverageFilesDirectory = resolve ( this . options . reportsDirectory , '.tmp' )
99101 }
100102
101103 resolveOptions ( ) {
@@ -121,43 +123,79 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
121123 * backwards compatibility is a breaking change.
122124 */
123125 onAfterSuiteRun ( { coverage, transformMode, projectName } : AfterSuiteRunMeta ) {
126+ if ( ! coverage )
127+ return
128+
124129 if ( transformMode !== 'web' && transformMode !== 'ssr' )
125130 throw new Error ( `Invalid transform mode: ${ transformMode } ` )
126131
127- let entry = this . coverages . get ( projectName || DEFAULT_PROJECT )
132+ let entry = this . coverageFiles . get ( projectName || DEFAULT_PROJECT )
128133
129134 if ( ! entry ) {
130135 entry = { web : [ ] , ssr : [ ] }
131- this . coverages . set ( projectName || DEFAULT_PROJECT , entry )
136+ this . coverageFiles . set ( projectName || DEFAULT_PROJECT , entry )
132137 }
133138
134- entry [ transformMode ] . push ( coverage as CoverageMapData )
139+ const filename = resolve ( this . coverageFilesDirectory , `coverage-${ uniqueId ++ } .json` )
140+ entry [ transformMode ] . push ( filename )
141+
142+ const promise = fs . writeFile ( filename , JSON . stringify ( coverage ) , 'utf-8' )
143+ this . pendingPromises . push ( promise )
135144 }
136145
137146 async clean ( clean = true ) {
138147 if ( clean && existsSync ( this . options . reportsDirectory ) )
139148 await fs . rm ( this . options . reportsDirectory , { recursive : true , force : true , maxRetries : 10 } )
140149
141- this . coverages = new Map ( )
150+ if ( existsSync ( this . coverageFilesDirectory ) )
151+ await fs . rm ( this . coverageFilesDirectory , { recursive : true , force : true , maxRetries : 10 } )
152+
153+ await fs . mkdir ( this . coverageFilesDirectory , { recursive : true } )
154+
155+ this . coverageFiles = new Map ( )
156+ this . pendingPromises = [ ]
142157 }
143158
144159 async reportCoverage ( { allTestsRun } : ReportContext = { } ) {
145- const coverageMaps = await Promise . all (
146- Array . from ( this . coverages . values ( ) ) . map ( coverages => [
147- mergeAndTransformCoverage ( coverages . ssr ) ,
148- mergeAndTransformCoverage ( coverages . web ) ,
149- ] ) . flat ( ) ,
150- )
160+ const coverageMap = libCoverage . createCoverageMap ( { } )
161+ let index = 0
162+ const total = this . pendingPromises . length
163+
164+ await Promise . all ( this . pendingPromises )
165+ this . pendingPromises = [ ]
166+
167+ for ( const coveragePerProject of this . coverageFiles . values ( ) ) {
168+ for ( const filenames of [ coveragePerProject . ssr , coveragePerProject . web ] ) {
169+ const coverageMapByTransformMode = libCoverage . createCoverageMap ( { } )
170+
171+ for ( const chunk of toSlices ( filenames , this . options . processingConcurrency ) ) {
172+ if ( debug . enabled ) {
173+ index += chunk . length
174+ debug ( 'Covered files %d/%d' , index , total )
175+ }
176+
177+ await Promise . all ( chunk . map ( async ( filename ) => {
178+ const contents = await fs . readFile ( filename , 'utf-8' )
179+ const coverage = JSON . parse ( contents ) as CoverageMap
180+
181+ coverageMapByTransformMode . merge ( coverage )
182+ } ) )
183+ }
184+
185+ // Source maps can change based on projectName and transform mode.
186+ // Coverage transform re-uses source maps so we need to separate transforms from each other.
187+ const transformedCoverage = await transformCoverage ( coverageMapByTransformMode )
188+ coverageMap . merge ( transformedCoverage )
189+ }
190+ }
151191
152192 if ( this . options . all && allTestsRun ) {
153- const coveredFiles = coverageMaps . map ( map => map . files ( ) ) . flat ( )
193+ const coveredFiles = coverageMap . files ( )
154194 const uncoveredCoverage = await this . getCoverageMapForUncoveredFiles ( coveredFiles )
155195
156- coverageMaps . push ( await mergeAndTransformCoverage ( [ uncoveredCoverage ] ) )
196+ coverageMap . merge ( await transformCoverage ( uncoveredCoverage ) )
157197 }
158198
159- const coverageMap = mergeCoverageMaps ( ...coverageMaps )
160-
161199 const context = libReport . createContext ( {
162200 dir : this . options . reportsDirectory ,
163201 coverageMap,
@@ -206,6 +244,9 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
206244 } )
207245 }
208246 }
247+
248+ await fs . rm ( this . coverageFilesDirectory , { recursive : true } )
249+ this . coverageFiles = new Map ( )
209250 }
210251
211252 async getCoverageMapForUncoveredFiles ( coveredFiles : string [ ] ) {
@@ -218,31 +259,31 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
218259
219260 const coverageMap = libCoverage . createCoverageMap ( { } )
220261
221- for ( const filename of uncoveredFiles ) {
262+ // Note that these cannot be run parallel as synchronous instrumenter.lastFileCoverage
263+ // returns the coverage of the last transformed file
264+ for ( const [ index , filename ] of uncoveredFiles . entries ( ) ) {
265+ debug ( 'Uncovered file %s %d/%d' , filename , index , uncoveredFiles . length )
266+
267+ // Make sure file is not served from cache
268+ // so that instrumenter loads up requested file coverage
269+ if ( this . ctx . vitenode . fetchCache . has ( filename ) )
270+ this . ctx . vitenode . fetchCache . delete ( filename )
271+
222272 await this . ctx . vitenode . transformRequest ( filename )
223273
224274 const lastCoverage = this . instrumenter . lastFileCoverage ( )
225275 coverageMap . addFileCoverage ( lastCoverage )
226276 }
227277
228- return coverageMap . data
278+ return coverageMap
229279 }
230280}
231281
232- async function mergeAndTransformCoverage ( coverages : CoverageMapData [ ] ) {
233- const mergedCoverage = mergeCoverageMaps ( ...coverages )
234- includeImplicitElseBranches ( mergedCoverage )
282+ async function transformCoverage ( coverageMap : CoverageMap ) {
283+ includeImplicitElseBranches ( coverageMap )
235284
236285 const sourceMapStore = libSourceMaps . createSourceMapStore ( )
237- return await sourceMapStore . transformCoverage ( mergedCoverage )
238- }
239-
240- function mergeCoverageMaps ( ...coverageMaps : ( CoverageMap | CoverageMapData ) [ ] ) {
241- return coverageMaps . reduce < CoverageMap > ( ( coverage , previousCoverageMap ) => {
242- const map = libCoverage . createCoverageMap ( coverage )
243- map . merge ( previousCoverageMap )
244- return map
245- } , libCoverage . createCoverageMap ( { } ) )
286+ return await sourceMapStore . transformCoverage ( coverageMap )
246287}
247288
248289/**
@@ -302,3 +343,19 @@ function hasTerminalReporter(reporters: Options['reporter']) {
302343 || reporter === 'text-lcov'
303344 || reporter === 'teamcity' )
304345}
346+
347+ function toSlices < T > ( array : T [ ] , size : number ) : T [ ] [ ] {
348+ return array . reduce < T [ ] [ ] > ( ( chunks , item ) => {
349+ const index = Math . max ( 0 , chunks . length - 1 )
350+ const lastChunk = chunks [ index ] || [ ]
351+ chunks [ index ] = lastChunk
352+
353+ if ( lastChunk . length >= size )
354+ chunks . push ( [ item ] )
355+
356+ else
357+ lastChunk . push ( item )
358+
359+ return chunks
360+ } , [ ] )
361+ }
0 commit comments