monocart-coverage-reports 2.10.9 → 2.11.0

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.
@@ -1,19 +1,90 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const Util = require('../utils/util.js');
4
+ const { ZipFile } = require('../packages/monocart-coverage-vendor.js');
5
+ const { mergeIstanbulDataList } = require('../istanbul/istanbul.js');
6
+ const { mergeV8DataList } = require('../v8/v8.js');
4
7
 
5
- const rawReport = (reportData, reportOptions, options) => {
8
+ const getMergedData = (type, dataList) => {
9
+ if (type === 'istanbul') {
10
+ return mergeIstanbulDataList(dataList);
11
+ }
12
+ return mergeV8DataList(dataList);
13
+ };
14
+
15
+ const mergeReport = async (rawDir, rawParent) => {
16
+ const coverageFiles = fs.readdirSync(rawDir).filter((n) => n.endsWith('.json') && n.startsWith('coverage-'));
17
+ // console.log(coverageFiles);
18
+
19
+ let type;
20
+ const dataList = [];
21
+ for (const coverageFile of coverageFiles) {
22
+ const jsonPath = path.resolve(rawDir, coverageFile);
23
+ const json = await Util.readJson(jsonPath);
24
+ if (json) {
25
+ // 'id', 'type', 'data'
26
+ type = json.type;
27
+ dataList.push(json);
28
+ }
29
+ Util.rmSync(jsonPath);
30
+ }
31
+
32
+ const mergedData = await getMergedData(type, dataList);
33
+
34
+ const dataId = Util.uid();
35
+ const results = {
36
+ id: dataId,
37
+ type,
38
+ data: mergedData
39
+ };
40
+
41
+ const { cachePath } = Util.getCacheFileInfo('coverage', `${dataId}.merged`, rawDir);
42
+ await Util.writeFile(cachePath, JSON.stringify(results));
43
+
44
+ };
45
+
46
+ const zipReport = (rawDir, rawParent) => {
47
+ const zipName = path.basename(rawDir);
48
+ const zipPath = path.resolve(rawParent, `${zipName}.zip`);
49
+
50
+ return new Promise((resolve) => {
51
+ const zipFile = new ZipFile();
52
+ zipFile.outputStream.pipe(fs.createWriteStream(zipPath)).on('close', function() {
53
+
54
+ // remove raw files after zip
55
+ Util.rmSync(rawDir);
56
+
57
+ resolve();
58
+
59
+ // console.log('done');
60
+ });
61
+ const list = fs.readdirSync(rawDir);
62
+ list.forEach((jsonName) => {
63
+ zipFile.addFile(path.resolve(rawDir, jsonName), `${zipName}/${jsonName}`);
64
+ });
65
+ zipFile.end();
66
+ });
67
+ };
68
+
69
+ const rawReport = async (reportData, reportOptions, options) => {
6
70
  const rawOptions = {
7
71
  outputDir: 'raw',
72
+ merge: false,
73
+ zip: false,
8
74
  ... reportOptions
9
75
  };
10
76
 
11
77
  const cacheDir = options.cacheDir;
78
+ if (!fs.existsSync(cacheDir)) {
79
+ // there is no cache if only inputDir
80
+ Util.logInfo('There is no cache dir for "raw" report');
81
+ return;
82
+ }
83
+
12
84
  const rawDir = path.resolve(options.outputDir, rawOptions.outputDir);
13
85
  // console.log(rawDir, cacheDir);
14
86
  if (fs.existsSync(rawDir)) {
15
- Util.logError(`Failed to save raw report because the dir already exists: ${Util.relativePath(rawDir)}`);
16
- return;
87
+ Util.rmSync(rawDir);
17
88
  }
18
89
 
19
90
  const rawParent = path.dirname(rawDir);
@@ -26,6 +97,17 @@ const rawReport = (reportData, reportOptions, options) => {
26
97
  // just rename the cache folder name
27
98
  fs.renameSync(cacheDir, rawDir);
28
99
 
100
+ // merge
101
+ if (rawOptions.merge) {
102
+ await mergeReport(rawDir, rawParent);
103
+ }
104
+
105
+ // zip
106
+ if (rawOptions.zip) {
107
+ await zipReport(rawDir, rawParent);
108
+ }
109
+
110
+
29
111
  };
30
112
 
31
113
  module.exports = {
@@ -1,8 +1,12 @@
1
+ /**
2
+ * The ranges are first ordered by ascending `startOffset` and then by descending `endOffset`.
3
+ * This corresponds to a pre-order tree traversal.
4
+ */
1
5
 
2
6
  const sortRanges = (ranges) => {
3
7
  ranges.sort((a, b) => {
4
8
  if (a.start === b.start) {
5
- return a.end - b.end;
9
+ return b.end - a.end;
6
10
  }
7
11
  return a.start - b.start;
8
12
  });
@@ -33,8 +37,9 @@ const dedupeFlatRanges = (ranges) => {
33
37
  if (range.end === prevRange.end) {
34
38
  return prevRange;
35
39
  }
36
- // great than the prev end, update the end
37
- prevRange.end = range.end;
40
+
41
+ // less than prev end after new sort
42
+
38
43
  return prevRange;
39
44
  }
40
45
 
@@ -0,0 +1,439 @@
1
+ /**
2
+ * The `mergeV8Coverage` was extracted from following repos for fixing several issues
3
+ * https://github.com/bcoe/v8-coverage
4
+ * https://github.com/demurgos/v8-coverage
5
+ */
6
+
7
+ const RangeTree = require('./range-tree.js');
8
+
9
+
10
+ /**
11
+ * Compares two range coverages.
12
+ *
13
+ * The ranges are first ordered by ascending `startOffset` and then by
14
+ * descending `endOffset`.
15
+ * This corresponds to a pre-order tree traversal.
16
+ */
17
+ function compareRangeCovs(a, b) {
18
+ if (a.startOffset !== b.startOffset) {
19
+ return a.startOffset - b.startOffset;
20
+ }
21
+ return b.endOffset - a.endOffset;
22
+ }
23
+
24
+ /**
25
+ * Compares two function coverages.
26
+ *
27
+ * The result corresponds to the comparison of the root ranges.
28
+ */
29
+ function compareFunctionCovs(a, b) {
30
+ return compareRangeCovs(a.ranges[0], b.ranges[0]);
31
+ }
32
+
33
+ /**
34
+ * @precodition `ranges` are well-formed and pre-order sorted
35
+ */
36
+ function fromSortedRanges(ranges) {
37
+ let root;
38
+ // Stack of parent trees and parent counts.
39
+ const stack = [];
40
+ for (const range of ranges) {
41
+ const node = new RangeTree(range.startOffset, range.endOffset, range.count, []);
42
+ if (!root) {
43
+ root = node;
44
+ stack.push([node, range.count]);
45
+ continue;
46
+ }
47
+ let parent;
48
+ let parentCount;
49
+ while (true) {
50
+ [parent, parentCount] = stack[stack.length - 1];
51
+ // assert: `top !== undefined` (the ranges are sorted)
52
+ if (range.startOffset < parent.end) {
53
+ break;
54
+ } else {
55
+ stack.pop();
56
+ }
57
+
58
+ // prevent crash when v8 incorrectly merges static_initializer's
59
+ if (stack.length === 0) {
60
+ break;
61
+ }
62
+ }
63
+ node.delta -= parentCount;
64
+ parent.children.push(node);
65
+ stack.push([node, range.count]);
66
+ }
67
+ return root;
68
+ }
69
+
70
+ /**
71
+ * Normalizes a function coverage.
72
+ *
73
+ * Sorts the ranges (pre-order sort).
74
+ * TODO: Tree-based normalization of the ranges.
75
+ *
76
+ * @param funcCov Function coverage to normalize.
77
+ */
78
+ function normalizeFunctionCov(funcCov) {
79
+ funcCov.ranges.sort(compareRangeCovs);
80
+ const tree = fromSortedRanges(funcCov.ranges);
81
+ tree.normalize();
82
+ funcCov.ranges = tree.toRanges();
83
+ }
84
+
85
+ /**
86
+ * Normalizes a script coverage.
87
+ *
88
+ * Sorts the function by root range (pre-order sort).
89
+ * This does not normalize the function coverages.
90
+ *
91
+ * @param scriptCov Script coverage to normalize.
92
+ */
93
+ function normalizeScriptCov(scriptCov) {
94
+ scriptCov.functions.sort(compareFunctionCovs);
95
+ }
96
+
97
+
98
+ /**
99
+ * Normalizes a script coverage deeply.
100
+ *
101
+ * Normalizes the function coverages deeply, then normalizes the script coverage
102
+ * itself.
103
+ *
104
+ * @param scriptCov Script coverage to normalize.
105
+ */
106
+ function deepNormalizeScriptCov(scriptCov) {
107
+ for (const funcCov of scriptCov.functions) {
108
+ normalizeFunctionCov(funcCov);
109
+ }
110
+ normalizeScriptCov(scriptCov);
111
+ }
112
+
113
+
114
+ /**
115
+ * Returns a string representation of the root range of the function.
116
+ *
117
+ * This string can be used to match function with same root range.
118
+ * The string is derived from the start and end offsets of the root range of
119
+ * the function.
120
+ * This assumes that `ranges` is non-empty (true for valid function coverages).
121
+ *
122
+ * @param funcCov Function coverage with the range to stringify
123
+ * @internal
124
+ */
125
+ function stringifyFunctionRootRange(funcCov) {
126
+ const rootRange = funcCov.ranges[0];
127
+ return `${rootRange.startOffset.toString(10)};${rootRange.endOffset.toString(10)}`;
128
+ }
129
+
130
+
131
+ function insertChild(parentToNested, parentIndex, tree) {
132
+ let nested = parentToNested.get(parentIndex);
133
+ if (!nested) {
134
+ nested = [];
135
+ parentToNested.set(parentIndex, nested);
136
+ }
137
+ nested.push(tree);
138
+ }
139
+
140
+ function nextChild(openRange, parentToNested) {
141
+ const matchingTrees = [];
142
+
143
+ for (const nested of parentToNested.values()) {
144
+ if (nested.length === 1 && nested[0].start === openRange.start && nested[0].end === openRange.end) {
145
+ matchingTrees.push(nested[0]);
146
+ } else {
147
+ matchingTrees.push(new RangeTree(
148
+ openRange.start,
149
+ openRange.end,
150
+ 0,
151
+ nested
152
+ ));
153
+ }
154
+ }
155
+ parentToNested.clear();
156
+ return mergeRangeTrees(matchingTrees);
157
+ }
158
+
159
+ class StartEventQueue {
160
+
161
+ constructor(queue) {
162
+ this.queue = queue;
163
+ this.nextIndex = 0;
164
+ this.pendingOffset = 0;
165
+ this.pendingTrees = null;
166
+ }
167
+
168
+ setPendingOffset(offset) {
169
+ this.pendingOffset = offset;
170
+ }
171
+
172
+ pushPendingTree(tree) {
173
+ if (this.pendingTrees === null) {
174
+ this.pendingTrees = [];
175
+ }
176
+ this.pendingTrees.push(tree);
177
+ }
178
+
179
+ next() {
180
+ const pendingTrees = this.pendingTrees;
181
+ const nextEvent = this.queue[this.nextIndex];
182
+ if (pendingTrees === null) {
183
+ this.nextIndex++;
184
+ return nextEvent;
185
+ }
186
+ if (!nextEvent) {
187
+ this.pendingTrees = null;
188
+ return {
189
+ offset: this.pendingOffset,
190
+ trees: pendingTrees
191
+ };
192
+ }
193
+ if (this.pendingOffset < nextEvent.offset) {
194
+ this.pendingTrees = null;
195
+ return {
196
+ offset: this.pendingOffset,
197
+ trees: pendingTrees
198
+ };
199
+ }
200
+ if (this.pendingOffset === nextEvent.offset) {
201
+ this.pendingTrees = null;
202
+ for (const tree of pendingTrees) {
203
+ nextEvent.trees.push(tree);
204
+ }
205
+ }
206
+ this.nextIndex++;
207
+ return nextEvent;
208
+
209
+
210
+ }
211
+ }
212
+
213
+ function fromParentTrees(parentTrees) {
214
+ const startToTrees = new Map();
215
+ for (const [parentIndex, parentTree] of parentTrees.entries()) {
216
+ for (const child of parentTree.children) {
217
+ let trees = startToTrees.get(child.start);
218
+ if (!trees) {
219
+ trees = [];
220
+ startToTrees.set(child.start, trees);
221
+ }
222
+ trees.push({
223
+ parentIndex,
224
+ tree: child
225
+ });
226
+ }
227
+ }
228
+ const queue = [];
229
+ for (const [startOffset, trees] of startToTrees) {
230
+ queue.push({
231
+ offset: startOffset,
232
+ trees
233
+ });
234
+ }
235
+ queue.sort((a, b) => {
236
+ return a.offset - b.offset;
237
+ });
238
+ return new StartEventQueue(queue);
239
+ }
240
+
241
+ // eslint-disable-next-line complexity
242
+ function mergeRangeTreeChildren(parentTrees) {
243
+ const result = [];
244
+ const startEventQueue = fromParentTrees(parentTrees);
245
+ const parentToNested = new Map();
246
+ let openRange = null;
247
+
248
+ while (true) {
249
+ const event = startEventQueue.next();
250
+ if (!event) {
251
+ break;
252
+ }
253
+
254
+ if (openRange !== null && openRange.end <= event.offset) {
255
+ result.push(nextChild(openRange, parentToNested));
256
+ openRange = null;
257
+ }
258
+
259
+ if (openRange === null) {
260
+ let openRangeEnd = event.offset + 1;
261
+ for (const { parentIndex, tree } of event.trees) {
262
+ openRangeEnd = Math.max(openRangeEnd, tree.end);
263
+ insertChild(parentToNested, parentIndex, tree);
264
+ }
265
+ startEventQueue.setPendingOffset(openRangeEnd);
266
+ openRange = {
267
+ start: event.offset, end: openRangeEnd
268
+ };
269
+ } else {
270
+ for (const { parentIndex, tree } of event.trees) {
271
+ if (tree.end > openRange.end) {
272
+ const right = tree.split(openRange.end);
273
+ startEventQueue.pushPendingTree({
274
+ parentIndex,
275
+ tree: right
276
+ });
277
+ }
278
+ insertChild(parentToNested, parentIndex, tree);
279
+ }
280
+ }
281
+ }
282
+ if (openRange !== null) {
283
+ result.push(nextChild(openRange, parentToNested));
284
+ }
285
+
286
+ return result;
287
+ }
288
+
289
+ /**
290
+ * @precondition Same `start` and `end` for all the trees
291
+ */
292
+ function mergeRangeTrees(trees) {
293
+ if (trees.length <= 1) {
294
+ return trees[0];
295
+ }
296
+ const first = trees[0];
297
+ let delta = 0;
298
+ for (const tree of trees) {
299
+ delta += tree.delta;
300
+ }
301
+ const children = mergeRangeTreeChildren(trees);
302
+ return new RangeTree(first.start, first.end, delta, children);
303
+ }
304
+
305
+ /**
306
+ * Merges a list of matching function coverages.
307
+ *
308
+ * Functions are matching if their root ranges have the same span.
309
+ * The result is normalized.
310
+ * The input values may be mutated, it is not safe to use them after passing
311
+ * them to this function.
312
+ * The computation is synchronous.
313
+ *
314
+ * @param funcCovs Function coverages to merge.
315
+ * @return Merged function coverage, or `undefined` if the input list was empty.
316
+ */
317
+ function mergeFunctionCovs(funcCovs) {
318
+ if (funcCovs.length === 0) {
319
+ return;
320
+ }
321
+
322
+ if (funcCovs.length === 1) {
323
+ const merged = funcCovs[0];
324
+ normalizeFunctionCov(merged);
325
+ return merged;
326
+ }
327
+
328
+ const first = funcCovs[0];
329
+ const functionName = first.functionName;
330
+ // assert: `first.ranges.length > 0`
331
+ const startOffset = first.ranges[0].startOffset;
332
+ const endOffset = first.ranges[0].endOffset;
333
+ let count = 0;
334
+
335
+ const trees = [];
336
+ for (const funcCov of funcCovs) {
337
+ // assert: `funcCov.ranges.length > 0`
338
+ // assert: `funcCov.ranges` is sorted
339
+ count += typeof funcCov.count === 'number' ? funcCov.count : funcCov.ranges[0].count;
340
+ if (funcCov.isBlockCoverage) {
341
+ trees.push(fromSortedRanges(funcCov.ranges));
342
+ }
343
+ }
344
+
345
+ let isBlockCoverage;
346
+ let ranges;
347
+ if (trees.length > 0) {
348
+ isBlockCoverage = true;
349
+ const mergedTree = mergeRangeTrees(trees);
350
+ mergedTree.normalize();
351
+ ranges = mergedTree.toRanges();
352
+ } else {
353
+ isBlockCoverage = false;
354
+ ranges = [{
355
+ startOffset,
356
+ endOffset,
357
+ count
358
+ }];
359
+ }
360
+
361
+ const merged = {
362
+ functionName,
363
+ ranges,
364
+ isBlockCoverage
365
+ };
366
+ if (count !== ranges[0].count) {
367
+ merged.count = count;
368
+ }
369
+ // assert: `merged` is normalized
370
+ return merged;
371
+ }
372
+
373
+
374
+ /**
375
+ * Merges a list of matching script coverages.
376
+ *
377
+ * Scripts are matching if they have the same `url`.
378
+ * The result is normalized.
379
+ * The input values may be mutated, it is not safe to use them after passing
380
+ * them to this function.
381
+ * The computation is synchronous.
382
+ *
383
+ * @param scriptCovs Process coverages to merge.
384
+ * @return Merged script coverage, or `undefined` if the input list was empty.
385
+ */
386
+
387
+ // eslint-disable-next-line complexity
388
+ function mergeV8Coverage(scriptCovs) {
389
+ if (!Array.isArray(scriptCovs)) {
390
+ return {
391
+ functions: []
392
+ };
393
+ }
394
+
395
+ if (scriptCovs.length === 0) {
396
+ return {
397
+ functions: []
398
+ };
399
+ }
400
+
401
+ if (scriptCovs.length === 1) {
402
+ const merged = scriptCovs[0];
403
+ deepNormalizeScriptCov(merged);
404
+ return merged;
405
+ }
406
+
407
+
408
+ const rangeToFuncs = new Map();
409
+ for (const scriptCov of scriptCovs) {
410
+ for (const funcCov of scriptCov.functions) {
411
+ const rootRange = stringifyFunctionRootRange(funcCov);
412
+ let funcCovs = rangeToFuncs.get(rootRange);
413
+ if (!funcCovs) {
414
+ funcCovs = [];
415
+ rangeToFuncs.set(rootRange, funcCovs);
416
+ }
417
+ funcCovs.push(funcCov);
418
+ }
419
+ }
420
+
421
+ const functions = [];
422
+ for (const funcCovs of rangeToFuncs.values()) {
423
+ // assert: `funcCovs.length > 0`
424
+ const block = mergeFunctionCovs(funcCovs);
425
+ if (block) {
426
+ functions.push(block);
427
+ }
428
+ }
429
+
430
+ const merged = {
431
+ functions
432
+ };
433
+ normalizeScriptCov(merged);
434
+ return merged;
435
+ }
436
+
437
+ module.exports = {
438
+ mergeV8Coverage
439
+ };
@@ -0,0 +1,123 @@
1
+ class RangeTree {
2
+
3
+ constructor(
4
+ start,
5
+ end,
6
+ delta,
7
+ children
8
+ ) {
9
+ this.start = start;
10
+ this.end = end;
11
+ this.delta = delta;
12
+ this.children = children;
13
+ }
14
+
15
+ // eslint-disable-next-line complexity
16
+ normalize() {
17
+ const children = [];
18
+ let curEnd;
19
+ let head;
20
+ const tail = [];
21
+ for (const child of this.children) {
22
+ if (!head) {
23
+ head = child;
24
+ } else if (child.delta === head.delta && child.start === curEnd) {
25
+ tail.push(child);
26
+ } else {
27
+ endChain();
28
+ head = child;
29
+ }
30
+ curEnd = child.end;
31
+ }
32
+ if (head) {
33
+ endChain();
34
+ }
35
+
36
+ if (children.length === 1) {
37
+ const child = children[0];
38
+ if (child.start === this.start && child.end === this.end) {
39
+ this.delta += child.delta;
40
+ this.children = child.children;
41
+ // `.lazyCount` is zero for both (both are after normalization)
42
+ return;
43
+ }
44
+ }
45
+
46
+ this.children = children;
47
+
48
+ function endChain() {
49
+ if (tail.length !== 0) {
50
+ head.end = tail[tail.length - 1].end;
51
+ for (const tailTree of tail) {
52
+ for (const subChild of tailTree.children) {
53
+ subChild.delta += tailTree.delta - head.delta;
54
+ head.children.push(subChild);
55
+ }
56
+ }
57
+ tail.length = 0;
58
+ }
59
+ head.normalize();
60
+ children.push(head);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * @precondition `tree.start < value && value < tree.end`
66
+ * @return RangeTree Right part
67
+ */
68
+ split(value) {
69
+ let leftChildLen = this.children.length;
70
+ let mid;
71
+
72
+ // TODO(perf): Binary search (check overhead)
73
+ for (let i = 0; i < this.children.length; i++) {
74
+ const child = this.children[i];
75
+ if (child.start < value && value < child.end) {
76
+ mid = child.split(value);
77
+ leftChildLen = i + 1;
78
+ break;
79
+ } else if (child.start >= value) {
80
+ leftChildLen = i;
81
+ break;
82
+ }
83
+ }
84
+
85
+ const rightLen = this.children.length - leftChildLen;
86
+ const rightChildren = this.children.splice(leftChildLen, rightLen);
87
+ if (mid) {
88
+ rightChildren.unshift(mid);
89
+ }
90
+ const result = new RangeTree(
91
+ value,
92
+ this.end,
93
+ this.delta,
94
+ rightChildren
95
+ );
96
+ this.end = value;
97
+ return result;
98
+ }
99
+
100
+ /**
101
+ * Get the range coverages corresponding to the tree.
102
+ *
103
+ * The ranges are pre-order sorted.
104
+ */
105
+ toRanges() {
106
+ const ranges = [];
107
+ // Stack of parent trees and counts.
108
+ const stack = [[this, 0]];
109
+ while (stack.length > 0) {
110
+ const [cur, parentCount] = stack.pop();
111
+ const count = parentCount + cur.delta;
112
+ ranges.push({
113
+ startOffset: cur.start, endOffset: cur.end, count
114
+ });
115
+ for (let i = cur.children.length - 1; i >= 0; i--) {
116
+ stack.push([cur.children[i], count]);
117
+ }
118
+ }
119
+ return ranges;
120
+ }
121
+ }
122
+
123
+ module.exports = RangeTree;