summaryrefslogtreecommitdiff
path: root/cli/tools/coverage/merge.rs
diff options
context:
space:
mode:
authorBartek IwaƄczuk <biwanczuk@gmail.com>2022-01-11 21:17:25 +0100
committerGitHub <noreply@github.com>2022-01-11 21:17:25 +0100
commit13751d9de6bb77daf38ac921e35015c238d06c35 (patch)
treeb158b1206225f64374020a8c338dc951bf2ac2fd /cli/tools/coverage/merge.rs
parentf3ece7457a2f87787da1d77afdd4ccec7ba03574 (diff)
fix(coverage): merge coverage ranges (#13334)
Covered ranges were not merged and thus it appeared that some lines might be uncovered. To fix this I used "v8-coverage" that takes care of merging the ranges properly. With this change, coverage collected from a file by multiple entrypoints is now correctly calculated. I ended up forking https://github.com/demurgos/v8-coverage and adding "cli/tools/coverage/merge.rs" and "cli/tools/coverage/range_tree.rs".
Diffstat (limited to 'cli/tools/coverage/merge.rs')
-rw-r--r--cli/tools/coverage/merge.rs840
1 files changed, 840 insertions, 0 deletions
diff --git a/cli/tools/coverage/merge.rs b/cli/tools/coverage/merge.rs
new file mode 100644
index 000000000..70e60edc2
--- /dev/null
+++ b/cli/tools/coverage/merge.rs
@@ -0,0 +1,840 @@
+// Forked from https://github.com/demurgos/v8-coverage/tree/d0ca18da8740198681e0bc68971b0a6cdb11db3e/rust
+// Copyright 2021 Charles Samborski. All rights reserved. MIT license.
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use super::json_types::CoverageRange;
+use super::json_types::FunctionCoverage;
+use super::json_types::ProcessCoverage;
+use super::json_types::ScriptCoverage;
+use super::range_tree::RangeTree;
+use super::range_tree::RangeTreeArena;
+use std::collections::BTreeMap;
+use std::collections::BTreeSet;
+use std::collections::HashMap;
+use std::iter::Peekable;
+
+pub fn merge_processes(
+ mut processes: Vec<ProcessCoverage>,
+) -> Option<ProcessCoverage> {
+ if processes.len() <= 1 {
+ return processes.pop();
+ }
+ let mut url_to_scripts: BTreeMap<String, Vec<ScriptCoverage>> =
+ BTreeMap::new();
+ for process_cov in processes {
+ for script_cov in process_cov.result {
+ url_to_scripts
+ .entry(script_cov.url.clone())
+ .or_insert_with(Vec::new)
+ .push(script_cov);
+ }
+ }
+
+ let result: Vec<ScriptCoverage> = url_to_scripts
+ .into_iter()
+ .enumerate()
+ .map(|(script_id, (_, scripts))| (script_id, scripts))
+ .map(|(script_id, scripts)| {
+ let mut merged: ScriptCoverage = merge_scripts(scripts.to_vec()).unwrap();
+ merged.script_id = script_id.to_string();
+ merged
+ })
+ .collect();
+
+ Some(ProcessCoverage { result })
+}
+
+pub fn merge_scripts(
+ mut scripts: Vec<ScriptCoverage>,
+) -> Option<ScriptCoverage> {
+ if scripts.len() <= 1 {
+ return scripts.pop();
+ }
+ let (script_id, url) = {
+ let first: &ScriptCoverage = &scripts[0];
+ (first.script_id.clone(), first.url.clone())
+ };
+ let mut range_to_funcs: BTreeMap<Range, Vec<FunctionCoverage>> =
+ BTreeMap::new();
+ for script_cov in scripts {
+ for func_cov in script_cov.functions {
+ let root_range = {
+ let root_range_cov: &CoverageRange = &func_cov.ranges[0];
+ Range {
+ start: root_range_cov.start_offset,
+ end: root_range_cov.end_offset,
+ }
+ };
+ range_to_funcs
+ .entry(root_range)
+ .or_insert_with(Vec::new)
+ .push(func_cov);
+ }
+ }
+
+ let functions: Vec<FunctionCoverage> = range_to_funcs
+ .into_iter()
+ .map(|(_, funcs)| merge_functions(funcs).unwrap())
+ .collect();
+
+ Some(ScriptCoverage {
+ script_id,
+ url,
+ functions,
+ })
+}
+
+#[derive(Eq, PartialEq, Hash, Copy, Clone, Debug)]
+struct Range {
+ start: usize,
+ end: usize,
+}
+
+impl Ord for Range {
+ fn cmp(&self, other: &Self) -> ::std::cmp::Ordering {
+ if self.start != other.start {
+ self.start.cmp(&other.start)
+ } else {
+ other.end.cmp(&self.end)
+ }
+ }
+}
+
+impl PartialOrd for Range {
+ fn partial_cmp(&self, other: &Self) -> Option<::std::cmp::Ordering> {
+ if self.start != other.start {
+ self.start.partial_cmp(&other.start)
+ } else {
+ other.end.partial_cmp(&self.end)
+ }
+ }
+}
+
+pub fn merge_functions(
+ mut funcs: Vec<FunctionCoverage>,
+) -> Option<FunctionCoverage> {
+ if funcs.len() <= 1 {
+ return funcs.pop();
+ }
+ let function_name = funcs[0].function_name.clone();
+ let rta_capacity: usize =
+ funcs.iter().fold(0, |acc, func| acc + func.ranges.len());
+ let rta = RangeTreeArena::with_capacity(rta_capacity);
+ let mut trees: Vec<&mut RangeTree> = Vec::new();
+ for func in funcs {
+ if let Some(tree) = RangeTree::from_sorted_ranges(&rta, &func.ranges) {
+ trees.push(tree);
+ }
+ }
+ let merged =
+ RangeTree::normalize(&rta, merge_range_trees(&rta, trees).unwrap());
+ let ranges = merged.to_ranges();
+ let is_block_coverage: bool = !(ranges.len() == 1 && ranges[0].count == 0);
+
+ Some(FunctionCoverage {
+ function_name,
+ ranges,
+ is_block_coverage,
+ })
+}
+
+fn merge_range_trees<'a>(
+ rta: &'a RangeTreeArena<'a>,
+ mut trees: Vec<&'a mut RangeTree<'a>>,
+) -> Option<&'a mut RangeTree<'a>> {
+ if trees.len() <= 1 {
+ return trees.pop();
+ }
+ let (start, end) = {
+ let first = &trees[0];
+ (first.start, first.end)
+ };
+ let delta: i64 = trees.iter().fold(0, |acc, tree| acc + tree.delta);
+ let children = merge_range_tree_children(rta, trees);
+
+ Some(rta.alloc(RangeTree::new(start, end, delta, children)))
+}
+
+struct StartEvent<'a> {
+ offset: usize,
+ trees: Vec<(usize, &'a mut RangeTree<'a>)>,
+}
+
+fn into_start_events<'a>(trees: Vec<&'a mut RangeTree<'a>>) -> Vec<StartEvent> {
+ let mut result: BTreeMap<usize, Vec<(usize, &'a mut RangeTree<'a>)>> =
+ BTreeMap::new();
+ for (parent_index, tree) in trees.into_iter().enumerate() {
+ for child in tree.children.drain(..) {
+ result
+ .entry(child.start)
+ .or_insert_with(Vec::new)
+ .push((parent_index, child));
+ }
+ }
+ result
+ .into_iter()
+ .map(|(offset, trees)| StartEvent { offset, trees })
+ .collect()
+}
+
+struct StartEventQueue<'a> {
+ pending: Option<StartEvent<'a>>,
+ queue: Peekable<::std::vec::IntoIter<StartEvent<'a>>>,
+}
+
+impl<'a> StartEventQueue<'a> {
+ pub fn new(queue: Vec<StartEvent<'a>>) -> StartEventQueue<'a> {
+ StartEventQueue {
+ pending: None,
+ queue: queue.into_iter().peekable(),
+ }
+ }
+
+ pub(crate) fn set_pending_offset(&mut self, offset: usize) {
+ self.pending = Some(StartEvent {
+ offset,
+ trees: Vec::new(),
+ });
+ }
+
+ pub(crate) fn push_pending_tree(
+ &mut self,
+ tree: (usize, &'a mut RangeTree<'a>),
+ ) {
+ self.pending = self.pending.take().map(|mut start_event| {
+ start_event.trees.push(tree);
+ start_event
+ });
+ }
+}
+
+impl<'a> Iterator for StartEventQueue<'a> {
+ type Item = StartEvent<'a>;
+
+ fn next(&mut self) -> Option<<Self as Iterator>::Item> {
+ let pending_offset: Option<usize> = match &self.pending {
+ Some(ref start_event) if !start_event.trees.is_empty() => {
+ Some(start_event.offset)
+ }
+ _ => None,
+ };
+
+ match pending_offset {
+ Some(pending_offset) => {
+ let queue_offset =
+ self.queue.peek().map(|start_event| start_event.offset);
+ match queue_offset {
+ None => self.pending.take(),
+ Some(queue_offset) => {
+ if pending_offset < queue_offset {
+ self.pending.take()
+ } else {
+ let mut result = self.queue.next().unwrap();
+ if pending_offset == queue_offset {
+ let pending_trees = self.pending.take().unwrap().trees;
+ result.trees.extend(pending_trees.into_iter())
+ }
+ Some(result)
+ }
+ }
+ }
+ }
+ None => self.queue.next(),
+ }
+ }
+}
+
+fn merge_range_tree_children<'a>(
+ rta: &'a RangeTreeArena<'a>,
+ parent_trees: Vec<&'a mut RangeTree<'a>>,
+) -> Vec<&'a mut RangeTree<'a>> {
+ let mut flat_children: Vec<Vec<&'a mut RangeTree<'a>>> =
+ Vec::with_capacity(parent_trees.len());
+ let mut wrapped_children: Vec<Vec<&'a mut RangeTree<'a>>> =
+ Vec::with_capacity(parent_trees.len());
+ let mut open_range: Option<Range> = None;
+
+ for _parent_tree in parent_trees.iter() {
+ flat_children.push(Vec::new());
+ wrapped_children.push(Vec::new());
+ }
+
+ let mut start_event_queue =
+ StartEventQueue::new(into_start_events(parent_trees));
+
+ let mut parent_to_nested: HashMap<usize, Vec<&'a mut RangeTree<'a>>> =
+ HashMap::new();
+
+ while let Some(event) = start_event_queue.next() {
+ open_range = if let Some(open_range) = open_range {
+ if open_range.end <= event.offset {
+ for (parent_index, nested) in parent_to_nested {
+ wrapped_children[parent_index].push(rta.alloc(RangeTree::new(
+ open_range.start,
+ open_range.end,
+ 0,
+ nested,
+ )));
+ }
+ parent_to_nested = HashMap::new();
+ None
+ } else {
+ Some(open_range)
+ }
+ } else {
+ None
+ };
+
+ match open_range {
+ Some(open_range) => {
+ for (parent_index, tree) in event.trees {
+ let child = if tree.end > open_range.end {
+ let (left, right) = RangeTree::split(rta, tree, open_range.end);
+ start_event_queue.push_pending_tree((parent_index, right));
+ left
+ } else {
+ tree
+ };
+ parent_to_nested
+ .entry(parent_index)
+ .or_insert_with(Vec::new)
+ .push(child);
+ }
+ }
+ None => {
+ let mut open_range_end: usize = event.offset + 1;
+ for (_, ref tree) in &event.trees {
+ open_range_end = if tree.end > open_range_end {
+ tree.end
+ } else {
+ open_range_end
+ };
+ }
+ for (parent_index, tree) in event.trees {
+ if tree.end == open_range_end {
+ flat_children[parent_index].push(tree);
+ continue;
+ }
+ parent_to_nested
+ .entry(parent_index)
+ .or_insert_with(Vec::new)
+ .push(tree);
+ }
+ start_event_queue.set_pending_offset(open_range_end);
+ open_range = Some(Range {
+ start: event.offset,
+ end: open_range_end,
+ });
+ }
+ }
+ }
+ if let Some(open_range) = open_range {
+ for (parent_index, nested) in parent_to_nested {
+ wrapped_children[parent_index].push(rta.alloc(RangeTree::new(
+ open_range.start,
+ open_range.end,
+ 0,
+ nested,
+ )));
+ }
+ }
+
+ let child_forests: Vec<Vec<&'a mut RangeTree<'a>>> = flat_children
+ .into_iter()
+ .zip(wrapped_children.into_iter())
+ .map(|(flat, wrapped)| merge_children_lists(flat, wrapped))
+ .collect();
+
+ let events = get_child_events_from_forests(&child_forests);
+
+ let mut child_forests: Vec<
+ Peekable<::std::vec::IntoIter<&'a mut RangeTree<'a>>>,
+ > = child_forests
+ .into_iter()
+ .map(|forest| forest.into_iter().peekable())
+ .collect();
+
+ let mut result: Vec<&'a mut RangeTree<'a>> = Vec::new();
+ for event in events.iter() {
+ let mut matching_trees: Vec<&'a mut RangeTree<'a>> = Vec::new();
+ for (_parent_index, children) in child_forests.iter_mut().enumerate() {
+ let next_tree: Option<&'a mut RangeTree<'a>> = {
+ if children.peek().map_or(false, |tree| tree.start == *event) {
+ children.next()
+ } else {
+ None
+ }
+ };
+ if let Some(next_tree) = next_tree {
+ matching_trees.push(next_tree);
+ }
+ }
+ if let Some(merged) = merge_range_trees(rta, matching_trees) {
+ result.push(merged);
+ }
+ }
+
+ result
+}
+
+fn get_child_events_from_forests<'a>(
+ forests: &[Vec<&'a mut RangeTree<'a>>],
+) -> BTreeSet<usize> {
+ let mut event_set: BTreeSet<usize> = BTreeSet::new();
+ for forest in forests {
+ for tree in forest {
+ event_set.insert(tree.start);
+ event_set.insert(tree.end);
+ }
+ }
+ event_set
+}
+
+// TODO: itertools?
+// https://play.integer32.com/?gist=ad2cd20d628e647a5dbdd82e68a15cb6&version=stable&mode=debug&edition=2015
+fn merge_children_lists<'a>(
+ a: Vec<&'a mut RangeTree<'a>>,
+ b: Vec<&'a mut RangeTree<'a>>,
+) -> Vec<&'a mut RangeTree<'a>> {
+ let mut merged: Vec<&'a mut RangeTree<'a>> = Vec::new();
+ let mut a = a.into_iter();
+ let mut b = b.into_iter();
+ let mut next_a = a.next();
+ let mut next_b = b.next();
+ loop {
+ match (next_a, next_b) {
+ (Some(tree_a), Some(tree_b)) => {
+ if tree_a.start < tree_b.start {
+ merged.push(tree_a);
+ next_a = a.next();
+ next_b = Some(tree_b);
+ } else {
+ merged.push(tree_b);
+ next_a = Some(tree_a);
+ next_b = b.next();
+ }
+ }
+ (Some(tree_a), None) => {
+ merged.push(tree_a);
+ merged.extend(a);
+ break;
+ }
+ (None, Some(tree_b)) => {
+ merged.push(tree_b);
+ merged.extend(b);
+ break;
+ }
+ (None, None) => break,
+ }
+ }
+
+ merged
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ // use test_generator::test_resources;
+
+ #[test]
+ fn empty() {
+ let inputs: Vec<ProcessCoverage> = Vec::new();
+ let expected: Option<ProcessCoverage> = None;
+
+ assert_eq!(merge_processes(inputs), expected);
+ }
+
+ #[test]
+ fn two_flat_trees() {
+ let inputs: Vec<ProcessCoverage> = vec![
+ ProcessCoverage {
+ result: vec![ScriptCoverage {
+ script_id: String::from("0"),
+ url: String::from("/lib.js"),
+ functions: vec![FunctionCoverage {
+ function_name: String::from("lib"),
+ is_block_coverage: true,
+ ranges: vec![CoverageRange {
+ start_offset: 0,
+ end_offset: 9,
+ count: 1,
+ }],
+ }],
+ }],
+ },
+ ProcessCoverage {
+ result: vec![ScriptCoverage {
+ script_id: String::from("0"),
+ url: String::from("/lib.js"),
+ functions: vec![FunctionCoverage {
+ function_name: String::from("lib"),
+ is_block_coverage: true,
+ ranges: vec![CoverageRange {
+ start_offset: 0,
+ end_offset: 9,
+ count: 2,
+ }],
+ }],
+ }],
+ },
+ ];
+ let expected: Option<ProcessCoverage> = Some(ProcessCoverage {
+ result: vec![ScriptCoverage {
+ script_id: String::from("0"),
+ url: String::from("/lib.js"),
+ functions: vec![FunctionCoverage {
+ function_name: String::from("lib"),
+ is_block_coverage: true,
+ ranges: vec![CoverageRange {
+ start_offset: 0,
+ end_offset: 9,
+ count: 3,
+ }],
+ }],
+ }],
+ });
+
+ assert_eq!(merge_processes(inputs), expected);
+ }
+
+ #[test]
+ fn two_trees_with_matching_children() {
+ let inputs: Vec<ProcessCoverage> = vec![
+ ProcessCoverage {
+ result: vec![ScriptCoverage {
+ script_id: String::from("0"),
+ url: String::from("/lib.js"),
+ functions: vec![FunctionCoverage {
+ function_name: String::from("lib"),
+ is_block_coverage: true,
+ ranges: vec![
+ CoverageRange {
+ start_offset: 0,
+ end_offset: 9,
+ count: 10,
+ },
+ CoverageRange {
+ start_offset: 3,
+ end_offset: 6,
+ count: 1,
+ },
+ ],
+ }],
+ }],
+ },
+ ProcessCoverage {
+ result: vec![ScriptCoverage {
+ script_id: String::from("0"),
+ url: String::from("/lib.js"),
+ functions: vec![FunctionCoverage {
+ function_name: String::from("lib"),
+ is_block_coverage: true,
+ ranges: vec![
+ CoverageRange {
+ start_offset: 0,
+ end_offset: 9,
+ count: 20,
+ },
+ CoverageRange {
+ start_offset: 3,
+ end_offset: 6,
+ count: 2,
+ },
+ ],
+ }],
+ }],
+ },
+ ];
+ let expected: Option<ProcessCoverage> = Some(ProcessCoverage {
+ result: vec![ScriptCoverage {
+ script_id: String::from("0"),
+ url: String::from("/lib.js"),
+ functions: vec![FunctionCoverage {
+ function_name: String::from("lib"),
+ is_block_coverage: true,
+ ranges: vec![
+ CoverageRange {
+ start_offset: 0,
+ end_offset: 9,
+ count: 30,
+ },
+ CoverageRange {
+ start_offset: 3,
+ end_offset: 6,
+ count: 3,
+ },
+ ],
+ }],
+ }],
+ });
+
+ assert_eq!(merge_processes(inputs), expected);
+ }
+
+ #[test]
+ fn two_trees_with_partially_overlapping_children() {
+ let inputs: Vec<ProcessCoverage> = vec![
+ ProcessCoverage {
+ result: vec![ScriptCoverage {
+ script_id: String::from("0"),
+ url: String::from("/lib.js"),
+ functions: vec![FunctionCoverage {
+ function_name: String::from("lib"),
+ is_block_coverage: true,
+ ranges: vec![
+ CoverageRange {
+ start_offset: 0,
+ end_offset: 9,
+ count: 10,
+ },
+ CoverageRange {
+ start_offset: 2,
+ end_offset: 5,
+ count: 1,
+ },
+ ],
+ }],
+ }],
+ },
+ ProcessCoverage {
+ result: vec![ScriptCoverage {
+ script_id: String::from("0"),
+ url: String::from("/lib.js"),
+ functions: vec![FunctionCoverage {
+ function_name: String::from("lib"),
+ is_block_coverage: true,
+ ranges: vec![
+ CoverageRange {
+ start_offset: 0,
+ end_offset: 9,
+ count: 20,
+ },
+ CoverageRange {
+ start_offset: 4,
+ end_offset: 7,
+ count: 2,
+ },
+ ],
+ }],
+ }],
+ },
+ ];
+ let expected: Option<ProcessCoverage> = Some(ProcessCoverage {
+ result: vec![ScriptCoverage {
+ script_id: String::from("0"),
+ url: String::from("/lib.js"),
+ functions: vec![FunctionCoverage {
+ function_name: String::from("lib"),
+ is_block_coverage: true,
+ ranges: vec![
+ CoverageRange {
+ start_offset: 0,
+ end_offset: 9,
+ count: 30,
+ },
+ CoverageRange {
+ start_offset: 2,
+ end_offset: 5,
+ count: 21,
+ },
+ CoverageRange {
+ start_offset: 4,
+ end_offset: 5,
+ count: 3,
+ },
+ CoverageRange {
+ start_offset: 5,
+ end_offset: 7,
+ count: 12,
+ },
+ ],
+ }],
+ }],
+ });
+
+ assert_eq!(merge_processes(inputs), expected);
+ }
+
+ #[test]
+ fn two_trees_with_with_complementary_children_summing_to_the_same_count() {
+ let inputs: Vec<ProcessCoverage> = vec![
+ ProcessCoverage {
+ result: vec![ScriptCoverage {
+ script_id: String::from("0"),
+ url: String::from("/lib.js"),
+ functions: vec![FunctionCoverage {
+ function_name: String::from("lib"),
+ is_block_coverage: true,
+ ranges: vec![
+ CoverageRange {
+ start_offset: 0,
+ end_offset: 9,
+ count: 1,
+ },
+ CoverageRange {
+ start_offset: 1,
+ end_offset: 8,
+ count: 6,
+ },
+ CoverageRange {
+ start_offset: 1,
+ end_offset: 5,
+ count: 5,
+ },
+ CoverageRange {
+ start_offset: 5,
+ end_offset: 8,
+ count: 7,
+ },
+ ],
+ }],
+ }],
+ },
+ ProcessCoverage {
+ result: vec![ScriptCoverage {
+ script_id: String::from("0"),
+ url: String::from("/lib.js"),
+ functions: vec![FunctionCoverage {
+ function_name: String::from("lib"),
+ is_block_coverage: true,
+ ranges: vec![
+ CoverageRange {
+ start_offset: 0,
+ end_offset: 9,
+ count: 4,
+ },
+ CoverageRange {
+ start_offset: 1,
+ end_offset: 8,
+ count: 8,
+ },
+ CoverageRange {
+ start_offset: 1,
+ end_offset: 5,
+ count: 9,
+ },
+ CoverageRange {
+ start_offset: 5,
+ end_offset: 8,
+ count: 7,
+ },
+ ],
+ }],
+ }],
+ },
+ ];
+ let expected: Option<ProcessCoverage> = Some(ProcessCoverage {
+ result: vec![ScriptCoverage {
+ script_id: String::from("0"),
+ url: String::from("/lib.js"),
+ functions: vec![FunctionCoverage {
+ function_name: String::from("lib"),
+ is_block_coverage: true,
+ ranges: vec![
+ CoverageRange {
+ start_offset: 0,
+ end_offset: 9,
+ count: 5,
+ },
+ CoverageRange {
+ start_offset: 1,
+ end_offset: 8,
+ count: 14,
+ },
+ ],
+ }],
+ }],
+ });
+
+ assert_eq!(merge_processes(inputs), expected);
+ }
+
+ #[test]
+ fn merges_a_similar_sliding_chain_a_bc() {
+ let inputs: Vec<ProcessCoverage> = vec![
+ ProcessCoverage {
+ result: vec![ScriptCoverage {
+ script_id: String::from("0"),
+ url: String::from("/lib.js"),
+ functions: vec![FunctionCoverage {
+ function_name: String::from("lib"),
+ is_block_coverage: true,
+ ranges: vec![
+ CoverageRange {
+ start_offset: 0,
+ end_offset: 7,
+ count: 10,
+ },
+ CoverageRange {
+ start_offset: 0,
+ end_offset: 4,
+ count: 1,
+ },
+ ],
+ }],
+ }],
+ },
+ ProcessCoverage {
+ result: vec![ScriptCoverage {
+ script_id: String::from("0"),
+ url: String::from("/lib.js"),
+ functions: vec![FunctionCoverage {
+ function_name: String::from("lib"),
+ is_block_coverage: true,
+ ranges: vec![
+ CoverageRange {
+ start_offset: 0,
+ end_offset: 7,
+ count: 20,
+ },
+ CoverageRange {
+ start_offset: 1,
+ end_offset: 6,
+ count: 11,
+ },
+ CoverageRange {
+ start_offset: 2,
+ end_offset: 5,
+ count: 2,
+ },
+ ],
+ }],
+ }],
+ },
+ ];
+ let expected: Option<ProcessCoverage> = Some(ProcessCoverage {
+ result: vec![ScriptCoverage {
+ script_id: String::from("0"),
+ url: String::from("/lib.js"),
+ functions: vec![FunctionCoverage {
+ function_name: String::from("lib"),
+ is_block_coverage: true,
+ ranges: vec![
+ CoverageRange {
+ start_offset: 0,
+ end_offset: 7,
+ count: 30,
+ },
+ CoverageRange {
+ start_offset: 0,
+ end_offset: 6,
+ count: 21,
+ },
+ CoverageRange {
+ start_offset: 1,
+ end_offset: 5,
+ count: 12,
+ },
+ CoverageRange {
+ start_offset: 2,
+ end_offset: 4,
+ count: 3,
+ },
+ ],
+ }],
+ }],
+ });
+
+ assert_eq!(merge_processes(inputs), expected);
+ }
+}