| import { |
| deepNormalizeScriptCov, |
| normalizeFunctionCov, |
| normalizeProcessCov, |
| normalizeRangeTree, |
| normalizeScriptCov, |
| } from "./normalize"; |
| import { RangeTree } from "./range-tree"; |
| import { FunctionCov, ProcessCov, Range, RangeCov, ScriptCov } from "./types"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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()) { |
| |
| result.push(mergeScriptCovs(scripts)!); |
| } |
| const merged: ProcessCov = {result}; |
|
|
| normalizeProcessCov(merged); |
| return merged; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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 || |
| |
| |
| (!funcCovs[0].isBlockCoverage && funcCov.isBlockCoverage)) { |
| funcCovs = []; |
| rangeToFuncs.set(rootRange, funcCovs); |
| } else if (funcCovs[0].isBlockCoverage && !funcCov.isBlockCoverage) { |
| |
| |
| continue; |
| } |
| funcCovs.push(funcCov); |
| } |
| } |
|
|
| const functions: FunctionCov[] = []; |
| for (const funcCovs of rangeToFuncs.values()) { |
| |
| functions.push(mergeFunctionCovs(funcCovs)!); |
| } |
|
|
| const merged: ScriptCov = {scriptId, url, functions}; |
| normalizeScriptCov(merged); |
| return merged; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function stringifyFunctionRootRange(funcCov: Readonly<FunctionCov>): string { |
| const rootRange: RangeCov = funcCov.ranges[0]; |
| return `${rootRange.startOffset.toString(10)};${rootRange.endOffset.toString(10)}`; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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) { |
| |
| |
| trees.push(RangeTree.fromSortedRanges(funcCov.ranges)!); |
| } |
|
|
| |
| 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}; |
| |
| return merged; |
| } |
|
|
| |
| |
| |
| 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)!; |
| } |
|
|