123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- 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)!;
- }
|