1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-27 16:10:57 -05:00

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".
This commit is contained in:
Bartek Iwańczuk 2022-01-11 21:17:25 +01:00 committed by crowlkats
parent 7c33e3e4d6
commit 219afc4877
No known key found for this signature in database
GPG key ID: A82C9D461FC483E8
12 changed files with 1250 additions and 84 deletions

1
Cargo.lock generated
View file

@ -708,6 +708,7 @@ dependencies = [
"tokio", "tokio",
"trust-dns-client", "trust-dns-client",
"trust-dns-server", "trust-dns-server",
"typed-arena",
"uuid", "uuid",
"walkdir", "walkdir",
"winapi 0.3.9", "winapi 0.3.9",

View file

@ -83,6 +83,7 @@ tempfile = "=3.2.0"
text-size = "=1.1.0" text-size = "=1.1.0"
text_lines = "=0.4.1" text_lines = "=0.4.1"
tokio = { version = "=1.14", features = ["full"] } tokio = { version = "=1.14", features = ["full"] }
typed-arena = "2.0.1"
uuid = { version = "=0.8.2", features = ["v4", "serde"] } uuid = { version = "=0.8.2", features = ["v4", "serde"] }
walkdir = "=2.3.2" walkdir = "=2.3.2"

View file

@ -97,3 +97,83 @@ fn run_coverage_text(test_name: &str, extension: &str) {
assert!(output.status.success()); 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());
}

View file

@ -0,0 +1,8 @@
import { test } from "./mod.js";
Deno.test({
name: "bugrepo a",
fn: () => {
test(true);
},
});

View file

@ -0,0 +1,8 @@
import { test } from "./mod.js";
Deno.test({
name: "bugrepo b",
fn: () => {
test(false);
},
});

View file

@ -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

View file

@ -0,0 +1 @@
cover [WILDCARD]/multifile/mod.js ... 100.000% (7/7)

View file

@ -0,0 +1,6 @@
export function test(a) {
if (a) {
return 0;
}
return 1;
}

View file

@ -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>,
}

840
cli/tools/coverage/merge.rs Normal file
View file

@ -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);
}
}

View file

@ -17,8 +17,6 @@ use deno_core::serde_json;
use deno_core::url::Url; use deno_core::url::Url;
use deno_core::LocalInspectorSession; use deno_core::LocalInspectorSession;
use regex::Regex; use regex::Regex;
use serde::Deserialize;
use serde::Serialize;
use sourcemap::SourceMap; use sourcemap::SourceMap;
use std::fs; use std::fs;
use std::fs::File; use std::fs::File;
@ -28,52 +26,11 @@ use std::path::PathBuf;
use text_lines::TextLines; use text_lines::TextLines;
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, Clone)] mod json_types;
#[serde(rename_all = "camelCase")] mod merge;
struct CoverageRange { mod range_tree;
/// Start byte index.
start_offset: usize,
/// End byte index.
end_offset: usize,
count: usize,
}
#[derive(Debug, Serialize, Deserialize, Clone)] use json_types::*;
#[serde(rename_all = "camelCase")]
struct FunctionCoverage {
function_name: String,
ranges: Vec<CoverageRange>,
is_block_coverage: bool,
}
#[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,
}
pub struct CoverageCollector { pub struct CoverageCollector {
pub dir: PathBuf, pub dir: PathBuf,
@ -175,21 +132,21 @@ struct BranchCoverageItem {
line_index: usize, line_index: usize,
block_number: usize, block_number: usize,
branch_number: usize, branch_number: usize,
taken: Option<usize>, taken: Option<i64>,
is_hit: bool, is_hit: bool,
} }
struct FunctionCoverageItem { struct FunctionCoverageItem {
name: String, name: String,
line_index: usize, line_index: usize,
execution_count: usize, execution_count: i64,
} }
struct CoverageReport { struct CoverageReport {
url: ModuleSpecifier, url: ModuleSpecifier,
named_functions: Vec<FunctionCoverageItem>, named_functions: Vec<FunctionCoverageItem>,
branches: Vec<BranchCoverageItem>, branches: Vec<BranchCoverageItem>,
found_lines: Vec<(usize, usize)>, found_lines: Vec<(usize, i64)>,
} }
fn generate_coverage_report( fn generate_coverage_report(
@ -353,7 +310,7 @@ fn generate_coverage_report(
results.into_iter() results.into_iter()
}) })
.flatten() .flatten()
.collect::<Vec<(usize, usize)>>(); .collect::<Vec<(usize, i64)>>();
found_lines.sort_unstable_by_key(|(index, _)| *index); found_lines.sort_unstable_by_key(|(index, _)| *index);
// combine duplicated lines // combine duplicated lines
@ -369,7 +326,7 @@ fn generate_coverage_report(
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(|(index, count)| (index, count)) .map(|(index, count)| (index, count))
.collect::<Vec<(usize, usize)>>() .collect::<Vec<(usize, i64)>>()
}; };
coverage_report coverage_report
@ -553,39 +510,8 @@ fn collect_coverages(
for file_path in file_paths { for file_path in file_paths {
let json = fs::read_to_string(file_path.as_path())?; let json = fs::read_to_string(file_path.as_path())?;
let new_coverage: ScriptCoverage = serde_json::from_str(&json)?; 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()); coverages.sort_by_key(|k| k.url.clone());
@ -632,6 +558,18 @@ pub async fn cover_files(
coverage_flags.exclude, 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 { let reporter_kind = if coverage_flags.lcov {
CoverageReporterKind::Lcov CoverageReporterKind::Lcov
} else { } else {

View file

@ -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);
}
}