source: frontend/node_modules/@bcoe/v8-coverage/src/lib/merge.ts

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 10.6 KB
Line 
1import {
2 deepNormalizeScriptCov,
3 normalizeFunctionCov,
4 normalizeProcessCov,
5 normalizeRangeTree,
6 normalizeScriptCov,
7} from "./normalize";
8import { RangeTree } from "./range-tree";
9import { FunctionCov, ProcessCov, Range, RangeCov, ScriptCov } from "./types";
10
11/**
12 * Merges a list of process coverages.
13 *
14 * The result is normalized.
15 * The input values may be mutated, it is not safe to use them after passing
16 * them to this function.
17 * The computation is synchronous.
18 *
19 * @param processCovs Process coverages to merge.
20 * @return Merged process coverage.
21 */
22export function mergeProcessCovs(processCovs: ReadonlyArray<ProcessCov>): ProcessCov {
23 if (processCovs.length === 0) {
24 return {result: []};
25 }
26
27 const urlToScripts: Map<string, ScriptCov[]> = new Map();
28 for (const processCov of processCovs) {
29 for (const scriptCov of processCov.result) {
30 let scriptCovs: ScriptCov[] | undefined = urlToScripts.get(scriptCov.url);
31 if (scriptCovs === undefined) {
32 scriptCovs = [];
33 urlToScripts.set(scriptCov.url, scriptCovs);
34 }
35 scriptCovs.push(scriptCov);
36 }
37 }
38
39 const result: ScriptCov[] = [];
40 for (const scripts of urlToScripts.values()) {
41 // assert: `scripts.length > 0`
42 result.push(mergeScriptCovs(scripts)!);
43 }
44 const merged: ProcessCov = {result};
45
46 normalizeProcessCov(merged);
47 return merged;
48}
49
50/**
51 * Merges a list of matching script coverages.
52 *
53 * Scripts are matching if they have the same `url`.
54 * The result is normalized.
55 * The input values may be mutated, it is not safe to use them after passing
56 * them to this function.
57 * The computation is synchronous.
58 *
59 * @param scriptCovs Process coverages to merge.
60 * @return Merged script coverage, or `undefined` if the input list was empty.
61 */
62export function mergeScriptCovs(scriptCovs: ReadonlyArray<ScriptCov>): ScriptCov | undefined {
63 if (scriptCovs.length === 0) {
64 return undefined;
65 } else if (scriptCovs.length === 1) {
66 const merged: ScriptCov = scriptCovs[0];
67 deepNormalizeScriptCov(merged);
68 return merged;
69 }
70
71 const first: ScriptCov = scriptCovs[0];
72 const scriptId: string = first.scriptId;
73 const url: string = first.url;
74
75 const rangeToFuncs: Map<string, FunctionCov[]> = new Map();
76 for (const scriptCov of scriptCovs) {
77 for (const funcCov of scriptCov.functions) {
78 const rootRange: string = stringifyFunctionRootRange(funcCov);
79 let funcCovs: FunctionCov[] | undefined = rangeToFuncs.get(rootRange);
80
81 if (funcCovs === undefined ||
82 // if the entry in rangeToFuncs is function-level granularity and
83 // the new coverage is block-level, prefer block-level.
84 (!funcCovs[0].isBlockCoverage && funcCov.isBlockCoverage)) {
85 funcCovs = [];
86 rangeToFuncs.set(rootRange, funcCovs);
87 } else if (funcCovs[0].isBlockCoverage && !funcCov.isBlockCoverage) {
88 // if the entry in rangeToFuncs is block-level granularity, we should
89 // not append function level granularity.
90 continue;
91 }
92 funcCovs.push(funcCov);
93 }
94 }
95
96 const functions: FunctionCov[] = [];
97 for (const funcCovs of rangeToFuncs.values()) {
98 // assert: `funcCovs.length > 0`
99 functions.push(mergeFunctionCovs(funcCovs)!);
100 }
101
102 const merged: ScriptCov = {scriptId, url, functions};
103 normalizeScriptCov(merged);
104 return merged;
105}
106
107/**
108 * Returns a string representation of the root range of the function.
109 *
110 * This string can be used to match function with same root range.
111 * The string is derived from the start and end offsets of the root range of
112 * the function.
113 * This assumes that `ranges` is non-empty (true for valid function coverages).
114 *
115 * @param funcCov Function coverage with the range to stringify
116 * @internal
117 */
118function stringifyFunctionRootRange(funcCov: Readonly<FunctionCov>): string {
119 const rootRange: RangeCov = funcCov.ranges[0];
120 return `${rootRange.startOffset.toString(10)};${rootRange.endOffset.toString(10)}`;
121}
122
123/**
124 * Merges a list of matching function coverages.
125 *
126 * Functions are matching if their root ranges have the same span.
127 * The result is normalized.
128 * The input values may be mutated, it is not safe to use them after passing
129 * them to this function.
130 * The computation is synchronous.
131 *
132 * @param funcCovs Function coverages to merge.
133 * @return Merged function coverage, or `undefined` if the input list was empty.
134 */
135export function mergeFunctionCovs(funcCovs: ReadonlyArray<FunctionCov>): FunctionCov | undefined {
136 if (funcCovs.length === 0) {
137 return undefined;
138 } else if (funcCovs.length === 1) {
139 const merged: FunctionCov = funcCovs[0];
140 normalizeFunctionCov(merged);
141 return merged;
142 }
143
144 const functionName: string = funcCovs[0].functionName;
145
146 const trees: RangeTree[] = [];
147 for (const funcCov of funcCovs) {
148 // assert: `fn.ranges.length > 0`
149 // assert: `fn.ranges` is sorted
150 trees.push(RangeTree.fromSortedRanges(funcCov.ranges)!);
151 }
152
153 // assert: `trees.length > 0`
154 const mergedTree: RangeTree = mergeRangeTrees(trees)!;
155 normalizeRangeTree(mergedTree);
156 const ranges: RangeCov[] = mergedTree.toRanges();
157 const isBlockCoverage: boolean = !(ranges.length === 1 && ranges[0].count === 0);
158
159 const merged: FunctionCov = {functionName, ranges, isBlockCoverage};
160 // assert: `merged` is normalized
161 return merged;
162}
163
164/**
165 * @precondition Same `start` and `end` for all the trees
166 */
167function mergeRangeTrees(trees: ReadonlyArray<RangeTree>): RangeTree | undefined {
168 if (trees.length <= 1) {
169 return trees[0];
170 }
171 const first: RangeTree = trees[0];
172 let delta: number = 0;
173 for (const tree of trees) {
174 delta += tree.delta;
175 }
176 const children: RangeTree[] = mergeRangeTreeChildren(trees);
177 return new RangeTree(first.start, first.end, delta, children);
178}
179
180class RangeTreeWithParent {
181 readonly parentIndex: number;
182 readonly tree: RangeTree;
183
184 constructor(parentIndex: number, tree: RangeTree) {
185 this.parentIndex = parentIndex;
186 this.tree = tree;
187 }
188}
189
190class StartEvent {
191 readonly offset: number;
192 readonly trees: RangeTreeWithParent[];
193
194 constructor(offset: number, trees: RangeTreeWithParent[]) {
195 this.offset = offset;
196 this.trees = trees;
197 }
198
199 static compare(a: StartEvent, b: StartEvent): number {
200 return a.offset - b.offset;
201 }
202}
203
204class StartEventQueue {
205 private readonly queue: StartEvent[];
206 private nextIndex: number;
207 private pendingOffset: number;
208 private pendingTrees: RangeTreeWithParent[] | undefined;
209
210 private constructor(queue: StartEvent[]) {
211 this.queue = queue;
212 this.nextIndex = 0;
213 this.pendingOffset = 0;
214 this.pendingTrees = undefined;
215 }
216
217 static fromParentTrees(parentTrees: ReadonlyArray<RangeTree>): StartEventQueue {
218 const startToTrees: Map<number, RangeTreeWithParent[]> = new Map();
219 for (const [parentIndex, parentTree] of parentTrees.entries()) {
220 for (const child of parentTree.children) {
221 let trees: RangeTreeWithParent[] | undefined = startToTrees.get(child.start);
222 if (trees === undefined) {
223 trees = [];
224 startToTrees.set(child.start, trees);
225 }
226 trees.push(new RangeTreeWithParent(parentIndex, child));
227 }
228 }
229 const queue: StartEvent[] = [];
230 for (const [startOffset, trees] of startToTrees) {
231 queue.push(new StartEvent(startOffset, trees));
232 }
233 queue.sort(StartEvent.compare);
234 return new StartEventQueue(queue);
235 }
236
237 setPendingOffset(offset: number): void {
238 this.pendingOffset = offset;
239 }
240
241 pushPendingTree(tree: RangeTreeWithParent): void {
242 if (this.pendingTrees === undefined) {
243 this.pendingTrees = [];
244 }
245 this.pendingTrees.push(tree);
246 }
247
248 next(): StartEvent | undefined {
249 const pendingTrees: RangeTreeWithParent[] | undefined = this.pendingTrees;
250 const nextEvent: StartEvent | undefined = this.queue[this.nextIndex];
251 if (pendingTrees === undefined) {
252 this.nextIndex++;
253 return nextEvent;
254 } else if (nextEvent === undefined) {
255 this.pendingTrees = undefined;
256 return new StartEvent(this.pendingOffset, pendingTrees);
257 } else {
258 if (this.pendingOffset < nextEvent.offset) {
259 this.pendingTrees = undefined;
260 return new StartEvent(this.pendingOffset, pendingTrees);
261 } else {
262 if (this.pendingOffset === nextEvent.offset) {
263 this.pendingTrees = undefined;
264 for (const tree of pendingTrees) {
265 nextEvent.trees.push(tree);
266 }
267 }
268 this.nextIndex++;
269 return nextEvent;
270 }
271 }
272 }
273}
274
275function mergeRangeTreeChildren(parentTrees: ReadonlyArray<RangeTree>): RangeTree[] {
276 const result: RangeTree[] = [];
277 const startEventQueue: StartEventQueue = StartEventQueue.fromParentTrees(parentTrees);
278 const parentToNested: Map<number, RangeTree[]> = new Map();
279 let openRange: Range | undefined;
280
281 while (true) {
282 const event: StartEvent | undefined = startEventQueue.next();
283 if (event === undefined) {
284 break;
285 }
286
287 if (openRange !== undefined && openRange.end <= event.offset) {
288 result.push(nextChild(openRange, parentToNested));
289 openRange = undefined;
290 }
291
292 if (openRange === undefined) {
293 let openRangeEnd: number = event.offset + 1;
294 for (const {parentIndex, tree} of event.trees) {
295 openRangeEnd = Math.max(openRangeEnd, tree.end);
296 insertChild(parentToNested, parentIndex, tree);
297 }
298 startEventQueue.setPendingOffset(openRangeEnd);
299 openRange = {start: event.offset, end: openRangeEnd};
300 } else {
301 for (const {parentIndex, tree} of event.trees) {
302 if (tree.end > openRange.end) {
303 const right: RangeTree = tree.split(openRange.end);
304 startEventQueue.pushPendingTree(new RangeTreeWithParent(parentIndex, right));
305 }
306 insertChild(parentToNested, parentIndex, tree);
307 }
308 }
309 }
310 if (openRange !== undefined) {
311 result.push(nextChild(openRange, parentToNested));
312 }
313
314 return result;
315}
316
317function insertChild(parentToNested: Map<number, RangeTree[]>, parentIndex: number, tree: RangeTree): void {
318 let nested: RangeTree[] | undefined = parentToNested.get(parentIndex);
319 if (nested === undefined) {
320 nested = [];
321 parentToNested.set(parentIndex, nested);
322 }
323 nested.push(tree);
324}
325
326function nextChild(openRange: Range, parentToNested: Map<number, RangeTree[]>): RangeTree {
327 const matchingTrees: RangeTree[] = [];
328
329 for (const nested of parentToNested.values()) {
330 if (nested.length === 1 && nested[0].start === openRange.start && nested[0].end === openRange.end) {
331 matchingTrees.push(nested[0]);
332 } else {
333 matchingTrees.push(new RangeTree(
334 openRange.start,
335 openRange.end,
336 0,
337 nested,
338 ));
339 }
340 }
341 parentToNested.clear();
342 return mergeRangeTrees(matchingTrees)!;
343}
Note: See TracBrowser for help on using the repository browser.