From 4b821e99c19554a3b525957e78a04b0090641ed6 Mon Sep 17 00:00:00 2001 From: Bastian Date: Fri, 11 Nov 2022 20:07:56 +0100 Subject: [PATCH] Add support for outputting with multiple formats (#893) Fixes #496 --- benches/output.rs | 73 ++++++++++--------- src/cobertura.rs | 33 ++++----- src/defs.rs | 2 +- src/main.rs | 160 ++++++++++++++++++++++++++---------------- src/output.rs | 47 ++++++------- src/path_rewriting.rs | 15 ++-- 6 files changed, 174 insertions(+), 156 deletions(-) diff --git a/benches/output.rs b/benches/output.rs index f0c834fa5..8904a2a97 100644 --- a/benches/output.rs +++ b/benches/output.rs @@ -3,54 +3,53 @@ extern crate test; use grcov::{ - output_activedata_etl, output_covdir, output_lcov, CovResult, CovResultIter, Function, - FunctionMap, + output_activedata_etl, output_covdir, output_lcov, CovResult, Function, FunctionMap, + ResultTuple, }; use rustc_hash::FxHashMap; use std::path::PathBuf; use tempfile::tempdir; use test::{black_box, Bencher}; -fn generate_cov_result_iter() -> CovResultIter { - Box::new( - FxHashMap::default() - .into_iter() - .map(|(_, _): (PathBuf, CovResult)| { - ( - PathBuf::from(""), - PathBuf::from(""), - CovResult { - branches: [].iter().cloned().collect(), - functions: { - let mut functions: FunctionMap = FxHashMap::default(); - functions.insert( - "f1".to_string(), - Function { - start: 1, - executed: true, - }, - ); - functions.insert( - "f2".to_string(), - Function { - start: 2, - executed: false, - }, - ); - functions - }, - lines: [(1, 21), (2, 7), (7, 0)].iter().cloned().collect(), +fn generate_cov_result_iter() -> Vec { + FxHashMap::default() + .into_iter() + .map(|(_, _): (PathBuf, CovResult)| { + ( + PathBuf::from(""), + PathBuf::from(""), + CovResult { + branches: [].iter().cloned().collect(), + functions: { + let mut functions: FunctionMap = FxHashMap::default(); + functions.insert( + "f1".to_string(), + Function { + start: 1, + executed: true, + }, + ); + functions.insert( + "f2".to_string(), + Function { + start: 2, + executed: false, + }, + ); + functions }, - ) - }), - ) + lines: [(1, 21), (2, 7), (7, 0)].iter().cloned().collect(), + }, + ) + }) + .collect::>() } #[bench] fn bench_output_activedata_etl(b: &mut Bencher) { let dir = tempdir().unwrap(); b.iter(|| { black_box(output_activedata_etl( - generate_cov_result_iter(), + &generate_cov_result_iter(), Some(&dir.path().join("temp")), false, )) @@ -62,7 +61,7 @@ fn bench_output_covdir(b: &mut Bencher) { let dir = tempdir().unwrap(); b.iter(|| { black_box(output_covdir( - generate_cov_result_iter(), + &generate_cov_result_iter(), Some(&dir.path().join("temp")), )); }); @@ -73,7 +72,7 @@ fn bench_output_lcov(b: &mut Bencher) { let dir = tempdir().unwrap(); b.iter(|| { black_box(output_lcov( - generate_cov_result_iter(), + &generate_cov_result_iter(), Some(&dir.path().join("temp")), false, )); diff --git a/src/cobertura.rs b/src/cobertura.rs index c22cf4dac..a8d6c5981 100644 --- a/src/cobertura.rs +++ b/src/cobertura.rs @@ -1,3 +1,4 @@ +use crate::defs::*; use quick_xml::{ events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event}, Writer, @@ -9,7 +10,6 @@ use std::time::{SystemTime, UNIX_EPOCH}; use symbolic_common::Name; use symbolic_demangle::{Demangle, DemangleOptions}; -use crate::defs::CovResultIter; use crate::output::get_target_output_writable; macro_rules! demangle { @@ -229,12 +229,13 @@ impl ToString for ConditionType { } fn get_coverage( - results: CovResultIter, + results: &[ResultTuple], sources: Vec, demangle: bool, demangle_options: DemangleOptions, ) -> Coverage { let packages: Vec = results + .iter() .map(|(_, rel_path, result)| { let all_lines: Vec = result.lines.iter().map(|(k, _)| k).cloned().collect(); @@ -246,13 +247,9 @@ fn get_coverage( } start_indexes.sort_unstable(); - let functions = result.functions; - let result_lines = result.lines; - let result_branches = result.branches; - let line_from_number = |number| { - let hits = result_lines.get(&number).cloned().unwrap_or_default(); - if let Some(branches) = result_branches.get(&number) { + let hits = result.lines.get(&number).cloned().unwrap_or_default(); + if let Some(branches) = result.branches.get(&number) { let conditions = branches .iter() .enumerate() @@ -272,7 +269,8 @@ fn get_coverage( } }; - let methods: Vec = functions + let methods: Vec = result + .functions .iter() .map(|(name, function)| { let mut func_end = end; @@ -329,7 +327,7 @@ fn get_coverage( pub fn output_cobertura( source_dir: Option<&Path>, - results: CovResultIter, + results: &[ResultTuple], output_file: Option<&Path>, demangle: bool, ) { @@ -714,8 +712,7 @@ mod tests { coverage_result(Result::Main), )]; - let results = Box::new(results.into_iter()); - output_cobertura(None, results, Some(&file_path), true); + output_cobertura(None, &results, Some(&file_path), true); let results = read_file(&file_path); @@ -749,8 +746,7 @@ mod tests { coverage_result(Result::Test), )]; - let results = Box::new(results.into_iter()); - output_cobertura(None, results, Some(file_path.as_ref()), true); + output_cobertura(None, &results, Some(file_path.as_ref()), true); let results = read_file(&file_path); @@ -789,8 +785,7 @@ mod tests { ), ]; - let results = Box::new(results.into_iter()); - output_cobertura(None, results, Some(file_path.as_ref()), true); + output_cobertura(None, &results, Some(file_path.as_ref()), true); let results = read_file(&file_path); @@ -822,8 +817,7 @@ mod tests { CovResult::default(), )]; - let results = Box::new(results.into_iter()); - output_cobertura(None, results, Some(&file_path), true); + output_cobertura(None, &results, Some(&file_path), true); let results = read_file(&file_path); @@ -843,8 +837,7 @@ mod tests { CovResult::default(), )]; - let results = Box::new(results.into_iter()); - output_cobertura(Some(Path::new("src")), results, Some(&file_path), true); + output_cobertura(Some(Path::new("src")), &results, Some(&file_path), true); let results = read_file(&file_path); diff --git a/src/defs.rs b/src/defs.rs index 842d4fbf1..764c6f710 100644 --- a/src/defs.rs +++ b/src/defs.rs @@ -58,7 +58,7 @@ pub type JobSender = Sender>; pub type CovResultMap = FxHashMap; pub type SyncCovResultMap = Mutex; -pub type CovResultIter = Box>; +pub type ResultTuple = (PathBuf, PathBuf, CovResult); #[derive(Debug, Default)] pub struct CDStats { diff --git a/src/main.rs b/src/main.rs index 36b82a429..ee12c5fcd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,6 +50,28 @@ impl FromStr for OutputType { } } +impl OutputType { + fn to_file_name(&self, output_path: Option<&Path>) -> Option { + output_path.map(|path| { + if path.is_dir() { + match self { + OutputType::Ade => path.join("activedata"), + OutputType::Lcov => path.join("lcov"), + OutputType::Coveralls => path.join("coveralls"), + OutputType::CoverallsPlus => path.join("coveralls+"), + OutputType::Files => path.join("files"), + OutputType::Covdir => path.join("covdir"), + OutputType::Html => path.join("html"), + OutputType::Cobertura => path.join("cobertura.xml"), + OutputType::Markdown => path.join("markdown.md"), + } + } else { + path.to_path_buf() + } + }) + } +} + enum Filter { Covered, Uncovered, @@ -87,7 +109,7 @@ struct Opt { short = "t", long, long_help = "\ - Sets a custom output type:\n\ + Comma separated list of custom output types:\n\ - *html* for a HTML coverage report;\n\ - *coveralls* for the Coveralls specific format;\n\ - *lcov* for the lcov INFO format;\n\ @@ -96,6 +118,7 @@ struct Opt { - *ade* for the ActiveData-ETL specific format;\n\ - *files* to only return a list of files.\n\ - *markdown* for human easy read.\n\ + - *cobertura* for output in cobertura format.\n\ ", value_name = "OUTPUT TYPE", default_value = "lcov", @@ -103,20 +126,12 @@ struct Opt { ("coveralls", "coveralls-auth"), ("coveralls+", "coveralls-auth"), ], - possible_values = &[ - "ade", - "lcov", - "coveralls", - "coveralls+", - "files", - "covdir", - "html", - "cobertura", - "markdown", - ], + + use_delimiter = true )] - output_type: OutputType, - /// Specifies the output path. + output_types: Vec, + /// Specifies the output path. This is a file for a single output type and must be a folder + /// for multiple output types. #[structopt(short, long, value_name = "PATH", alias = "output-file")] output_path: Option, /// Specifies the output config file. @@ -422,52 +437,75 @@ fn main() { file_filter, ); - match opt.output_type { - OutputType::Ade => output_activedata_etl(iterator, opt.output_path.as_deref(), demangle), - OutputType::Lcov => output_lcov(iterator, opt.output_path.as_deref(), demangle), - OutputType::Coveralls => output_coveralls( - iterator, - opt.token.as_deref(), - opt.service_name.as_deref(), - &opt.service_number.unwrap_or_default(), - opt.service_job_id.as_deref(), - &opt.service_pull_request.unwrap_or_default(), - &opt.commit_sha.unwrap_or_default(), - false, - opt.output_path.as_deref(), - &opt.vcs_branch, - opt.parallel, - demangle, - ), - OutputType::CoverallsPlus => output_coveralls( - iterator, - opt.token.as_deref(), - opt.service_name.as_deref(), - &opt.service_number.unwrap_or_default(), - opt.service_job_id.as_deref(), - &opt.service_pull_request.unwrap_or_default(), - &opt.commit_sha.unwrap_or_default(), - true, - opt.output_path.as_deref(), - &opt.vcs_branch, - opt.parallel, - demangle, - ), - OutputType::Files => output_files(iterator, opt.output_path.as_deref()), - OutputType::Covdir => output_covdir(iterator, opt.output_path.as_deref()), - OutputType::Html => output_html( - iterator, - opt.output_path.as_deref(), - num_threads, - opt.branch, - opt.output_config_file.as_deref(), - ), - OutputType::Cobertura => output_cobertura( - source_root.as_deref(), - iterator, - opt.output_path.as_deref(), - demangle, - ), - OutputType::Markdown => output_markdown(iterator, opt.output_path.as_deref()), + let service_number = opt.service_number.unwrap_or_default(); + let service_pull_request = opt.service_pull_request.unwrap_or_default(); + let commit_sha = opt.commit_sha.unwrap_or_default(); + + let output_path = match opt.output_types.len() { + 0 => return, + 1 => opt.output_path.as_deref(), + _ => match opt.output_path.as_deref() { + Some(output_path) => { + if output_path.is_dir() { + Some(output_path) + } else { + panic!("output_path must be a directory when using multiple outputs"); + } + } + _ => None, + }, }; + + for output_type in &opt.output_types { + let output_path = output_type.to_file_name(output_path); + + match output_type { + OutputType::Ade => output_activedata_etl(&iterator, output_path.as_deref(), demangle), + OutputType::Lcov => output_lcov(&iterator, output_path.as_deref(), demangle), + OutputType::Coveralls => output_coveralls( + &iterator, + opt.token.as_deref(), + opt.service_name.as_deref(), + &service_number, + opt.service_job_id.as_deref(), + &service_pull_request, + &commit_sha, + false, + output_path.as_deref(), + &opt.vcs_branch, + opt.parallel, + demangle, + ), + OutputType::CoverallsPlus => output_coveralls( + &iterator, + opt.token.as_deref(), + opt.service_name.as_deref(), + &service_number, + opt.service_job_id.as_deref(), + &service_pull_request, + &commit_sha, + true, + output_path.as_deref(), + &opt.vcs_branch, + opt.parallel, + demangle, + ), + OutputType::Files => output_files(&iterator, output_path.as_deref()), + OutputType::Covdir => output_covdir(&iterator, output_path.as_deref()), + OutputType::Html => output_html( + &iterator, + output_path.as_deref(), + num_threads, + opt.branch, + opt.output_config_file.as_deref(), + ), + OutputType::Cobertura => output_cobertura( + source_root.as_deref(), + &iterator, + output_path.as_deref(), + demangle, + ), + OutputType::Markdown => output_markdown(&iterator, output_path.as_deref()), + }; + } } diff --git a/src/output.rs b/src/output.rs index 23df84db3..635c09910 100644 --- a/src/output.rs +++ b/src/output.rs @@ -70,7 +70,7 @@ pub fn get_target_output_writable(output_file: Option<&Path>) -> Box write_target } -pub fn output_activedata_etl(results: CovResultIter, output_file: Option<&Path>, demangle: bool) { +pub fn output_activedata_etl(results: &[ResultTuple], output_file: Option<&Path>, demangle: bool) { let demangle_options = DemangleOptions::name_only(); let mut writer = BufWriter::new(get_target_output_writable(output_file)); @@ -180,7 +180,7 @@ pub fn output_activedata_etl(results: CovResultIter, output_file: Option<&Path>, } } -pub fn output_covdir(results: CovResultIter, output_file: Option<&Path>) { +pub fn output_covdir(results: &[ResultTuple], output_file: Option<&Path>) { let mut writer = BufWriter::new(get_target_output_writable(output_file)); let mut relative: FxHashMap>> = FxHashMap::default(); let global = Rc::new(RefCell::new(CDDirStats::new("".to_string()))); @@ -226,7 +226,7 @@ pub fn output_covdir(results: CovResultIter, output_file: Option<&Path>) { prev_stats.borrow_mut().files.push(CDFileStats::new( path.file_name().unwrap().to_str().unwrap().to_string(), - result.lines, + result.lines.clone(), )); } @@ -236,7 +236,7 @@ pub fn output_covdir(results: CovResultIter, output_file: Option<&Path>) { serde_json::to_writer(&mut writer, &global.into_json()).unwrap(); } -pub fn output_lcov(results: CovResultIter, output_file: Option<&Path>, demangle: bool) { +pub fn output_lcov(results: &[ResultTuple], output_file: Option<&Path>, demangle: bool) { let demangle_options = DemangleOptions::name_only(); let mut writer = BufWriter::new(get_target_output_writable(output_file)); writer.write_all(b"TN:\n").unwrap(); @@ -413,7 +413,7 @@ fn get_coveralls_git_info(commit_sha: &str, vcs_branch: &str) -> Value { } pub fn output_coveralls( - results: CovResultIter, + results: &[ResultTuple], repo_token: Option<&str>, service_name: Option<&str>, service_number: &str, @@ -455,7 +455,7 @@ pub fn output_coveralls( if !with_function_info { source_files.push(json!({ "name": rel_path, - "source_digest": get_digest(abs_path), + "source_digest": get_digest(abs_path.clone()), "coverage": coverage, "branches": branches, })); @@ -471,7 +471,7 @@ pub fn output_coveralls( source_files.push(json!({ "name": rel_path, - "source_digest": get_digest(abs_path), + "source_digest": get_digest(abs_path.clone()), "coverage": coverage, "branches": branches, "functions": functions, @@ -505,7 +505,7 @@ pub fn output_coveralls( serde_json::to_writer(&mut writer, &result).unwrap(); } -pub fn output_files(results: CovResultIter, output_file: Option<&Path>) { +pub fn output_files(results: &[ResultTuple], output_file: Option<&Path>) { let mut writer = BufWriter::new(get_target_output_writable(output_file)); for (_, rel_path, _) in results { writeln!(writer, "{}", rel_path.display()).unwrap(); @@ -513,7 +513,7 @@ pub fn output_files(results: CovResultIter, output_file: Option<&Path>) { } pub fn output_html( - results: CovResultIter, + results: &[ResultTuple], output_dir: Option<&Path>, num_threads: usize, branch_enabled: bool, @@ -559,9 +559,9 @@ pub fn output_html( for (abs_path, rel_path, result) in results { sender .send(Some(HtmlItem { - abs_path, - rel_path, - result, + abs_path: abs_path.to_path_buf(), + rel_path: rel_path.to_path_buf(), + result: result.clone(), })) .unwrap(); } @@ -587,7 +587,7 @@ pub fn output_html( html::gen_coverage_json(&global.stats, &config, &output); } -pub fn output_markdown(results: CovResultIter, output_file: Option<&Path>) { +pub fn output_markdown(results: &[ResultTuple], output_file: Option<&Path>) { #[derive(Tabled)] struct LineSummary { file: String, @@ -688,8 +688,7 @@ mod tests { }, )]; - let results = Box::new(results.into_iter()); - output_lcov(results, Some(&file_path), false); + output_lcov(&results, Some(&file_path), false); let results = read_file(&file_path); @@ -737,8 +736,7 @@ mod tests { }, )]; - let results = Box::new(results.into_iter()); - output_lcov(results, Some(&file_path), true); + output_lcov(&results, Some(&file_path), true); let results = read_file(&file_path); @@ -792,8 +790,7 @@ mod tests { ), ]; - let results = Box::new(results.into_iter()); - output_covdir(results, Some(&file_path)); + output_covdir(&results, Some(&file_path)); let results: Value = serde_json::from_str(&read_file(&file_path)).unwrap(); let expected_path = PathBuf::from("./test/").join(file_name); @@ -818,12 +815,11 @@ mod tests { }, )]; - let results = Box::new(results.into_iter()); let expected_service_job_id: &str = "100500"; let with_function_info: bool = true; let parallel: bool = true; output_coveralls( - results, + &results, None, None, "unused", @@ -858,12 +854,11 @@ mod tests { }, )]; - let results = Box::new(results.into_iter()); let token = None; let with_function_info: bool = true; let parallel: bool = true; output_coveralls( - results, + &results, token, None, "unused", @@ -898,13 +893,12 @@ mod tests { }, )]; - let results = Box::new(results.into_iter()); let service_name = None; let service_job_id = None; let with_function_info: bool = true; let parallel: bool = true; output_coveralls( - results, + &results, None, service_name, "unused", @@ -954,8 +948,7 @@ mod tests { ), ]; - let results = Box::new(results.into_iter()); - output_markdown(results, Some(&file_path)); + output_markdown(&results, Some(&file_path)); let results = &read_file(&file_path); let expected = "| file | coverage | covered | missed_lines | diff --git a/src/path_rewriting.rs b/src/path_rewriting.rs index db9c5ced0..1b0dff7b1 100644 --- a/src/path_rewriting.rs +++ b/src/path_rewriting.rs @@ -235,7 +235,7 @@ pub fn rewrite_paths( to_keep_dirs: &[impl AsRef], filter_option: Option, file_filter: crate::FileFilter, -) -> CovResultIter { +) -> Vec { let to_ignore_globset = to_globset(to_ignore_dirs); let to_keep_globset = to_globset(to_keep_dirs); @@ -341,11 +341,7 @@ pub fn rewrite_paths( Some((abs_path, rel_path, result)) }); - Box::new( - results - .collect::>() - .into_iter(), - ) + results.collect() } #[cfg(test)] @@ -933,6 +929,7 @@ mod tests { None, Default::default(), ) + .iter() .any(|_| false); } @@ -1052,7 +1049,7 @@ mod tests { let mut result_map: CovResultMap = FxHashMap::default(); result_map.insert("java/main.java".to_string(), empty_result!()); result_map.insert("main.rs".to_string(), empty_result!()); - let results = rewrite_paths( + let mut results = rewrite_paths( result_map, None, Some(&canonicalize_path(".").unwrap()), @@ -1063,7 +1060,6 @@ mod tests { None, Default::default(), ); - let mut results: Vec<(PathBuf, PathBuf, CovResult)> = results.collect(); assert!(results.len() == 1); let (abs_path, rel_path, result) = results.remove(0); @@ -1079,7 +1075,7 @@ mod tests { let mut result_map: CovResultMap = FxHashMap::default(); result_map.insert("java\\main.java".to_string(), empty_result!()); result_map.insert("main.rs".to_string(), empty_result!()); - let results = rewrite_paths( + let mut results = rewrite_paths( result_map, None, Some(&canonicalize_path(".").unwrap()), @@ -1090,7 +1086,6 @@ mod tests { None, Default::default(), ); - let mut results: Vec<(PathBuf, PathBuf, CovResult)> = results.collect(); assert!(results.len() == 1); let (abs_path, rel_path, result) = results.remove(0);