@@ -5,9 +5,10 @@ import { coverageConfigDefaults, defaultExclude, defaultInclude } from 'vitest/c
5
5
import { BaseCoverageProvider } from 'vitest/coverage'
6
6
import c from 'picocolors'
7
7
import { parseModule } from 'magicast'
8
+ import createDebug from 'debug'
8
9
import libReport from 'istanbul-lib-report'
9
10
import reports from 'istanbul-reports'
10
- import type { CoverageMap , CoverageMapData } from 'istanbul-lib-coverage'
11
+ import type { CoverageMap } from 'istanbul-lib-coverage'
11
12
import libCoverage from 'istanbul-lib-coverage'
12
13
import libSourceMaps from 'istanbul-lib-source-maps'
13
14
import { type Instrumenter , createInstrumenter } from 'istanbul-lib-instrument'
@@ -17,7 +18,8 @@ import _TestExclude from 'test-exclude'
17
18
import { COVERAGE_STORE_KEY } from './constants'
18
19
19
20
type Options = ResolvedCoverageOptions < 'istanbul' >
20
- type CoverageByTransformMode = Record < AfterSuiteRunMeta [ 'transformMode' ] , CoverageMapData [ ] >
21
+ type Filename = string
22
+ type CoverageFilesByTransformMode = Record < AfterSuiteRunMeta [ 'transformMode' ] , Filename [ ] >
21
23
type ProjectName = NonNullable < AfterSuiteRunMeta [ 'projectName' ] > | typeof DEFAULT_PROJECT
22
24
23
25
interface TestExclude {
@@ -35,6 +37,8 @@ interface TestExclude {
35
37
}
36
38
37
39
const DEFAULT_PROJECT = Symbol . for ( 'default-project' )
40
+ const debug = createDebug ( 'vitest:coverage' )
41
+ let uniqueId = 0
38
42
39
43
export class IstanbulCoverageProvider extends BaseCoverageProvider implements CoverageProvider {
40
44
name = 'istanbul'
@@ -44,13 +48,9 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
44
48
instrumenter ! : Instrumenter
45
49
testExclude ! : InstanceType < TestExclude >
46
50
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 > [ ] = [ ]
54
54
55
55
initialize ( ctx : Vitest ) {
56
56
const config : CoverageIstanbulOptions = ctx . config . coverage
@@ -96,6 +96,8 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
96
96
extension : this . options . extension ,
97
97
relativePath : ! this . options . allowExternal ,
98
98
} )
99
+
100
+ this . coverageFilesDirectory = resolve ( this . options . reportsDirectory , '.tmp' )
99
101
}
100
102
101
103
resolveOptions ( ) {
@@ -121,43 +123,79 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
121
123
* backwards compatibility is a breaking change.
122
124
*/
123
125
onAfterSuiteRun ( { coverage, transformMode, projectName } : AfterSuiteRunMeta ) {
126
+ if ( ! coverage )
127
+ return
128
+
124
129
if ( transformMode !== 'web' && transformMode !== 'ssr' )
125
130
throw new Error ( `Invalid transform mode: ${ transformMode } ` )
126
131
127
- let entry = this . coverages . get ( projectName || DEFAULT_PROJECT )
132
+ let entry = this . coverageFiles . get ( projectName || DEFAULT_PROJECT )
128
133
129
134
if ( ! entry ) {
130
135
entry = { web : [ ] , ssr : [ ] }
131
- this . coverages . set ( projectName || DEFAULT_PROJECT , entry )
136
+ this . coverageFiles . set ( projectName || DEFAULT_PROJECT , entry )
132
137
}
133
138
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 )
135
144
}
136
145
137
146
async clean ( clean = true ) {
138
147
if ( clean && existsSync ( this . options . reportsDirectory ) )
139
148
await fs . rm ( this . options . reportsDirectory , { recursive : true , force : true , maxRetries : 10 } )
140
149
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 = [ ]
142
157
}
143
158
144
159
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
+ }
151
191
152
192
if ( this . options . all && allTestsRun ) {
153
- const coveredFiles = coverageMaps . map ( map => map . files ( ) ) . flat ( )
193
+ const coveredFiles = coverageMap . files ( )
154
194
const uncoveredCoverage = await this . getCoverageMapForUncoveredFiles ( coveredFiles )
155
195
156
- coverageMaps . push ( await mergeAndTransformCoverage ( [ uncoveredCoverage ] ) )
196
+ coverageMap . merge ( await transformCoverage ( uncoveredCoverage ) )
157
197
}
158
198
159
- const coverageMap = mergeCoverageMaps ( ...coverageMaps )
160
-
161
199
const context = libReport . createContext ( {
162
200
dir : this . options . reportsDirectory ,
163
201
coverageMap,
@@ -206,6 +244,9 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
206
244
} )
207
245
}
208
246
}
247
+
248
+ await fs . rm ( this . coverageFilesDirectory , { recursive : true } )
249
+ this . coverageFiles = new Map ( )
209
250
}
210
251
211
252
async getCoverageMapForUncoveredFiles ( coveredFiles : string [ ] ) {
@@ -218,31 +259,31 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
218
259
219
260
const coverageMap = libCoverage . createCoverageMap ( { } )
220
261
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
+
222
272
await this . ctx . vitenode . transformRequest ( filename )
223
273
224
274
const lastCoverage = this . instrumenter . lastFileCoverage ( )
225
275
coverageMap . addFileCoverage ( lastCoverage )
226
276
}
227
277
228
- return coverageMap . data
278
+ return coverageMap
229
279
}
230
280
}
231
281
232
- async function mergeAndTransformCoverage ( coverages : CoverageMapData [ ] ) {
233
- const mergedCoverage = mergeCoverageMaps ( ...coverages )
234
- includeImplicitElseBranches ( mergedCoverage )
282
+ async function transformCoverage ( coverageMap : CoverageMap ) {
283
+ includeImplicitElseBranches ( coverageMap )
235
284
236
285
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 )
246
287
}
247
288
248
289
/**
@@ -302,3 +343,19 @@ function hasTerminalReporter(reporters: Options['reporter']) {
302
343
|| reporter === 'text-lcov'
303
344
|| reporter === 'teamcity' )
304
345
}
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