From 74e48c5149694fc122efc0dcc63c7613aa7de958 Mon Sep 17 00:00:00 2001 From: Rob Shearman Date: Wed, 27 Mar 2024 08:17:37 +0000 Subject: [PATCH 1/3] Make output of plain lines in cobertura reports more concise For plain lines (without branch information) then there's no need to write out opening and closing tags for the line element. It is more concise to output an empty element, and conciseness is important to avoid hitting processing limits from certain services that parse the output earlier than is necessary. --- src/cobertura.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cobertura.rs b/src/cobertura.rs index c0cdc28e0..290bfcb13 100644 --- a/src/cobertura.rs +++ b/src/cobertura.rs @@ -512,7 +512,7 @@ fn write_lines(writer: &mut Writer>>, lines: &[Line]) { } => { l.push_attribute(("number", number.to_string().as_ref())); l.push_attribute(("hits", hits.to_string().as_ref())); - writer.write_event(Event::Start(l)).unwrap(); + writer.write_event(Event::Empty(l)).unwrap(); } Line::Branch { ref number, @@ -543,11 +543,11 @@ fn write_lines(writer: &mut Writer>>, lines: &[Line]) { writer .write_event(Event::End(BytesEnd::new(conditions_tag))) .unwrap(); + writer + .write_event(Event::End(BytesEnd::new(line_tag))) + .unwrap(); } } - writer - .write_event(Event::End(BytesEnd::new(line_tag))) - .unwrap(); } writer .write_event(Event::End(BytesEnd::new(lines_tag))) @@ -721,7 +721,7 @@ mod tests { assert!(results.contains(r#"package name="src/main.rs""#)); assert!(results.contains(r#"class name="main" filename="src/main.rs""#)); assert!(results.contains(r#"method name="cov_test::main""#)); - assert!(results.contains(r#"line number="1" hits="1">"#)); + assert!(results.contains(r#"line number="1" hits="1"/>"#)); assert!(results.contains(r#"line number="3" hits="2" branch="true""#)); assert!(results.contains(r#""#)); From 9db5eeb47bb12bbcae1e0ba22c541877af2096a2 Mon Sep 17 00:00:00 2001 From: Rob Shearman Date: Wed, 27 Mar 2024 10:15:56 +0000 Subject: [PATCH 2/3] Make cobertura output more concise and add cobertura-pretty output option Some parsers of cobertura coverage files places limits on the file size (e.g. GitLab - 10MB), so being as concise as possible is advantageous. For this reason, change the cobertura output to not add newlines and indent between XML elements, and add a new output type cobertura-pretty for cases where the newlines and indent is desired. Changing the format for the existing cobertura output type rather than keeping it the same and introducing a new concise output type was opted for since cobertura is designed to be machine-readable and most of the time users would not need to inspect the file manually. This results in a 42% reduction in output file size for a repository of mine. --- README.md | 2 ++ src/cobertura.rs | 53 ++++++++++++++++++++++++++++++------------------ src/main.rs | 15 +++++++++++++- 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index e3ed2047f..5bbd104db 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ Options: - *files* to only return a list of files. - *markdown* for human easy read. - *cobertura* for output in cobertura format. + - *cobertura-pretty* to pretty-print in cobertura format. [default: lcov] @@ -405,6 +406,7 @@ grcov provides the following output types: | covdir | Provides coverage in a recursive JSON format. | | html | Output a HTML coverage report, including coverage badges for your README. | | cobertura | Cobertura XML. Used for coverage analysis in some IDEs and Gitlab CI. | +| cobertura-pretty | Pretty-printed Cobertura XML. | ### Hosting HTML reports and using coverage badges diff --git a/src/cobertura.rs b/src/cobertura.rs index 290bfcb13..b0d95314f 100644 --- a/src/cobertura.rs +++ b/src/cobertura.rs @@ -330,6 +330,7 @@ pub fn output_cobertura( results: &[ResultTuple], output_file: Option<&Path>, demangle: bool, + pretty: bool, ) { let demangle_options = DemangleOptions::name_only(); let sources = vec![source_dir @@ -338,7 +339,11 @@ pub fn output_cobertura( .to_string()]; let coverage = get_coverage(results, sources, demangle, demangle_options); - let mut writer = Writer::new_with_indent(Cursor::new(vec![]), b' ', 4); + let mut writer = if pretty { + Writer::new_with_indent(Cursor::new(vec![]), b' ', 4) + } else { + Writer::new(Cursor::new(vec![])) + }; writer .write_event(Event::Decl(BytesDecl::new("1.0", None, None))) .unwrap(); @@ -712,26 +717,28 @@ mod tests { coverage_result(Result::Main), )]; - output_cobertura(None, &results, Some(&file_path), true); + for pretty in [false, true] { + output_cobertura(None, &results, Some(&file_path), true, pretty); - let results = read_file(&file_path); + let results = read_file(&file_path); - assert!(results.contains(r#"."#)); + assert!(results.contains(r#"."#)); - assert!(results.contains(r#"package name="src/main.rs""#)); - assert!(results.contains(r#"class name="main" filename="src/main.rs""#)); - assert!(results.contains(r#"method name="cov_test::main""#)); - assert!(results.contains(r#"line number="1" hits="1"/>"#)); - assert!(results.contains(r#"line number="3" hits="2" branch="true""#)); - assert!(results.contains(r#""#)); + assert!(results.contains(r#"package name="src/main.rs""#)); + assert!(results.contains(r#"class name="main" filename="src/main.rs""#)); + assert!(results.contains(r#"method name="cov_test::main""#)); + assert!(results.contains(r#"line number="1" hits="1"/>"#)); + assert!(results.contains(r#"line number="3" hits="2" branch="true""#)); + assert!(results.contains(r#""#)); - assert!(results.contains(r#"lines-covered="6""#)); - assert!(results.contains(r#"lines-valid="8""#)); - assert!(results.contains(r#"line-rate="0.75""#)); + assert!(results.contains(r#"lines-covered="6""#)); + assert!(results.contains(r#"lines-valid="8""#)); + assert!(results.contains(r#"line-rate="0.75""#)); - assert!(results.contains(r#"branches-covered="1""#)); - assert!(results.contains(r#"branches-valid="4""#)); - assert!(results.contains(r#"branch-rate="0.25""#)); + assert!(results.contains(r#"branches-covered="1""#)); + assert!(results.contains(r#"branches-valid="4""#)); + assert!(results.contains(r#"branch-rate="0.25""#)); + } } #[test] @@ -746,7 +753,7 @@ mod tests { coverage_result(Result::Test), )]; - output_cobertura(None, &results, Some(file_path.as_ref()), true); + output_cobertura(None, &results, Some(file_path.as_ref()), true, true); let results = read_file(&file_path); @@ -785,7 +792,7 @@ mod tests { ), ]; - output_cobertura(None, &results, Some(file_path.as_ref()), true); + output_cobertura(None, &results, Some(file_path.as_ref()), true, true); let results = read_file(&file_path); @@ -817,7 +824,7 @@ mod tests { CovResult::default(), )]; - output_cobertura(None, &results, Some(&file_path), true); + output_cobertura(None, &results, Some(&file_path), true, true); let results = read_file(&file_path); @@ -837,7 +844,13 @@ mod tests { CovResult::default(), )]; - output_cobertura(Some(Path::new("src")), &results, Some(&file_path), true); + output_cobertura( + Some(Path::new("src")), + &results, + Some(&file_path), + true, + true, + ); let results = read_file(&file_path); diff --git a/src/main.rs b/src/main.rs index e05191fa0..ccc3d6c5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,7 @@ enum OutputType { Covdir, Html, Cobertura, + CoberturaPretty, Markdown, } @@ -45,6 +46,7 @@ impl FromStr for OutputType { "covdir" => Self::Covdir, "html" => Self::Html, "cobertura" => Self::Cobertura, + "cobertura-pretty" => Self::CoberturaPretty, "markdown" => Self::Markdown, _ => return Err(format!("{} is not a supported output type", s)), }) @@ -63,7 +65,9 @@ impl OutputType { OutputType::Files => path.join("files"), OutputType::Covdir => path.join("covdir"), OutputType::Html => path.join("html"), - OutputType::Cobertura => path.join("cobertura.xml"), + OutputType::Cobertura | OutputType::CoberturaPretty => { + path.join("cobertura.xml") + } OutputType::Markdown => path.join("markdown.md"), } } else { @@ -166,6 +170,7 @@ struct Opt { - *files* to only return a list of files.\n\ - *markdown* for human easy read.\n\ - *cobertura* for output in cobertura format.\n\ + - *cobertura-pretty* to pretty-print in cobertura format.\n\ ", value_name = "OUTPUT TYPE", requires_ifs = [ @@ -563,6 +568,14 @@ fn main() { results, output_path.as_deref(), demangle, + false, + ), + OutputType::CoberturaPretty => output_cobertura( + source_root.as_deref(), + results, + output_path.as_deref(), + demangle, + true, ), OutputType::Markdown => output_markdown(results, output_path.as_deref(), opt.precision), }; From b4c0c2162aec08ed1b7dc882dcb4d04d219bb4ce Mon Sep 17 00:00:00 2001 From: Rob Shearman Date: Fri, 5 Apr 2024 21:19:23 +0100 Subject: [PATCH 3/3] Add --print-summary option Add a --print-summary option along the lines of what gcovr supports and outputting in a compatible format. The number of functions covered isn't output (it is with gcovr) as this is more complicated to calculate and it's unclear if it would be useful. Fixes #556 --- src/lib.rs | 3 ++ src/main.rs | 7 +++ src/summary.rs | 138 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 src/summary.rs diff --git a/src/lib.rs b/src/lib.rs index b028891ba..1c6f007d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,9 @@ pub mod html; mod file_filter; pub use crate::file_filter::*; +mod summary; +pub use crate::summary::*; + use log::{error, warn}; use std::fs; use std::io::{BufReader, Cursor}; diff --git a/src/main.rs b/src/main.rs index ccc3d6c5c..ff29a15d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -297,6 +297,9 @@ struct Opt { /// No symbol demangling. #[arg(long)] no_demangle: bool, + /// Print a summary of the results + #[arg(long)] + print_summary: bool, } fn main() { @@ -580,6 +583,10 @@ fn main() { OutputType::Markdown => output_markdown(results, output_path.as_deref(), opt.precision), }; } + + if opt.print_summary { + print_summary(&iterator); + } } #[cfg(test)] diff --git a/src/summary.rs b/src/summary.rs new file mode 100644 index 000000000..bbcb12835 --- /dev/null +++ b/src/summary.rs @@ -0,0 +1,138 @@ +use std::path::PathBuf; + +use crate::CovResult; + +#[derive(Debug, Default)] +struct CoverageStats { + lines_covered: i64, + lines_valid: i64, + branches_covered: i64, + branches_valid: i64, +} + +fn get_coverage_stats(results: &[(PathBuf, PathBuf, CovResult)]) -> CoverageStats { + results + .iter() + .fold(CoverageStats::default(), |stats, (_, _, result)| { + let (lines_covered, lines_valid) = result.lines.values().fold( + (stats.lines_covered, stats.lines_valid), + |(covered, valid), l| { + if *l == 0 { + (covered, valid + 1) + } else { + (covered + 1, valid + 1) + } + }, + ); + let (branches_covered, branches_valid) = result.branches.values().fold( + (stats.branches_covered, stats.branches_valid), + |(covered, valid), branches| { + branches + .iter() + .fold((covered, valid), |(covered, valid), b| { + if *b { + (covered + 1, valid + 1) + } else { + (covered, valid + 1) + } + }) + }, + ); + CoverageStats { + lines_covered, + lines_valid, + branches_covered, + branches_valid, + } + }) +} + +pub fn print_summary(results: &[(PathBuf, PathBuf, CovResult)]) { + let stats = get_coverage_stats(results); + let lines_percentage = if stats.lines_valid == 0 { + 0.0 + } else { + (stats.lines_covered as f64 / stats.lines_valid as f64) * 100.0 + }; + let branches_percentage = if stats.branches_valid == 0 { + 0.0 + } else { + (stats.branches_covered as f64 / stats.branches_valid as f64) * 100.0 + }; + println!( + "lines: {:.1}% ({} out of {})", + lines_percentage, stats.lines_covered, stats.lines_valid + ); + println!( + "branches: {:.1}% ({} out of {})", + branches_percentage, stats.branches_covered, stats.branches_valid + ); +} + +#[cfg(test)] +mod tests { + use std::{collections::BTreeMap, path::PathBuf}; + + use rustc_hash::FxHashMap; + + use crate::{CovResult, Function}; + + use super::get_coverage_stats; + + #[test] + fn test_summary() { + let results = vec![( + PathBuf::from("src/main.rs"), + PathBuf::from("src/main.rs"), + CovResult { + /* main.rs + fn main() { + let inp = "a"; + if "a" == inp { + println!("a"); + } else if "b" == inp { + println!("b"); + } + println!("what?"); + } + */ + lines: [ + (1, 1), + (2, 1), + (3, 2), + (4, 1), + (5, 0), + (6, 0), + (8, 1), + (9, 1), + ] + .iter() + .cloned() + .collect(), + branches: { + let mut map = BTreeMap::new(); + map.insert(3, vec![true, false]); + map.insert(5, vec![false, false]); + map + }, + functions: { + let mut map = FxHashMap::default(); + map.insert( + "_ZN8cov_test4main17h7eb435a3fb3e6f20E".to_string(), + Function { + start: 1, + executed: true, + }, + ); + map + }, + }, + )]; + + let stats = get_coverage_stats(&results); + assert_eq!(stats.lines_covered, 6); + assert_eq!(stats.lines_valid, 8); + assert_eq!(stats.branches_covered, 1); + assert_eq!(stats.branches_valid, 4); + } +}