11'use strict' ;
22const {
3+ ArrayFrom,
34 ArrayPrototypeMap,
45 ArrayPrototypePush,
56 JSONParse,
67 MathFloor,
78 NumberParseInt,
8- RegExp,
99 RegExpPrototypeExec,
1010 RegExpPrototypeSymbolSplit,
11+ SafeMap,
12+ SafeSet,
1113 StringPrototypeIncludes,
1214 StringPrototypeLocaleCompare,
1315 StringPrototypeStartsWith,
@@ -23,9 +25,7 @@ const { setupCoverageHooks } = require('internal/util');
2325const { tmpdir } = require ( 'os' ) ;
2426const { join, resolve } = require ( 'path' ) ;
2527const { fileURLToPath } = require ( 'url' ) ;
26- const kCoveragePattern =
27- `^coverage\\-${ process . pid } \\-(\\d{13})\\-(\\d+)\\.json$` ;
28- const kCoverageFileRegex = new RegExp ( kCoveragePattern ) ;
28+ const kCoverageFileRegex = / ^ c o v e r a g e - ( \d + ) - ( \d { 13 } ) - ( \d + ) \. j s o n $ / ;
2929const kIgnoreRegex = / \/ \* n o d e : c o v e r a g e i g n o r e n e x t (?< count > \d + ) ? \* \/ / ;
3030const kLineEndingRegex = / \r ? \n $ / u;
3131const kLineSplitRegex = / (?< = \r ? \n ) / u;
@@ -95,13 +95,6 @@ class TestCoverage {
9595 for ( let i = 0 ; i < coverage . length ; ++ i ) {
9696 const { functions, url } = coverage [ i ] ;
9797
98- if ( StringPrototypeStartsWith ( url , 'node:' ) ||
99- StringPrototypeIncludes ( url , '/node_modules/' ) ||
100- // On Windows some generated coverages are invalid.
101- ! StringPrototypeStartsWith ( url , 'file:' ) ) {
102- continue ;
103- }
104-
10598 // Split the file source into lines. Make sure the lines maintain their
10699 // original line endings because those characters are necessary for
107100 // determining offsets in the file.
@@ -345,8 +338,7 @@ function mapRangeToLines(range, lines) {
345338}
346339
347340function getCoverageFromDirectory ( coverageDirectory ) {
348- // TODO(cjihrig): Instead of only reading the coverage file for this process,
349- // combine all coverage files in the directory into a single data structure.
341+ const result = new SafeMap ( ) ;
350342 let dir ;
351343
352344 try {
@@ -359,13 +351,149 @@ function getCoverageFromDirectory(coverageDirectory) {
359351
360352 const coverageFile = join ( coverageDirectory , entry . name ) ;
361353 const coverage = JSONParse ( readFileSync ( coverageFile , 'utf8' ) ) ;
362- return coverage . result ;
354+
355+ mergeCoverage ( result , coverage . result ) ;
363356 }
357+
358+ return ArrayFrom ( result . values ( ) ) ;
364359 } finally {
365360 if ( dir ) {
366361 dir . closeSync ( ) ;
367362 }
368363 }
369364}
370365
366+ function mergeCoverage ( merged , coverage ) {
367+ for ( let i = 0 ; i < coverage . length ; ++ i ) {
368+ const newScript = coverage [ i ] ;
369+ const { url } = newScript ;
370+
371+ // Filter out core modules and the node_modules/ directory from results.
372+ if ( StringPrototypeStartsWith ( url , 'node:' ) ||
373+ StringPrototypeIncludes ( url , '/node_modules/' ) ||
374+ // On Windows some generated coverages are invalid.
375+ ! StringPrototypeStartsWith ( url , 'file:' ) ) {
376+ continue ;
377+ }
378+
379+ const oldScript = merged . get ( url ) ;
380+
381+ if ( oldScript === undefined ) {
382+ merged . set ( url , newScript ) ;
383+ } else {
384+ mergeCoverageScripts ( oldScript , newScript ) ;
385+ }
386+ }
387+ }
388+
389+ function mergeCoverageScripts ( oldScript , newScript ) {
390+ // Merge the functions from the new coverage into the functions from the
391+ // existing (merged) coverage.
392+ for ( let i = 0 ; i < newScript . functions . length ; ++ i ) {
393+ const newFn = newScript . functions [ i ] ;
394+ let found = false ;
395+
396+ for ( let j = 0 ; j < oldScript . functions . length ; ++ j ) {
397+ const oldFn = oldScript . functions [ j ] ;
398+
399+ if ( newFn . functionName === oldFn . functionName &&
400+ newFn . ranges ?. [ 0 ] . startOffset === oldFn . ranges ?. [ 0 ] . startOffset &&
401+ newFn . ranges ?. [ 0 ] . endOffset === oldFn . ranges ?. [ 0 ] . endOffset ) {
402+ // These are the same functions.
403+ found = true ;
404+
405+ // If newFn is block level coverage, then it will:
406+ // - Replace oldFn if oldFn is not block level coverage.
407+ // - Merge with oldFn if it is also block level coverage.
408+ // If newFn is not block level coverage, then it has no new data.
409+ if ( newFn . isBlockCoverage ) {
410+ if ( oldFn . isBlockCoverage ) {
411+ // Merge the oldFn ranges with the newFn ranges.
412+ mergeCoverageRanges ( oldFn , newFn ) ;
413+ } else {
414+ // Replace oldFn with newFn.
415+ oldFn . isBlockCoverage = true ;
416+ oldFn . ranges = newFn . ranges ;
417+ }
418+ }
419+
420+ break ;
421+ }
422+ }
423+
424+ if ( ! found ) {
425+ // This is a new function to track. This is possible because V8 can
426+ // generate a different list of functions depending on which code paths
427+ // are executed. For example, if a code path dynamically creates a
428+ // function, but that code path is not executed then the function does
429+ // not show up in the coverage report. Unfortunately, this also means
430+ // that the function counts in the coverage summary can never be
431+ // guaranteed to be 100% accurate.
432+ ArrayPrototypePush ( oldScript . functions , newFn ) ;
433+ }
434+ }
435+ }
436+
437+ function mergeCoverageRanges ( oldFn , newFn ) {
438+ const mergedRanges = new SafeSet ( ) ;
439+
440+ // Keep all of the existing covered ranges.
441+ for ( let i = 0 ; i < oldFn . ranges . length ; ++ i ) {
442+ const oldRange = oldFn . ranges [ i ] ;
443+
444+ if ( oldRange . count > 0 ) {
445+ mergedRanges . add ( oldRange ) ;
446+ }
447+ }
448+
449+ // Merge in the new ranges where appropriate.
450+ for ( let i = 0 ; i < newFn . ranges . length ; ++ i ) {
451+ const newRange = newFn . ranges [ i ] ;
452+ let exactMatch = false ;
453+
454+ for ( let j = 0 ; j < oldFn . ranges . length ; ++ j ) {
455+ const oldRange = oldFn . ranges [ j ] ;
456+
457+ if ( doesRangeEqualOtherRange ( newRange , oldRange ) ) {
458+ // These are the same ranges, so keep the existing one.
459+ oldRange . count += newRange . count ;
460+ mergedRanges . add ( oldRange ) ;
461+ exactMatch = true ;
462+ break ;
463+ }
464+
465+ // Look at ranges representing missing coverage and add ranges that
466+ // represent the intersection.
467+ if ( oldRange . count === 0 && newRange . count === 0 ) {
468+ if ( doesRangeContainOtherRange ( oldRange , newRange ) ) {
469+ // The new range is completely within the old range. Discard the
470+ // larger (old) range, and keep the smaller (new) range.
471+ mergedRanges . add ( newRange ) ;
472+ } else if ( doesRangeContainOtherRange ( newRange , oldRange ) ) {
473+ // The old range is completely within the new range. Discard the
474+ // larger (new) range, and keep the smaller (old) range.
475+ mergedRanges . add ( oldRange ) ;
476+ }
477+ }
478+ }
479+
480+ // Add new ranges that do not represent missing coverage.
481+ if ( newRange . count > 0 && ! exactMatch ) {
482+ mergedRanges . add ( newRange ) ;
483+ }
484+ }
485+
486+ oldFn . ranges = ArrayFrom ( mergedRanges ) ;
487+ }
488+
489+ function doesRangeEqualOtherRange ( range , otherRange ) {
490+ return range . startOffset === otherRange . startOffset &&
491+ range . endOffset === otherRange . endOffset ;
492+ }
493+
494+ function doesRangeContainOtherRange ( range , otherRange ) {
495+ return range . startOffset <= otherRange . startOffset &&
496+ range . endOffset >= otherRange . endOffset ;
497+ }
498+
371499module . exports = { setupCoverage } ;
0 commit comments