From 7ebbda7fd71495b6f4e9bf87d385d19c44102c62 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Thu, 23 Dec 2021 20:02:54 -0500 Subject: fix(coverage): use only string byte indexes and 0-indexed line numbers (#13190) --- Cargo.lock | 1 + cli/Cargo.toml | 1 + cli/tests/integration/coverage_tests.rs | 95 +--- cli/tests/testdata/coverage/branch_expected.lcov | 27 + cli/tests/testdata/coverage/branch_expected.out | 11 + cli/tests/testdata/coverage/complex_expected.lcov | 53 ++ cli/tests/testdata/coverage/complex_expected.out | 19 + cli/tests/testdata/coverage/expected_branch.lcov | 27 - cli/tests/testdata/coverage/expected_branch.out | 12 - cli/tests/testdata/coverage/expected_complex.lcov | 53 -- cli/tests/testdata/coverage/expected_complex.out | 19 - cli/tests/testdata/coverage/final_blankline.js | 5 + .../coverage/final_blankline_expected.lcov | 16 + .../testdata/coverage/final_blankline_expected.out | 1 + .../testdata/coverage/final_blankline_test.js | 5 + cli/tools/coverage.rs | 570 ++++++++++----------- 16 files changed, 419 insertions(+), 496 deletions(-) create mode 100644 cli/tests/testdata/coverage/branch_expected.lcov create mode 100644 cli/tests/testdata/coverage/branch_expected.out create mode 100644 cli/tests/testdata/coverage/complex_expected.lcov create mode 100644 cli/tests/testdata/coverage/complex_expected.out delete mode 100644 cli/tests/testdata/coverage/expected_branch.lcov delete mode 100644 cli/tests/testdata/coverage/expected_branch.out delete mode 100644 cli/tests/testdata/coverage/expected_complex.lcov delete mode 100644 cli/tests/testdata/coverage/expected_complex.out create mode 100644 cli/tests/testdata/coverage/final_blankline.js create mode 100644 cli/tests/testdata/coverage/final_blankline_expected.lcov create mode 100644 cli/tests/testdata/coverage/final_blankline_expected.out create mode 100644 cli/tests/testdata/coverage/final_blankline_test.js diff --git a/Cargo.lock b/Cargo.lock index d2f28125c..15b7509ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -695,6 +695,7 @@ dependencies = [ "tempfile", "test_util", "text-size", + "text_lines", "tokio", "trust-dns-client", "trust-dns-server", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 25bd91c38..c02735a0f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -81,6 +81,7 @@ shell-escape = "=0.1.5" sourcemap = "=6.0.1" tempfile = "=3.2.0" text-size = "=1.1.0" +text_lines = "=0.4.1" tokio = { version = "=1.14", features = ["full"] } uuid = { version = "=0.8.2", features = ["v4", "serde"] } walkdir = "=2.3.2" diff --git a/cli/tests/integration/coverage_tests.rs b/cli/tests/integration/coverage_tests.rs index 4580b5cec..2f7250817 100644 --- a/cli/tests/integration/coverage_tests.rs +++ b/cli/tests/integration/coverage_tests.rs @@ -6,90 +6,29 @@ use test_util as util; #[test] fn branch() { - let tempdir = TempDir::new().expect("tempdir fail"); - let tempdir = tempdir.path().join("cov"); - let status = util::deno_cmd() - .current_dir(util::testdata_path()) - .arg("test") - .arg("--quiet") - .arg("--unstable") - .arg(format!("--coverage={}", tempdir.to_str().unwrap())) - .arg("coverage/branch_test.ts") - .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() - .current_dir(util::testdata_path()) - .arg("coverage") - .arg("--quiet") - .arg("--unstable") - .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/expected_branch.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() - .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/expected_branch.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()); + run_coverage_text("branch", "ts"); } #[test] fn complex() { + run_coverage_text("complex", "ts"); +} + +#[test] +fn final_blankline() { + run_coverage_text("final_blankline", "js"); +} + +fn run_coverage_text(test_name: &str, extension: &str) { let tempdir = TempDir::new().expect("tempdir fail"); + let tempdir = tempdir.path().join("cov"); let status = util::deno_cmd() .current_dir(util::testdata_path()) .arg("test") .arg("--quiet") .arg("--unstable") - .arg(format!("--coverage={}", tempdir.path().to_str().unwrap())) - .arg("coverage/complex_test.ts") + .arg(format!("--coverage={}", tempdir.to_str().unwrap())) + .arg(format!("coverage/{}_test.{}", test_name, extension)) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::inherit()) .status() @@ -102,7 +41,7 @@ fn complex() { .arg("coverage") .arg("--quiet") .arg("--unstable") - .arg(format!("{}/", tempdir.path().to_str().unwrap())) + .arg(format!("{}/", tempdir.to_str().unwrap())) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::inherit()) .output() @@ -113,7 +52,7 @@ fn complex() { .to_string(); let expected = fs::read_to_string( - util::testdata_path().join("coverage/expected_complex.out"), + util::testdata_path().join(format!("coverage/{}_expected.out", test_name)), ) .unwrap(); @@ -131,7 +70,7 @@ fn complex() { .arg("--quiet") .arg("--unstable") .arg("--lcov") - .arg(format!("{}/", tempdir.path().to_str().unwrap())) + .arg(format!("{}/", tempdir.to_str().unwrap())) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::inherit()) .output() @@ -142,7 +81,7 @@ fn complex() { .to_string(); let expected = fs::read_to_string( - util::testdata_path().join("coverage/expected_complex.lcov"), + util::testdata_path().join(format!("coverage/{}_expected.lcov", test_name)), ) .unwrap(); diff --git a/cli/tests/testdata/coverage/branch_expected.lcov b/cli/tests/testdata/coverage/branch_expected.lcov new file mode 100644 index 000000000..31da70224 --- /dev/null +++ b/cli/tests/testdata/coverage/branch_expected.lcov @@ -0,0 +1,27 @@ +SF:[WILDCARD]branch.ts +FN:1,branch +FN:9,unused +FNDA:1,branch +FNDA:0,unused +FNF:2 +FNH:1 +BRDA:4,1,0,0 +BRF:1 +BRH:0 +DA:1,1 +DA:2,2 +DA:3,2 +DA:4,2 +DA:5,0 +DA:6,0 +DA:7,2 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +LH:5 +LF:14 +end_of_record diff --git a/cli/tests/testdata/coverage/branch_expected.out b/cli/tests/testdata/coverage/branch_expected.out new file mode 100644 index 000000000..2ff5e911e --- /dev/null +++ b/cli/tests/testdata/coverage/branch_expected.out @@ -0,0 +1,11 @@ +cover [WILDCARD]/coverage/branch.ts ... 35.714% (5/14) + 5 | return false; + 6 | } +-----|----- + 9 | export function unused(condition: boolean): boolean { + 10 | if (condition) { + 11 | return false; + 12 | } else { + 13 | return true; + 14 | } + 15 | } diff --git a/cli/tests/testdata/coverage/complex_expected.lcov b/cli/tests/testdata/coverage/complex_expected.lcov new file mode 100644 index 000000000..a0af4cff3 --- /dev/null +++ b/cli/tests/testdata/coverage/complex_expected.lcov @@ -0,0 +1,53 @@ +SF:[WILDCARD]complex.ts +FN:17,dependency +FN:32,complex +FN:46,unused +FN:64,ƒ +FNDA:1,dependency +FNDA:1,complex +FNDA:0,unused +FNDA:0,ƒ +FNF:4 +FNH:2 +BRF:0 +BRH:0 +DA:17,2 +DA:18,2 +DA:19,2 +DA:20,2 +DA:22,2 +DA:23,2 +DA:24,2 +DA:25,2 +DA:26,2 +DA:27,2 +DA:32,1 +DA:33,1 +DA:34,1 +DA:35,1 +DA:37,2 +DA:38,2 +DA:39,2 +DA:40,2 +DA:41,2 +DA:42,2 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:60,1 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,1 +DA:71,0 +LH:22 +LF:37 +end_of_record diff --git a/cli/tests/testdata/coverage/complex_expected.out b/cli/tests/testdata/coverage/complex_expected.out new file mode 100644 index 000000000..e9f9a453f --- /dev/null +++ b/cli/tests/testdata/coverage/complex_expected.out @@ -0,0 +1,19 @@ +cover [WILDCARD]/coverage/complex.ts ... 59.459% (22/37) + 46 | export function unused( + 47 | foo: string, + 48 | bar: string, + 49 | baz: string, +-----|----- + 51 | return complex( + 52 | foo, + 53 | bar, + 54 | baz, + 55 | ); + 56 | } +-----|----- + 64 | export function ƒ(): number { + 65 | return ( + 66 | 0 + 67 | ); +-----|----- + 71 | console.log("%s", () => 1); diff --git a/cli/tests/testdata/coverage/expected_branch.lcov b/cli/tests/testdata/coverage/expected_branch.lcov deleted file mode 100644 index 07e29cca5..000000000 --- a/cli/tests/testdata/coverage/expected_branch.lcov +++ /dev/null @@ -1,27 +0,0 @@ -SF:[WILDCARD]branch.ts -FN:2,branch -FN:10,unused -FNDA:1,branch -FNDA:0,unused -FNF:2 -FNH:1 -BRDA:4,1,0,0 -BRF:1 -BRH:0 -DA:1,1 -DA:2,2 -DA:3,2 -DA:4,0 -DA:5,0 -DA:6,0 -DA:7,1 -DA:9,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:15,0 -LH:4 -LF:14 -end_of_record diff --git a/cli/tests/testdata/coverage/expected_branch.out b/cli/tests/testdata/coverage/expected_branch.out deleted file mode 100644 index 630ea93b2..000000000 --- a/cli/tests/testdata/coverage/expected_branch.out +++ /dev/null @@ -1,12 +0,0 @@ -cover [WILDCARD]/coverage/branch.ts ... 28.571% (4/14) - 4 | } else { - 5 | return false; - 6 | } ------|----- - 9 | export function unused(condition: boolean): boolean { - 10 | if (condition) { - 11 | return false; - 12 | } else { - 13 | return true; - 14 | } - 15 | } diff --git a/cli/tests/testdata/coverage/expected_complex.lcov b/cli/tests/testdata/coverage/expected_complex.lcov deleted file mode 100644 index 962ebee96..000000000 --- a/cli/tests/testdata/coverage/expected_complex.lcov +++ /dev/null @@ -1,53 +0,0 @@ -SF:[WILDCARD]complex.ts -FN:22,dependency -FN:37,complex -FN:51,unused -FN:65,ƒ -FNDA:1,dependency -FNDA:1,complex -FNDA:0,unused -FNDA:0,ƒ -FNF:4 -FNH:2 -BRF:0 -BRH:0 -DA:17,2 -DA:18,2 -DA:19,2 -DA:20,2 -DA:22,2 -DA:23,2 -DA:24,2 -DA:25,2 -DA:26,2 -DA:27,1 -DA:32,1 -DA:33,1 -DA:34,1 -DA:35,1 -DA:37,2 -DA:38,2 -DA:39,2 -DA:40,2 -DA:41,2 -DA:42,1 -DA:46,0 -DA:47,0 -DA:48,0 -DA:49,0 -DA:51,0 -DA:52,0 -DA:53,0 -DA:54,0 -DA:55,0 -DA:56,0 -DA:60,1 -DA:64,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:68,1 -DA:71,0 -LH:22 -LF:37 -end_of_record diff --git a/cli/tests/testdata/coverage/expected_complex.out b/cli/tests/testdata/coverage/expected_complex.out deleted file mode 100644 index e9f9a453f..000000000 --- a/cli/tests/testdata/coverage/expected_complex.out +++ /dev/null @@ -1,19 +0,0 @@ -cover [WILDCARD]/coverage/complex.ts ... 59.459% (22/37) - 46 | export function unused( - 47 | foo: string, - 48 | bar: string, - 49 | baz: string, ------|----- - 51 | return complex( - 52 | foo, - 53 | bar, - 54 | baz, - 55 | ); - 56 | } ------|----- - 64 | export function ƒ(): number { - 65 | return ( - 66 | 0 - 67 | ); ------|----- - 71 | console.log("%s", () => 1); diff --git a/cli/tests/testdata/coverage/final_blankline.js b/cli/tests/testdata/coverage/final_blankline.js new file mode 100644 index 000000000..bb5ab0378 --- /dev/null +++ b/cli/tests/testdata/coverage/final_blankline.js @@ -0,0 +1,5 @@ +// deno-fmt-ignore-file - has blankline at end +export default function example() { + return true; +} + diff --git a/cli/tests/testdata/coverage/final_blankline_expected.lcov b/cli/tests/testdata/coverage/final_blankline_expected.lcov new file mode 100644 index 000000000..48af66180 --- /dev/null +++ b/cli/tests/testdata/coverage/final_blankline_expected.lcov @@ -0,0 +1,16 @@ +SF:[WILDCARD]final_blankline.js +FN:2,example +FNDA:1,example +FNF:1 +FNH:1 +BRF:0 +BRH:0 +DA:1,1 +DA:2,1 +DA:3,2 +DA:4,2 +DA:5,1 +DA:6,1 +LH:6 +LF:6 +end_of_record diff --git a/cli/tests/testdata/coverage/final_blankline_expected.out b/cli/tests/testdata/coverage/final_blankline_expected.out new file mode 100644 index 000000000..8dc5ce30d --- /dev/null +++ b/cli/tests/testdata/coverage/final_blankline_expected.out @@ -0,0 +1 @@ +cover file:///[WILDCARD]final_blankline.js ... 100.000% (6/6) diff --git a/cli/tests/testdata/coverage/final_blankline_test.js b/cli/tests/testdata/coverage/final_blankline_test.js new file mode 100644 index 000000000..e7331c537 --- /dev/null +++ b/cli/tests/testdata/coverage/final_blankline_test.js @@ -0,0 +1,5 @@ +import example from "./final_blankline.js"; + +Deno.test("Example.", () => { + example(); +}); diff --git a/cli/tools/coverage.rs b/cli/tools/coverage.rs index 042724e81..c91fca512 100644 --- a/cli/tools/coverage.rs +++ b/cli/tools/coverage.rs @@ -9,8 +9,8 @@ use crate::proc_state::ProcState; use crate::source_maps::SourceMapGetter; use crate::tools::fmt::format_json; -use deno_ast::swc::common::Span; use deno_ast::MediaType; +use deno_ast::ModuleSpecifier; use deno_core::error::AnyError; use deno_core::serde_json; use deno_core::url::Url; @@ -25,15 +25,15 @@ use std::fs::File; use std::io::BufWriter; use std::io::Write; use std::path::PathBuf; -use std::sync::Arc; +use text_lines::TextLines; use uuid::Uuid; -// TODO(caspervonb) all of these structs can and should be made private, possibly moved to -// inspector::protocol. #[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, } @@ -171,392 +171,339 @@ impl CoverageCollector { } } -enum CoverageReporterKind { - Pretty, - Lcov, +struct BranchCoverageItem { + line_index: usize, + block_number: usize, + branch_number: usize, + taken: Option, + is_hit: bool, } -fn create_reporter( - kind: CoverageReporterKind, -) -> Box { - match kind { - CoverageReporterKind::Lcov => Box::new(LcovCoverageReporter::new()), - CoverageReporterKind::Pretty => Box::new(PrettyCoverageReporter::new()), - } +struct FunctionCoverageItem { + name: String, + line_index: usize, + execution_count: usize, } -trait CoverageReporter { - fn visit_coverage( - &mut self, - script_coverage: &ScriptCoverage, - script_source: &str, - maybe_source_map: Option>, - maybe_original_source: Option>, - ); - - fn done(&mut self); +struct CoverageReport { + url: ModuleSpecifier, + named_functions: Vec, + branches: Vec, + found_lines: Vec<(usize, usize)>, } -struct LcovCoverageReporter {} +fn generate_coverage_report( + script_coverage: &ScriptCoverage, + script_source: &str, + maybe_source_map: &Option>, +) -> CoverageReport { + let maybe_source_map = maybe_source_map + .as_ref() + .map(|source_map| SourceMap::from_slice(source_map).unwrap()); + let text_lines = TextLines::new(script_source); + + let comment_spans = deno_ast::lex(script_source, MediaType::JavaScript) + .into_iter() + .filter(|item| { + matches!(item.inner, deno_ast::TokenOrComment::Comment { .. }) + }) + .map(|item| item.span) + .collect::>(); + + let url = Url::parse(&script_coverage.url).unwrap(); + let mut coverage_report = CoverageReport { + url, + named_functions: Vec::with_capacity( + script_coverage + .functions + .iter() + .filter(|f| !f.function_name.is_empty()) + .count(), + ), + branches: Vec::new(), + found_lines: Vec::new(), + }; -impl LcovCoverageReporter { - pub fn new() -> LcovCoverageReporter { - LcovCoverageReporter {} - } -} + for function in &script_coverage.functions { + if function.function_name.is_empty() { + continue; + } -impl CoverageReporter for LcovCoverageReporter { - fn visit_coverage( - &mut self, - script_coverage: &ScriptCoverage, - script_source: &str, - maybe_source_map: Option>, - _maybe_original_source: Option>, - ) { - // TODO(caspervonb) cleanup and reduce duplication between reporters, pre-compute line coverage - // elsewhere. - let maybe_source_map = maybe_source_map - .map(|source_map| SourceMap::from_slice(&source_map).unwrap()); - - let url = Url::parse(&script_coverage.url).unwrap(); - let file_path = url.to_file_path().unwrap(); - println!("SF:{}", file_path.to_str().unwrap()); - - let mut functions_found = 0; - for function in &script_coverage.functions { - if function.function_name.is_empty() { - continue; - } + let source_line_index = + text_lines.line_index(function.ranges[0].start_offset); + let line_index = if let Some(source_map) = maybe_source_map.as_ref() { + source_map + .tokens() + .find(|token| token.get_dst_line() as usize == source_line_index) + .map(|token| token.get_src_line() as usize) + .unwrap_or(0) + } else { + source_line_index + }; - let source_line = script_source - .chars() - .take(function.ranges[0].start_offset) - .filter(|c| *c == '\n') - .count() - + 1; + coverage_report.named_functions.push(FunctionCoverageItem { + name: function.function_name.clone(), + line_index, + execution_count: function.ranges[0].count, + }); + } + for (block_number, function) in script_coverage.functions.iter().enumerate() { + let block_hits = function.ranges[0].count; + for (branch_number, range) in function.ranges[1..].iter().enumerate() { + let source_line_index = text_lines.line_index(range.start_offset); let line_index = if let Some(source_map) = maybe_source_map.as_ref() { source_map .tokens() - .find(|token| token.get_dst_line() as usize == source_line) + .find(|token| token.get_dst_line() as usize == source_line_index) .map(|token| token.get_src_line() as usize) .unwrap_or(0) } else { - source_line + source_line_index }; - let function_name = &function.function_name; - - println!("FN:{},{}", line_index + 1, function_name); - - functions_found += 1; - } - - let mut functions_hit = 0; - for function in &script_coverage.functions { - if function.function_name.is_empty() { - continue; - } - - let execution_count = function.ranges[0].count; - let function_name = &function.function_name; - - println!("FNDA:{},{}", execution_count, function_name); + // From https://manpages.debian.org/unstable/lcov/geninfo.1.en.html: + // + // Block number and branch number are gcc internal IDs for the branch. Taken is either '-' + // if the basic block containing the branch was never executed or a number indicating how + // often that branch was taken. + // + // However with the data we get from v8 coverage profiles it seems we can't actually hit + // this as appears it won't consider any nested branches it hasn't seen but its here for + // the sake of accuracy. + let taken = if block_hits > 0 { + Some(range.count) + } else { + None + }; - if execution_count != 0 { - functions_hit += 1; - } + coverage_report.branches.push(BranchCoverageItem { + line_index, + block_number, + branch_number, + taken, + is_hit: range.count > 0, + }) } + } - println!("FNF:{}", functions_found); - println!("FNH:{}", functions_hit); - - let mut branches_found = 0; - let mut branches_hit = 0; - for (block_number, function) in script_coverage.functions.iter().enumerate() - { - let block_hits = function.ranges[0].count; - for (branch_number, range) in function.ranges[1..].iter().enumerate() { - let source_line = script_source - .chars() - .take(range.start_offset) - .filter(|c| *c == '\n') - .count() - + 1; - - let line_index = if let Some(source_map) = maybe_source_map.as_ref() { - source_map - .tokens() - .find(|token| token.get_dst_line() as usize == source_line) - .map(|token| token.get_src_line() as usize) - .unwrap_or(0) - } else { - source_line - }; - - // From https://manpages.debian.org/unstable/lcov/geninfo.1.en.html: - // - // Block number and branch number are gcc internal IDs for the branch. Taken is either '-' - // if the basic block containing the branch was never executed or a number indicating how - // often that branch was taken. - // - // However with the data we get from v8 coverage profiles it seems we can't actually hit - // this as appears it won't consider any nested branches it hasn't seen but its here for - // the sake of accuracy. - let taken = if block_hits > 0 { - range.count.to_string() - } else { - "-".to_string() - }; - - println!( - "BRDA:{},{},{},{}", - line_index + 1, - block_number, - branch_number, - taken - ); - - branches_found += 1; - if range.count > 0 { - branches_hit += 1; + // TODO(caspervonb): collect uncovered ranges on the lines so that we can highlight specific + // parts of a line in color (word diff style) instead of the entire line. + let mut line_counts = Vec::with_capacity(text_lines.lines_count()); + for line_index in 0..text_lines.lines_count() { + let line_start_offset = text_lines.line_start(line_index); + let line_end_offset = text_lines.line_end(line_index); + let ignore = comment_spans.iter().any(|span| { + (span.lo.0 as usize) <= line_start_offset + && (span.hi.0 as usize) >= line_end_offset + }) || script_source[line_start_offset..line_end_offset] + .trim() + .is_empty(); + let mut count = 0; + + if ignore { + count = 1; + } else { + // Count the hits of ranges that include the entire line which will always be at-least one + // as long as the code has been evaluated. + for function in &script_coverage.functions { + for range in &function.ranges { + if range.start_offset <= line_start_offset + && range.end_offset >= line_end_offset + { + count += range.count; + } } } - } - - println!("BRF:{}", branches_found); - println!("BRH:{}", branches_hit); - - let lines = script_source.split('\n').collect::>(); - let line_offsets = { - let mut offsets: Vec<(usize, usize)> = Vec::new(); - let mut index = 0; - - for line in &lines { - offsets.push((index, index + line.len() + 1)); - index += line.len() + 1; - } - offsets - }; - - let line_counts = line_offsets - .iter() - .map(|(line_start_offset, line_end_offset)| { - let mut count = 0; - - // Count the hits of ranges that include the entire line which will always be at-least one - // as long as the code has been evaluated. - for function in &script_coverage.functions { - for range in &function.ranges { - if range.start_offset <= *line_start_offset - && range.end_offset >= *line_end_offset - { - count += range.count; - } + // We reset the count if any block with a zero count overlaps with the line range. + for function in &script_coverage.functions { + for range in &function.ranges { + if range.count > 0 { + continue; } - } - // We reset the count if any block with a zero count overlaps with the line range. - for function in &script_coverage.functions { - for range in &function.ranges { - if range.count > 0 { - continue; - } - - let overlaps = std::cmp::max(line_end_offset, &range.end_offset) - - std::cmp::min(line_start_offset, &range.start_offset) - < (line_end_offset - line_start_offset) - + (range.end_offset - range.start_offset); - - if overlaps { - count = 0; - } + let overlaps = range.start_offset < line_end_offset + && range.end_offset > line_start_offset; + if overlaps { + count = 0; } } + } + } - count - }) - .collect::>(); + line_counts.push(count); + } - let found_lines = if let Some(source_map) = maybe_source_map.as_ref() { + coverage_report.found_lines = + if let Some(source_map) = maybe_source_map.as_ref() { let mut found_lines = line_counts .iter() .enumerate() .map(|(index, count)| { - source_map + // get all the mappings from this destination line to a different src line + let mut results = source_map .tokens() .filter(move |token| token.get_dst_line() as usize == index) .map(move |token| (token.get_src_line() as usize, *count)) + .collect::>(); + // only keep the results that point at different src lines + results.sort_unstable_by_key(|(index, _)| *index); + results.dedup_by_key(|(index, _)| *index); + results.into_iter() }) .flatten() .collect::>(); found_lines.sort_unstable_by_key(|(index, _)| *index); - found_lines.dedup_by_key(|(index, _)| *index); + // combine duplicated lines + for i in (1..found_lines.len()).rev() { + if found_lines[i].0 == found_lines[i - 1].0 { + found_lines[i - 1].1 += found_lines[i].1; + found_lines.remove(i); + } + } found_lines } else { line_counts - .iter() + .into_iter() .enumerate() - .map(|(index, count)| (index, *count)) + .map(|(index, count)| (index, count)) .collect::>() }; - for (index, count) in &found_lines { - println!("DA:{},{}", index + 1, count); - } - - let lines_hit = found_lines.iter().filter(|(_, count)| *count != 0).count(); - - println!("LH:{}", lines_hit); + coverage_report +} - let lines_found = found_lines.len(); - println!("LF:{}", lines_found); +enum CoverageReporterKind { + Pretty, + Lcov, +} - println!("end_of_record"); +fn create_reporter( + kind: CoverageReporterKind, +) -> Box { + match kind { + CoverageReporterKind::Lcov => Box::new(LcovCoverageReporter::new()), + CoverageReporterKind::Pretty => Box::new(PrettyCoverageReporter::new()), } +} - fn done(&mut self) {} +trait CoverageReporter { + fn report(&mut self, coverage_report: &CoverageReport, file_text: &str); + + fn done(&mut self); } -struct PrettyCoverageReporter {} +struct LcovCoverageReporter {} -impl PrettyCoverageReporter { - pub fn new() -> PrettyCoverageReporter { - PrettyCoverageReporter {} +impl LcovCoverageReporter { + pub fn new() -> LcovCoverageReporter { + LcovCoverageReporter {} } } -impl CoverageReporter for PrettyCoverageReporter { - fn visit_coverage( - &mut self, - script_coverage: &ScriptCoverage, - script_source: &str, - maybe_source_map: Option>, - maybe_original_source: Option>, - ) { - let maybe_source_map = maybe_source_map - .map(|source_map| SourceMap::from_slice(&source_map).unwrap()); - - let mut ignored_spans: Vec = Vec::new(); - for item in deno_ast::lex(script_source, MediaType::JavaScript) { - if let deno_ast::TokenOrComment::Token(_) = item.inner { - continue; - } - - ignored_spans.push(item.span); +impl CoverageReporter for LcovCoverageReporter { + fn report(&mut self, coverage_report: &CoverageReport, _file_text: &str) { + let file_path = coverage_report + .url + .to_file_path() + .ok() + .map(|p| p.to_str().map(|p| p.to_string())) + .flatten() + .unwrap_or_else(|| coverage_report.url.to_string()); + println!("SF:{}", file_path); + + for function in &coverage_report.named_functions { + println!("FN:{},{}", function.line_index + 1, function.name); } - let lines = script_source.split('\n').collect::>(); - - let line_offsets = { - let mut offsets: Vec<(usize, usize)> = Vec::new(); - let mut index = 0; - - for line in &lines { - offsets.push((index, index + line.len() + 1)); - index += line.len() + 1; - } - - offsets - }; + for function in &coverage_report.named_functions { + println!("FNDA:{},{}", function.execution_count, function.name); + } - // TODO(caspervonb): collect uncovered ranges on the lines so that we can highlight specific - // parts of a line in color (word diff style) instead of the entire line. - let line_counts = line_offsets + let functions_found = coverage_report.named_functions.len(); + println!("FNF:{}", functions_found); + let functions_hit = coverage_report + .named_functions .iter() - .enumerate() - .map(|(index, (line_start_offset, line_end_offset))| { - let ignore = ignored_spans.iter().any(|span| { - (span.lo.0 as usize) <= *line_start_offset - && (span.hi.0 as usize) >= *line_end_offset - }); - - if ignore { - return (index, 1); - } + .filter(|f| f.execution_count > 0) + .count(); + println!("FNH:{}", functions_hit); - let mut count = 0; + for branch in &coverage_report.branches { + let taken = if let Some(taken) = &branch.taken { + taken.to_string() + } else { + "-".to_string() + }; - // Count the hits of ranges that include the entire line which will always be at-least one - // as long as the code has been evaluated. - for function in &script_coverage.functions { - for range in &function.ranges { - if range.start_offset <= *line_start_offset - && range.end_offset >= *line_end_offset - { - count += range.count; - } - } - } + println!( + "BRDA:{},{},{},{}", + branch.line_index + 1, + branch.block_number, + branch.branch_number, + taken + ); + } - // We reset the count if any block with a zero count overlaps with the line range. - for function in &script_coverage.functions { - for range in &function.ranges { - if range.count > 0 { - continue; - } + let branches_found = coverage_report.branches.len(); + println!("BRF:{}", branches_found); + let branches_hit = + coverage_report.branches.iter().filter(|b| b.is_hit).count(); + println!("BRH:{}", branches_hit); - let overlaps = std::cmp::max(line_end_offset, &range.end_offset) - - std::cmp::min(line_start_offset, &range.start_offset) - < (line_end_offset - line_start_offset) - + (range.end_offset - range.start_offset); + for (index, count) in &coverage_report.found_lines { + println!("DA:{},{}", index + 1, count); + } - if overlaps { - count = 0; - } - } - } + let lines_hit = coverage_report + .found_lines + .iter() + .filter(|(_, count)| *count != 0) + .count(); + println!("LH:{}", lines_hit); - (index, count) - }) - .collect::>(); + let lines_found = coverage_report.found_lines.len(); + println!("LF:{}", lines_found); - let lines = if let Some(original_source) = maybe_original_source.as_ref() { - original_source.split('\n').collect::>() - } else { - lines - }; + println!("end_of_record"); + } - let line_counts = if let Some(source_map) = maybe_source_map.as_ref() { - let mut line_counts = line_counts - .iter() - .map(|(index, count)| { - source_map - .tokens() - .filter(move |token| token.get_dst_line() as usize == *index) - .map(move |token| (token.get_src_line() as usize, *count)) - }) - .flatten() - .collect::>(); + fn done(&mut self) {} +} - line_counts.sort_unstable_by_key(|(index, _)| *index); - line_counts.dedup_by_key(|(index, _)| *index); +struct PrettyCoverageReporter {} - line_counts - } else { - line_counts - }; +impl PrettyCoverageReporter { + pub fn new() -> PrettyCoverageReporter { + PrettyCoverageReporter {} + } +} - print!("cover {} ... ", script_coverage.url); +impl CoverageReporter for PrettyCoverageReporter { + fn report(&mut self, coverage_report: &CoverageReport, file_text: &str) { + let lines = file_text.split('\n').collect::>(); + print!("cover {} ... ", coverage_report.url); - let hit_lines = line_counts + let hit_lines = coverage_report + .found_lines .iter() - .filter(|(_, count)| *count != 0) + .filter(|(_, count)| *count > 0) .map(|(index, _)| *index); - let missed_lines = line_counts + let missed_lines = coverage_report + .found_lines .iter() .filter(|(_, count)| *count == 0) .map(|(index, _)| *index); - let lines_found = line_counts.len(); + let lines_found = coverage_report.found_lines.len(); let lines_hit = hit_lines.count(); let line_ratio = lines_hit as f32 / lines_found as f32; let line_coverage = - format!("{:.3}% ({}/{})", line_ratio * 100.0, lines_hit, lines_found,); + format!("{:.3}% ({}/{})", line_ratio * 100.0, lines_hit, lines_found); if line_ratio >= 0.9 { println!("{}", colors::green(&line_coverage)); @@ -715,12 +662,21 @@ pub async fn cover_files( .get_source(&module_specifier) .map(|f| f.source); - reporter.visit_coverage( + let coverage_report = generate_coverage_report( &script_coverage, script_source, - maybe_source_map, - maybe_cached_source, + &maybe_source_map, ); + + let file_text = if let Some(original_source) = + maybe_source_map.and(maybe_cached_source.as_ref()) + { + original_source.as_str() + } else { + script_source + }; + + reporter.report(&coverage_report, file_text); } reporter.done(); -- cgit v1.2.3