diff --git a/README.md b/README.md index d54059fc1..3bba01276 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 dbec965b6..dd1b0e93a 100644 --- a/src/cobertura.rs +++ b/src/cobertura.rs @@ -333,6 +333,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 @@ -341,7 +342,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(); @@ -515,7 +520,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, @@ -546,11 +551,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))) @@ -715,26 +720,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] @@ -749,7 +756,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); @@ -788,7 +795,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); @@ -820,7 +827,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); @@ -840,7 +847,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/lib.rs b/src/lib.rs index 3c9eb3b24..2995a6622 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 e05191fa0..ff29a15d1 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 = [ @@ -292,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() { @@ -563,10 +571,22 @@ 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), }; } + + 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); + } +}