@@ -2,6 +2,12 @@ const Exclude = require('test-exclude')
2
2
const libCoverage = require ( 'istanbul-lib-coverage' )
3
3
const libReport = require ( 'istanbul-lib-report' )
4
4
const reports = require ( 'istanbul-reports' )
5
+ let readFile
6
+ try {
7
+ ; ( { readFile } = require ( 'fs/promises' ) )
8
+ } catch ( err ) {
9
+ ; ( { readFile } = require ( 'fs' ) . promises )
10
+ }
5
11
const { readdirSync, readFileSync, statSync } = require ( 'fs' )
6
12
const { isAbsolute, resolve, extname } = require ( 'path' )
7
13
const { pathToFileURL, fileURLToPath } = require ( 'url' )
@@ -30,7 +36,8 @@ class Report {
30
36
src,
31
37
allowExternal = false ,
32
38
skipFull,
33
- excludeNodeModules
39
+ excludeNodeModules,
40
+ mergeAsync
34
41
} ) {
35
42
this . reporter = reporter
36
43
this . reporterOptions = reporterOptions || { }
@@ -53,6 +60,7 @@ class Report {
53
60
this . all = all
54
61
this . src = this . _getSrc ( src )
55
62
this . skipFull = skipFull
63
+ this . mergeAsync = mergeAsync
56
64
}
57
65
58
66
_getSrc ( src ) {
@@ -90,7 +98,13 @@ class Report {
90
98
if ( this . _allCoverageFiles ) return this . _allCoverageFiles
91
99
92
100
const map = libCoverage . createCoverageMap ( )
93
- const v8ProcessCov = this . _getMergedProcessCov ( )
101
+ let v8ProcessCov
102
+
103
+ if ( this . mergeAsync ) {
104
+ v8ProcessCov = await this . _getMergedProcessCovAsync ( )
105
+ } else {
106
+ v8ProcessCov = this . _getMergedProcessCov ( )
107
+ }
94
108
const resultCountPerPath = new Map ( )
95
109
const possibleCjsEsmBridges = new Map ( )
96
110
@@ -188,43 +202,106 @@ class Report {
188
202
}
189
203
190
204
if ( this . all ) {
191
- const emptyReports = [ ]
205
+ const emptyReports = this . _includeUncoveredFiles ( fileIndex )
192
206
v8ProcessCovs . unshift ( {
193
207
result : emptyReports
194
208
} )
195
- const workingDirs = this . src
196
- const { extension } = this . exclude
197
- for ( const workingDir of workingDirs ) {
198
- this . exclude . globSync ( workingDir ) . forEach ( ( f ) => {
199
- const fullPath = resolve ( workingDir , f )
200
- if ( ! fileIndex . has ( fullPath ) ) {
201
- const ext = extname ( fullPath )
202
- if ( extension . includes ( ext ) ) {
203
- const stat = statSync ( fullPath )
204
- const sourceMap = getSourceMapFromFile ( fullPath )
205
- if ( sourceMap ) {
206
- this . sourceMapCache [ pathToFileURL ( fullPath ) ] = { data : sourceMap }
207
- }
208
- emptyReports . push ( {
209
- scriptId : 0 ,
210
- url : resolve ( fullPath ) ,
211
- functions : [ {
212
- functionName : '(empty-report)' ,
213
- ranges : [ {
214
- startOffset : 0 ,
215
- endOffset : stat . size ,
216
- count : 0
217
- } ] ,
218
- isBlockCoverage : true
219
- } ]
220
- } )
221
- }
209
+ }
210
+
211
+ return mergeProcessCovs ( v8ProcessCovs )
212
+ }
213
+
214
+ /**
215
+ * Returns the merged V8 process coverage.
216
+ *
217
+ * It asynchronously and incrementally reads and merges individual process coverages
218
+ * generated by Node. This can be used via the `--merge-async` CLI arg. It's intended
219
+ * to be used across a large multi-process test run.
220
+ *
221
+ * @return {ProcessCov } Merged V8 process coverage.
222
+ * @private
223
+ */
224
+ async _getMergedProcessCovAsync ( ) {
225
+ const { mergeProcessCovs } = require ( '@bcoe/v8-coverage' )
226
+ const fileIndex = new Set ( ) // Set<string>
227
+ let mergedCov = null
228
+ for ( const file of readdirSync ( this . tempDirectory ) ) {
229
+ try {
230
+ const rawFile = await readFile (
231
+ resolve ( this . tempDirectory , file ) ,
232
+ 'utf8'
233
+ )
234
+ let report = JSON . parse ( rawFile )
235
+
236
+ if ( this . _isCoverageObject ( report ) ) {
237
+ if ( report [ 'source-map-cache' ] ) {
238
+ Object . assign ( this . sourceMapCache , this . _normalizeSourceMapCache ( report [ 'source-map-cache' ] ) )
222
239
}
223
- } )
240
+ report = this . _normalizeProcessCov ( report , fileIndex )
241
+ if ( mergedCov ) {
242
+ mergedCov = mergeProcessCovs ( [ mergedCov , report ] )
243
+ } else {
244
+ mergedCov = mergeProcessCovs ( [ report ] )
245
+ }
246
+ }
247
+ } catch ( err ) {
248
+ debuglog ( `${ err . stack } ` )
224
249
}
225
250
}
226
251
227
- return mergeProcessCovs ( v8ProcessCovs )
252
+ if ( this . all ) {
253
+ const emptyReports = this . _includeUncoveredFiles ( fileIndex )
254
+ const emptyReport = {
255
+ result : emptyReports
256
+ }
257
+
258
+ mergedCov = mergeProcessCovs ( [ emptyReport , mergedCov ] )
259
+ }
260
+
261
+ return mergedCov
262
+ }
263
+
264
+ /**
265
+ * Adds empty coverage reports to account for uncovered/untested code.
266
+ * This is only done when the `--all` flag is present.
267
+ *
268
+ * @param {Set } fileIndex list of files that have coverage
269
+ * @returns {Array } list of empty coverage reports
270
+ */
271
+ _includeUncoveredFiles ( fileIndex ) {
272
+ const emptyReports = [ ]
273
+ const workingDirs = this . src
274
+ const { extension } = this . exclude
275
+ for ( const workingDir of workingDirs ) {
276
+ this . exclude . globSync ( workingDir ) . forEach ( ( f ) => {
277
+ const fullPath = resolve ( workingDir , f )
278
+ if ( ! fileIndex . has ( fullPath ) ) {
279
+ const ext = extname ( fullPath )
280
+ if ( extension . includes ( ext ) ) {
281
+ const stat = statSync ( fullPath )
282
+ const sourceMap = getSourceMapFromFile ( fullPath )
283
+ if ( sourceMap ) {
284
+ this . sourceMapCache [ pathToFileURL ( fullPath ) ] = { data : sourceMap }
285
+ }
286
+ emptyReports . push ( {
287
+ scriptId : 0 ,
288
+ url : resolve ( fullPath ) ,
289
+ functions : [ {
290
+ functionName : '(empty-report)' ,
291
+ ranges : [ {
292
+ startOffset : 0 ,
293
+ endOffset : stat . size ,
294
+ count : 0
295
+ } ] ,
296
+ isBlockCoverage : true
297
+ } ]
298
+ } )
299
+ }
300
+ }
301
+ } )
302
+ }
303
+
304
+ return emptyReports
228
305
}
229
306
230
307
/**
0 commit comments