diff --git a/Cargo.lock b/Cargo.lock index f270f956ca..097eb1926c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -753,6 +753,7 @@ dependencies = [ "tokio", "trust-dns-client", "trust-dns-server", + "typed-arena", "uuid", "walkdir", "winapi 0.3.9", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 7fd34e2f17..66fa281ee5 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -84,6 +84,7 @@ tempfile = "=3.2.0" text-size = "=1.1.0" text_lines = "=0.4.1" tokio = { version = "=1.14", features = ["full"] } +typed-arena = "2.0.1" uuid = { version = "=0.8.2", features = ["v4", "serde"] } walkdir = "=2.3.2" zstd = '=0.9.2' diff --git a/cli/tests/integration/coverage_tests.rs b/cli/tests/integration/coverage_tests.rs index 637185e295..e6eb5fe3af 100644 --- a/cli/tests/integration/coverage_tests.rs +++ b/cli/tests/integration/coverage_tests.rs @@ -97,3 +97,83 @@ fn run_coverage_text(test_name: &str, extension: &str) { assert!(output.status.success()); } + +#[test] +fn multifile_coverage() { + let deno_dir = TempDir::new().expect("tempdir fail"); + let tempdir = TempDir::new().expect("tempdir fail"); + let tempdir = tempdir.path().join("cov"); + + let status = util::deno_cmd_with_deno_dir(deno_dir.path()) + .current_dir(util::testdata_path()) + .arg("test") + .arg("--quiet") + .arg("--unstable") + .arg(format!("--coverage={}", tempdir.to_str().unwrap())) + .arg("coverage/multifile/") + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .status() + .expect("failed to spawn test runner"); + + assert!(status.success()); + + let output = util::deno_cmd_with_deno_dir(deno_dir.path()) + .current_dir(util::testdata_path()) + .arg("coverage") + .arg("--unstable") + .arg(format!("{}/", tempdir.to_str().unwrap())) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .output() + .expect("failed to spawn coverage reporter"); + + // Verify there's no "Check" being printed + assert!(output.stderr.is_empty()); + + let actual = + util::strip_ansi_codes(std::str::from_utf8(&output.stdout).unwrap()) + .to_string(); + + let expected = fs::read_to_string( + util::testdata_path().join("coverage/multifile/expected.out"), + ) + .unwrap(); + + if !util::wildcard_match(&expected, &actual) { + println!("OUTPUT\n{}\nOUTPUT", actual); + println!("EXPECTED\n{}\nEXPECTED", expected); + panic!("pattern match failed"); + } + + assert!(output.status.success()); + + let output = util::deno_cmd_with_deno_dir(deno_dir.path()) + .current_dir(util::testdata_path()) + .arg("coverage") + .arg("--quiet") + .arg("--unstable") + .arg("--lcov") + .arg(format!("{}/", tempdir.to_str().unwrap())) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .output() + .expect("failed to spawn coverage reporter"); + + let actual = + util::strip_ansi_codes(std::str::from_utf8(&output.stdout).unwrap()) + .to_string(); + + let expected = fs::read_to_string( + util::testdata_path().join("coverage/multifile/expected.lcov"), + ) + .unwrap(); + + if !util::wildcard_match(&expected, &actual) { + println!("OUTPUT\n{}\nOUTPUT", actual); + println!("EXPECTED\n{}\nEXPECTED", expected); + panic!("pattern match failed"); + } + + assert!(output.status.success()); +} diff --git a/cli/tests/testdata/coverage/multifile/a_test.js b/cli/tests/testdata/coverage/multifile/a_test.js new file mode 100644 index 0000000000..d5d9c3533c --- /dev/null +++ b/cli/tests/testdata/coverage/multifile/a_test.js @@ -0,0 +1,8 @@ +import { test } from "./mod.js"; + +Deno.test({ + name: "bugrepo a", + fn: () => { + test(true); + }, +}); diff --git a/cli/tests/testdata/coverage/multifile/b_test.js b/cli/tests/testdata/coverage/multifile/b_test.js new file mode 100644 index 0000000000..d93b15a172 --- /dev/null +++ b/cli/tests/testdata/coverage/multifile/b_test.js @@ -0,0 +1,8 @@ +import { test } from "./mod.js"; + +Deno.test({ + name: "bugrepo b", + fn: () => { + test(false); + }, +}); diff --git a/cli/tests/testdata/coverage/multifile/expected.lcov b/cli/tests/testdata/coverage/multifile/expected.lcov new file mode 100644 index 0000000000..03ad5e7bd8 --- /dev/null +++ b/cli/tests/testdata/coverage/multifile/expected.lcov @@ -0,0 +1,18 @@ +SF:[WILDCARD]mod.js +FN:1,test +FNDA:2,test +FNF:1 +FNH:1 +BRDA:2,1,0,1 +BRF:1 +BRH:1 +DA:1,2 +DA:2,4 +DA:3,5 +DA:4,5 +DA:5,5 +DA:6,4 +DA:7,1 +LH:7 +LF:7 +end_of_record diff --git a/cli/tests/testdata/coverage/multifile/expected.out b/cli/tests/testdata/coverage/multifile/expected.out new file mode 100644 index 0000000000..fde26e1653 --- /dev/null +++ b/cli/tests/testdata/coverage/multifile/expected.out @@ -0,0 +1 @@ +cover [WILDCARD]/multifile/mod.js ... 100.000% (7/7) diff --git a/cli/tests/testdata/coverage/multifile/mod.js b/cli/tests/testdata/coverage/multifile/mod.js new file mode 100644 index 0000000000..b9f8d627af --- /dev/null +++ b/cli/tests/testdata/coverage/multifile/mod.js @@ -0,0 +1,6 @@ +export function test(a) { + if (a) { + return 0; + } + return 1; +} diff --git a/cli/tools/coverage/json_types.rs b/cli/tools/coverage/json_types.rs new file mode 100644 index 0000000000..1e46cc7fde --- /dev/null +++ b/cli/tools/coverage/json_types.rs @@ -0,0 +1,58 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use serde::Deserialize; +use serde::Serialize; + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CoverageRange { + /// Start byte index. + pub start_offset: usize, + /// End byte index. + pub end_offset: usize, + pub count: i64, +} + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FunctionCoverage { + pub function_name: String, + pub ranges: Vec, + pub is_block_coverage: bool, +} + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ScriptCoverage { + pub script_id: String, + pub url: String, + pub functions: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StartPreciseCoverageParameters { + pub call_count: bool, + pub detailed: bool, + pub allow_triggered_updates: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StartPreciseCoverageReturnObject { + pub timestamp: f64, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TakePreciseCoverageReturnObject { + pub result: Vec, + pub timestamp: f64, +} + +// TODO(bartlomieju): remove me +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProcessCoverage { + pub result: Vec, +} diff --git a/cli/tools/coverage/merge.rs b/cli/tools/coverage/merge.rs new file mode 100644 index 0000000000..70e60edc21 --- /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, +) -> Option { + if processes.len() <= 1 { + return processes.pop(); + } + let mut url_to_scripts: BTreeMap> = + 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 = 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, +) -> Option { + 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> = + 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 = 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, +) -> Option { + 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 { + let mut result: BTreeMap)>> = + 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>, + queue: Peekable<::std::vec::IntoIter>>, +} + +impl<'a> StartEventQueue<'a> { + pub fn new(queue: Vec>) -> 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<::Item> { + let pending_offset: Option = 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::with_capacity(parent_trees.len()); + let mut wrapped_children: Vec>> = + Vec::with_capacity(parent_trees.len()); + let mut open_range: Option = 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>> = + 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>> = 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 { + let mut event_set: BTreeSet = 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 = Vec::new(); + let expected: Option = None; + + assert_eq!(merge_processes(inputs), expected); + } + + #[test] + fn two_flat_trees() { + let inputs: Vec = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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); + } +} diff --git a/cli/tools/coverage.rs b/cli/tools/coverage/mod.rs similarity index 88% rename from cli/tools/coverage.rs rename to cli/tools/coverage/mod.rs index 3cec356065..1c14859ea9 100644 --- a/cli/tools/coverage.rs +++ b/cli/tools/coverage/mod.rs @@ -17,8 +17,6 @@ use deno_core::serde_json; use deno_core::url::Url; use deno_core::LocalInspectorSession; use regex::Regex; -use serde::Deserialize; -use serde::Serialize; use sourcemap::SourceMap; use std::fs; use std::fs::File; @@ -28,52 +26,11 @@ use std::path::PathBuf; use text_lines::TextLines; use uuid::Uuid; -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -struct CoverageRange { - /// Start byte index. - start_offset: usize, - /// End byte index. - end_offset: usize, - count: usize, -} +mod json_types; +mod merge; +mod range_tree; -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -struct FunctionCoverage { - function_name: String, - ranges: Vec, - is_block_coverage: bool, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -struct ScriptCoverage { - script_id: String, - url: String, - functions: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct StartPreciseCoverageParameters { - call_count: bool, - detailed: bool, - allow_triggered_updates: bool, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct StartPreciseCoverageReturnObject { - timestamp: f64, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct TakePreciseCoverageReturnObject { - result: Vec, - timestamp: f64, -} +use json_types::*; pub struct CoverageCollector { pub dir: PathBuf, @@ -175,21 +132,21 @@ struct BranchCoverageItem { line_index: usize, block_number: usize, branch_number: usize, - taken: Option, + taken: Option, is_hit: bool, } struct FunctionCoverageItem { name: String, line_index: usize, - execution_count: usize, + execution_count: i64, } struct CoverageReport { url: ModuleSpecifier, named_functions: Vec, branches: Vec, - found_lines: Vec<(usize, usize)>, + found_lines: Vec<(usize, i64)>, } fn generate_coverage_report( @@ -353,7 +310,7 @@ fn generate_coverage_report( results.into_iter() }) .flatten() - .collect::>(); + .collect::>(); found_lines.sort_unstable_by_key(|(index, _)| *index); // combine duplicated lines @@ -369,7 +326,7 @@ fn generate_coverage_report( .into_iter() .enumerate() .map(|(index, count)| (index, count)) - .collect::>() + .collect::>() }; coverage_report @@ -553,38 +510,7 @@ fn collect_coverages( for file_path in file_paths { let json = fs::read_to_string(file_path.as_path())?; let new_coverage: ScriptCoverage = serde_json::from_str(&json)?; - - let existing_coverage = - coverages.iter_mut().find(|x| x.url == new_coverage.url); - - if let Some(existing_coverage) = existing_coverage { - for new_function in new_coverage.functions { - let existing_function = existing_coverage - .functions - .iter_mut() - .find(|x| x.function_name == new_function.function_name); - - if let Some(existing_function) = existing_function { - for new_range in new_function.ranges { - let existing_range = - existing_function.ranges.iter_mut().find(|x| { - x.start_offset == new_range.start_offset - && x.end_offset == new_range.end_offset - }); - - if let Some(existing_range) = existing_range { - existing_range.count += new_range.count; - } else { - existing_function.ranges.push(new_range); - } - } - } else { - existing_coverage.functions.push(new_function); - } - } - } else { - coverages.push(new_coverage); - } + coverages.push(new_coverage); } coverages.sort_by_key(|k| k.url.clone()); @@ -632,6 +558,18 @@ pub async fn cover_files( coverage_flags.exclude, ); + let proc_coverages: Vec<_> = script_coverages + .into_iter() + .map(|cov| ProcessCoverage { result: vec![cov] }) + .collect(); + + let script_coverages = if let Some(c) = merge::merge_processes(proc_coverages) + { + c.result + } else { + vec![] + }; + let reporter_kind = if coverage_flags.lcov { CoverageReporterKind::Lcov } else { diff --git a/cli/tools/coverage/range_tree.rs b/cli/tools/coverage/range_tree.rs new file mode 100644 index 0000000000..24d0a9ffcf --- /dev/null +++ b/cli/tools/coverage/range_tree.rs @@ -0,0 +1,207 @@ +// 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 std::iter::Peekable; +use typed_arena::Arena; + +pub struct RangeTreeArena<'a>(Arena>); + +impl<'a> RangeTreeArena<'a> { + #[cfg(test)] + pub fn new() -> Self { + RangeTreeArena(Arena::new()) + } + + pub fn with_capacity(n: usize) -> Self { + RangeTreeArena(Arena::with_capacity(n)) + } + + #[allow(clippy::mut_from_ref)] + pub fn alloc(&'a self, value: RangeTree<'a>) -> &'a mut RangeTree<'a> { + self.0.alloc(value) + } +} + +#[derive(Eq, PartialEq, Debug)] +pub struct RangeTree<'a> { + pub start: usize, + pub end: usize, + pub delta: i64, + pub children: Vec<&'a mut RangeTree<'a>>, +} + +impl<'rt> RangeTree<'rt> { + pub fn new<'a>( + start: usize, + end: usize, + delta: i64, + children: Vec<&'a mut RangeTree<'a>>, + ) -> RangeTree<'a> { + RangeTree { + start, + end, + delta, + children, + } + } + + pub fn split<'a>( + rta: &'a RangeTreeArena<'a>, + tree: &'a mut RangeTree<'a>, + value: usize, + ) -> (&'a mut RangeTree<'a>, &'a mut RangeTree<'a>) { + let mut left_children: Vec<&'a mut RangeTree<'a>> = Vec::new(); + let mut right_children: Vec<&'a mut RangeTree<'a>> = Vec::new(); + for child in tree.children.iter_mut() { + if child.end <= value { + left_children.push(child); + } else if value <= child.start { + right_children.push(child); + } else { + let (left_child, right_child) = Self::split(rta, child, value); + left_children.push(left_child); + right_children.push(right_child); + } + } + + let left = RangeTree::new(tree.start, value, tree.delta, left_children); + let right = RangeTree::new(value, tree.end, tree.delta, right_children); + (rta.alloc(left), rta.alloc(right)) + } + + pub fn normalize<'a>( + rta: &'a RangeTreeArena<'a>, + tree: &'a mut RangeTree<'a>, + ) -> &'a mut RangeTree<'a> { + tree.children = { + let mut children: Vec<&'a mut RangeTree<'a>> = Vec::new(); + let mut chain: Vec<&'a mut RangeTree<'a>> = Vec::new(); + for child in tree.children.drain(..) { + let is_chain_end: bool = + match chain.last().map(|tree| (tree.delta, tree.end)) { + Some((delta, chain_end)) => { + (delta, chain_end) != (child.delta, child.start) + } + None => false, + }; + if is_chain_end { + let mut chain_iter = chain.drain(..); + let mut head: &'a mut RangeTree<'a> = chain_iter.next().unwrap(); + for tree in chain_iter { + head.end = tree.end; + for sub_child in tree.children.drain(..) { + sub_child.delta += tree.delta - head.delta; + head.children.push(sub_child); + } + } + children.push(RangeTree::normalize(rta, head)); + } + chain.push(child) + } + if !chain.is_empty() { + let mut chain_iter = chain.drain(..); + let mut head: &'a mut RangeTree<'a> = chain_iter.next().unwrap(); + for tree in chain_iter { + head.end = tree.end; + for sub_child in tree.children.drain(..) { + sub_child.delta += tree.delta - head.delta; + head.children.push(sub_child); + } + } + children.push(RangeTree::normalize(rta, head)); + } + + if children.len() == 1 + && children[0].start == tree.start + && children[0].end == tree.end + { + let normalized = children.remove(0); + normalized.delta += tree.delta; + return normalized; + } + + children + }; + + tree + } + + pub fn to_ranges(&self) -> Vec { + let mut ranges: Vec = Vec::new(); + let mut stack: Vec<(&RangeTree, i64)> = vec![(self, 0)]; + while let Some((cur, parent_count)) = stack.pop() { + let count: i64 = parent_count + cur.delta; + ranges.push(CoverageRange { + start_offset: cur.start, + end_offset: cur.end, + count, + }); + for child in cur.children.iter().rev() { + stack.push((child, count)) + } + } + ranges + } + + pub fn from_sorted_ranges<'a>( + rta: &'a RangeTreeArena<'a>, + ranges: &[CoverageRange], + ) -> Option<&'a mut RangeTree<'a>> { + Self::from_sorted_ranges_inner( + rta, + &mut ranges.iter().peekable(), + ::std::usize::MAX, + 0, + ) + } + + fn from_sorted_ranges_inner<'a, 'b, 'c: 'b>( + rta: &'a RangeTreeArena<'a>, + ranges: &'b mut Peekable>, + parent_end: usize, + parent_count: i64, + ) -> Option<&'a mut RangeTree<'a>> { + let has_range: bool = match ranges.peek() { + None => false, + Some(range) => range.start_offset < parent_end, + }; + if !has_range { + return None; + } + let range = ranges.next().unwrap(); + let start: usize = range.start_offset; + let end: usize = range.end_offset; + let count: i64 = range.count; + let delta: i64 = count - parent_count; + let mut children: Vec<&mut RangeTree> = Vec::new(); + while let Some(child) = + Self::from_sorted_ranges_inner(rta, ranges, end, count) + { + children.push(child); + } + Some(rta.alloc(RangeTree::new(start, end, delta, children))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_sorted_ranges_empty() { + let rta = RangeTreeArena::new(); + let inputs: Vec = vec![CoverageRange { + start_offset: 0, + end_offset: 9, + count: 1, + }]; + let actual: Option<&mut RangeTree> = + RangeTree::from_sorted_ranges(&rta, &inputs); + let expected: Option<&mut RangeTree> = + Some(rta.alloc(RangeTree::new(0, 9, 1, Vec::new()))); + + assert_eq!(actual, expected); + } +}