monocart-coverage-reports 2.5.7 → 2.5.9

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
@@ -37,6 +37,8 @@
37
37
  - [Playwright](#playwright)
38
38
  - [Jest](#jest)
39
39
  - [Vitest](#vitest)
40
+ - [CodeceptJS](#codeceptjs)
41
+ - [WebdriverIO](#webdriverio)
40
42
  - [Codecov](#codecov)
41
43
  - [Coveralls](#coveralls)
42
44
  - [Sonar Cloud](#sonar-cloud)
@@ -46,15 +48,22 @@
46
48
  ## Usage
47
49
  ```js
48
50
  const MCR = require('monocart-coverage-reports');
49
- const options = {
51
+ const coverageOptions = {
52
+ name: 'My Coverage Report - 2024-02-28',
50
53
  outputDir: './coverage-reports',
51
- reports: "v8"
54
+ reports: ["v8", "console-details"]
52
55
  }
53
- const coverageReport = MCR(options);
56
+ const coverageReport = MCR(coverageOptions);
57
+ coverageReport.cleanCache();
58
+
54
59
  await coverageReport.add(coverageData1);
55
60
  await coverageReport.add(coverageData2);
56
- const coverageResults = await coverageReport.generate();
57
- console.log(coverageResults.summary);
61
+
62
+ await coverageReport.generate();
63
+
64
+ // Or
65
+ // const { CoverageReport } = require('monocart-coverage-reports');
66
+ // const coverageReport = new CoverageReport(coverageOptions);
58
67
  ```
59
68
  - [example v8](https://github.com/cenfun/monocart-coverage-reports/blob/main/test/test-v8.js)
60
69
  - [example istanbul](https://github.com/cenfun/monocart-coverage-reports/blob/main/test/test-istanbul.js)
@@ -132,7 +141,7 @@ console.log(coverageResults.summary);
132
141
  ### Multiple Reports:
133
142
  ```js
134
143
  const MCR = require('monocart-coverage-reports');
135
- const options = {
144
+ const coverageOptions = {
136
145
  outputDir: './coverage-reports',
137
146
  reports: [
138
147
  // build-in reports
@@ -151,14 +160,15 @@ const options = {
151
160
  ["custom-reporter-1"],
152
161
  ["custom-reporter-2", {
153
162
  type: "istanbul",
154
- option: "value"
163
+ key: "value"
155
164
  }],
156
165
  // Specify reporter name with local path
157
166
  ['/absolute/path/to/custom-reporter.js']
158
167
 
159
168
  ]
160
169
  }
161
- const coverageReport = MCR(options);
170
+ const coverageReport = MCR(coverageOptions);
171
+ coverageReport.cleanCache();
162
172
  ```
163
173
 
164
174
  ## Using `entryFilter` and `sourceFilter` to filter the results for V8 report
@@ -190,12 +200,22 @@ const coverageOptions = {
190
200
  sourceFilter: (sourcePath) => sourcePath.search(/src\//) !== -1
191
201
  };
192
202
  ```
193
- Or `minimatch` pattern:
203
+ Or using `minimatch` pattern:
194
204
  ```js
195
205
  const coverageOptions = {
196
206
  entryFilter: "**/main.js",
197
207
  sourceFilter: "**/src/**"
198
208
  };
209
+ // supports multiple patterns:
210
+ const coverageOptions = {
211
+ entryFilter: {
212
+ '**/vendor.js': false,
213
+ '**/main.js': true
214
+ },
215
+ sourceFilter: {
216
+ '**/src/**': true
217
+ }
218
+ };
199
219
  ```
200
220
 
201
221
  ## onEnd Hook
@@ -343,21 +363,31 @@ await MCR(coverageOptions).add(jsCoverage);
343
363
  ## Collecting Raw V8 Coverage Data with Puppeteer
344
364
  > Puppeteer does not provide raw v8 coverage data by default. A simple conversion is required, see example: [./test/test-puppeteer.js](./test/test-puppeteer.js)
345
365
  ```js
346
- await page.coverage.startJSCoverage({
347
- // provide raw v8 coverage data
348
- includeRawScriptCoverage: true
349
- });
366
+ await Promise.all([
367
+ page.coverage.startJSCoverage({
368
+ resetOnNavigation: false,
369
+ // provide raw v8 coverage data
370
+ includeRawScriptCoverage: true
371
+ }),
372
+ page.coverage.startCSSCoverage({
373
+ resetOnNavigation: false
374
+ })
375
+ ]);
350
376
 
351
377
  await page.goto(url);
352
378
 
353
- const jsCoverage = await page.coverage.stopJSCoverage();
354
- const rawV8CoverageData = jsCoverage.map((it) => {
355
- // Convert to raw v8 coverage format
379
+ const [jsCoverage, cssCoverage] = await Promise.all([
380
+ page.coverage.stopJSCoverage(),
381
+ page.coverage.stopCSSCoverage()
382
+ ]);
383
+
384
+ // to raw V8 script coverage
385
+ const coverageData = [... jsCoverage.map((it) => {
356
386
  return {
357
387
  source: it.text,
358
388
  ... it.rawScriptCoverage
359
389
  };
360
- }
390
+ }), ... cssCoverage];
361
391
  ```
362
392
 
363
393
  ## Node.js V8 Coverage Report for Server Side
@@ -397,34 +427,40 @@ Possible solutions:
397
427
  > The data will be added to `[outputDir]/.cache`, After the generation of the report, this data will be removed unless debugging has been enabled or a raw report has been used, see [Debug for Coverage and Sourcemap](#debug-for-coverage-and-sourcemap)
398
428
  - Main process, before the start of testing
399
429
  ```js
400
- // clean previous cache before the start of testing (option)
401
430
  const MCR = require('monocart-coverage-reports');
402
- const options = require('path-to/same-options.js');
403
- MCR(options).cleanCache();
431
+ const coverageOptions = require('path-to/same-options.js');
432
+ const coverageReport = MCR(coverageOptions);
433
+ // clean previous cache before the start of testing
434
+ // unless the running environment is new and no cache
435
+ coverageReport.cleanCache();
404
436
  ```
405
437
 
406
438
  - Sub process 1, testing stage 1
407
439
  ```js
408
440
  const MCR = require('monocart-coverage-reports');
409
- const options = require('path-to/same-options.js');
410
- await MCR(options).add(coverageData1);
441
+ const coverageOptions = require('path-to/same-options.js');
442
+ const coverageReport = MCR(coverageOptions);
443
+ // do not clean cache in the stage
444
+ await coverageReport.add(coverageData1);
411
445
  ```
412
446
 
413
447
  - Sub process 2, testing stage 2
414
448
  ```js
415
449
  const MCR = require('monocart-coverage-reports');
416
- const options = require('path-to/same-options.js');
417
- await MCR(options).add(coverageData2);
450
+ const coverageOptions = require('path-to/same-options.js');
451
+ const coverageReport = MCR(coverageOptions);
452
+ // do not clean cache in the stage
453
+ await coverageReport.add(coverageData2);
418
454
  ```
419
455
 
420
456
  - Main process, after the completion of testing
421
457
  ```js
422
458
  // generate coverage reports after the completion of testing
423
459
  const MCR = require('monocart-coverage-reports');
424
- const options = require('path-to/same-options.js');
425
- const coverageReport = MCR(options);
426
- const coverageResults = await coverageReport.generate();
427
- console.log(coverageResults.summary);
460
+ const coverageOptions = require('path-to/same-options.js');
461
+ const coverageReport = MCR(coverageOptions);
462
+ // do not clean cache before generating reports
463
+ await coverageReport.generate();
428
464
  ```
429
465
 
430
466
  ## Merge Coverage Reports
@@ -451,6 +487,7 @@ const coverageOptions = {
451
487
  ```
452
488
  Then, after all the tests are completed, generate a merged report with option `inputDir`:
453
489
  ```js
490
+ // esm syntax
454
491
  import fs from "fs";
455
492
  import { CoverageReport } from 'monocart-coverage-reports';
456
493
  const coverageOptions = {
@@ -503,42 +540,41 @@ const coverageOptions = {
503
540
  ```
504
541
 
505
542
  ## Adding Empty Coverage for Untested Files
506
- 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.
543
+ By default the untested files will not be included in the coverage report, we can add empty coverage data for all files with option `all`, the untested files will show 0% coverage.
507
544
  ```js
508
- const fs = require('fs');
509
- const path = require('path');
510
- const { pathToFileURL } = require('url');
511
- const MCR = require('monocart-coverage-reports');
512
-
513
- const dir = "./src";
514
- const coverageData = [];
515
- const fileList = fs.readdirSync(dir);
516
- for (const filename of fileList) {
517
- const filePath = path.resolve(dir, filename);
518
- const source = fs.readFileSync(filePath).toString('utf-8');
519
- const sourcePath = path.relative(process.cwd(), filePath);
520
- const url = pathToFileURL(sourcePath).toString();
521
- const extname = path.extname(filename);
522
- if (['.css'].includes(extname)) {
523
- coverageData.push({
524
- empty: true,
525
- type: 'css',
526
- url,
527
- text: source
528
- });
529
- } else {
530
- coverageData.push({
531
- empty: true,
532
- type: 'js',
533
- url,
534
- source
535
- });
545
+ const coverageOptions = {
546
+ all: {
547
+ dir: ['./src'],
548
+ filter: (filePath) => {
549
+ return true;
550
+ }
536
551
  }
537
- }
538
- const options = require('path-to/same-options.js');
539
- await MCR(options).add(coverageData);
552
+ };
553
+ ```
554
+ The filter also supports `minimatch` pattern:
555
+ ```js
556
+ const coverageOptions = {
557
+ all: {
558
+ dir: ['./src'],
559
+ filter: '**/*.js'
560
+ }
561
+ };
562
+ // or multiple patterns
563
+ const coverageOptions = {
564
+ all: {
565
+ dir: ['./src'],
566
+ filter: {
567
+ // exclude files
568
+ '**/ignored-*.js': false,
569
+ '**/*.html': false,
570
+ '**/*.ts': false,
571
+ // empty css coverage
572
+ '**/*.scss': "css",
573
+ '**/*': true
574
+ }
575
+ }
576
+ };
540
577
  ```
541
- see example: [./test/cli-options.js](./test/cli-options.js)
542
578
 
543
579
  ## Ignoring Uncovered Codes
544
580
  To ignore codes, use the special comment which starts with `v8 ignore `:
@@ -675,6 +711,12 @@ When `logging` is `debug`, the raw report data will be preserved in `[outputDir]
675
711
  ### [Vitest](https://github.com/vitest-dev/vitest)
676
712
  - [vitest-monocart-coverage](https://github.com/cenfun/vitest-monocart-coverage) - A Vitest custom provider module for coverage reports
677
713
 
714
+ ### [CodeceptJS](https://github.com/codeceptjs/CodeceptJS)
715
+ - [codeceptjs-monocart-coverage](https://github.com/cenfun/codeceptjs-monocart-coverage) - A CodeceptJS plugin for coverage reports
716
+
717
+ ### [WebdriverIO](https://github.com/webdriverio/webdriverio)
718
+ - [wdio-monocart-service](https://github.com/cenfun/wdio-monocart-service) - A WebdriverIO service for coverage reports
719
+
678
720
  ### [Codecov](https://codecov.com/)
679
721
  [![codecov](https://codecov.io/gh/cenfun/monocart-coverage-reports/graph/badge.svg?token=H0LW7UKYU3)](https://codecov.io/gh/cenfun/monocart-coverage-reports)
680
722
  - Supports native `codecov` built-in report ([specification](https://docs.codecov.com/docs/codecov-custom-coverage-format))
@@ -167,19 +167,34 @@ const getJsAstInfo = (item, coverageList) => {
167
167
  allowHashBang: true
168
168
  };
169
169
 
170
+ let err;
170
171
  let ast;
171
172
  try {
172
173
  ast = acorn.parse(source, options);
173
174
  } catch (e) {
175
+ err = e;
176
+ }
177
+
178
+ if (err) {
174
179
 
175
- // empty = untested file, could be .ts, .vue and so on, that can not be parsed normally
176
- if (!empty) {
177
- Util.logInfo(`failed to parse source file: ${item.sourcePath} ${e.message}`);
180
+ // empty = untested file
181
+ // could be .ts, .vue and so on, that can not be parsed normally
182
+ if (empty) {
183
+ return {
184
+ functions: [],
185
+ branches: [],
186
+ statements: []
187
+ };
178
188
  }
189
+
190
+ // runtime code should be parsed successful
191
+ Util.logInfo(`failed to parse source file: ${item.sourcePath} ${err.message}`);
192
+
179
193
  // https://github.com/acornjs/acorn/tree/master/acorn-loose
180
194
  // It is recommended to always try a parse with the regular acorn parser first,
181
195
  // and only fall back to this parser when that one finds syntax errors.
182
196
  ast = acornLoose.parse(source, options);
197
+
183
198
  }
184
199
 
185
200
  const coverageInfo = getCoverageInfo(coverageList);
@@ -697,6 +697,9 @@ const handleOriginalBytesCoverage = (state, originalStateMap) => {
697
697
  // });
698
698
  }
699
699
 
700
+
701
+ const checkList = [];
702
+
700
703
  // check original bytes if fully in a wrapper range
701
704
  originalStateMap.forEach((originalState) => {
702
705
  const bytes = originalState.bytes;
@@ -727,6 +730,30 @@ const handleOriginalBytesCoverage = (state, originalStateMap) => {
727
730
  // console.log('===========================================================', originalState.sourcePath);
728
731
  // console.log(originalState.source.length, endMapping);
729
732
 
733
+ checkList.push({
734
+ originalState,
735
+ startOffset,
736
+ endOffset
737
+ });
738
+
739
+ });
740
+
741
+ if (!checkList.length) {
742
+ return;
743
+ }
744
+
745
+ // there is no state.bytes if not in debug
746
+ // should using coverageList to generate bytes first
747
+ handleBytesCoverage(state);
748
+
749
+ checkList.forEach((it) => {
750
+
751
+ const {
752
+ originalState, startOffset, endOffset
753
+ } = it;
754
+
755
+ // console.log('=============', 'no bytes', originalState.sourcePath);
756
+
730
757
  // only check uncovered range
731
758
  // because a uncovered range could be in a covered wrapper
732
759
  // { start: 0, end: 12137, count: 1 }, could be { start: > 0, end: < 12137, count: 0 }
@@ -735,7 +762,8 @@ const handleOriginalBytesCoverage = (state, originalStateMap) => {
735
762
  continue;
736
763
  }
737
764
  if (startOffset >= range.start && endOffset <= range.end) {
738
- bytes.push({
765
+ // console.log('------------', 'added');
766
+ originalState.bytes.push({
739
767
  start: 0,
740
768
  end: originalState.source.length,
741
769
  count: 0
@@ -746,6 +774,7 @@ const handleOriginalBytesCoverage = (state, originalStateMap) => {
746
774
 
747
775
  });
748
776
 
777
+
749
778
  };
750
779
 
751
780
  // ========================================================================================================
@@ -841,17 +870,34 @@ const getOriginalDecodedMappings = (originalDecodedMap, sourceIndex, locator) =>
841
870
  // ========================================================================================================
842
871
 
843
872
  const getSourceFilter = (options) => {
844
- const sourceFilter = options.sourceFilter;
845
- if (typeof sourceFilter === 'function') {
846
- return sourceFilter;
873
+ const input = options.sourceFilter;
874
+
875
+ // for function handler
876
+ if (typeof input === 'function') {
877
+ return input;
847
878
  }
848
879
 
849
- if (sourceFilter && typeof sourceFilter === 'string') {
880
+ // for single minimatch pattern
881
+ if (input && typeof input === 'string') {
850
882
  return (sourcePath) => {
851
- return minimatch(sourcePath, sourceFilter);
883
+ return minimatch(sourcePath, input);
884
+ };
885
+ }
886
+
887
+ // for patterns
888
+ if (input && typeof input === 'object') {
889
+ const patterns = Object.keys(input);
890
+ return (sourcePath) => {
891
+ for (const pattern of patterns) {
892
+ if (minimatch(sourcePath, pattern)) {
893
+ return input[pattern];
894
+ }
895
+ }
896
+ // false if not matched
852
897
  };
853
898
  }
854
899
 
900
+ // default
855
901
  return () => true;
856
902
  };
857
903
 
@@ -46,6 +46,13 @@ module.exports = {
46
46
  // sourceFilter: (sourcePath) => sourcePath.search(/src\/.+/) !== -1,
47
47
  sourceFilter: null,
48
48
 
49
+ // options for adding empty coverage for all files
50
+ // all: {
51
+ // dir: ['src'],
52
+ // filter: (sourcePath) => true
53
+ // },
54
+ all: null,
55
+
49
56
  // (V8 only) {boolean} Enable/Disable ignoring uncovered codes with the special comments: v8 ignore next/next N/start/stop
50
57
  v8Ignore: true,
51
58
 
@@ -65,9 +72,12 @@ module.exports = {
65
72
  // {object} { bytes:[50,80], statements:[50,80], branches:[50,80], functions:[50,80], lines:[50,80] }
66
73
  watermarks: [50, 80],
67
74
 
68
- // {boolean} clean previous files in output dir before generating report. Defaults to true.
75
+ // {boolean} Indicates whether to clean previous files in output dir before generating report. Defaults to true.
69
76
  clean: true,
70
77
 
78
+ // {boolean} Indicates whether to clean previous cache in output dir before generating report. Defaults to false.
79
+ cleanCache: false,
80
+
71
81
  // {function} onEnd hook
72
82
  // onEnd: async (reportData) => {}
73
83
  onEnd: null
package/lib/generate.js CHANGED
@@ -11,6 +11,9 @@ const { mergeV8Coverage, saveV8Report } = require('./v8/v8.js');
11
11
 
12
12
  const { convertV8List } = require('./converter/converter.js');
13
13
 
14
+ const { minimatch } = require('./packages/monocart-coverage-vendor.js');
15
+ const { pathToFileURL } = require('url');
16
+
14
17
  // ========================================================================================================
15
18
 
16
19
  // maybe v8 or to istanbul reports
@@ -288,8 +291,175 @@ const generateCoverageReports = async (dataList, sourceCache, options) => {
288
291
 
289
292
  // ========================================================================================================
290
293
 
294
+ const resolveAllDirList = (input) => {
295
+ const dirs = Util.toList(input, ',');
296
+ if (!dirs.length) {
297
+ return;
298
+ }
299
+ const dirList = dirs.filter((it) => fs.existsSync(it));
300
+ if (!dirList.length) {
301
+ return;
302
+ }
303
+ return dirList;
304
+ };
305
+
306
+ const resolveAllFilter = (input) => {
307
+ // for function handler
308
+ if (typeof input === 'function') {
309
+ return input;
310
+ }
311
+
312
+ // for single minimatch pattern
313
+ if (input && typeof input === 'string') {
314
+ return (filePath) => {
315
+ return minimatch(filePath, input);
316
+ };
317
+ }
318
+
319
+ // for patterns
320
+ if (input && typeof input === 'object') {
321
+ const patterns = Object.keys(input);
322
+ return (filePath) => {
323
+ for (const pattern of patterns) {
324
+ if (minimatch(filePath, pattern)) {
325
+ return input[pattern];
326
+ }
327
+ }
328
+ // false if not matched
329
+ };
330
+ }
331
+
332
+ // default
333
+ return () => true;
334
+ };
335
+
336
+ const resolveAllDataType = (type, dataList) => {
337
+ if (!type && dataList.length) {
338
+ const firstData = dataList[0];
339
+ type = firstData.type;
340
+ }
341
+ const types = {
342
+ v8: 'v8',
343
+ istanbul: 'istanbul'
344
+ };
345
+ return types[type] || types.v8;
346
+ };
291
347
 
292
- const getInputData = async (inputList) => {
348
+ const resolveAllOptions = (input, dataList) => {
349
+ if (!input) {
350
+ return;
351
+ }
352
+
353
+ let type;
354
+ let dir;
355
+ let filter;
356
+ if (typeof input === 'string' || Array.isArray(input)) {
357
+ dir = input;
358
+ filter = () => true;
359
+ } else {
360
+ type = input.type;
361
+ dir = input.dir;
362
+ filter = input.filter;
363
+ }
364
+
365
+ const dirList = resolveAllDirList(dir);
366
+ if (!dirList) {
367
+ return;
368
+ }
369
+
370
+ const dataType = resolveAllDataType(type, dataList);
371
+ const fileFilter = resolveAllFilter(filter);
372
+
373
+ return {
374
+ dirList,
375
+ dataType,
376
+ fileFilter
377
+ };
378
+ };
379
+
380
+ const resolveFileType = (fileType, filePath) => {
381
+ if (fileType === 'js' || fileType === 'css') {
382
+ return fileType;
383
+ }
384
+ const extname = path.extname(filePath);
385
+ if (['.css', '.scss', '.sass', '.less'].includes(extname)) {
386
+ return 'css';
387
+ }
388
+ return 'js';
389
+ };
390
+
391
+ const getV8EmptyData = (fileList) => {
392
+ const v8DataList = [];
393
+ fileList.forEach((item) => {
394
+ const { filePath, fileType } = item;
395
+ const type = resolveFileType(fileType, filePath);
396
+ const url = pathToFileURL(filePath).toString();
397
+ const source = Util.readFileSync(filePath);
398
+ const entryFile = {
399
+ empty: true,
400
+ type,
401
+ url,
402
+ // for empty, css supports both source and text
403
+ source
404
+ };
405
+ v8DataList.push(entryFile);
406
+ });
407
+ return v8DataList;
408
+ };
409
+
410
+ const getIstanbulEmptyData = (fileList) => {
411
+ const istanbulData = {};
412
+ fileList.forEach((item) => {
413
+ const { filePath } = item;
414
+ istanbulData[filePath] = {
415
+ path: filePath,
416
+ statementMap: {},
417
+ fnMap: {},
418
+ branchMap: {},
419
+ s: {},
420
+ f: {},
421
+ b: {}
422
+ };
423
+ });
424
+ return istanbulData;
425
+ };
426
+
427
+ const getEmptyData = (all, dataList) => {
428
+ const allOptions = resolveAllOptions(all, dataList);
429
+ // console.log('allOptions', all, allOptions);
430
+ if (!allOptions) {
431
+ return;
432
+ }
433
+ const {
434
+ dirList, dataType, fileFilter
435
+ } = allOptions;
436
+ const fileList = [];
437
+ dirList.forEach((dir) => {
438
+ Util.forEachFile(dir, [], (fileName, fileDir) => {
439
+ const filePath = path.resolve(fileDir, fileName);
440
+ const fileType = fileFilter(filePath);
441
+ if (fileType) {
442
+ fileList.push({
443
+ filePath,
444
+ fileType
445
+ });
446
+ }
447
+ });
448
+ });
449
+ // console.log('fileList', fileList);
450
+ if (!fileList.length) {
451
+ return;
452
+ }
453
+ if (dataType === 'v8') {
454
+ return getV8EmptyData(fileList);
455
+ }
456
+ return getIstanbulEmptyData(fileList);
457
+ };
458
+
459
+ // ========================================================================================================
460
+
461
+
462
+ const getInputData = async (inputList, all, cacheDir, addHandler) => {
293
463
  const dataList = [];
294
464
  const sourceCache = new Map();
295
465
 
@@ -321,6 +491,35 @@ const getInputData = async (inputList) => {
321
491
  }
322
492
  }
323
493
  }
494
+
495
+ // add empty coverage for all files
496
+ // dataList for data type, before `add` we don't which one is tested, because using hash id
497
+ const emptyData = getEmptyData(all, dataList);
498
+ if (emptyData) {
499
+ const coverageResults = await addHandler(emptyData);
500
+ // after `add`
501
+ if (coverageResults) {
502
+ dataList.push(coverageResults);
503
+ if (coverageResults.type === 'v8') {
504
+ // sourceCache
505
+ coverageResults.data.forEach((entry) => {
506
+ const { id } = entry;
507
+ if (sourceCache.has(id)) {
508
+ // console.log('+', entry.sourcePath);
509
+ return;
510
+ }
511
+ // console.log('-', entry.sourcePath);
512
+ const sourcePath = Util.resolveCacheSourcePath(cacheDir, id);
513
+ const content = Util.readFileSync(sourcePath);
514
+ if (content) {
515
+ const json = JSON.parse(content);
516
+ sourceCache.set(json.id, json);
517
+ }
518
+ });
519
+ }
520
+ }
521
+ }
522
+
324
523
  return {
325
524
  dataList,
326
525
  sourceCache