mirror of
https://github.com/denoland/deno.git
synced 2024-11-24 15:19:26 -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
7c33e3e4d6
commit
219afc4877
12 changed files with 1250 additions and 84 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
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::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,38 +510,7 @@ 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)?;
|
||||||
|
coverages.push(new_coverage);
|
||||||
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.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 {
|
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