monocart-coverage-reports 2.3.4 → 2.4.1

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
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![](https://img.shields.io/npm/v/monocart-coverage-reports)](https://www.npmjs.com/package/monocart-coverage-reports)
4
4
  [![](https://badgen.net/npm/dw/monocart-coverage-reports)](https://www.npmjs.com/package/monocart-coverage-reports)
5
+ ![](https://img.shields.io/librariesio/github/cenfun/monocart-coverage-reports)
5
6
  ![](https://img.shields.io/github/license/cenfun/monocart-coverage-reports)
6
7
  ![](https://img.shields.io/github/actions/workflow/status/cenfun/monocart-coverage-reports/static.yml)
7
8
 
@@ -405,10 +406,11 @@ console.log(coverageResults.summary);
405
406
 
406
407
  ## Merge Coverage Reports
407
408
  The following usage scenarios may require merging coverage reports:
408
- - When the code is executed in different environments, like Node.js Server Side and browser Client Side (Next.js for instance). Each environment may generate its own coverage report. Merging them can give a more comprehensive view of the test coverage.
409
+ - When the code is executed in different environments, like Node.js Server Side and browser Client Side (Next.js for instance). Each environment may generate its own coverage report. Merging them can give a more comprehensive view of the test coverage. see example [nextjs-with-playwright](https://github.com/cenfun/nextjs-with-playwright) for automatic report merging.
409
410
  - When the code is subjected to different kinds of testing. For example, unit tests with Jest might cover certain parts of the code, while end-to-end tests with Playwright might cover other parts. Merging these different coverage reports can provide a holistic view of what code has been tested.
410
411
  - When tests are run on different machines or different shards, each might produce its own coverage report. Merging these can give a complete picture of the test coverage across all machines or shards.
411
412
 
413
+ If the reports cannot be merged automatically, then here is how to manually merge the reports.
412
414
  First, using the `raw` report to export the original coverage data to the specified directory.
413
415
  ```js
414
416
  const coverageOptions = {
@@ -0,0 +1,646 @@
1
+ const Util = require('../utils/util.js');
2
+
3
+ const getParentFunctionState = (reverseParents) => {
4
+ const parentFunction = reverseParents.find((it) => it._state && it._state.isFunction);
5
+ if (!parentFunction) {
6
+ return;
7
+ }
8
+ return parentFunction._state;
9
+ };
10
+
11
+ const getParentCount = (reverseParents, functionCount) => {
12
+ // parent count
13
+ const parent = reverseParents.find((it) => it._state);
14
+ if (parent) {
15
+ return parent._state.count;
16
+ }
17
+ // root function count
18
+ return functionCount;
19
+ };
20
+
21
+ const getFunctionRange = (start, end, coverageInfo) => {
22
+
23
+ const {
24
+ functionMap, functionNameMap, functionUncoveredRanges
25
+ } = coverageInfo;
26
+
27
+ // exact matched in functionMap
28
+ const range = functionMap.get(start);
29
+ if (range) {
30
+ return range;
31
+ }
32
+ // exact matched in functionNameMap
33
+ const nameRange = functionNameMap.get(start);
34
+ if (nameRange) {
35
+ return nameRange;
36
+ }
37
+
38
+ // find in uncoveredRanges
39
+ return Util.findInRanges(start, end, functionUncoveredRanges, 'startOffset', 'endOffset');
40
+ };
41
+
42
+ const getFunctionBlock = (start, end, functionState) => {
43
+ if (!functionState) {
44
+ return;
45
+ }
46
+
47
+ const { range } = functionState;
48
+ if (!range) {
49
+ return;
50
+ }
51
+
52
+ const {
53
+ blockMap, blockUncoveredRanges, blockCoveredRanges
54
+ } = range;
55
+
56
+ if (!blockMap) {
57
+ return;
58
+ }
59
+
60
+ const block = blockMap.get(start);
61
+ if (block) {
62
+ return block;
63
+ }
64
+
65
+ const uncoveredBlock = Util.findInRanges(start, end, blockUncoveredRanges, 'startOffset', 'endOffset');
66
+ if (uncoveredBlock) {
67
+ return uncoveredBlock;
68
+ }
69
+
70
+ // the block is not exact correct, if there is a block wrapped
71
+ // [x8]{ var a = b || [x4]c }, b is no block, but it can be found in x8 block
72
+ const coveredBlocks = blockCoveredRanges.filter((it) => start >= it.startOffset && end <= it.endOffset);
73
+ if (coveredBlocks.length) {
74
+ return coveredBlocks.pop();
75
+ }
76
+
77
+ };
78
+
79
+ const addNodeCount = (item) => {
80
+
81
+ const { node, reverseParents } = item;
82
+
83
+ if (node._state) {
84
+ return;
85
+ }
86
+
87
+ const { start, end } = node;
88
+ const functionState = getParentFunctionState(reverseParents);
89
+ const block = getFunctionBlock(start, end, functionState);
90
+ if (block) {
91
+ node._state = {
92
+ count: block.count
93
+ };
94
+ }
95
+
96
+ };
97
+
98
+ // =======================================================================================
99
+
100
+ const createBranchGroup = (type, node, parents, branchMap) => {
101
+ const { start, end } = node;
102
+ // clone and reverse parents
103
+ const reverseParents = [].concat(parents).reverse();
104
+ const group = {
105
+ type,
106
+
107
+ start,
108
+ // could be updated if multiple locations
109
+ end,
110
+
111
+ locations: [],
112
+ reverseParents
113
+ };
114
+
115
+ if (type === 'LogicalExpression') {
116
+ // && or ||
117
+ group.operator = node.operator;
118
+ }
119
+
120
+ branchMap.set(start, group);
121
+ return group;
122
+ };
123
+
124
+ const addBranch = (group, node, locationMap) => {
125
+ const { start, end } = node;
126
+
127
+ const branchStart = group.start;
128
+
129
+ const branchInfo = {
130
+ // for get previous group for LogicalExpression
131
+ branchStart,
132
+ start,
133
+ end,
134
+ // branch count default to 0
135
+ count: 0
136
+ };
137
+
138
+ group.locations.push(branchInfo);
139
+
140
+ // update group end
141
+ if (end > group.end) {
142
+ group.end = end;
143
+ }
144
+
145
+ locationMap.set(start, branchInfo);
146
+
147
+ return branchInfo;
148
+ };
149
+
150
+ const addNoneBranch = (group) => {
151
+ group.locations.push({
152
+ none: true,
153
+ count: 0
154
+ });
155
+ };
156
+
157
+ // =======================================================================================
158
+
159
+ const updateBlockLocations = (locations) => {
160
+ const noBlockList = [];
161
+ let blockCount = 0;
162
+ locations.forEach((item) => {
163
+ if (item.block) {
164
+ item.count = item.block.count;
165
+ blockCount += item.count;
166
+ return;
167
+ }
168
+ noBlockList.push(item);
169
+ });
170
+
171
+ return {
172
+ noBlockList,
173
+ blockCount
174
+ };
175
+ };
176
+
177
+ // const a = tf1 ? 'true' : 'false';
178
+ const ConditionalExpression = (group, parentCount) => {
179
+ const { noBlockList, blockCount } = updateBlockLocations(group.locations);
180
+ if (!noBlockList.length) {
181
+ return;
182
+ }
183
+ let count = parentCount - blockCount;
184
+ noBlockList.forEach((item) => {
185
+ item.count = count;
186
+ count = 0;
187
+ });
188
+ };
189
+
190
+ const IfStatement = (group, parentCount) => {
191
+ const { noBlockList, blockCount } = updateBlockLocations(group.locations);
192
+ if (!noBlockList.length) {
193
+ return;
194
+ }
195
+ // console.log(parentCount, 'uncovered list', noBlockList.length, group.start);
196
+ let count = parentCount - blockCount;
197
+ noBlockList.forEach((item) => {
198
+ item.count = count;
199
+ count = 0;
200
+ });
201
+
202
+ };
203
+
204
+ // const b = tf2 || tf1 || a;
205
+ const LogicalExpression = (group, parentCount) => {
206
+ const { noBlockList, blockCount } = updateBlockLocations(group.locations);
207
+ if (!noBlockList.length) {
208
+ return;
209
+ }
210
+
211
+ if (group.operator === '&&') {
212
+ // console.log(group.start, noBlockList);
213
+ noBlockList.forEach((item) => {
214
+ item.count = parentCount;
215
+ });
216
+ return;
217
+ }
218
+
219
+ const count = parentCount - blockCount;
220
+ noBlockList.forEach((item) => {
221
+ item.count = count;
222
+ });
223
+ };
224
+
225
+ const SwitchStatement = (group, parentCount) => {
226
+ const { noBlockList, blockCount } = updateBlockLocations(group.locations);
227
+ if (!noBlockList.length) {
228
+ return;
229
+ }
230
+ const count = parentCount - blockCount;
231
+ noBlockList.forEach((item) => {
232
+ item.count = count;
233
+ });
234
+ };
235
+
236
+ const AssignmentPattern = (group, parentCount) => {
237
+ group.locations.forEach((item) => {
238
+ item.count = parentCount;
239
+ });
240
+ };
241
+
242
+ // =======================================================================================
243
+
244
+ const updateBranchCount = (group) => {
245
+
246
+ const {
247
+ type, locations, reverseParents
248
+ } = group;
249
+
250
+ const functionState = getParentFunctionState(reverseParents);
251
+
252
+ const functionCount = functionState.count;
253
+
254
+ // default is 0, no need continue
255
+ if (functionCount === 0) {
256
+ return;
257
+ }
258
+
259
+ // parent is block statement or function
260
+ let parentCount = getParentCount(reverseParents, functionCount);
261
+ // parent is group range
262
+ const groupBlock = getFunctionBlock(group.start, group.end, functionState);
263
+ if (groupBlock) {
264
+ parentCount = groupBlock.count;
265
+ }
266
+
267
+ // calculate branches count
268
+ locations.forEach((item) => {
269
+ const {
270
+ start, end, none
271
+ } = item;
272
+ if (none) {
273
+ return;
274
+ }
275
+ item.block = getFunctionBlock(start, end, functionState);
276
+ });
277
+
278
+
279
+ const handlers = {
280
+ ConditionalExpression,
281
+ IfStatement,
282
+ LogicalExpression,
283
+ SwitchStatement,
284
+ AssignmentPattern
285
+ };
286
+
287
+ const handler = handlers[type];
288
+ if (handler) {
289
+ handler(group, parentCount);
290
+ }
291
+
292
+ };
293
+
294
+ const generateBranches = (branchMap) => {
295
+
296
+ // calculate count for all branches
297
+ branchMap.forEach((group) => {
298
+ updateBranchCount(group);
299
+ });
300
+
301
+ // init branches
302
+ const branches = [];
303
+ branchMap.forEach((group) => {
304
+
305
+ // add start/end for none with group start/end
306
+ group.locations.forEach((item) => {
307
+ if (item.none) {
308
+ item.start = group.start;
309
+ item.end = group.end;
310
+ }
311
+ });
312
+
313
+ branches.push({
314
+ type: group.type,
315
+ start: group.start,
316
+ end: group.end,
317
+ locations: group.locations
318
+ });
319
+ });
320
+
321
+ // sort branches
322
+ branches.sort((a, b) => {
323
+ return a.start - b.start;
324
+ });
325
+
326
+ return branches;
327
+ };
328
+
329
+ // =======================================================================================
330
+ // All programs in JavaScript are made of statements and they end with semicolons (;) except block statements which is used to group zero or more statements.
331
+ // Statements are just perform some actions but do not produce any value or output whereas expressions return some value.
332
+ // Expressions return value, statements do not.
333
+ const generateStatements = (statementNodes) => {
334
+
335
+ statementNodes.forEach((item) => {
336
+ const {
337
+ node,
338
+ reverseParents
339
+ } = item;
340
+
341
+ const { start, end } = node;
342
+
343
+ item.count = 1;
344
+
345
+ // isFunction
346
+ if (node._state) {
347
+ item.count = node._state.count;
348
+ return;
349
+ }
350
+
351
+ const functionState = getParentFunctionState(reverseParents);
352
+ if (!functionState) {
353
+ // there is a root range, it impossible not found
354
+ return;
355
+ }
356
+
357
+ // function uncovered
358
+ if (functionState.count === 0) {
359
+ item.count = 0;
360
+ return;
361
+ }
362
+
363
+ const block = getFunctionBlock(start, end, functionState);
364
+ if (block) {
365
+ item.count = block.count;
366
+ return;
367
+ }
368
+
369
+ item.count = functionState.count;
370
+
371
+ });
372
+
373
+ // remove block statement
374
+ statementNodes = statementNodes.filter((item) => {
375
+ return item.node.type !== 'BlockStatement';
376
+ });
377
+
378
+ const statements = statementNodes.map((item) => {
379
+ const {
380
+ node,
381
+ count
382
+ } = item;
383
+ const { start, end } = node;
384
+ return {
385
+ start,
386
+ end,
387
+ count
388
+ };
389
+ });
390
+
391
+ return statements;
392
+ };
393
+
394
+ // =======================================================================================
395
+
396
+ const collectNodes = (ast) => {
397
+ const functionNodes = [];
398
+ const statementNodes = [];
399
+ const blockNodes = [];
400
+ const branchMap = new Map();
401
+ // locationMap for chain LogicalExpression locations
402
+ const locationMap = new Map();
403
+
404
+ Util.visitAst(ast, {
405
+
406
+ VariableDeclarator(node, parents) {
407
+
408
+ // =============================================================
409
+ // id: { type: 'Identifier', name: '__webpack_modules__' },
410
+ const name = node.id && node.id.name;
411
+ if (name === '__webpack_modules__' && node.init) {
412
+ // mark all as wrap function
413
+ node.init.properties.forEach((it) => {
414
+ it.value._wrapKey = it.key && it.key.value;
415
+ });
416
+ // console.log('==========================', node.type);
417
+ // console.log(name, parents.length);
418
+ }
419
+ // =============================================================
420
+
421
+ },
422
+
423
+ // ===============================================================================
424
+ // statements
425
+
426
+ Statement: (node, parents) => {
427
+ const reverseParents = [].concat(parents).reverse();
428
+ statementNodes.push({
429
+ node,
430
+ reverseParents
431
+ });
432
+ },
433
+
434
+
435
+ // ===============================================================================
436
+ // functions
437
+ // Function include FunctionDeclaration, ArrowFunctionExpression, FunctionExpression
438
+ Function(node, parents) {
439
+ const reverseParents = [].concat(parents).reverse();
440
+ functionNodes.push({
441
+ node,
442
+ reverseParents
443
+ });
444
+ },
445
+
446
+ // ===============================================================================
447
+ // branches
448
+
449
+ // `for` block, the count not equal to parent
450
+ BlockStatement: (node, parents) => {
451
+
452
+ // fix branch count: BRDA
453
+ const reverseParents = [].concat(parents).reverse();
454
+ blockNodes.push({
455
+ node,
456
+ reverseParents
457
+ });
458
+
459
+ },
460
+
461
+ // default-arg assignment logic.
462
+ // function default arguments
463
+ AssignmentPattern: (node, parents) => {
464
+ const group = createBranchGroup('AssignmentPattern', node, parents, branchMap);
465
+ addBranch(group, node, locationMap);
466
+ },
467
+
468
+ // cond-expr a ternary expression. e.g.: x ? y : z
469
+ // Ternary
470
+ // var b = a ? 'consequent' : 'alternate';
471
+ ConditionalExpression: (node, parents) => {
472
+ const { consequent, alternate } = node;
473
+ const group = createBranchGroup('ConditionalExpression', node, parents, branchMap);
474
+ addBranch(group, consequent, locationMap);
475
+ addBranch(group, alternate, locationMap);
476
+ },
477
+
478
+ // if an if statement; can also be else if.
479
+ // An IF statement always has exactly two branches:
480
+ // one where the condition is FALSE and one where the condition is TRUE
481
+ IfStatement: (node, parents) => {
482
+ const { consequent, alternate } = node;
483
+ const group = createBranchGroup('IfStatement', node, parents, branchMap);
484
+ addBranch(group, consequent, locationMap);
485
+
486
+ // console.log('if type', consequent.type);
487
+
488
+ if (alternate) {
489
+ addBranch(group, alternate, locationMap);
490
+ } else {
491
+ // add none branch
492
+ addNoneBranch(group);
493
+ // no need update group end, there is no end
494
+ }
495
+ },
496
+
497
+ // binary-expr a logical expression with a binary operand. e.g.: x && y
498
+ // var b = a || b || c;
499
+ // do not use BinaryExpression
500
+ LogicalExpression: (node, parents) => {
501
+ const { left, right } = node;
502
+ // console.log(left.start, right.start);
503
+
504
+ let group;
505
+ // link to same branch start if LogicalExpression
506
+ const prevLocation = locationMap.get(node.start);
507
+ if (prevLocation) {
508
+ // console.log('link branch ==================', type);
509
+ group = branchMap.get(prevLocation.branchStart);
510
+ } else {
511
+ group = createBranchGroup('LogicalExpression', node, parents, branchMap);
512
+ addBranch(group, left, locationMap);
513
+ }
514
+
515
+ addBranch(group, right, locationMap);
516
+
517
+ // console.log(group.locations.map((it) => it.start));
518
+
519
+ // sort branch locations
520
+ // a || b || c
521
+ // first, left a and right c
522
+ // then, left a and right b
523
+ if (prevLocation) {
524
+ const { locations } = group;
525
+ locations.sort((a, b) => {
526
+ return a.start - b.start;
527
+ });
528
+ // update group end after sorted
529
+ const lastEnd = locations[locations.length - 1].end;
530
+ if (lastEnd > group.end) {
531
+ group.end = lastEnd;
532
+ }
533
+ }
534
+ },
535
+
536
+ // switch a switch statement.
537
+ SwitchStatement: (node, parents) => {
538
+ const group = createBranchGroup('SwitchStatement', node, parents, branchMap);
539
+ const cases = node.cases;
540
+ cases.forEach((switchCase) => {
541
+ // console.log('switchCase', switchCase.start);
542
+ addBranch(group, switchCase, locationMap);
543
+ });
544
+ }
545
+
546
+ });
547
+
548
+ return {
549
+ functionNodes,
550
+ statementNodes,
551
+ blockNodes,
552
+ branchMap
553
+ };
554
+ };
555
+
556
+ const getRootFunctionState = (ast, coverageInfo) => {
557
+ const rootState = {
558
+ isFunction: true,
559
+ count: 1
560
+ };
561
+ const rootRange = coverageInfo.rootRange;
562
+ if (rootRange) {
563
+ rootState.range = rootRange;
564
+ rootState.count = rootRange.count;
565
+
566
+ // could be not from 0
567
+ // 0 881 { startOffset: 77, endOffset: 881,
568
+ // const { start, end } = ast;
569
+ // console.log(start, end, rootRange);
570
+
571
+ }
572
+ ast._state = rootState;
573
+
574
+ return rootState;
575
+ };
576
+
577
+ const collectAstInfo = (ast, coverageInfo) => {
578
+
579
+ const {
580
+ functionNodes, statementNodes, blockNodes, branchMap
581
+ } = collectNodes(ast);
582
+
583
+ // root function state
584
+ const rootState = getRootFunctionState(ast, coverageInfo);
585
+
586
+ const functions = [];
587
+ functionNodes.forEach((item) => {
588
+ const { node } = item;
589
+ const {
590
+ start, end, id
591
+ } = node;
592
+ const bodyStart = node.body.start;
593
+ const bodyEnd = node.body.end;
594
+ const functionName = id && id.name;
595
+ const isWrap = Boolean(node._wrapKey);
596
+
597
+ const functionItem = {
598
+ start,
599
+ end,
600
+ bodyStart,
601
+ bodyEnd,
602
+ functionName,
603
+ count: rootState.count
604
+ };
605
+ if (isWrap) {
606
+ functionItem.wrap = true;
607
+ }
608
+ functions.push(functionItem);
609
+
610
+ const functionState = {
611
+ isFunction: true,
612
+ count: functionItem.count
613
+ };
614
+ const functionRange = getFunctionRange(start, end, coverageInfo);
615
+ if (functionRange) {
616
+ if (isWrap) {
617
+ functionRange.wrap = true;
618
+ }
619
+ functionState.range = functionRange;
620
+ functionState.count = functionRange.count;
621
+ functionItem.count = functionRange.count;
622
+ }
623
+ node._state = functionState;
624
+
625
+ // console.log(item.reverseParents.map((it) => it.type));
626
+
627
+ });
628
+
629
+ blockNodes.forEach((item) => {
630
+ addNodeCount(item);
631
+ });
632
+
633
+ const branches = generateBranches(branchMap);
634
+ const statements = generateStatements(statementNodes);
635
+
636
+ return {
637
+ functions,
638
+ branches,
639
+ statements
640
+ };
641
+
642
+ };
643
+
644
+ module.exports = {
645
+ collectAstInfo
646
+ };