@@ -2,6 +2,12 @@ const Exclude = require('test-exclude')
22const libCoverage = require ( 'istanbul-lib-coverage' )
33const libReport = require ( 'istanbul-lib-report' )
44const reports = require ( 'istanbul-reports' )
5+ let readFile
6+ try {
7+ ; ( { readFile } = require ( 'fs/promises' ) )
8+ } catch ( err ) {
9+ ; ( { readFile } = require ( 'fs' ) . promises )
10+ }
511const { readdirSync, readFileSync, statSync } = require ( 'fs' )
612const { isAbsolute, resolve, extname } = require ( 'path' )
713const { pathToFileURL, fileURLToPath } = require ( 'url' )
@@ -30,7 +36,8 @@ class Report {
3036 src,
3137 allowExternal = false ,
3238 skipFull,
33- excludeNodeModules
39+ excludeNodeModules,
40+ mergeAsync
3441 } ) {
3542 this . reporter = reporter
3643 this . reporterOptions = reporterOptions || { }
@@ -53,6 +60,7 @@ class Report {
5360 this . all = all
5461 this . src = this . _getSrc ( src )
5562 this . skipFull = skipFull
63+ this . mergeAsync = mergeAsync
5664 }
5765
5866 _getSrc ( src ) {
@@ -90,7 +98,13 @@ class Report {
9098 if ( this . _allCoverageFiles ) return this . _allCoverageFiles
9199
92100 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+ }
94108 const resultCountPerPath = new Map ( )
95109 const possibleCjsEsmBridges = new Map ( )
96110
@@ -188,43 +202,106 @@ class Report {
188202 }
189203
190204 if ( this . all ) {
191- const emptyReports = [ ]
205+ const emptyReports = this . _includeUncoveredFiles ( fileIndex )
192206 v8ProcessCovs . unshift ( {
193207 result : emptyReports
194208 } )
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' ] ) )
222239 }
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 } ` )
224249 }
225250 }
226251
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
228305 }
229306
230307 /**
0 commit comments