Spaces:
Sleeping
Sleeping
| 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)!; | |
| } | |