From 0c5e2570f418eac53b73974e08b444b588ecd867 Mon Sep 17 00:00:00 2001 From: cenfun Date: Wed, 25 Sep 2024 23:18:51 +0800 Subject: [PATCH 1/8] merge raw --- lib/reports/raw.js | 59 +++-- lib/v8/merge/merge.js | 422 +++++++++++++++++++++++++++++++++++ lib/v8/merge/range-tree.js | 123 ++++++++++ lib/v8/v8.js | 2 +- packages/vendor/src/index.js | 4 - test/mcr.config.cli.js | 3 +- test/test-merge.js | 2 +- 7 files changed, 585 insertions(+), 30 deletions(-) create mode 100644 lib/v8/merge/merge.js create mode 100644 lib/v8/merge/range-tree.js diff --git a/lib/reports/raw.js b/lib/reports/raw.js index 873da847..884b3127 100644 --- a/lib/reports/raw.js +++ b/lib/reports/raw.js @@ -3,7 +3,35 @@ const path = require('path'); const Util = require('../utils/util.js'); const { ZipFile } = require('../packages/monocart-coverage-vendor.js'); -const rawReport = (reportData, reportOptions, options) => { +const mergeReport = async (rawDir, rawParent) => { + // const coverageFiles = fs.readdirSync(rawDir).filter((n) => n.endsWith('.json') && n.startsWith('coverage-')); + // console.log(coverageFiles); +}; + +const zipReport = (rawDir, rawParent) => { + const zipName = path.basename(rawDir); + const zipPath = path.resolve(rawParent, `${zipName}.zip`); + + return new Promise((resolve) => { + const zipFile = new ZipFile(); + zipFile.outputStream.pipe(fs.createWriteStream(zipPath)).on('close', function() { + + // remove raw files after zip + Util.rmSync(rawDir); + + resolve(); + + // console.log('done'); + }); + const list = fs.readdirSync(rawDir); + list.forEach((jsonName) => { + zipFile.addFile(path.resolve(rawDir, jsonName), `${zipName}/${jsonName}`); + }); + zipFile.end(); + }); +}; + +const rawReport = async (reportData, reportOptions, options) => { const rawOptions = { outputDir: 'raw', zip: false, @@ -27,31 +55,16 @@ const rawReport = (reportData, reportOptions, options) => { // just rename the cache folder name fs.renameSync(cacheDir, rawDir); - // zip - if (!rawOptions.zip) { - return; + // merge + if (rawOptions.merge) { + await mergeReport(rawDir, rawParent); } - const zipName = path.basename(rawDir); - const zipPath = path.resolve(rawParent, `${zipName}.zip`); - - return new Promise((resolve) => { - const zipFile = new ZipFile(); - zipFile.outputStream.pipe(fs.createWriteStream(zipPath)).on('close', function() { - - // remove raw files after zip - Util.rmSync(rawDir); - - resolve(); + // zip + if (rawOptions.zip) { + await zipReport(rawDir, rawParent); + } - // console.log('done'); - }); - const list = fs.readdirSync(rawDir); - list.forEach((jsonName) => { - zipFile.addFile(path.resolve(rawDir, jsonName), `${zipName}/${jsonName}`); - }); - zipFile.end(); - }); }; diff --git a/lib/v8/merge/merge.js b/lib/v8/merge/merge.js new file mode 100644 index 00000000..031bef8a --- /dev/null +++ b/lib/v8/merge/merge.js @@ -0,0 +1,422 @@ +/** + * The `mergeScriptCovs` was extracted from following repos for fixing several issues + * https://github.com/bcoe/v8-coverage + * https://github.com/demurgos/v8-coverage + */ + +const RangeTree = require('./range-tree.js'); + + +/** + * Compares two range coverages. + * + * The ranges are first ordered by ascending `startOffset` and then by + * descending `endOffset`. + * This corresponds to a pre-order tree traversal. + */ +function compareRangeCovs(a, b) { + if (a.startOffset !== b.startOffset) { + return a.startOffset - b.startOffset; + } + return b.endOffset - a.endOffset; +} + +/** + * Compares two function coverages. + * + * The result corresponds to the comparison of the root ranges. + */ +function compareFunctionCovs(a, b) { + return compareRangeCovs(a.ranges[0], b.ranges[0]); +} + +/** + * @precodition `ranges` are well-formed and pre-order sorted + */ +function fromSortedRanges(ranges) { + let root; + // Stack of parent trees and parent counts. + const stack = []; + for (const range of ranges) { + const node = new RangeTree(range.startOffset, range.endOffset, range.count, []); + if (root === undefined) { + root = node; + stack.push([node, range.count]); + continue; + } + let parent; + let parentCount; + while (true) { + [parent, parentCount] = stack[stack.length - 1]; + // assert: `top !== undefined` (the ranges are sorted) + if (range.startOffset < parent.end) { + break; + } else { + stack.pop(); + } + + // prevent crash when v8 incorrectly merges static_initializer's + if (stack.length === 0) { + break; + } + } + node.delta -= parentCount; + parent.children.push(node); + stack.push([node, range.count]); + } + return root; +} + +/** + * Normalizes a function coverage. + * + * Sorts the ranges (pre-order sort). + * TODO: Tree-based normalization of the ranges. + * + * @param funcCov Function coverage to normalize. + */ +function normalizeFunctionCov(funcCov) { + funcCov.ranges.sort(compareRangeCovs); + const tree = fromSortedRanges(funcCov.ranges); + tree.normalize(); + funcCov.ranges = tree.toRanges(); +} + +/** + * Normalizes a script coverage. + * + * Sorts the function by root range (pre-order sort). + * This does not normalize the function coverages. + * + * @param scriptCov Script coverage to normalize. + */ +function normalizeScriptCov(scriptCov) { + scriptCov.functions.sort(compareFunctionCovs); +} + + +/** + * Normalizes a script coverage deeply. + * + * Normalizes the function coverages deeply, then normalizes the script coverage + * itself. + * + * @param scriptCov Script coverage to normalize. + */ +function deepNormalizeScriptCov(scriptCov) { + for (const funcCov of scriptCov.functions) { + normalizeFunctionCov(funcCov); + } + normalizeScriptCov(scriptCov); +} + + +/** + * Returns a string representation of the root range of the function. + * + * This string can be used to match function with same root range. + * The string is derived from the start and end offsets of the root range of + * the function. + * This assumes that `ranges` is non-empty (true for valid function coverages). + * + * @param funcCov Function coverage with the range to stringify + * @internal + */ +function stringifyFunctionRootRange(funcCov) { + const rootRange = funcCov.ranges[0]; + return `${rootRange.startOffset.toString(10)};${rootRange.endOffset.toString(10)}`; +} + + +function insertChild(parentToNested, parentIndex, tree) { + let nested = parentToNested.get(parentIndex); + if (nested === undefined) { + nested = []; + parentToNested.set(parentIndex, nested); + } + nested.push(tree); +} + +function nextChild(openRange, parentToNested) { + const matchingTrees = []; + + for (const nested of parentToNested.values()) { + if (nested.length === 1 && nested[0].start === openRange.start && nested[0].end === openRange.end) { + matchingTrees.push(nested[0]); + } else { + matchingTrees.push(new RangeTree( + openRange.start, + openRange.end, + 0, + nested + )); + } + } + parentToNested.clear(); + return mergeRangeTrees(matchingTrees); +} + +class StartEventQueue { + + constructor(queue) { + this.queue = queue; + this.nextIndex = 0; + this.pendingOffset = 0; + this.pendingTrees = undefined; + } + + setPendingOffset(offset) { + this.pendingOffset = offset; + } + + pushPendingTree(tree) { + if (this.pendingTrees === undefined) { + this.pendingTrees = []; + } + this.pendingTrees.push(tree); + } + + next() { + const pendingTrees = this.pendingTrees; + const nextEvent = this.queue[this.nextIndex]; + if (pendingTrees === undefined) { + this.nextIndex++; + return nextEvent; + } else if (nextEvent === undefined) { + this.pendingTrees = undefined; + return { + offset: this.pendingOffset, + trees: pendingTrees + }; + } + if (this.pendingOffset < nextEvent.offset) { + this.pendingTrees = undefined; + return { + offset: this.pendingOffset, + trees: pendingTrees + }; + } + if (this.pendingOffset === nextEvent.offset) { + this.pendingTrees = undefined; + for (const tree of pendingTrees) { + nextEvent.trees.push(tree); + } + } + this.nextIndex++; + return nextEvent; + + + } +} + +function fromParentTrees(parentTrees) { + const startToTrees = new Map(); + for (const [parentIndex, parentTree] of parentTrees.entries()) { + for (const child of parentTree.children) { + let trees = startToTrees.get(child.start); + if (trees === undefined) { + trees = []; + startToTrees.set(child.start, trees); + } + trees.push({ + parentIndex, + tree: child + }); + } + } + const queue = []; + for (const [startOffset, trees] of startToTrees) { + queue.push({ + offset: startOffset, + trees + }); + } + queue.sort((a, b) => { + return a.offset - b.offset; + }); + return new StartEventQueue(queue); +} + +// eslint-disable-next-line complexity +function mergeRangeTreeChildren(parentTrees) { + const result = []; + const startEventQueue = fromParentTrees(parentTrees); + const parentToNested = new Map(); + let openRange; + + while (true) { + const event = startEventQueue.next(); + if (event === undefined) { + break; + } + + if (openRange !== undefined && openRange.end <= event.offset) { + result.push(nextChild(openRange, parentToNested)); + openRange = undefined; + } + + if (openRange === undefined) { + let openRangeEnd = event.offset + 1; + for (const { parentIndex, tree } of event.trees) { + openRangeEnd = Math.max(openRangeEnd, tree.end); + insertChild(parentToNested, parentIndex, tree); + } + startEventQueue.setPendingOffset(openRangeEnd); + openRange = { + start: event.offset, end: openRangeEnd + }; + } else { + for (const { parentIndex, tree } of event.trees) { + if (tree.end > openRange.end) { + const right = tree.split(openRange.end); + startEventQueue.pushPendingTree({ + parentIndex, + tree: right + }); + } + insertChild(parentToNested, parentIndex, tree); + } + } + } + if (openRange !== undefined) { + result.push(nextChild(openRange, parentToNested)); + } + + return result; +} + +/** + * @precondition Same `start` and `end` for all the trees + */ +function mergeRangeTrees(trees) { + if (trees.length <= 1) { + return trees[0]; + } + const first = trees[0]; + let delta = 0; + for (const tree of trees) { + delta += tree.delta; + } + const children = mergeRangeTreeChildren(trees); + return new RangeTree(first.start, first.end, delta, children); +} + +/** + * Merges a list of matching function coverages. + * + * Functions are matching if their root ranges have the same span. + * The result is normalized. + * The input values may be mutated, it is not safe to use them after passing + * them to this function. + * The computation is synchronous. + * + * @param funcCovs Function coverages to merge. + * @return Merged function coverage, or `undefined` if the input list was empty. + */ +function mergeFunctionCovs(funcCovs) { + if (funcCovs.length === 0) { + return undefined; + } else if (funcCovs.length === 1) { + const merged = funcCovs[0]; + normalizeFunctionCov(merged); + return merged; + } + + const first = funcCovs[0]; + const functionName = first.functionName; + // assert: `first.ranges.length > 0` + const startOffset = first.ranges[0].startOffset; + const endOffset = first.ranges[0].endOffset; + let count = 0; + + const trees = []; + for (const funcCov of funcCovs) { + // assert: `funcCov.ranges.length > 0` + // assert: `funcCov.ranges` is sorted + count += funcCov.count === undefined ? funcCov.ranges[0].count : funcCov.count; + if (funcCov.isBlockCoverage) { + trees.push(fromSortedRanges(funcCov.ranges)); + } + } + + let isBlockCoverage; + let ranges; + if (trees.length > 0) { + isBlockCoverage = true; + const mergedTree = mergeRangeTrees(trees); + mergedTree.normalize(); + ranges = mergedTree.toRanges(); + } else { + isBlockCoverage = false; + ranges = [{ + startOffset, endOffset, count + }]; + } + + const merged = { + functionName, ranges, isBlockCoverage + }; + if (count !== ranges[0].count) { + merged.count = count; + } + // assert: `merged` is normalized + return merged; +} + + +/** + * Merges a list of matching script coverages. + * + * Scripts are matching if they have the same `url`. + * The result is normalized. + * The input values may be mutated, it is not safe to use them after passing + * them to this function. + * The computation is synchronous. + * + * @param scriptCovs Process coverages to merge. + * @return Merged script coverage, or `undefined` if the input list was empty. + */ + +function mergeScriptCovs(scriptCovs) { + if (scriptCovs.length === 0) { + return undefined; + } else if (scriptCovs.length === 1) { + const merged = scriptCovs[0]; + deepNormalizeScriptCov(merged); + return merged; + } + + const first = scriptCovs[0]; + const scriptId = first.scriptId; + const url = first.url; + + const rangeToFuncs = new Map(); + for (const scriptCov of scriptCovs) { + for (const funcCov of scriptCov.functions) { + const rootRange = stringifyFunctionRootRange(funcCov); + let funcCovs = rangeToFuncs.get(rootRange); + + if (funcCovs === undefined) { + funcCovs = []; + rangeToFuncs.set(rootRange, funcCovs); + } + funcCovs.push(funcCov); + } + } + + const functions = []; + for (const funcCovs of rangeToFuncs.values()) { + // assert: `funcCovs.length > 0` + functions.push(mergeFunctionCovs(funcCovs)); + } + + const merged = { + scriptId, url, functions + }; + normalizeScriptCov(merged); + return merged; +} + +module.exports = { + mergeScriptCovs +}; diff --git a/lib/v8/merge/range-tree.js b/lib/v8/merge/range-tree.js new file mode 100644 index 00000000..97932a2c --- /dev/null +++ b/lib/v8/merge/range-tree.js @@ -0,0 +1,123 @@ +class RangeTree { + + constructor( + start, + end, + delta, + children + ) { + this.start = start; + this.end = end; + this.delta = delta; + this.children = children; + } + + // eslint-disable-next-line complexity + normalize() { + const children = []; + let curEnd; + let head; + const tail = []; + for (const child of this.children) { + if (head === undefined) { + head = child; + } else if (child.delta === head.delta && child.start === curEnd) { + tail.push(child); + } else { + endChain(); + head = child; + } + curEnd = child.end; + } + if (head !== undefined) { + endChain(); + } + + if (children.length === 1) { + const child = children[0]; + if (child.start === this.start && child.end === this.end) { + this.delta += child.delta; + this.children = child.children; + // `.lazyCount` is zero for both (both are after normalization) + return; + } + } + + this.children = children; + + function endChain() { + if (tail.length !== 0) { + head.end = tail[tail.length - 1].end; + for (const tailTree of tail) { + for (const subChild of tailTree.children) { + subChild.delta += tailTree.delta - head.delta; + head.children.push(subChild); + } + } + tail.length = 0; + } + head.normalize(); + children.push(head); + } + } + + /** + * @precondition `tree.start < value && value < tree.end` + * @return RangeTree Right part + */ + split(value) { + let leftChildLen = this.children.length; + let mid; + + // TODO(perf): Binary search (check overhead) + for (let i = 0; i < this.children.length; i++) { + const child = this.children[i]; + if (child.start < value && value < child.end) { + mid = child.split(value); + leftChildLen = i + 1; + break; + } else if (child.start >= value) { + leftChildLen = i; + break; + } + } + + const rightLen = this.children.length - leftChildLen; + const rightChildren = this.children.splice(leftChildLen, rightLen); + if (mid !== undefined) { + rightChildren.unshift(mid); + } + const result = new RangeTree( + value, + this.end, + this.delta, + rightChildren + ); + this.end = value; + return result; + } + + /** + * Get the range coverages corresponding to the tree. + * + * The ranges are pre-order sorted. + */ + toRanges() { + const ranges = []; + // Stack of parent trees and counts. + const stack = [[this, 0]]; + while (stack.length > 0) { + const [cur, parentCount] = stack.pop(); + const count = parentCount + cur.delta; + ranges.push({ + startOffset: cur.start, endOffset: cur.end, count + }); + for (let i = cur.children.length - 1; i >= 0; i--) { + stack.push([cur.children[i], count]); + } + } + return ranges; + } +} + +module.exports = RangeTree; diff --git a/lib/v8/v8.js b/lib/v8/v8.js index 435d1526..1c72b5f7 100644 --- a/lib/v8/v8.js +++ b/lib/v8/v8.js @@ -1,5 +1,5 @@ const EC = require('eight-colors'); -const { mergeScriptCovs } = require('../packages/monocart-coverage-vendor.js'); +const { mergeScriptCovs } = require('./merge/merge.js'); const Util = require('../utils/util.js'); const { getV8Summary } = require('./v8-summary.js'); const { dedupeFlatRanges } = require('../utils/dedupe.js'); diff --git a/packages/vendor/src/index.js b/packages/vendor/src/index.js index 89656f3e..0da56010 100644 --- a/packages/vendor/src/index.js +++ b/packages/vendor/src/index.js @@ -7,8 +7,6 @@ import * as convertSourceMap from 'convert-source-map'; import { decode } from '@jridgewell/sourcemap-codec'; -import { mergeScriptCovs } from '@bcoe/v8-coverage'; - import parseCss from 'postcss/lib/parse'; import WebSocket from 'ws'; @@ -30,8 +28,6 @@ export { decode, - mergeScriptCovs, - parseCss, WebSocket, diff --git a/test/mcr.config.cli.js b/test/mcr.config.cli.js index 0aff6ab0..91bea7db 100644 --- a/test/mcr.config.cli.js +++ b/test/mcr.config.cli.js @@ -9,7 +9,8 @@ module.exports = { 'v8', 'console-summary', ['raw', { - zip: true + merge: true + // zip: true }], 'codecov' ], diff --git a/test/test-merge.js b/test/test-merge.js index 340b3579..be111b39 100644 --- a/test/test-merge.js +++ b/test/test-merge.js @@ -18,7 +18,7 @@ const coverageOptions = { inline: true, // merge from exists raw dirs - inputDir: './docs/node-vm/raw, ./docs/cli/raw.zip, ./wrong-raw-dir', + inputDir: './docs/node-vm/raw, ./docs/cli/raw, ./wrong-raw-dir', name: 'My Merge Coverage Report', assetsPath: '../assets', From d1f0c405d97f2fb5570a847be95d0d31567f1166 Mon Sep 17 00:00:00 2001 From: cenfun Date: Thu, 26 Sep 2024 21:47:44 +0800 Subject: [PATCH 2/8] rename mergeV8Coverage --- lib/{v8 => utils}/merge/merge.js | 4 ++-- lib/{v8 => utils}/merge/range-tree.js | 0 lib/utils/util.js | 2 ++ lib/v8/v8.js | 3 +-- 4 files changed, 5 insertions(+), 4 deletions(-) rename lib/{v8 => utils}/merge/merge.js (99%) rename lib/{v8 => utils}/merge/range-tree.js (100%) diff --git a/lib/v8/merge/merge.js b/lib/utils/merge/merge.js similarity index 99% rename from lib/v8/merge/merge.js rename to lib/utils/merge/merge.js index 031bef8a..14668235 100644 --- a/lib/v8/merge/merge.js +++ b/lib/utils/merge/merge.js @@ -377,7 +377,7 @@ function mergeFunctionCovs(funcCovs) { * @return Merged script coverage, or `undefined` if the input list was empty. */ -function mergeScriptCovs(scriptCovs) { +function mergeV8Coverage(scriptCovs) { if (scriptCovs.length === 0) { return undefined; } else if (scriptCovs.length === 1) { @@ -418,5 +418,5 @@ function mergeScriptCovs(scriptCovs) { } module.exports = { - mergeScriptCovs + mergeV8Coverage }; diff --git a/lib/v8/merge/range-tree.js b/lib/utils/merge/range-tree.js similarity index 100% rename from lib/v8/merge/range-tree.js rename to lib/utils/merge/range-tree.js diff --git a/lib/utils/util.js b/lib/utils/util.js index 7fa96ff4..c5766759 100644 --- a/lib/utils/util.js +++ b/lib/utils/util.js @@ -11,6 +11,7 @@ const Share = require('../platform/share.js'); const request = require('./request.js'); const version = require('../../package.json').version; const markdownGrid = require('./markdown.js'); +const { mergeV8Coverage } = require('./merge/merge.js'); const { findUpSync, supportsColor, minimatch @@ -34,6 +35,7 @@ const Util = { request, markdownGrid, + mergeV8Coverage, relativePath: function(p, root) { p = `${p}`; diff --git a/lib/v8/v8.js b/lib/v8/v8.js index 1c72b5f7..4b06dd95 100644 --- a/lib/v8/v8.js +++ b/lib/v8/v8.js @@ -1,5 +1,4 @@ const EC = require('eight-colors'); -const { mergeScriptCovs } = require('./merge/merge.js'); const Util = require('../utils/util.js'); const { getV8Summary } = require('./v8-summary.js'); const { dedupeFlatRanges } = require('../utils/dedupe.js'); @@ -172,7 +171,7 @@ const mergeCssRanges = (itemList) => { const mergeJsFunctions = (itemList) => { return new Promise((resolve) => { - const res = mergeScriptCovs(itemList); + const res = Util.mergeV8Coverage(itemList); const functions = res && res.functions; resolve(functions || []); From 19300557683982702828e1fb8c1d57765de01a1f Mon Sep 17 00:00:00 2001 From: cenfun Date: Thu, 26 Sep 2024 22:04:55 +0800 Subject: [PATCH 3/8] fixed undefined for es-lint --- lib/utils/merge/merge.js | 79 +++++++++++++++++++++-------------- lib/utils/merge/range-tree.js | 6 +-- lib/v8/v8.js | 11 ++--- 3 files changed, 54 insertions(+), 42 deletions(-) diff --git a/lib/utils/merge/merge.js b/lib/utils/merge/merge.js index 14668235..098983c5 100644 --- a/lib/utils/merge/merge.js +++ b/lib/utils/merge/merge.js @@ -1,5 +1,5 @@ /** - * The `mergeScriptCovs` was extracted from following repos for fixing several issues + * The `mergeV8Coverage` was extracted from following repos for fixing several issues * https://github.com/bcoe/v8-coverage * https://github.com/demurgos/v8-coverage */ @@ -39,7 +39,7 @@ function fromSortedRanges(ranges) { const stack = []; for (const range of ranges) { const node = new RangeTree(range.startOffset, range.endOffset, range.count, []); - if (root === undefined) { + if (!root) { root = node; stack.push([node, range.count]); continue; @@ -130,7 +130,7 @@ function stringifyFunctionRootRange(funcCov) { function insertChild(parentToNested, parentIndex, tree) { let nested = parentToNested.get(parentIndex); - if (nested === undefined) { + if (!nested) { nested = []; parentToNested.set(parentIndex, nested); } @@ -162,7 +162,7 @@ class StartEventQueue { this.queue = queue; this.nextIndex = 0; this.pendingOffset = 0; - this.pendingTrees = undefined; + this.pendingTrees = null; } setPendingOffset(offset) { @@ -170,7 +170,7 @@ class StartEventQueue { } pushPendingTree(tree) { - if (this.pendingTrees === undefined) { + if (this.pendingTrees === null) { this.pendingTrees = []; } this.pendingTrees.push(tree); @@ -179,25 +179,26 @@ class StartEventQueue { next() { const pendingTrees = this.pendingTrees; const nextEvent = this.queue[this.nextIndex]; - if (pendingTrees === undefined) { + if (pendingTrees === null) { this.nextIndex++; return nextEvent; - } else if (nextEvent === undefined) { - this.pendingTrees = undefined; + } + if (!nextEvent) { + this.pendingTrees = null; return { offset: this.pendingOffset, trees: pendingTrees }; } if (this.pendingOffset < nextEvent.offset) { - this.pendingTrees = undefined; + this.pendingTrees = null; return { offset: this.pendingOffset, trees: pendingTrees }; } if (this.pendingOffset === nextEvent.offset) { - this.pendingTrees = undefined; + this.pendingTrees = null; for (const tree of pendingTrees) { nextEvent.trees.push(tree); } @@ -214,7 +215,7 @@ function fromParentTrees(parentTrees) { for (const [parentIndex, parentTree] of parentTrees.entries()) { for (const child of parentTree.children) { let trees = startToTrees.get(child.start); - if (trees === undefined) { + if (!trees) { trees = []; startToTrees.set(child.start, trees); } @@ -242,20 +243,20 @@ function mergeRangeTreeChildren(parentTrees) { const result = []; const startEventQueue = fromParentTrees(parentTrees); const parentToNested = new Map(); - let openRange; + let openRange = null; while (true) { const event = startEventQueue.next(); - if (event === undefined) { + if (!event) { break; } - if (openRange !== undefined && openRange.end <= event.offset) { + if (openRange !== null && openRange.end <= event.offset) { result.push(nextChild(openRange, parentToNested)); - openRange = undefined; + openRange = null; } - if (openRange === undefined) { + if (openRange === null) { let openRangeEnd = event.offset + 1; for (const { parentIndex, tree } of event.trees) { openRangeEnd = Math.max(openRangeEnd, tree.end); @@ -278,7 +279,7 @@ function mergeRangeTreeChildren(parentTrees) { } } } - if (openRange !== undefined) { + if (openRange !== null) { result.push(nextChild(openRange, parentToNested)); } @@ -315,8 +316,10 @@ function mergeRangeTrees(trees) { */ function mergeFunctionCovs(funcCovs) { if (funcCovs.length === 0) { - return undefined; - } else if (funcCovs.length === 1) { + return; + } + + if (funcCovs.length === 1) { const merged = funcCovs[0]; normalizeFunctionCov(merged); return merged; @@ -333,7 +336,7 @@ function mergeFunctionCovs(funcCovs) { for (const funcCov of funcCovs) { // assert: `funcCov.ranges.length > 0` // assert: `funcCov.ranges` is sorted - count += funcCov.count === undefined ? funcCov.ranges[0].count : funcCov.count; + count += typeof funcCov.count === 'number' ? funcCov.count : funcCov.ranges[0].count; if (funcCov.isBlockCoverage) { trees.push(fromSortedRanges(funcCov.ranges)); } @@ -349,12 +352,16 @@ function mergeFunctionCovs(funcCovs) { } else { isBlockCoverage = false; ranges = [{ - startOffset, endOffset, count + startOffset, + endOffset, + count }]; } const merged = { - functionName, ranges, isBlockCoverage + functionName, + ranges, + isBlockCoverage }; if (count !== ranges[0].count) { merged.count = count; @@ -377,26 +384,33 @@ function mergeFunctionCovs(funcCovs) { * @return Merged script coverage, or `undefined` if the input list was empty. */ +// eslint-disable-next-line complexity function mergeV8Coverage(scriptCovs) { + if (!Array.isArray(scriptCovs)) { + return { + functions: [] + }; + } + if (scriptCovs.length === 0) { - return undefined; - } else if (scriptCovs.length === 1) { + return { + functions: [] + }; + } + + if (scriptCovs.length === 1) { const merged = scriptCovs[0]; deepNormalizeScriptCov(merged); return merged; } - const first = scriptCovs[0]; - const scriptId = first.scriptId; - const url = first.url; const rangeToFuncs = new Map(); for (const scriptCov of scriptCovs) { for (const funcCov of scriptCov.functions) { const rootRange = stringifyFunctionRootRange(funcCov); let funcCovs = rangeToFuncs.get(rootRange); - - if (funcCovs === undefined) { + if (!funcCovs) { funcCovs = []; rangeToFuncs.set(rootRange, funcCovs); } @@ -407,11 +421,14 @@ function mergeV8Coverage(scriptCovs) { const functions = []; for (const funcCovs of rangeToFuncs.values()) { // assert: `funcCovs.length > 0` - functions.push(mergeFunctionCovs(funcCovs)); + const block = mergeFunctionCovs(funcCovs); + if (block) { + functions.push(block); + } } const merged = { - scriptId, url, functions + functions }; normalizeScriptCov(merged); return merged; diff --git a/lib/utils/merge/range-tree.js b/lib/utils/merge/range-tree.js index 97932a2c..baffb48d 100644 --- a/lib/utils/merge/range-tree.js +++ b/lib/utils/merge/range-tree.js @@ -19,7 +19,7 @@ class RangeTree { let head; const tail = []; for (const child of this.children) { - if (head === undefined) { + if (!head) { head = child; } else if (child.delta === head.delta && child.start === curEnd) { tail.push(child); @@ -29,7 +29,7 @@ class RangeTree { } curEnd = child.end; } - if (head !== undefined) { + if (head) { endChain(); } @@ -84,7 +84,7 @@ class RangeTree { const rightLen = this.children.length - leftChildLen; const rightChildren = this.children.splice(leftChildLen, rightLen); - if (mid !== undefined) { + if (mid) { rightChildren.unshift(mid); } const result = new RangeTree( diff --git a/lib/v8/v8.js b/lib/v8/v8.js index 4b06dd95..559b27b7 100644 --- a/lib/v8/v8.js +++ b/lib/v8/v8.js @@ -168,14 +168,9 @@ const mergeCssRanges = (itemList) => { }); }; -const mergeJsFunctions = (itemList) => { - return new Promise((resolve) => { - - const res = Util.mergeV8Coverage(itemList); - const functions = res && res.functions; - - resolve(functions || []); - }); +const mergeJsFunctions = async (itemList) => { + const res = await Util.mergeV8Coverage(itemList); + return res.functions; }; const mergeV8Coverage = async (dataList, sourceCache, options) => { From 2cf6663769188503899457fb3d8cd31bf07114e5 Mon Sep 17 00:00:00 2001 From: cenfun Date: Sun, 29 Sep 2024 10:54:25 +0800 Subject: [PATCH 4/8] remove --- CHANGELOG.md | 3 +-- lib/index.d.ts | 3 +-- lib/index.js | 9 +-------- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d48ba0c9..29072fd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,7 @@ ## Changelog - 2.11.0 - - removed the data dir for `addFromDir` and `dataDir` - - added `zip` option for `raw` report + - added `zip` and `merge` option for `raw` report - 2.10.9 - fixed empty coverage issue diff --git a/lib/index.d.ts b/lib/index.d.ts index 1e0dafdd..175c382e 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -439,9 +439,8 @@ declare namespace MCR { /** * add V8 coverage from a dir * @param dir node v8 coverage dir - * @param remove whether to remove dir after added */ - addFromDir: (dir: string, remove?: boolean) => Promise; + addFromDir: (dir: string) => Promise; /** generate report */ generate: () => Promise; diff --git a/lib/index.js b/lib/index.js index 0344fae7..5d945e7d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -122,16 +122,9 @@ class CoverageReport { } // add coverage from dir - async addFromDir(dir, remove) { + async addFromDir(dir) { const time_start = Date.now(); const results = await readFromDir(this, dir); - // remove dir after added - if (typeof remove !== 'boolean') { - remove = !Util.isDebug(); - } - if (remove) { - Util.rmSync(dir); - } Util.logTime(`added from dir: ${dir}`, time_start); return results; } From a4c8e9e47d9b3516eb544116d2b1e68f7789eeaf Mon Sep 17 00:00:00 2001 From: cenfun Date: Sun, 29 Sep 2024 15:44:33 +0800 Subject: [PATCH 5/8] supports merge raw --- lib/generate.js | 5 +---- lib/istanbul/istanbul.js | 8 ++++++-- lib/reports/raw.js | 43 +++++++++++++++++++++++++++++++++++++++- lib/utils/util.js | 7 +++++++ lib/v8/v8.js | 12 +++++++++-- 5 files changed, 66 insertions(+), 9 deletions(-) diff --git a/lib/generate.js b/lib/generate.js index ab2b5644..862bee5c 100644 --- a/lib/generate.js +++ b/lib/generate.js @@ -251,10 +251,7 @@ const addJsonData = async (mcr, dataList, sourceCache, input, filename) => { if (mcr.fileCache.has(filename)) { json = mcr.fileCache.get(filename); } else { - const content = await Util.readFile(path.resolve(input, filename)); - if (content) { - json = JSON.parse(content); - } + json = await Util.readJson(path.resolve(input, filename)); } } if (json) { diff --git a/lib/istanbul/istanbul.js b/lib/istanbul/istanbul.js index 5d569861..e50d5622 100644 --- a/lib/istanbul/istanbul.js +++ b/lib/istanbul/istanbul.js @@ -144,16 +144,19 @@ const addUntestedFiles = async (istanbulData, options) => { }; -const mergeIstanbulCoverage = async (dataList, options) => { +const mergeIstanbulDataList = (dataList) => { const istanbulCoverageList = dataList.map((it) => it.data); const coverageMap = istanbulLibCoverage.createCoverageMap(); istanbulCoverageList.forEach((coverage) => { coverageMap.merge(coverage); }); const istanbulData = coverageMap.toJSON(); + return istanbulData; +}; +const mergeIstanbulCoverage = async (dataList, options) => { + const istanbulData = mergeIstanbulDataList(dataList); await addUntestedFiles(istanbulData, options); - return istanbulData; }; @@ -169,6 +172,7 @@ const initIstanbulData = (istanbulData, options) => { module.exports = { saveIstanbulReports, + mergeIstanbulDataList, mergeIstanbulCoverage, initIstanbulData }; diff --git a/lib/reports/raw.js b/lib/reports/raw.js index 884b3127..a98c35b7 100644 --- a/lib/reports/raw.js +++ b/lib/reports/raw.js @@ -2,10 +2,45 @@ const fs = require('fs'); const path = require('path'); const Util = require('../utils/util.js'); const { ZipFile } = require('../packages/monocart-coverage-vendor.js'); +const { mergeIstanbulDataList } = require('../istanbul/istanbul.js'); +const { mergeV8DataList } = require('../v8/v8.js'); + +const getMergedData = (type, dataList) => { + if (type === 'istanbul') { + return mergeIstanbulDataList(dataList); + } + return mergeV8DataList(dataList); +}; const mergeReport = async (rawDir, rawParent) => { - // const coverageFiles = fs.readdirSync(rawDir).filter((n) => n.endsWith('.json') && n.startsWith('coverage-')); + const coverageFiles = fs.readdirSync(rawDir).filter((n) => n.endsWith('.json') && n.startsWith('coverage-')); // console.log(coverageFiles); + + let type; + const dataList = []; + for (const coverageFile of coverageFiles) { + const jsonPath = path.resolve(rawDir, coverageFile); + const json = await Util.readJson(jsonPath); + if (json) { + // 'id', 'type', 'data' + type = json.type; + dataList.push(json); + } + Util.rmSync(jsonPath); + } + + const mergedData = await getMergedData(type, dataList); + + const dataId = Util.uid(); + const results = { + id: dataId, + type, + data: mergedData + }; + + const { cachePath } = Util.getCacheFileInfo('coverage', `${dataId}.merged`, rawDir); + await Util.writeFile(cachePath, JSON.stringify(results)); + }; const zipReport = (rawDir, rawParent) => { @@ -39,6 +74,12 @@ const rawReport = async (reportData, reportOptions, options) => { }; const cacheDir = options.cacheDir; + if (!fs.existsSync(cacheDir)) { + // there is no cache if only inputDir + Util.logInfo('There is no cache dir for "raw" report'); + return; + } + const rawDir = path.resolve(options.outputDir, rawOptions.outputDir); // console.log(rawDir, cacheDir); if (fs.existsSync(rawDir)) { diff --git a/lib/utils/util.js b/lib/utils/util.js index c5766759..1d1de349 100644 --- a/lib/utils/util.js +++ b/lib/utils/util.js @@ -466,6 +466,13 @@ const Util = { } }, + readJson: async (jsonPath) => { + const content = await Util.readFile(jsonPath); + if (content) { + return JSON.parse(content); + } + }, + writeFileSync: function(filePath, content) { if (!fs.existsSync(filePath)) { const p = path.dirname(filePath); diff --git a/lib/v8/v8.js b/lib/v8/v8.js index 559b27b7..62839db6 100644 --- a/lib/v8/v8.js +++ b/lib/v8/v8.js @@ -173,8 +173,7 @@ const mergeJsFunctions = async (itemList) => { return res.functions; }; -const mergeV8Coverage = async (dataList, sourceCache, options) => { - +const mergeV8DataList = async (dataList) => { let allList = []; dataList.forEach((d) => { allList = allList.concat(d.data); @@ -229,6 +228,14 @@ const mergeV8Coverage = async (dataList, sourceCache, options) => { // empty list + merged list const mergedList = emptyList.concat(Object.values(itemMap)); + + return mergedList; +}; + +const mergeV8Coverage = async (dataList, sourceCache, options) => { + + const mergedList = await mergeV8DataList(dataList); + // try to load coverage and source by id for (const entry of mergedList) { const json = sourceCache.get(entry.id); @@ -323,6 +330,7 @@ const saveV8Report = async (v8list, options, istanbulReportPath) => { module.exports = { initV8ListAndSourcemap, + mergeV8DataList, mergeV8Coverage, saveV8Report }; From 609a3f71125ab457804ce348f084b00cce71b022 Mon Sep 17 00:00:00 2001 From: cenfun Date: Sun, 29 Sep 2024 16:26:39 +0800 Subject: [PATCH 6/8] test for merge raw --- package.json | 1 + test/test-merge-v8.js | 46 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 test/test-merge-v8.js diff --git a/package.json b/package.json index 3b363c6c..11e9c524 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "test-demo": "node ./test/test-demo.js", "test-merge": "node ./test/test-merge.js", "test-merge-istanbul": "node ./test/test-merge-istanbul.js", + "test-merge-v8": "node ./test/test-merge-v8.js", "test-client": "node ./test/test-client.js", "test-all": "node ./test/test.js", "test-pr": "node --inspect=9230 ./test/test-pr.js", diff --git a/test/test-merge-v8.js b/test/test-merge-v8.js new file mode 100644 index 00000000..46fc45e9 --- /dev/null +++ b/test/test-merge-v8.js @@ -0,0 +1,46 @@ +const fs = require('fs'); +const MCR = require('../'); +const coverageOptions = { + // logging: 'debug', + reports: [ + ['v8', { + metrics: ['lines'] + }], + ['raw', { + // zip: true, + merge: true + }] + ], + + cleanCache: false, + // clean: false, + inputDir: './.temp/code-coverage/raw-merged', + + filter: { + '**/webpack/**': false, + '**/playground/$*': false, + '**/node_modules/**': false, + '**/*': true + }, + + name: 'My Merge V8 Coverage Report', + outputDir: './.temp/merge-v8-merged', + onEnd: function(coverageResults) { + console.log('end'); + } +}; + +const generate = async () => { + + fs.mkdirSync('./.temp/merge-v8/.cache'); + fs.readdirSync('./.temp/code-coverage/raw').forEach((n) => { + fs.copyFileSync(`./.temp/code-coverage/raw/${n}`, `./.temp/merge-v8/.cache/${n}`); + }); + + const mcr = MCR(coverageOptions); + await mcr.generate(); + + +}; + +generate(); From 9c06bc87a7402f8f090fe41988d85ca729de7c07 Mon Sep 17 00:00:00 2001 From: cenfun Date: Sun, 29 Sep 2024 16:30:34 +0800 Subject: [PATCH 7/8] merge and zip option for raw report --- lib/index.d.ts | 2 ++ lib/reports/raw.js | 1 + 2 files changed, 3 insertions(+) diff --git a/lib/index.d.ts b/lib/index.d.ts index 175c382e..f62d1f7d 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -156,6 +156,8 @@ declare namespace MCR { outputFile?: string; }] | ['raw'] | ['raw', { + merge?: boolean; + zip?: boolean; outputDir?: string; }] | [string] | [string, { diff --git a/lib/reports/raw.js b/lib/reports/raw.js index a98c35b7..18f7f309 100644 --- a/lib/reports/raw.js +++ b/lib/reports/raw.js @@ -69,6 +69,7 @@ const zipReport = (rawDir, rawParent) => { const rawReport = async (reportData, reportOptions, options) => { const rawOptions = { outputDir: 'raw', + merge: false, zip: false, ... reportOptions }; From 60cdb2df10fef9a07d6827662353a09ddbce5fa5 Mon Sep 17 00:00:00 2001 From: cenfun Date: Sun, 29 Sep 2024 16:41:35 +0800 Subject: [PATCH 8/8] test for merge raw --- test/test-merge-v8.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test-merge-v8.js b/test/test-merge-v8.js index 46fc45e9..5519fa69 100644 --- a/test/test-merge-v8.js +++ b/test/test-merge-v8.js @@ -14,7 +14,7 @@ const coverageOptions = { cleanCache: false, // clean: false, - inputDir: './.temp/code-coverage/raw-merged', + inputDir: './.temp/code-coverage/raw', filter: { '**/webpack/**': false, @@ -24,7 +24,7 @@ const coverageOptions = { }, name: 'My Merge V8 Coverage Report', - outputDir: './.temp/merge-v8-merged', + outputDir: './.temp/merge-v8', onEnd: function(coverageResults) { console.log('end'); }