monocart-coverage-reports 2.4.2 → 2.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,6 +24,8 @@
24
24
  * [Node.js V8 Coverage Report for Server Side](#nodejs-v8-coverage-report-for-server-side)
25
25
  * [Multiprocessing Support](#multiprocessing-support)
26
26
  * [Merge Coverage Reports](#merge-coverage-reports)
27
+ * [Resolve `sourcePath` for the Source Files](#resolve-sourcepath-for-the-source-files)
28
+ * [Adding Empty Coverage for Untested Files](#adding-empty-coverage-for-untested-files)
27
29
  * [Ignoring Uncovered Codes](#ignoring-uncovered-codes)
28
30
  * [Chromium Coverage API](#chromium-coverage-api)
29
31
  * [V8 Coverage Data Format](#v8-coverage-data-format)
@@ -70,8 +72,6 @@ console.log(coverageResults.summary);
70
72
  - [V8 coverage-report.json](https://cenfun.github.io/monocart-coverage-reports/v8-and-istanbul/coverage-report.json)
71
73
  - `codecov` (V8 data only)
72
74
  - coverage data for [Codecov](https://docs.codecov.com/docs/codecov-custom-coverage-format), see [example](https://app.codecov.io/github/cenfun/monocart-coverage-reports)
73
-
74
- ![](test/console-summary.png)
75
75
 
76
76
  > Istanbul [build-in reports](https://github.com/istanbuljs/istanbuljs/tree/master/packages/istanbul-reports/lib)
77
77
  - `clover`
@@ -95,6 +95,7 @@ console.log(coverageResults.summary);
95
95
 
96
96
  > Other reports
97
97
  - `console-summary` shows coverage summary in console
98
+ ![](test/console-summary.png)
98
99
  - `raw` only keep all original data, which can be used for other reports input with `inputDir`
99
100
  - see [Merge Coverage Reports](#merge-coverage-reports)
100
101
 
@@ -258,16 +259,14 @@ mcr "node ./test/test-node-env.js" -c test/cli-options.js
258
259
  | Coverage data | [Istanbul](https://github.com/gotwarlost/istanbul/blob/master/coverage.json.md) (Object) | [V8](#v8-coverage-data-format) (Array) | [V8](#v8-coverage-data-format) (Array) |
259
260
  | Output | [Istanbul reports](#available-reports) | [V8 reports](#available-reports) | [Istanbul reports](#available-reports) |
260
261
  | - Bytes | ❌ | ✅ | ❌ |
261
- | - Statements | ✅ | ❌ | ☑️❔ |
262
- | - Branches | ✅ | ☑️❔ | ☑️❔ |
262
+ | - Statements | ✅ | ✅ | ✅ |
263
+ | - Branches | ✅ | ✅ | ✅ |
263
264
  | - Functions | ✅ | ✅ | ✅ |
264
265
  | - Lines | ✅ | ✅ | ✅ |
265
266
  | - Execution counts | ✅ | ✅ | ✅ |
266
267
  | CSS coverage | ❌ | ✅ | ✅ |
267
268
  | Minified code | ❌ | ✅ | ❌ |
268
269
 
269
- ❔ - Partial or conditional support
270
-
271
270
  ## Compare Workflows
272
271
  - Istanbul Workflows
273
272
  - 1, [Collecting Istanbul coverage data](#collecting-istanbul-coverage-data)
@@ -451,7 +450,10 @@ const coverageOptions = {
451
450
  };
452
451
  await new CoverageReport(coverageOptions).generate();
453
452
  ```
454
- If the source file comes from the sourcemap, then its path is a virtual path. Using the `sourcePath` option to convert it.
453
+
454
+ ## Resolve `sourcePath` for the Source Files
455
+ If the source file comes from the sourcemap, then its path is a virtual path. Using the `sourcePath` option to resolve a custom path.
456
+ For example, we have tested multiple dist files, which contain some common files. We hope to merge the coverage of the same files, so we need to unify the `sourcePath` in order to be able to merge the coverage data.
455
457
  ```js
456
458
  const coverageOptions = {
457
459
  sourcePath: (filePath) => {
@@ -468,6 +470,44 @@ const coverageOptions = {
468
470
  ```
469
471
  see example: [./test/test-merge.js](./test/test-merge.js)
470
472
 
473
+ ## Adding Empty Coverage for Untested Files
474
+ By default the untested files will not be included in the coverage report, we can first add empty coverage data for all files, so the files with coverage data will be merged, and untested files will retain 0% coverage.
475
+ ```js
476
+ const fs = require('fs');
477
+ const path = require('path');
478
+ const { pathToFileURL } = require('url');
479
+ const MCR = require('monocart-coverage-reports');
480
+
481
+ const dir = "./src";
482
+ const coverageData = [];
483
+ const fileList = fs.readdirSync(dir);
484
+ for (const filename of fileList) {
485
+ const filePath = path.resolve(dir, filename);
486
+ const source = fs.readFileSync(filePath).toString('utf-8');
487
+ const sourcePath = path.relative(process.cwd(), filePath);
488
+ const url = pathToFileURL(sourcePath).toString();
489
+ const extname = path.extname(filename);
490
+ if (['.css'].includes(extname)) {
491
+ coverageData.push({
492
+ empty: true,
493
+ type: 'css',
494
+ url,
495
+ text: source
496
+ });
497
+ } else {
498
+ coverageData.push({
499
+ empty: true,
500
+ type: 'js',
501
+ url,
502
+ source
503
+ });
504
+ }
505
+ }
506
+ const options = require('path-to/same-options.js');
507
+ await MCR(options).add(coverageData);
508
+ ```
509
+ see example: [./test/cli-options.js](./test/cli-options.js)
510
+
471
511
  ## Ignoring Uncovered Codes
472
512
  To ignore codes, use the special comment which starts with `v8 ignore `:
473
513
  - Ignoring all until stop
@@ -575,15 +615,15 @@ see source code [v8-to-istanbul.js](https://github.com/istanbuljs/v8-to-istanbul
575
615
  ### How Monocart Works
576
616
  We implemented new converter instead of v8-to-istanbul:
577
617
  - 1, Trying to fix the middle position if not found the exact mapping for the position.
578
- - 2, Finding all functions and branches by parsing the source code [AST](https://github.com/acornjs/acorn), however the V8 cannot provide effective branch coverage information, so the branches is still not perfect but close to reality.
618
+ - 2, Finding all functions, statements and branches by parsing the source code [AST](https://github.com/acornjs/acorn), however the V8 cannot provide effective branch coverage information for `AssignmentPattern`.
579
619
 
580
620
  | AST | V8 |
581
621
  | :---------------------| :------------- |
582
622
  | AssignmentPattern | 🛇 Not Support |
583
- | ConditionalExpression | ✔ Not Perfect |
584
- | IfStatement | ✔ Not Perfect |
585
- | LogicalExpression | ✔ Not Perfect |
586
- | SwitchStatement | ✔ Not Perfect |
623
+ | ConditionalExpression | ✔ |
624
+ | IfStatement | ✔ |
625
+ | LogicalExpression | ✔ |
626
+ | SwitchStatement | ✔ |
587
627
 
588
628
  ## Debug for Coverage and Sourcemap
589
629
  > Sometimes, the coverage is not what we expect. The next step is to figure out why, and we can easily find out the answer step by step through debugging.
@@ -117,18 +117,21 @@ const createBranchGroup = (type, node, parents, branchMap) => {
117
117
  group.operator = node.operator;
118
118
  }
119
119
 
120
- branchMap.set(start, group);
120
+ // could be same start
121
+ const branchKey = `${start}_${end}`;
122
+
123
+ branchMap.set(branchKey, group);
121
124
  return group;
122
125
  };
123
126
 
124
127
  const addBranch = (group, node, locationMap) => {
125
128
  const { start, end } = node;
126
129
 
127
- const branchStart = group.start;
130
+ const branchKey = `${group.start}_${group.end}`;
128
131
 
129
132
  const branchInfo = {
130
133
  // for get previous group for LogicalExpression
131
- branchStart,
134
+ branchKey,
132
135
  start,
133
136
  end,
134
137
  // branch count default to 0
@@ -203,23 +206,16 @@ const IfStatement = (group, parentCount) => {
203
206
 
204
207
  // const b = tf2 || tf1 || a;
205
208
  const LogicalExpression = (group, parentCount) => {
206
- const { noBlockList, blockCount } = updateBlockLocations(group.locations);
207
- if (!noBlockList.length) {
208
- return;
209
- }
210
209
 
211
- if (group.operator === '&&') {
212
- // console.log(group.start, noBlockList);
213
- noBlockList.forEach((item) => {
210
+ // from left to right
211
+ group.locations.forEach((item, i) => {
212
+ if (item.block) {
213
+ item.count = item.block.count;
214
+ } else {
214
215
  item.count = parentCount;
215
- });
216
- return;
217
- }
218
-
219
- const count = parentCount - blockCount;
220
- noBlockList.forEach((item) => {
221
- item.count = count;
216
+ }
222
217
  });
218
+
223
219
  };
224
220
 
225
221
  const SwitchStatement = (group, parentCount) => {
@@ -408,7 +404,7 @@ const collectNodes = (ast) => {
408
404
  // =============================================================
409
405
  // id: { type: 'Identifier', name: '__webpack_modules__' },
410
406
  const name = node.id && node.id.name;
411
- if (name === '__webpack_modules__' && node.init) {
407
+ if (name === '__webpack_modules__' && node.init && node.init.properties) {
412
408
  // mark all as wrap function
413
409
  node.init.properties.forEach((it) => {
414
410
  it.value._wrapKey = it.key && it.key.value;
@@ -501,12 +497,15 @@ const collectNodes = (ast) => {
501
497
  const { left, right } = node;
502
498
  // console.log(left.start, right.start);
503
499
 
500
+ // could be same branch start
501
+ // const da = arguments.length > 1 && typeof arguments[1] !== 'undefined' ? arguments[1] : true;
502
+
504
503
  let group;
505
504
  // link to same branch start if LogicalExpression
506
505
  const prevLocation = locationMap.get(node.start);
507
506
  if (prevLocation) {
508
507
  // console.log('link branch ==================', type);
509
- group = branchMap.get(prevLocation.branchStart);
508
+ group = branchMap.get(prevLocation.branchKey);
510
509
  } else {
511
510
  group = createBranchGroup('LogicalExpression', node, parents, branchMap);
512
511
  addBranch(group, left, locationMap);
@@ -574,6 +573,26 @@ const getRootFunctionState = (ast, coverageInfo) => {
574
573
  return rootState;
575
574
  };
576
575
 
576
+ const findFunctionRange = (item, coverageInfo) => {
577
+
578
+ const { node, reverseParents } = item;
579
+ const { start, end } = node;
580
+
581
+ // try function start/end
582
+ const functionRange = getFunctionRange(start, end, coverageInfo);
583
+ if (functionRange) {
584
+ return functionRange;
585
+ }
586
+
587
+ // try if class MethodDefinition
588
+ // 0 is function self
589
+ const parent = reverseParents[1];
590
+ if (parent && parent.type === 'MethodDefinition') {
591
+ return getFunctionRange(parent.start, parent.end, coverageInfo);
592
+ }
593
+
594
+ };
595
+
577
596
  const collectAstInfo = (ast, coverageInfo) => {
578
597
 
579
598
  const {
@@ -611,7 +630,8 @@ const collectAstInfo = (ast, coverageInfo) => {
611
630
  isFunction: true,
612
631
  count: functionItem.count
613
632
  };
614
- const functionRange = getFunctionRange(start, end, coverageInfo);
633
+
634
+ const functionRange = findFunctionRange(item, coverageInfo);
615
635
  if (functionRange) {
616
636
  if (isWrap) {
617
637
  functionRange.wrap = true;
@@ -159,8 +159,8 @@ const getJsAstInfo = (item, coverageList) => {
159
159
  // most time for node.js file
160
160
  allowReturnOutsideFunction: true,
161
161
  allowImportExportEverywhere: true,
162
- allowSuperOutsideMethod: true,
163
162
  allowAwaitOutsideFunction: true,
163
+ allowSuperOutsideMethod: true,
164
164
  // first line: #!/usr/bin/env node
165
165
  allowHashBang: true
166
166
  };
@@ -579,11 +579,12 @@ const handleOriginalBranchesCoverage = (state, originalStateMap) => {
579
579
  return newLoc;
580
580
  }
581
581
 
582
+ // before new range
583
+ newLoc.generatedStart = newLoc.start;
584
+ newLoc.generatedEnd = newLoc.end;
582
585
  // mapping to new range
583
586
  newLoc.start = locResult.start;
584
587
  newLoc.end = locResult.end;
585
- newLoc.generatedStart = newLoc.start;
586
- newLoc.generatedEnd = newLoc.end;
587
588
 
588
589
  return newLoc;
589
590
  });
@@ -826,7 +827,6 @@ const getOriginalDecodedMappings = (originalDecodedMap, sourceIndex, locator) =>
826
827
 
827
828
  // add offset and index
828
829
  decodedMappings.forEach((item, i) => {
829
- item.originalIndex = i;
830
830
  item.originalOffset = locator.locationToOffset({
831
831
  line: item.originalLine + 1,
832
832
  column: item.originalColumn
@@ -266,13 +266,13 @@ const getOriginalEndOffset = (m, rangeInfo) => {
266
266
  return m.originalEndOffset;
267
267
  };
268
268
 
269
- const getOriginalText = (m1, m2, rangeInfo) => {
269
+ const getOriginalText = (m1, m2, rangeInfo, direction) => {
270
270
  const originalLocator = rangeInfo.originalState.locator;
271
271
 
272
272
  const o1 = m1.originalOffset;
273
273
  const o2 = m2.originalOffset;
274
274
 
275
- // most of time o1 < o2
275
+ // o1 < o2: most of time
276
276
  if (o1 < o2) {
277
277
  // could be multiple lines for original text
278
278
  return {
@@ -291,10 +291,28 @@ const getOriginalText = (m1, m2, rangeInfo) => {
291
291
  };
292
292
  }
293
293
 
294
- // o1 > o2
294
+ // o1 > o2: should be wrong sourcemap
295
+
296
+ // just reverse offsets if few cross lines
297
+ const crossLines = m1.originalLine - m2.originalLine;
298
+ if (crossLines < 2) {
299
+ return {
300
+ originalOffset: o2,
301
+ originalText: originalLocator.getSlice(o2, o1)
302
+ };
303
+ }
304
+
305
+ // if (direction === 'start') {
306
+ // should be m2 ?
307
+ // console.log(direction, rangeInfo.originalState.sourcePath);
308
+ // console.log(m1, m2);
309
+ // }
310
+
311
+ // should be wrong, ignore m2
312
+ const originalEndOffset = getOriginalEndOffset(m1, rangeInfo);
295
313
  return {
296
- originalOffset: o2,
297
- originalText: originalLocator.getSlice(o2, o1)
314
+ originalOffset: o1,
315
+ originalText: originalLocator.getSlice(o1, originalEndOffset)
298
316
  };
299
317
 
300
318
  };
@@ -350,34 +368,12 @@ const getGeneratedText = (m1, m2, state, offset) => {
350
368
 
351
369
  };
352
370
 
353
- const checkOriginalMappings = (m1, m2, direction) => {
354
- // check original offset order
355
- if (m1.originalOffset <= m2.originalOffset) {
356
- return;
357
- }
358
-
359
- // just reverse offsets if few cross lines
360
- const crossLines = m1.originalLine - m2.originalLine;
361
- if (crossLines < 2) {
362
- return;
363
- }
364
-
365
- // console.log('================================================');
366
- // console.log(crossLines);
367
- // console.log(m1, m2);
368
-
369
- // should be mistake
370
- return {
371
- error: true,
372
- errors: [`invalid original ${direction}: ${EC.yellow(m1.originalOffset)} > ${EC.yellow(m2.originalOffset)}`]
373
- };
374
- };
375
-
376
371
  // ========================================================================================================
377
372
 
378
373
  const getFixedOriginalStart = (start, mappings, state, rangeInfo) => {
379
- const { originalState, crossStart } = rangeInfo;
374
+
380
375
  const [m1, m2] = mappings;
376
+ const { originalState, crossStart } = rangeInfo;
381
377
 
382
378
  // cross file from start of line
383
379
  if (crossStart) {
@@ -387,12 +383,7 @@ const getFixedOriginalStart = (start, mappings, state, rangeInfo) => {
387
383
  }
388
384
 
389
385
  const direction = 'start';
390
- const errorInfo = checkOriginalMappings(m1, m2, direction);
391
- if (errorInfo) {
392
- return errorInfo;
393
- }
394
-
395
- const { originalText, originalOffset } = getOriginalText(m1, m2, rangeInfo);
386
+ const { originalText, originalOffset } = getOriginalText(m1, m2, rangeInfo, direction);
396
387
  const { generatedText, generatedPos } = getGeneratedText(m1, m2, state, start);
397
388
 
398
389
  const originalPos = getFixedPosition({
@@ -402,6 +393,17 @@ const getFixedOriginalStart = (start, mappings, state, rangeInfo) => {
402
393
  originalState
403
394
  }, direction);
404
395
 
396
+ // if (start === 2660) {
397
+ // console.log('=====================================================');
398
+ // console.log('fixed start', originalState.sourcePath);
399
+ // console.log({
400
+ // generatedText,
401
+ // generatedPos,
402
+ // originalText,
403
+ // originalPos
404
+ // });
405
+ // }
406
+
405
407
  return {
406
408
  originalStart: originalOffset + originalPos
407
409
  };
@@ -409,18 +411,8 @@ const getFixedOriginalStart = (start, mappings, state, rangeInfo) => {
409
411
 
410
412
  const getFixedOriginalEnd = (end, mappings, state, rangeInfo) => {
411
413
 
412
- const { decodedMappings } = state;
413
-
414
- // end is exclusive, try previous one if exact match
415
- const prevMapping = findMapping(decodedMappings, end - 1);
416
- if (prevMapping && !Array.isArray(prevMapping)) {
417
- return {
418
- originalEnd: prevMapping.originalOffset + 1
419
- };
420
- }
421
-
422
- const { originalState, crossEnd } = rangeInfo;
423
414
  const [m1, m2] = mappings;
415
+ const { originalState, crossEnd } = rangeInfo;
424
416
 
425
417
  // cross file until e1 line end (not file end)
426
418
  if (crossEnd) {
@@ -430,13 +422,41 @@ const getFixedOriginalEnd = (end, mappings, state, rangeInfo) => {
430
422
  };
431
423
  }
432
424
 
433
- const direction = 'end';
434
- const errorInfo = checkOriginalMappings(m1, m2, direction);
435
- if (errorInfo) {
436
- return errorInfo;
425
+ // end is exclusive
426
+ // quick check previous one if exact match
427
+ // most of case is matching "}"
428
+ if (m1.generatedOffset === end - 1) {
429
+ return {
430
+ originalEnd: m1.originalOffset + 1
431
+ };
432
+ }
433
+
434
+ // quick check next one if exact match
435
+ // most of case is same string
436
+ if (m2.generatedOffset === end + 1) {
437
+ const gt = state.locator.source[end];
438
+ const oi = m2.originalOffset - 1;
439
+ const ot = originalState.locator.source[oi];
440
+ if (gt === ot) {
441
+ return {
442
+ originalEnd: oi
443
+ };
444
+ }
445
+
446
+ // Not applicable
447
+ // console.log('==================================');
448
+ // console.log({
449
+ // gp: state.sourcePath,
450
+ // g: end,
451
+ // gt,
452
+ // op: originalState.sourcePath,
453
+ // o: oi,
454
+ // ot
455
+ // });
437
456
  }
438
457
 
439
- const { originalText, originalOffset } = getOriginalText(m1, m2, rangeInfo);
458
+ const direction = 'end';
459
+ const { originalText, originalOffset } = getOriginalText(m1, m2, rangeInfo, direction);
440
460
  const { generatedText, generatedPos } = getGeneratedText(m1, m2, state, end);
441
461
 
442
462
  const originalPos = getFixedPosition({
@@ -446,6 +466,17 @@ const getFixedOriginalEnd = (end, mappings, state, rangeInfo) => {
446
466
  originalState
447
467
  }, direction);
448
468
 
469
+ // if (end === 2858) {
470
+ // console.log('=====================================================');
471
+ // console.log('fixed end', originalState.sourcePath);
472
+ // console.log({
473
+ // generatedText,
474
+ // generatedPos,
475
+ // originalText,
476
+ // originalPos
477
+ // });
478
+ // }
479
+
449
480
  return {
450
481
  originalEnd: originalOffset + originalPos
451
482
  };
@@ -614,23 +645,13 @@ const getOriginalState = (startMappings, endMappings, originalMap) => {
614
645
  };
615
646
  };
616
647
 
617
- const findOriginalRange = (start, end, state, originalMap) => {
618
-
619
- const { sourcePath, decodedMappings } = state;
620
-
621
- const createMappingError = (errors) => {
648
+ const getMappingInfo = (decodedMappings, start, end) => {
649
+ // possible no length
650
+ if (decodedMappings.length < 2) {
622
651
  return {
623
652
  error: true,
624
- start,
625
- end,
626
- sourcePath,
627
- errors
653
+ errors: ['invalid decoded mappings (length < 2)']
628
654
  };
629
- };
630
-
631
- // possible no length
632
- if (decodedMappings.length < 2) {
633
- return createMappingError(['not found decoded mappings']);
634
655
  }
635
656
 
636
657
  // start: inclusive
@@ -653,6 +674,33 @@ const findOriginalRange = (start, end, state, originalMap) => {
653
674
  };
654
675
  }
655
676
 
677
+ return {
678
+ startMappings,
679
+ endMappings
680
+ };
681
+ };
682
+
683
+ const findOriginalRange = (start, end, state, originalMap) => {
684
+
685
+ const { sourcePath, decodedMappings } = state;
686
+
687
+ const createMappingError = (errors) => {
688
+ return {
689
+ error: true,
690
+ start,
691
+ end,
692
+ sourcePath,
693
+ errors
694
+ };
695
+ };
696
+
697
+ const mappingInfo = getMappingInfo(decodedMappings, start, end);
698
+ if (mappingInfo.error) {
699
+ return createMappingError(mappingInfo.errors);
700
+ }
701
+
702
+ const { startMappings, endMappings } = mappingInfo;
703
+
656
704
  const rangeInfo = getOriginalState(startMappings, endMappings, originalMap);
657
705
  if (rangeInfo.error) {
658
706
  return createMappingError(rangeInfo.errors);
package/lib/generate.js CHANGED
@@ -63,7 +63,7 @@ const getReportGroup = (reports, lcov, dataType) => {
63
63
  }
64
64
 
65
65
  // add lcovonly report after default report
66
- if (lcov) {
66
+ if (lcov && !reportMap.lcovonly) {
67
67
  reportMap.lcovonly = {};
68
68
  }
69
69