merge.ts 11 KB

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