monocart-coverage-reports 2.5.7 → 2.5.8

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)
@@ -190,12 +192,22 @@ const coverageOptions = {
190
192
  sourceFilter: (sourcePath) => sourcePath.search(/src\//) !== -1
191
193
  };
192
194
  ```
193
- Or `minimatch` pattern:
195
+ Or using `minimatch` pattern:
194
196
  ```js
195
197
  const coverageOptions = {
196
198
  entryFilter: "**/main.js",
197
199
  sourceFilter: "**/src/**"
198
200
  };
201
+ // supports multiple patterns:
202
+ const coverageOptions = {
203
+ entryFilter: {
204
+ '**/vendor.js': false,
205
+ '**/main.js': true
206
+ },
207
+ sourceFilter: {
208
+ '**/src/**': true
209
+ }
210
+ };
199
211
  ```
200
212
 
201
213
  ## onEnd Hook
@@ -343,21 +355,31 @@ await MCR(coverageOptions).add(jsCoverage);
343
355
  ## Collecting Raw V8 Coverage Data with Puppeteer
344
356
  > 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
357
  ```js
346
- await page.coverage.startJSCoverage({
347
- // provide raw v8 coverage data
348
- includeRawScriptCoverage: true
349
- });
358
+ await Promise.all([
359
+ page.coverage.startJSCoverage({
360
+ resetOnNavigation: false,
361
+ // provide raw v8 coverage data
362
+ includeRawScriptCoverage: true
363
+ }),
364
+ page.coverage.startCSSCoverage({
365
+ resetOnNavigation: false
366
+ })
367
+ ]);
350
368
 
351
369
  await page.goto(url);
352
370
 
353
- const jsCoverage = await page.coverage.stopJSCoverage();
354
- const rawV8CoverageData = jsCoverage.map((it) => {
355
- // Convert to raw v8 coverage format
371
+ const [jsCoverage, cssCoverage] = await Promise.all([
372
+ page.coverage.stopJSCoverage(),
373
+ page.coverage.stopCSSCoverage()
374
+ ]);
375
+
376
+ // to raw V8 script coverage
377
+ const coverageData = [... jsCoverage.map((it) => {
356
378
  return {
357
379
  source: it.text,
358
380
  ... it.rawScriptCoverage
359
381
  };
360
- }
382
+ }), ... cssCoverage];
361
383
  ```
362
384
 
363
385
  ## Node.js V8 Coverage Report for Server Side
@@ -503,42 +525,41 @@ const coverageOptions = {
503
525
  ```
504
526
 
505
527
  ## 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.
528
+ 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
529
  ```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
- });
530
+ const coverageOptions = {
531
+ all: {
532
+ dir: ['./src'],
533
+ filter: (filePath) => {
534
+ return true;
535
+ }
536
536
  }
537
- }
538
- const options = require('path-to/same-options.js');
539
- await MCR(options).add(coverageData);
537
+ };
538
+ ```
539
+ The filter also supports `minimatch` pattern:
540
+ ```js
541
+ const coverageOptions = {
542
+ all: {
543
+ dir: ['./src'],
544
+ filter: '**/*.js'
545
+ }
546
+ };
547
+ // or multiple patterns
548
+ const coverageOptions = {
549
+ all: {
550
+ dir: ['./src'],
551
+ filter: {
552
+ // exclude files
553
+ '**/ignored-*.js': false,
554
+ '**/*.html': false,
555
+ '**/*.ts': false,
556
+ // empty css coverage
557
+ '**/*.scss': "css",
558
+ '**/*': true
559
+ }
560
+ }
561
+ };
540
562
  ```
541
- see example: [./test/cli-options.js](./test/cli-options.js)
542
563
 
543
564
  ## Ignoring Uncovered Codes
544
565
  To ignore codes, use the special comment which starts with `v8 ignore `:
@@ -675,6 +696,12 @@ When `logging` is `debug`, the raw report data will be preserved in `[outputDir]
675
696
  ### [Vitest](https://github.com/vitest-dev/vitest)
676
697
  - [vitest-monocart-coverage](https://github.com/cenfun/vitest-monocart-coverage) - A Vitest custom provider module for coverage reports
677
698
 
699
+ ### [CodeceptJS](https://github.com/codeceptjs/CodeceptJS)
700
+ - [codeceptjs-monocart-coverage](https://github.com/cenfun/codeceptjs-monocart-coverage) - A CodeceptJS plugin for coverage reports
701
+
702
+ ### [WebdriverIO](https://github.com/webdriverio/webdriverio)
703
+ - [wdio-monocart-service](https://github.com/cenfun/wdio-monocart-service) - A WebdriverIO service for coverage reports
704
+
678
705
  ### [Codecov](https://codecov.com/)
679
706
  [![codecov](https://codecov.io/gh/cenfun/monocart-coverage-reports/graph/badge.svg?token=H0LW7UKYU3)](https://codecov.io/gh/cenfun/monocart-coverage-reports)
680
707
  - 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);
@@ -841,17 +841,34 @@ const getOriginalDecodedMappings = (originalDecodedMap, sourceIndex, locator) =>
841
841
  // ========================================================================================================
842
842
 
843
843
  const getSourceFilter = (options) => {
844
- const sourceFilter = options.sourceFilter;
845
- if (typeof sourceFilter === 'function') {
846
- return sourceFilter;
844
+ const input = options.sourceFilter;
845
+
846
+ // for function handler
847
+ if (typeof input === 'function') {
848
+ return input;
847
849
  }
848
850
 
849
- if (sourceFilter && typeof sourceFilter === 'string') {
851
+ // for single minimatch pattern
852
+ if (input && typeof input === 'string') {
850
853
  return (sourcePath) => {
851
- return minimatch(sourcePath, sourceFilter);
854
+ return minimatch(sourcePath, input);
855
+ };
856
+ }
857
+
858
+ // for patterns
859
+ if (input && typeof input === 'object') {
860
+ const patterns = Object.keys(input);
861
+ return (sourcePath) => {
862
+ for (const pattern of patterns) {
863
+ if (minimatch(sourcePath, pattern)) {
864
+ return input[pattern];
865
+ }
866
+ }
867
+ // false if not matched
852
868
  };
853
869
  }
854
870
 
871
+ // default
855
872
  return () => true;
856
873
  };
857
874
 
@@ -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
 
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
package/lib/index.d.ts CHANGED
@@ -220,19 +220,41 @@ declare namespace MCR {
220
220
 
221
221
  /** (V8 only)
222
222
  *
223
- * {string} `minimatch` pattern for entry url; multiple patterns: "{pattern1,pattern2}";
223
+ * {string} `minimatch` pattern for entry url; {object} multiple patterns;
224
224
  *
225
225
  * {function} A filter function for each entry file in the V8 list.
226
226
  * */
227
- entryFilter?: string | ((entry: V8CoverageEntry) => boolean);
227
+ entryFilter?: string | {
228
+ [pattern: string]: boolean;
229
+ } | ((entry: V8CoverageEntry) => boolean);
228
230
 
229
231
  /** (V8 only)
230
232
  *
231
- * {string} `minimatch` pattern for source path; multiple patterns: "{pattern1,pattern2}";
233
+ * {string} `minimatch` pattern for source path; {object} multiple patterns;
232
234
  *
233
235
  * {function} A filter function for each source path when the source is unpacked from the source map.
234
236
  * */
235
- sourceFilter?: string | ((sourcePath: string) => boolean);
237
+ sourceFilter?: string | {
238
+ [pattern: string]: boolean;
239
+ } | ((sourcePath: string) => boolean);
240
+
241
+ /**
242
+ * options for adding empty coverage for all files
243
+ */
244
+ all?: string | string[] | {
245
+ /** the dir(s) of all files */
246
+ dir: string | string[];
247
+ /** the coverage data type, v8 or istanbul */
248
+ type?: "v8" | "istanbul";
249
+ /**
250
+ * the file filter, return the file type, if true, defaults to css if ".css" otherwise is js
251
+ *
252
+ * {string} `minimatch` pattern for file path; {object} multiple patterns; {function} A filter function for file path;
253
+ */
254
+ filter?: string | {
255
+ [pattern: string]: "js" | "css" | boolean;
256
+ } | ((filePath: string) => "js" | "css" | boolean)
257
+ };
236
258
 
237
259
  /** (V8 only) {boolean} Enable/Disable ignoring uncovered codes with the special comments: v8 ignore next/next N/start/stop */
238
260
  v8Ignore?: boolean;
package/lib/index.js CHANGED
@@ -83,7 +83,10 @@ class CoverageReport {
83
83
 
84
84
  this.initOptions();
85
85
 
86
- const { inputDir, cacheDir } = this.options;
86
+ const {
87
+ all, inputDir, cacheDir
88
+ } = this.options;
89
+
87
90
  const inputDirs = Util.toList(inputDir, ',');
88
91
  if (this.hasCache()) {
89
92
  inputDirs.push(cacheDir);
@@ -92,16 +95,16 @@ class CoverageReport {
92
95
  const inputList = inputDirs.filter((dir) => {
93
96
  const hasDir = fs.existsSync(dir);
94
97
  if (!hasDir) {
95
- Util.logError(`Not found coverage data dir: ${Util.relativePath(dir)}`);
98
+ // could be empty
99
+ Util.logInfo(`Not found coverage data dir: ${Util.relativePath(dir)}`);
96
100
  }
97
101
  return hasDir;
98
102
  });
99
103
 
100
- if (!inputList.length) {
101
- return;
102
- }
104
+ const { dataList, sourceCache } = await getInputData(inputList, all, cacheDir, (emptyData) => {
105
+ return this.add(emptyData);
106
+ });
103
107
 
104
- const { dataList, sourceCache } = await getInputData(inputList);
105
108
  if (!dataList.length) {
106
109
  const dirs = inputList.map((dir) => Util.relativePath(dir));
107
110
  Util.logError(`Not found coverage data in dir(s): ${dirs.join(', ')}`);