You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
343 lines
11 KiB
343 lines
11 KiB
import { |
|
deepNormalizeScriptCov, |
|
normalizeFunctionCov, |
|
normalizeProcessCov, |
|
normalizeRangeTree, |
|
normalizeScriptCov, |
|
} from "./normalize"; |
|
import { RangeTree } from "./range-tree"; |
|
import { FunctionCov, ProcessCov, Range, RangeCov, ScriptCov } from "./types"; |
|
|
|
/** |
|
* Merges a list of process coverages. |
|
* |
|
* 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 processCovs Process coverages to merge. |
|
* @return Merged process coverage. |
|
*/ |
|
export function mergeProcessCovs(processCovs: ReadonlyArray<ProcessCov>): ProcessCov { |
|
if (processCovs.length === 0) { |
|
return {result: []}; |
|
} |
|
|
|
const urlToScripts: Map<string, ScriptCov[]> = new Map(); |
|
for (const processCov of processCovs) { |
|
for (const scriptCov of processCov.result) { |
|
let scriptCovs: ScriptCov[] | undefined = urlToScripts.get(scriptCov.url); |
|
if (scriptCovs === undefined) { |
|
scriptCovs = []; |
|
urlToScripts.set(scriptCov.url, scriptCovs); |
|
} |
|
scriptCovs.push(scriptCov); |
|
} |
|
} |
|
|
|
const result: ScriptCov[] = []; |
|
for (const scripts of urlToScripts.values()) { |
|
// assert: `scripts.length > 0` |
|
result.push(mergeScriptCovs(scripts)!); |
|
} |
|
const merged: ProcessCov = {result}; |
|
|
|
normalizeProcessCov(merged); |
|
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. |
|
*/ |
|
export function mergeScriptCovs(scriptCovs: ReadonlyArray<ScriptCov>): ScriptCov | undefined { |
|
if (scriptCovs.length === 0) { |
|
return undefined; |
|
} else if (scriptCovs.length === 1) { |
|
const merged: ScriptCov = scriptCovs[0]; |
|
deepNormalizeScriptCov(merged); |
|
return merged; |
|
} |
|
|
|
const first: ScriptCov = scriptCovs[0]; |
|
const scriptId: string = first.scriptId; |
|
const url: string = first.url; |
|
|
|
const rangeToFuncs: Map<string, FunctionCov[]> = new Map(); |
|
for (const scriptCov of scriptCovs) { |
|
for (const funcCov of scriptCov.functions) { |
|
const rootRange: string = stringifyFunctionRootRange(funcCov); |
|
let funcCovs: FunctionCov[] | undefined = rangeToFuncs.get(rootRange); |
|
|
|
if (funcCovs === undefined || |
|
// if the entry in rangeToFuncs is function-level granularity and |
|
// the new coverage is block-level, prefer block-level. |
|
(!funcCovs[0].isBlockCoverage && funcCov.isBlockCoverage)) { |
|
funcCovs = []; |
|
rangeToFuncs.set(rootRange, funcCovs); |
|
} else if (funcCovs[0].isBlockCoverage && !funcCov.isBlockCoverage) { |
|
// if the entry in rangeToFuncs is block-level granularity, we should |
|
// not append function level granularity. |
|
continue; |
|
} |
|
funcCovs.push(funcCov); |
|
} |
|
} |
|
|
|
const functions: FunctionCov[] = []; |
|
for (const funcCovs of rangeToFuncs.values()) { |
|
// assert: `funcCovs.length > 0` |
|
functions.push(mergeFunctionCovs(funcCovs)!); |
|
} |
|
|
|
const merged: ScriptCov = {scriptId, url, functions}; |
|
normalizeScriptCov(merged); |
|
return merged; |
|
} |
|
|
|
/** |
|
* 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: Readonly<FunctionCov>): string { |
|
const rootRange: RangeCov = funcCov.ranges[0]; |
|
return `${rootRange.startOffset.toString(10)};${rootRange.endOffset.toString(10)}`; |
|
} |
|
|
|
/** |
|
* 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. |
|
*/ |
|
export function mergeFunctionCovs(funcCovs: ReadonlyArray<FunctionCov>): FunctionCov | undefined { |
|
if (funcCovs.length === 0) { |
|
return undefined; |
|
} else if (funcCovs.length === 1) { |
|
const merged: FunctionCov = funcCovs[0]; |
|
normalizeFunctionCov(merged); |
|
return merged; |
|
} |
|
|
|
const functionName: string = funcCovs[0].functionName; |
|
|
|
const trees: RangeTree[] = []; |
|
for (const funcCov of funcCovs) { |
|
// assert: `fn.ranges.length > 0` |
|
// assert: `fn.ranges` is sorted |
|
trees.push(RangeTree.fromSortedRanges(funcCov.ranges)!); |
|
} |
|
|
|
// assert: `trees.length > 0` |
|
const mergedTree: RangeTree = mergeRangeTrees(trees)!; |
|
normalizeRangeTree(mergedTree); |
|
const ranges: RangeCov[] = mergedTree.toRanges(); |
|
const isBlockCoverage: boolean = !(ranges.length === 1 && ranges[0].count === 0); |
|
|
|
const merged: FunctionCov = {functionName, ranges, isBlockCoverage}; |
|
// assert: `merged` is normalized |
|
return merged; |
|
} |
|
|
|
/** |
|
* @precondition Same `start` and `end` for all the trees |
|
*/ |
|
function mergeRangeTrees(trees: ReadonlyArray<RangeTree>): RangeTree | undefined { |
|
if (trees.length <= 1) { |
|
return trees[0]; |
|
} |
|
const first: RangeTree = trees[0]; |
|
let delta: number = 0; |
|
for (const tree of trees) { |
|
delta += tree.delta; |
|
} |
|
const children: RangeTree[] = mergeRangeTreeChildren(trees); |
|
return new RangeTree(first.start, first.end, delta, children); |
|
} |
|
|
|
class RangeTreeWithParent { |
|
readonly parentIndex: number; |
|
readonly tree: RangeTree; |
|
|
|
constructor(parentIndex: number, tree: RangeTree) { |
|
this.parentIndex = parentIndex; |
|
this.tree = tree; |
|
} |
|
} |
|
|
|
class StartEvent { |
|
readonly offset: number; |
|
readonly trees: RangeTreeWithParent[]; |
|
|
|
constructor(offset: number, trees: RangeTreeWithParent[]) { |
|
this.offset = offset; |
|
this.trees = trees; |
|
} |
|
|
|
static compare(a: StartEvent, b: StartEvent): number { |
|
return a.offset - b.offset; |
|
} |
|
} |
|
|
|
class StartEventQueue { |
|
private readonly queue: StartEvent[]; |
|
private nextIndex: number; |
|
private pendingOffset: number; |
|
private pendingTrees: RangeTreeWithParent[] | undefined; |
|
|
|
private constructor(queue: StartEvent[]) { |
|
this.queue = queue; |
|
this.nextIndex = 0; |
|
this.pendingOffset = 0; |
|
this.pendingTrees = undefined; |
|
} |
|
|
|
static fromParentTrees(parentTrees: ReadonlyArray<RangeTree>): StartEventQueue { |
|
const startToTrees: Map<number, RangeTreeWithParent[]> = new Map(); |
|
for (const [parentIndex, parentTree] of parentTrees.entries()) { |
|
for (const child of parentTree.children) { |
|
let trees: RangeTreeWithParent[] | undefined = startToTrees.get(child.start); |
|
if (trees === undefined) { |
|
trees = []; |
|
startToTrees.set(child.start, trees); |
|
} |
|
trees.push(new RangeTreeWithParent(parentIndex, child)); |
|
} |
|
} |
|
const queue: StartEvent[] = []; |
|
for (const [startOffset, trees] of startToTrees) { |
|
queue.push(new StartEvent(startOffset, trees)); |
|
} |
|
queue.sort(StartEvent.compare); |
|
return new StartEventQueue(queue); |
|
} |
|
|
|
setPendingOffset(offset: number): void { |
|
this.pendingOffset = offset; |
|
} |
|
|
|
pushPendingTree(tree: RangeTreeWithParent): void { |
|
if (this.pendingTrees === undefined) { |
|
this.pendingTrees = []; |
|
} |
|
this.pendingTrees.push(tree); |
|
} |
|
|
|
next(): StartEvent | undefined { |
|
const pendingTrees: RangeTreeWithParent[] | undefined = this.pendingTrees; |
|
const nextEvent: StartEvent | undefined = this.queue[this.nextIndex]; |
|
if (pendingTrees === undefined) { |
|
this.nextIndex++; |
|
return nextEvent; |
|
} else if (nextEvent === undefined) { |
|
this.pendingTrees = undefined; |
|
return new StartEvent(this.pendingOffset, pendingTrees); |
|
} else { |
|
if (this.pendingOffset < nextEvent.offset) { |
|
this.pendingTrees = undefined; |
|
return new StartEvent(this.pendingOffset, pendingTrees); |
|
} else { |
|
if (this.pendingOffset === nextEvent.offset) { |
|
this.pendingTrees = undefined; |
|
for (const tree of pendingTrees) { |
|
nextEvent.trees.push(tree); |
|
} |
|
} |
|
this.nextIndex++; |
|
return nextEvent; |
|
} |
|
} |
|
} |
|
} |
|
|
|
function mergeRangeTreeChildren(parentTrees: ReadonlyArray<RangeTree>): RangeTree[] { |
|
const result: RangeTree[] = []; |
|
const startEventQueue: StartEventQueue = StartEventQueue.fromParentTrees(parentTrees); |
|
const parentToNested: Map<number, RangeTree[]> = new Map(); |
|
let openRange: Range | undefined; |
|
|
|
while (true) { |
|
const event: StartEvent | undefined = 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: number = 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: RangeTree = tree.split(openRange.end); |
|
startEventQueue.pushPendingTree(new RangeTreeWithParent(parentIndex, right)); |
|
} |
|
insertChild(parentToNested, parentIndex, tree); |
|
} |
|
} |
|
} |
|
if (openRange !== undefined) { |
|
result.push(nextChild(openRange, parentToNested)); |
|
} |
|
|
|
return result; |
|
} |
|
|
|
function insertChild(parentToNested: Map<number, RangeTree[]>, parentIndex: number, tree: RangeTree): void { |
|
let nested: RangeTree[] | undefined = parentToNested.get(parentIndex); |
|
if (nested === undefined) { |
|
nested = []; |
|
parentToNested.set(parentIndex, nested); |
|
} |
|
nested.push(tree); |
|
} |
|
|
|
function nextChild(openRange: Range, parentToNested: Map<number, RangeTree[]>): RangeTree { |
|
const matchingTrees: RangeTree[] = []; |
|
|
|
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)!; |
|
}
|
|
|