summaryrefslogtreecommitdiff
path: root/cli
diff options
context:
space:
mode:
Diffstat (limited to 'cli')
-rw-r--r--cli/Cargo.toml1
-rw-r--r--cli/tests/integration/coverage_tests.rs80
-rw-r--r--cli/tests/testdata/coverage/multifile/a_test.js8
-rw-r--r--cli/tests/testdata/coverage/multifile/b_test.js8
-rw-r--r--cli/tests/testdata/coverage/multifile/expected.lcov18
-rw-r--r--cli/tests/testdata/coverage/multifile/expected.out1
-rw-r--r--cli/tests/testdata/coverage/multifile/mod.js6
-rw-r--r--cli/tools/coverage/json_types.rs58
-rw-r--r--cli/tools/coverage/merge.rs840
-rw-r--r--cli/tools/coverage/mod.rs (renamed from cli/tools/coverage.rs)106
-rw-r--r--cli/tools/coverage/range_tree.rs207
11 files changed, 1249 insertions, 84 deletions
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index 7fd34e2f1..66fa281ee 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 637185e29..e6eb5fe3a 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 000000000..d5d9c3533
--- /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 000000000..d93b15a17
--- /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 000000000..03ad5e7bd
--- /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 000000000..fde26e165
--- /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 000000000..b9f8d627a
--- /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 000000000..1e46cc7fd
--- /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<CoverageRange>,
+ 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<FunctionCoverage>,
+}
+
+#[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<ScriptCoverage>,
+ pub timestamp: f64,
+}
+
+// TODO(bartlomieju): remove me
+#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ProcessCoverage {
+ pub result: Vec<ScriptCoverage>,
+}
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);
+ }
+}
diff --git a/cli/tools/coverage.rs b/cli/tools/coverage/mod.rs
index 3cec35606..1c14859ea 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,
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone)]
-#[serde(rename_all = "camelCase")]
-struct FunctionCoverage {
- function_name: String,
- ranges: Vec<CoverageRange>,
- is_block_coverage: bool,
-}
+mod json_types;
+mod merge;
+mod range_tree;
-#[derive(Debug, Serialize, Deserialize, Clone)]
-#[serde(rename_all = "camelCase")]
-struct ScriptCoverage {
- script_id: String,
- url: String,
- functions: Vec<FunctionCoverage>,
-}
-
-#[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<ScriptCoverage>,
- 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<usize>,
+ taken: Option<i64>,
is_hit: bool,
}
struct FunctionCoverageItem {
name: String,
line_index: usize,
- execution_count: usize,
+ execution_count: i64,
}
struct CoverageReport {
url: ModuleSpecifier,
named_functions: Vec<FunctionCoverageItem>,
branches: Vec<BranchCoverageItem>,
- 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::<Vec<(usize, usize)>>();
+ .collect::<Vec<(usize, i64)>>();
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::<Vec<(usize, usize)>>()
+ .collect::<Vec<(usize, i64)>>()
};
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 000000000..24d0a9ffc
--- /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<RangeTree<'a>>);
+
+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<CoverageRange> {
+ let mut ranges: Vec<CoverageRange> = 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<impl Iterator<Item = &'c CoverageRange>>,
+ 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<CoverageRange> = 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);
+ }
+}