mirror of
https://github.com/denoland/deno.git
synced 2024-12-25 16:49:18 -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:
parent
f3ece7457a
commit
13751d9de6
12 changed files with 1250 additions and 84 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -753,6 +753,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"trust-dns-client",
|
||||
"trust-dns-server",
|
||||
"typed-arena",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
"winapi 0.3.9",
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
8
cli/tests/testdata/coverage/multifile/a_test.js
vendored
Normal file
8
cli/tests/testdata/coverage/multifile/a_test.js
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { test } from "./mod.js";
|
||||
|
||||
Deno.test({
|
||||
name: "bugrepo a",
|
||||
fn: () => {
|
||||
test(true);
|
||||
},
|
||||
});
|
8
cli/tests/testdata/coverage/multifile/b_test.js
vendored
Normal file
8
cli/tests/testdata/coverage/multifile/b_test.js
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { test } from "./mod.js";
|
||||
|
||||
Deno.test({
|
||||
name: "bugrepo b",
|
||||
fn: () => {
|
||||
test(false);
|
||||
},
|
||||
});
|
18
cli/tests/testdata/coverage/multifile/expected.lcov
vendored
Normal file
18
cli/tests/testdata/coverage/multifile/expected.lcov
vendored
Normal 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
|
1
cli/tests/testdata/coverage/multifile/expected.out
vendored
Normal file
1
cli/tests/testdata/coverage/multifile/expected.out
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
cover [WILDCARD]/multifile/mod.js ... 100.000% (7/7)
|
6
cli/tests/testdata/coverage/multifile/mod.js
vendored
Normal file
6
cli/tests/testdata/coverage/multifile/mod.js
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
export function test(a) {
|
||||
if (a) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
58
cli/tools/coverage/json_types.rs
Normal file
58
cli/tools/coverage/json_types.rs
Normal 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
840
cli/tools/coverage/merge.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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<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,
|
||||
}
|
||||
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 {
|
207
cli/tools/coverage/range_tree.rs
Normal file
207
cli/tools/coverage/range_tree.rs
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue