1
1
'use strict' ;
2
2
const {
3
+ ArrayFrom,
3
4
ArrayPrototypeMap,
4
5
ArrayPrototypePush,
5
6
JSONParse,
6
7
MathFloor,
7
8
NumberParseInt,
8
- RegExp,
9
9
RegExpPrototypeExec,
10
10
RegExpPrototypeSymbolSplit,
11
+ SafeMap,
12
+ SafeSet,
11
13
StringPrototypeIncludes,
12
14
StringPrototypeLocaleCompare,
13
15
StringPrototypeStartsWith,
@@ -23,9 +25,7 @@ const { setupCoverageHooks } = require('internal/util');
23
25
const { tmpdir } = require ( 'os' ) ;
24
26
const { join, resolve } = require ( 'path' ) ;
25
27
const { 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 $ / ;
29
29
const 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 + ) ? \* \/ / ;
30
30
const kLineEndingRegex = / \r ? \n $ / u;
31
31
const kLineSplitRegex = / (?< = \r ? \n ) / u;
@@ -95,13 +95,6 @@ class TestCoverage {
95
95
for ( let i = 0 ; i < coverage . length ; ++ i ) {
96
96
const { functions, url } = coverage [ i ] ;
97
97
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
-
105
98
// Split the file source into lines. Make sure the lines maintain their
106
99
// original line endings because those characters are necessary for
107
100
// determining offsets in the file.
@@ -345,8 +338,7 @@ function mapRangeToLines(range, lines) {
345
338
}
346
339
347
340
function 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 ( ) ;
350
342
let dir ;
351
343
352
344
try {
@@ -359,13 +351,149 @@ function getCoverageFromDirectory(coverageDirectory) {
359
351
360
352
const coverageFile = join ( coverageDirectory , entry . name ) ;
361
353
const coverage = JSONParse ( readFileSync ( coverageFile , 'utf8' ) ) ;
362
- return coverage . result ;
354
+
355
+ mergeCoverage ( result , coverage . result ) ;
363
356
}
357
+
358
+ return ArrayFrom ( result . values ( ) ) ;
364
359
} finally {
365
360
if ( dir ) {
366
361
dir . closeSync ( ) ;
367
362
}
368
363
}
369
364
}
370
365
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
+
371
499
module . exports = { setupCoverage } ;
0 commit comments