summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/args/flags.rs13
-rw-r--r--cli/tests/integration/coverage_tests.rs61
-rw-r--r--cli/tests/testdata/coverage/multisource/baz/quux.ts9
-rw-r--r--cli/tests/testdata/coverage/multisource/foo.ts7
-rw-r--r--cli/tests/testdata/coverage/multisource/test.ts2
-rw-r--r--cli/tools/coverage/reporter.rs212
6 files changed, 246 insertions, 58 deletions
diff --git a/cli/args/flags.rs b/cli/args/flags.rs
index 8b4fa445e..5d692cafc 100644
--- a/cli/args/flags.rs
+++ b/cli/args/flags.rs
@@ -86,6 +86,7 @@ pub struct CompletionsFlags {
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CoverageType {
+ Summary,
Pretty,
Lcov,
Html,
@@ -1414,6 +1415,12 @@ Generate html reports from lcov:
.action(ArgAction::SetTrue),
)
.arg(
+ Arg::new("pretty")
+ .long("pretty")
+ .help("Output coverage report in pretty format in the terminal.")
+ .action(ArgAction::SetTrue),
+ )
+ .arg(
Arg::new("files")
.num_args(1..)
.value_parser(value_parser!(PathBuf))
@@ -3320,8 +3327,10 @@ fn coverage_parse(flags: &mut Flags, matches: &mut ArgMatches) {
CoverageType::Lcov
} else if matches.get_flag("html") {
CoverageType::Html
- } else {
+ } else if matches.get_flag("pretty") {
CoverageType::Pretty
+ } else {
+ CoverageType::Summary
};
let output = matches.remove_one::<PathBuf>("output");
flags.subcommand = DenoSubcommand::Coverage(CoverageFlags {
@@ -7908,7 +7917,7 @@ mod tests {
output: None,
include: vec![r"^file:".to_string()],
exclude: vec![r"test\.(js|mjs|ts|jsx|tsx)$".to_string()],
- r#type: CoverageType::Pretty
+ r#type: CoverageType::Summary
}),
..Flags::default()
}
diff --git a/cli/tests/integration/coverage_tests.rs b/cli/tests/integration/coverage_tests.rs
index d3affd4de..4d927a16d 100644
--- a/cli/tests/integration/coverage_tests.rs
+++ b/cli/tests/integration/coverage_tests.rs
@@ -115,7 +115,11 @@ fn run_coverage_text(test_name: &str, extension: &str) {
let output = context
.new_command()
- .args_vec(vec!["coverage".to_string(), format!("{}/", tempdir)])
+ .args_vec(vec![
+ "coverage".to_string(),
+ "--pretty".to_string(),
+ format!("{}/", tempdir),
+ ])
.split_output()
.run();
@@ -184,7 +188,11 @@ fn multifile_coverage() {
let output = context
.new_command()
- .args_vec(vec!["coverage".to_string(), format!("{}/", tempdir)])
+ .args_vec(vec![
+ "coverage".to_string(),
+ "--pretty".to_string(),
+ format!("{}/", tempdir),
+ ])
.split_output()
.run();
@@ -255,6 +263,7 @@ fn no_snaps_included(test_name: &str, extension: &str) {
.args_vec(vec![
"coverage".to_string(),
"--include=no_snaps_included.ts".to_string(),
+ "--pretty".to_string(),
format!("{}/", tempdir),
])
.split_output()
@@ -303,6 +312,7 @@ fn no_tests_included(test_name: &str, extension: &str) {
.args_vec(vec![
"coverage".to_string(),
format!("--exclude={}", util::std_path().canonicalize()),
+ "--pretty".to_string(),
format!("{}/", tempdir),
])
.split_output()
@@ -350,7 +360,11 @@ fn no_npm_cache_coverage() {
let output = context
.new_command()
- .args_vec(vec!["coverage".to_string(), format!("{}/", tempdir)])
+ .args_vec(vec![
+ "coverage".to_string(),
+ "--pretty".to_string(),
+ format!("{}/", tempdir),
+ ])
.split_output()
.run();
@@ -397,6 +411,7 @@ fn no_transpiled_lines() {
.args_vec(vec![
"coverage".to_string(),
"--include=no_transpiled_lines/index.ts".to_string(),
+ "--pretty".to_string(),
format!("{}/", tempdir),
])
.run();
@@ -575,3 +590,43 @@ fn test_html_reporter() {
.unwrap();
assert!(baz_quux_ts_html.contains("<h1>Coverage report for baz/quux.ts</h1>"));
}
+
+#[test]
+fn test_summary_reporter() {
+ let context = TestContext::default();
+ let tempdir = context.temp_dir();
+ let tempdir = tempdir.path().join("cov");
+
+ let output = context
+ .new_command()
+ .args_vec(vec![
+ "test".to_string(),
+ "--quiet".to_string(),
+ format!("--coverage={}", tempdir),
+ "coverage/multisource".to_string(),
+ ])
+ .run();
+
+ output.assert_exit_code(0);
+ output.skip_output_check();
+
+ let output = context
+ .new_command()
+ .args_vec(vec!["coverage".to_string(), format!("{}/", tempdir)])
+ .run();
+
+ output.assert_exit_code(0);
+ output.assert_matches_text(
+ "----------------------------------
+File | Branch % | Line % |
+----------------------------------
+ bar.ts | 0.0 | 57.1 |
+ baz/quux.ts | 0.0 | 28.6 |
+ baz/qux.ts | 100.0 | 100.0 |
+ foo.ts | 50.0 | 76.9 |
+----------------------------------
+ All files | 40.0 | 61.0 |
+----------------------------------
+",
+ );
+}
diff --git a/cli/tests/testdata/coverage/multisource/baz/quux.ts b/cli/tests/testdata/coverage/multisource/baz/quux.ts
index ab8c62db6..6032f6f3c 100644
--- a/cli/tests/testdata/coverage/multisource/baz/quux.ts
+++ b/cli/tests/testdata/coverage/multisource/baz/quux.ts
@@ -1,6 +1,13 @@
export function quux(cond: boolean) {
if (cond) {
- return 1;
+ const a = 1;
+ const b = a;
+ const c = b;
+ const d = c;
+ const e = d;
+ const f = e;
+ const g = f;
+ return g;
} else {
return 2;
}
diff --git a/cli/tests/testdata/coverage/multisource/foo.ts b/cli/tests/testdata/coverage/multisource/foo.ts
index 023f82556..0559cadd8 100644
--- a/cli/tests/testdata/coverage/multisource/foo.ts
+++ b/cli/tests/testdata/coverage/multisource/foo.ts
@@ -1,5 +1,12 @@
export function foo(cond: boolean) {
+ let a = 0;
if (cond) {
+ a = 1;
+ } else {
+ a = 2;
+ }
+
+ if (a == 4) {
return 1;
} else {
return 2;
diff --git a/cli/tests/testdata/coverage/multisource/test.ts b/cli/tests/testdata/coverage/multisource/test.ts
index 6adf6f52c..350421177 100644
--- a/cli/tests/testdata/coverage/multisource/test.ts
+++ b/cli/tests/testdata/coverage/multisource/test.ts
@@ -5,6 +5,7 @@ import { quux } from "./baz/quux.ts";
Deno.test("foo", () => {
foo(true);
+ foo(false);
});
Deno.test("bar", () => {
@@ -13,6 +14,7 @@ Deno.test("bar", () => {
Deno.test("qux", () => {
qux(true);
+ qux(false);
});
Deno.test("quux", () => {
diff --git a/cli/tools/coverage/reporter.rs b/cli/tools/coverage/reporter.rs
index e94b54255..25a75dd32 100644
--- a/cli/tools/coverage/reporter.rs
+++ b/cli/tools/coverage/reporter.rs
@@ -16,7 +16,7 @@ use std::path::Path;
use std::path::PathBuf;
#[derive(Default)]
-struct CoverageStats<'a> {
+pub struct CoverageStats<'a> {
pub line_hit: usize,
pub line_miss: usize,
pub branch_hit: usize,
@@ -30,6 +30,7 @@ type CoverageSummary<'a> = HashMap<String, CoverageStats<'a>>;
pub fn create(kind: CoverageType) -> Box<dyn CoverageReporter + Send> {
match kind {
+ CoverageType::Summary => Box::new(SummaryCoverageReporter::new()),
CoverageType::Lcov => Box::new(LcovCoverageReporter::new()),
CoverageType::Pretty => Box::new(PrettyCoverageReporter::new()),
CoverageType::Html => Box::new(HtmlCoverageReporter::new()),
@@ -44,6 +45,163 @@ pub trait CoverageReporter {
) -> Result<(), AnyError>;
fn done(&mut self, _coverage_root: &Path) {}
+
+ /// Collects the coverage summary of each file or directory.
+ fn collect_summary<'a>(
+ &'a self,
+ file_reports: &'a Vec<(CoverageReport, String)>,
+ ) -> CoverageSummary {
+ let urls = file_reports.iter().map(|rep| &rep.0.url).collect();
+ let root = util::find_root(urls).unwrap().to_file_path().unwrap();
+ // summary by file or directory
+ // tuple of (line hit, line miss, branch hit, branch miss, parent)
+ let mut summary = HashMap::new();
+ summary.insert("".to_string(), CoverageStats::default()); // root entry
+ for (report, file_text) in file_reports {
+ let path = report.url.to_file_path().unwrap();
+ let relative_path = path.strip_prefix(&root).unwrap();
+ let mut file_text = Some(file_text.to_string());
+
+ let mut summary_path = Some(relative_path);
+ // From leaf to root, adds up the coverage stats
+ while let Some(path) = summary_path {
+ let path_str = path.to_str().unwrap().to_string();
+ let parent = path
+ .parent()
+ .and_then(|p| p.to_str())
+ .map(|p| p.to_string());
+ let stats = summary.entry(path_str).or_insert(CoverageStats {
+ parent,
+ file_text,
+ report: Some(report),
+ ..CoverageStats::default()
+ });
+
+ stats.line_hit += report
+ .found_lines
+ .iter()
+ .filter(|(_, count)| *count > 0)
+ .count();
+ stats.line_miss += report
+ .found_lines
+ .iter()
+ .filter(|(_, count)| *count == 0)
+ .count();
+ stats.branch_hit += report.branches.iter().filter(|b| b.is_hit).count();
+ stats.branch_miss +=
+ report.branches.iter().filter(|b| !b.is_hit).count();
+
+ file_text = None;
+ summary_path = path.parent();
+ }
+ }
+ summary
+ }
+}
+
+struct SummaryCoverageReporter {
+ file_reports: Vec<(CoverageReport, String)>,
+}
+
+impl SummaryCoverageReporter {
+ pub fn new() -> SummaryCoverageReporter {
+ SummaryCoverageReporter {
+ file_reports: Vec::new(),
+ }
+ }
+
+ fn print_coverage_line(
+ &self,
+ node: &str,
+ node_max: usize,
+ stats: &CoverageStats,
+ ) {
+ let CoverageStats {
+ line_hit,
+ line_miss,
+ branch_hit,
+ branch_miss,
+ ..
+ } = stats;
+ let (_, line_percent, line_class) =
+ util::calc_coverage_display_info(*line_hit, *line_miss);
+ let (_, branch_percent, branch_class) =
+ util::calc_coverage_display_info(*branch_hit, *branch_miss);
+
+ let file_name = format!(
+ "{node:node_max$}",
+ node = node.replace('\\', "/"),
+ node_max = node_max
+ );
+ let file_name = if line_class == "high" {
+ format!("{}", colors::green(&file_name))
+ } else if line_class == "medium" {
+ format!("{}", colors::yellow(&file_name))
+ } else {
+ format!("{}", colors::red(&file_name))
+ };
+
+ let branch_percent = if branch_class == "high" {
+ format!("{}", colors::green(&format!("{:>8.1}", branch_percent)))
+ } else if branch_class == "medium" {
+ format!("{}", colors::yellow(&format!("{:>8.1}", branch_percent)))
+ } else {
+ format!("{}", colors::red(&format!("{:>8.1}", branch_percent)))
+ };
+
+ let line_percent = if line_class == "high" {
+ format!("{}", colors::green(&format!("{:>6.1}", line_percent)))
+ } else if line_class == "medium" {
+ format!("{}", colors::yellow(&format!("{:>6.1}", line_percent)))
+ } else {
+ format!("{}", colors::red(&format!("{:>6.1}", line_percent)))
+ };
+
+ println!(
+ " {file_name} | {branch_percent} | {line_percent} |",
+ file_name = file_name,
+ branch_percent = branch_percent,
+ line_percent = line_percent,
+ );
+ }
+}
+
+impl CoverageReporter for SummaryCoverageReporter {
+ fn report(
+ &mut self,
+ coverage_report: &CoverageReport,
+ file_text: &str,
+ ) -> Result<(), AnyError> {
+ self
+ .file_reports
+ .push((coverage_report.clone(), file_text.to_string()));
+ Ok(())
+ }
+
+ fn done(&mut self, _coverage_root: &Path) {
+ let summary = self.collect_summary(&self.file_reports);
+ let root_stats = summary.get("").unwrap();
+
+ let mut entries = summary
+ .iter()
+ .filter(|(_, stats)| stats.file_text.is_some())
+ .collect::<Vec<_>>();
+ entries.sort_by_key(|(node, _)| node.to_owned());
+ let node_max = entries.iter().map(|(node, _)| node.len()).max().unwrap();
+
+ let header =
+ format!("{node:node_max$} | Branch % | Line % |", node = "File");
+ let separator = "-".repeat(header.len());
+ println!("{}", separator);
+ println!("{}", header);
+ println!("{}", separator);
+ entries.iter().for_each(|(node, stats)| {
+ self.print_coverage_line(node, node_max, stats);
+ });
+ println!("{}", separator);
+ self.print_coverage_line("All files", node_max, root_stats);
+ println!("{}", separator);
+ }
}
struct LcovCoverageReporter {}
@@ -232,7 +390,7 @@ impl CoverageReporter for HtmlCoverageReporter {
}
fn done(&mut self, coverage_root: &Path) {
- let summary = self.collect_summary();
+ let summary = self.collect_summary(&self.file_reports);
let now = crate::util::time::utc_now().to_rfc2822();
for (node, stats) in &summary {
@@ -269,56 +427,6 @@ impl HtmlCoverageReporter {
}
}
- /// Collects the coverage summary of each file or directory.
- pub fn collect_summary(&self) -> CoverageSummary {
- let urls = self.file_reports.iter().map(|rep| &rep.0.url).collect();
- let root = util::find_root(urls).unwrap().to_file_path().unwrap();
- // summary by file or directory
- // tuple of (line hit, line miss, branch hit, branch miss, parent)
- let mut summary = HashMap::new();
- summary.insert("".to_string(), CoverageStats::default()); // root entry
- for (report, file_text) in &self.file_reports {
- let path = report.url.to_file_path().unwrap();
- let relative_path = path.strip_prefix(&root).unwrap();
- let mut file_text = Some(file_text.to_string());
-
- let mut summary_path = Some(relative_path);
- // From leaf to root, adds up the coverage stats
- while let Some(path) = summary_path {
- let path_str = path.to_str().unwrap().to_string();
- let parent = path
- .parent()
- .and_then(|p| p.to_str())
- .map(|p| p.to_string());
- let stats = summary.entry(path_str).or_insert(CoverageStats {
- parent,
- file_text,
- report: Some(report),
- ..CoverageStats::default()
- });
-
- stats.line_hit += report
- .found_lines
- .iter()
- .filter(|(_, count)| *count > 0)
- .count();
- stats.line_miss += report
- .found_lines
- .iter()
- .filter(|(_, count)| *count == 0)
- .count();
- stats.branch_hit += report.branches.iter().filter(|b| b.is_hit).count();
- stats.branch_miss +=
- report.branches.iter().filter(|b| !b.is_hit).count();
-
- file_text = None;
- summary_path = path.parent();
- }
- }
-
- summary
- }
-
/// Gets the report path for a single file
pub fn get_report_path(
&self,