@@ -5,7 +5,7 @@ import v8ToIstanbul from 'v8-to-istanbul'
5
5
import { mergeProcessCovs } from '@bcoe/v8-coverage'
6
6
import libReport from 'istanbul-lib-report'
7
7
import reports from 'istanbul-reports'
8
- import type { CoverageMap } from 'istanbul-lib-coverage'
8
+ import type { CoverageMap , CoverageMapData } from 'istanbul-lib-coverage'
9
9
import libCoverage from 'istanbul-lib-coverage'
10
10
import libSourceMaps from 'istanbul-lib-source-maps'
11
11
import MagicString from 'magic-string'
@@ -39,20 +39,24 @@ interface TestExclude {
39
39
40
40
type Options = ResolvedCoverageOptions < 'v8' >
41
41
type TransformResults = Map < string , FetchResult >
42
+ type RawCoverage = Profiler . TakePreciseCoverageReturnType
43
+ type CoverageByTransformMode = Record < AfterSuiteRunMeta [ 'transformMode' ] , RawCoverage [ ] >
44
+ type ProjectName = NonNullable < AfterSuiteRunMeta [ 'projectName' ] > | typeof DEFAULT_PROJECT
42
45
43
46
// TODO: vite-node should export this
44
47
const WRAPPER_LENGTH = 185
45
48
46
49
// Note that this needs to match the line ending as well
47
50
const VITE_EXPORTS_LINE_PATTERN = / O b j e c t \. d e f i n e P r o p e r t y \( _ _ v i t e _ s s r _ e x p o r t s _ _ .* \n / g
51
+ const DEFAULT_PROJECT = Symbol . for ( 'default-project' )
48
52
49
53
export class V8CoverageProvider extends BaseCoverageProvider implements CoverageProvider {
50
54
name = 'v8'
51
55
52
56
ctx ! : Vitest
53
57
options ! : Options
54
58
testExclude ! : InstanceType < TestExclude >
55
- coverages : Profiler . TakePreciseCoverageReturnType [ ] = [ ]
59
+ coverages = new Map < ProjectName , CoverageByTransformMode > ( )
56
60
57
61
initialize ( ctx : Vitest ) {
58
62
const config : CoverageV8Options = ctx . config . coverage
@@ -92,54 +96,52 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
92
96
if ( clean && existsSync ( this . options . reportsDirectory ) )
93
97
await fs . rm ( this . options . reportsDirectory , { recursive : true , force : true , maxRetries : 10 } )
94
98
95
- this . coverages = [ ]
99
+ this . coverages = new Map ( )
96
100
}
97
101
98
- onAfterSuiteRun ( { coverage } : AfterSuiteRunMeta ) {
99
- this . coverages . push ( coverage as Profiler . TakePreciseCoverageReturnType )
102
+ /*
103
+ * Coverage and meta information passed from Vitest runners.
104
+ * Note that adding new entries here and requiring on those without
105
+ * backwards compatibility is a breaking change.
106
+ */
107
+ onAfterSuiteRun ( { coverage, transformMode, projectName } : AfterSuiteRunMeta ) {
108
+ if ( transformMode !== 'web' && transformMode !== 'ssr' )
109
+ throw new Error ( `Invalid transform mode: ${ transformMode } ` )
110
+
111
+ let entry = this . coverages . get ( projectName || DEFAULT_PROJECT )
112
+
113
+ if ( ! entry ) {
114
+ entry = { web : [ ] , ssr : [ ] }
115
+ this . coverages . set ( projectName || DEFAULT_PROJECT , entry )
116
+ }
117
+
118
+ entry [ transformMode ] . push ( coverage as RawCoverage )
100
119
}
101
120
102
121
async reportCoverage ( { allTestsRun } : ReportContext = { } ) {
103
122
if ( provider === 'stackblitz' )
104
123
this . ctx . logger . log ( c . blue ( ' % ' ) + c . yellow ( '@vitest/coverage-v8 does not work on Stackblitz. Report will be empty.' ) )
105
124
106
- const transformResults = normalizeTransformResults ( this . ctx . projects . map ( project => project . vitenode . fetchCache ) )
107
- const merged = mergeProcessCovs ( this . coverages )
108
- const scriptCoverages = merged . result . filter ( result => this . testExclude . shouldInstrument ( fileURLToPath ( result . url ) ) )
125
+ const coverageMaps = await Promise . all (
126
+ Array . from ( this . coverages . entries ( ) ) . map ( ( [ projectName , coverages ] ) => [
127
+ this . mergeAndTransformCoverage ( coverages . ssr , projectName , 'ssr' ) ,
128
+ this . mergeAndTransformCoverage ( coverages . web , projectName , 'web' ) ,
129
+ ] ) . flat ( ) ,
130
+ )
109
131
110
132
if ( this . options . all && allTestsRun ) {
111
- const coveredFiles = Array . from ( scriptCoverages . map ( r => r . url ) )
112
- const untestedFiles = await this . getUntestedFiles ( coveredFiles , transformResults )
133
+ const coveredFiles = coverageMaps . map ( map => map . files ( ) ) . flat ( )
134
+ const untestedCoverage = await this . getUntestedFiles ( coveredFiles )
135
+ const untestedCoverageResults = untestedCoverage . map ( files => ( { result : [ files ] } ) )
113
136
114
- scriptCoverages . push ( ... untestedFiles )
137
+ coverageMaps . push ( await this . mergeAndTransformCoverage ( untestedCoverageResults ) )
115
138
}
116
139
117
- const converted = await Promise . all ( scriptCoverages . map ( async ( { url, functions } ) => {
118
- const sources = await this . getSources ( url , transformResults , functions )
119
-
120
- // If no source map was found from vite-node we can assume this file was not run in the wrapper
121
- const wrapperLength = sources . sourceMap ? WRAPPER_LENGTH : 0
122
-
123
- const converter = v8ToIstanbul ( url , wrapperLength , sources )
124
- await converter . load ( )
125
-
126
- converter . applyCoverage ( functions )
127
- return converter . toIstanbul ( )
128
- } ) )
129
-
130
- const mergedCoverage = converted . reduce ( ( coverage , previousCoverageMap ) => {
131
- const map = libCoverage . createCoverageMap ( coverage )
132
- map . merge ( previousCoverageMap )
133
- return map
134
- } , libCoverage . createCoverageMap ( { } ) )
135
-
136
- const sourceMapStore = libSourceMaps . createSourceMapStore ( )
137
- const coverageMap : CoverageMap = await sourceMapStore . transformCoverage ( mergedCoverage )
140
+ const coverageMap = mergeCoverageMaps ( ...coverageMaps )
138
141
139
142
const context = libReport . createContext ( {
140
143
dir : this . options . reportsDirectory ,
141
144
coverageMap,
142
- sourceFinder : sourceMapStore . sourceFinder ,
143
145
watermarks : this . options . watermarks ,
144
146
} )
145
147
@@ -185,11 +187,13 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
185
187
}
186
188
}
187
189
188
- private async getUntestedFiles ( testedFiles : string [ ] , transformResults : TransformResults ) : Promise < Profiler . ScriptCoverage [ ] > {
190
+ private async getUntestedFiles ( testedFiles : string [ ] ) : Promise < Profiler . ScriptCoverage [ ] > {
191
+ const transformResults = normalizeTransformResults ( this . ctx . vitenode . fetchCache )
192
+
189
193
const includedFiles = await this . testExclude . glob ( this . ctx . config . root )
190
194
const uncoveredFiles = includedFiles
191
195
. map ( file => pathToFileURL ( resolve ( this . ctx . config . root , file ) ) )
192
- . filter ( file => ! testedFiles . includes ( file . href ) )
196
+ . filter ( file => ! testedFiles . includes ( file . pathname ) )
193
197
194
198
return await Promise . all ( uncoveredFiles . map ( async ( uncoveredFile ) => {
195
199
const { source } = await this . getSources ( uncoveredFile . href , transformResults )
@@ -247,6 +251,41 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
247
251
} ,
248
252
}
249
253
}
254
+
255
+ private async mergeAndTransformCoverage ( coverages : RawCoverage [ ] , projectName ?: ProjectName , transformMode ?: 'web' | 'ssr' ) {
256
+ const viteNode = this . ctx . projects . find ( project => project . getName ( ) === projectName ) ?. vitenode || this . ctx . vitenode
257
+ const fetchCache = transformMode ? viteNode . fetchCaches [ transformMode ] : viteNode . fetchCache
258
+ const transformResults = normalizeTransformResults ( fetchCache )
259
+
260
+ const merged = mergeProcessCovs ( coverages )
261
+ const scriptCoverages = merged . result . filter ( result => this . testExclude . shouldInstrument ( fileURLToPath ( result . url ) ) )
262
+
263
+ const converted = await Promise . all ( scriptCoverages . map ( async ( { url, functions } ) => {
264
+ const sources = await this . getSources ( url , transformResults , functions )
265
+
266
+ // If no source map was found from vite-node we can assume this file was not run in the wrapper
267
+ const wrapperLength = sources . sourceMap ? WRAPPER_LENGTH : 0
268
+
269
+ const converter = v8ToIstanbul ( url , wrapperLength , sources )
270
+ await converter . load ( )
271
+
272
+ converter . applyCoverage ( functions )
273
+ return converter . toIstanbul ( )
274
+ } ) )
275
+
276
+ const mergedCoverage = mergeCoverageMaps ( ...converted )
277
+
278
+ const sourceMapStore = libSourceMaps . createSourceMapStore ( )
279
+ return sourceMapStore . transformCoverage ( mergedCoverage )
280
+ }
281
+ }
282
+
283
+ function mergeCoverageMaps ( ...coverageMaps : ( CoverageMap | CoverageMapData ) [ ] ) {
284
+ return coverageMaps . reduce < CoverageMap > ( ( coverage , previousCoverageMap ) => {
285
+ const map = libCoverage . createCoverageMap ( coverage )
286
+ map . merge ( previousCoverageMap )
287
+ return map
288
+ } , libCoverage . createCoverageMap ( { } ) )
250
289
}
251
290
252
291
/**
@@ -284,16 +323,14 @@ function findLongestFunctionLength(functions: Profiler.FunctionCoverage[]) {
284
323
} , 0 )
285
324
}
286
325
287
- function normalizeTransformResults ( fetchCaches : Map < string , { result : FetchResult } > [ ] ) {
326
+ function normalizeTransformResults ( fetchCache : Map < string , { result : FetchResult } > ) {
288
327
const normalized : TransformResults = new Map ( )
289
328
290
- for ( const fetchCache of fetchCaches ) {
291
- for ( const [ key , value ] of fetchCache . entries ( ) ) {
292
- const cleanEntry = cleanUrl ( key )
329
+ for ( const [ key , value ] of fetchCache . entries ( ) ) {
330
+ const cleanEntry = cleanUrl ( key )
293
331
294
- if ( ! normalized . has ( cleanEntry ) )
295
- normalized . set ( cleanEntry , value . result )
296
- }
332
+ if ( ! normalized . has ( cleanEntry ) )
333
+ normalized . set ( cleanEntry , value . result )
297
334
}
298
335
299
336
return normalized
0 commit comments