mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 17:34:47 -05:00
feat(coverage): add summary reporter (#21535)
This commit is contained in:
parent
a4f45f7092
commit
5ddf8732f0
6 changed files with 246 additions and 58 deletions
|
@ -86,6 +86,7 @@ pub struct CompletionsFlags {
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum CoverageType {
|
pub enum CoverageType {
|
||||||
|
Summary,
|
||||||
Pretty,
|
Pretty,
|
||||||
Lcov,
|
Lcov,
|
||||||
Html,
|
Html,
|
||||||
|
@ -1413,6 +1414,12 @@ Generate html reports from lcov:
|
||||||
)
|
)
|
||||||
.action(ArgAction::SetTrue),
|
.action(ArgAction::SetTrue),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("pretty")
|
||||||
|
.long("pretty")
|
||||||
|
.help("Output coverage report in pretty format in the terminal.")
|
||||||
|
.action(ArgAction::SetTrue),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("files")
|
Arg::new("files")
|
||||||
.num_args(1..)
|
.num_args(1..)
|
||||||
|
@ -3320,8 +3327,10 @@ fn coverage_parse(flags: &mut Flags, matches: &mut ArgMatches) {
|
||||||
CoverageType::Lcov
|
CoverageType::Lcov
|
||||||
} else if matches.get_flag("html") {
|
} else if matches.get_flag("html") {
|
||||||
CoverageType::Html
|
CoverageType::Html
|
||||||
} else {
|
} else if matches.get_flag("pretty") {
|
||||||
CoverageType::Pretty
|
CoverageType::Pretty
|
||||||
|
} else {
|
||||||
|
CoverageType::Summary
|
||||||
};
|
};
|
||||||
let output = matches.remove_one::<PathBuf>("output");
|
let output = matches.remove_one::<PathBuf>("output");
|
||||||
flags.subcommand = DenoSubcommand::Coverage(CoverageFlags {
|
flags.subcommand = DenoSubcommand::Coverage(CoverageFlags {
|
||||||
|
@ -7908,7 +7917,7 @@ mod tests {
|
||||||
output: None,
|
output: None,
|
||||||
include: vec![r"^file:".to_string()],
|
include: vec![r"^file:".to_string()],
|
||||||
exclude: vec![r"test\.(js|mjs|ts|jsx|tsx)$".to_string()],
|
exclude: vec![r"test\.(js|mjs|ts|jsx|tsx)$".to_string()],
|
||||||
r#type: CoverageType::Pretty
|
r#type: CoverageType::Summary
|
||||||
}),
|
}),
|
||||||
..Flags::default()
|
..Flags::default()
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,7 +115,11 @@ fn run_coverage_text(test_name: &str, extension: &str) {
|
||||||
|
|
||||||
let output = context
|
let output = context
|
||||||
.new_command()
|
.new_command()
|
||||||
.args_vec(vec!["coverage".to_string(), format!("{}/", tempdir)])
|
.args_vec(vec![
|
||||||
|
"coverage".to_string(),
|
||||||
|
"--pretty".to_string(),
|
||||||
|
format!("{}/", tempdir),
|
||||||
|
])
|
||||||
.split_output()
|
.split_output()
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
|
@ -184,7 +188,11 @@ fn multifile_coverage() {
|
||||||
|
|
||||||
let output = context
|
let output = context
|
||||||
.new_command()
|
.new_command()
|
||||||
.args_vec(vec!["coverage".to_string(), format!("{}/", tempdir)])
|
.args_vec(vec![
|
||||||
|
"coverage".to_string(),
|
||||||
|
"--pretty".to_string(),
|
||||||
|
format!("{}/", tempdir),
|
||||||
|
])
|
||||||
.split_output()
|
.split_output()
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
|
@ -255,6 +263,7 @@ fn no_snaps_included(test_name: &str, extension: &str) {
|
||||||
.args_vec(vec![
|
.args_vec(vec![
|
||||||
"coverage".to_string(),
|
"coverage".to_string(),
|
||||||
"--include=no_snaps_included.ts".to_string(),
|
"--include=no_snaps_included.ts".to_string(),
|
||||||
|
"--pretty".to_string(),
|
||||||
format!("{}/", tempdir),
|
format!("{}/", tempdir),
|
||||||
])
|
])
|
||||||
.split_output()
|
.split_output()
|
||||||
|
@ -303,6 +312,7 @@ fn no_tests_included(test_name: &str, extension: &str) {
|
||||||
.args_vec(vec![
|
.args_vec(vec![
|
||||||
"coverage".to_string(),
|
"coverage".to_string(),
|
||||||
format!("--exclude={}", util::std_path().canonicalize()),
|
format!("--exclude={}", util::std_path().canonicalize()),
|
||||||
|
"--pretty".to_string(),
|
||||||
format!("{}/", tempdir),
|
format!("{}/", tempdir),
|
||||||
])
|
])
|
||||||
.split_output()
|
.split_output()
|
||||||
|
@ -350,7 +360,11 @@ fn no_npm_cache_coverage() {
|
||||||
|
|
||||||
let output = context
|
let output = context
|
||||||
.new_command()
|
.new_command()
|
||||||
.args_vec(vec!["coverage".to_string(), format!("{}/", tempdir)])
|
.args_vec(vec![
|
||||||
|
"coverage".to_string(),
|
||||||
|
"--pretty".to_string(),
|
||||||
|
format!("{}/", tempdir),
|
||||||
|
])
|
||||||
.split_output()
|
.split_output()
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
|
@ -397,6 +411,7 @@ fn no_transpiled_lines() {
|
||||||
.args_vec(vec![
|
.args_vec(vec![
|
||||||
"coverage".to_string(),
|
"coverage".to_string(),
|
||||||
"--include=no_transpiled_lines/index.ts".to_string(),
|
"--include=no_transpiled_lines/index.ts".to_string(),
|
||||||
|
"--pretty".to_string(),
|
||||||
format!("{}/", tempdir),
|
format!("{}/", tempdir),
|
||||||
])
|
])
|
||||||
.run();
|
.run();
|
||||||
|
@ -575,3 +590,43 @@ fn test_html_reporter() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(baz_quux_ts_html.contains("<h1>Coverage report for baz/quux.ts</h1>"));
|
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 |
|
||||||
|
----------------------------------
|
||||||
|
",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
export function quux(cond: boolean) {
|
export function quux(cond: boolean) {
|
||||||
if (cond) {
|
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 {
|
} else {
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
export function foo(cond: boolean) {
|
export function foo(cond: boolean) {
|
||||||
|
let a = 0;
|
||||||
if (cond) {
|
if (cond) {
|
||||||
|
a = 1;
|
||||||
|
} else {
|
||||||
|
a = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a == 4) {
|
||||||
return 1;
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
return 2;
|
return 2;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { quux } from "./baz/quux.ts";
|
||||||
|
|
||||||
Deno.test("foo", () => {
|
Deno.test("foo", () => {
|
||||||
foo(true);
|
foo(true);
|
||||||
|
foo(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("bar", () => {
|
Deno.test("bar", () => {
|
||||||
|
@ -13,6 +14,7 @@ Deno.test("bar", () => {
|
||||||
|
|
||||||
Deno.test("qux", () => {
|
Deno.test("qux", () => {
|
||||||
qux(true);
|
qux(true);
|
||||||
|
qux(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("quux", () => {
|
Deno.test("quux", () => {
|
||||||
|
|
|
@ -16,7 +16,7 @@ use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct CoverageStats<'a> {
|
pub struct CoverageStats<'a> {
|
||||||
pub line_hit: usize,
|
pub line_hit: usize,
|
||||||
pub line_miss: usize,
|
pub line_miss: usize,
|
||||||
pub branch_hit: 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> {
|
pub fn create(kind: CoverageType) -> Box<dyn CoverageReporter + Send> {
|
||||||
match kind {
|
match kind {
|
||||||
|
CoverageType::Summary => Box::new(SummaryCoverageReporter::new()),
|
||||||
CoverageType::Lcov => Box::new(LcovCoverageReporter::new()),
|
CoverageType::Lcov => Box::new(LcovCoverageReporter::new()),
|
||||||
CoverageType::Pretty => Box::new(PrettyCoverageReporter::new()),
|
CoverageType::Pretty => Box::new(PrettyCoverageReporter::new()),
|
||||||
CoverageType::Html => Box::new(HtmlCoverageReporter::new()),
|
CoverageType::Html => Box::new(HtmlCoverageReporter::new()),
|
||||||
|
@ -44,6 +45,163 @@ pub trait CoverageReporter {
|
||||||
) -> Result<(), AnyError>;
|
) -> Result<(), AnyError>;
|
||||||
|
|
||||||
fn done(&mut self, _coverage_root: &Path) {}
|
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 {}
|
struct LcovCoverageReporter {}
|
||||||
|
@ -232,7 +390,7 @@ impl CoverageReporter for HtmlCoverageReporter {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn done(&mut self, coverage_root: &Path) {
|
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();
|
let now = crate::util::time::utc_now().to_rfc2822();
|
||||||
|
|
||||||
for (node, stats) in &summary {
|
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
|
/// Gets the report path for a single file
|
||||||
pub fn get_report_path(
|
pub fn get_report_path(
|
||||||
&self,
|
&self,
|
||||||
|
|
Loading…
Add table
Reference in a new issue