cargo_tarpaulin/report/
mod.rs1#![allow(unreachable_patterns)] use crate::config::*;
3use crate::errors::*;
4use crate::test_loader::TracerData;
5use crate::traces::*;
6use cargo_metadata::Metadata;
7use serde::Serialize;
8use std::fs::{create_dir_all, File};
9use std::io::{self, BufReader, Write};
10use tracing::{error, info};
11
12pub mod cobertura;
13#[cfg(feature = "coveralls")]
14pub mod coveralls;
15pub mod html;
16pub mod json;
17pub mod lcov;
18mod safe_json;
19pub trait Report<Out: Serialize> {
22 fn export(coverage_data: &[TracerData], config: &Config);
24}
25
26fn coverage_report_name(config: &Config) -> String {
27 config
28 .get_metadata()
29 .as_ref()
30 .and_then(Metadata::root_package)
31 .map(|x| format!("{}-coverage.json", x.name))
32 .unwrap_or_else(|| "coverage.json".to_string())
33}
34
35pub fn report_coverage(config: &Config, result: &TraceMap) -> Result<(), RunError> {
38 if !result.is_empty() {
39 generate_requested_reports(config, result)?;
40 let mut report_dir = config.target_dir();
41 report_dir.push("tarpaulin");
42 if !report_dir.exists() {
43 let _ = create_dir_all(&report_dir);
44 }
45 report_dir.push(coverage_report_name(config));
46 let file = File::create(&report_dir)
47 .map_err(|_| RunError::CovReport("Failed to create run report".to_string()))?;
48 serde_json::to_writer(&file, &result)
49 .map_err(|_| RunError::CovReport("Failed to save run report".to_string()))?;
50 Ok(())
51 } else if !config.no_run {
52 Err(RunError::CovReport(
53 "No coverage results collected.".to_string(),
54 ))
55 } else {
56 Ok(())
57 }
58}
59
60fn generate_requested_reports(config: &Config, result: &TraceMap) -> Result<(), RunError> {
61 #[cfg(feature = "coveralls")]
62 if config.is_coveralls() {
63 coveralls::export(result, config)?;
64 info!("Coverage data sent");
65 }
66 info!("Coverage Results:");
67
68 if !config.is_default_output_dir() && create_dir_all(config.output_dir()).is_err() {
69 return Err(RunError::OutFormat(format!(
70 "Failed to create or locate custom output directory: {:?}",
71 config.output_directory,
72 )));
73 }
74
75 if config.verbose || config.generate.is_empty() {
76 print_missing_lines(config, result);
77 }
78 for g in &config.generate {
79 match *g {
80 OutputFile::Xml => {
81 cobertura::report(result, config).map_err(RunError::XML)?;
82 }
83 OutputFile::Html => {
84 html::export(result, config)?;
85 }
86 OutputFile::Lcov => {
87 lcov::export(result, config)?;
88 }
89 OutputFile::Json => {
90 json::export(result, config)?;
91 }
92 OutputFile::Stdout => {
93 if !config.verbose {
95 print_missing_lines(config, result);
96 }
97 }
98 _ => {
99 return Err(RunError::OutFormat(
100 "Output format is currently not supported!".to_string(),
101 ));
102 }
103 }
104 }
105 print_summary(config, result);
107 Ok(())
108}
109
110fn print_missing_lines(config: &Config, result: &TraceMap) {
111 let mut w: Box<dyn Write> = if config.stderr {
112 Box::new(io::stderr().lock())
113 } else {
114 Box::new(io::stdout().lock())
115 };
116 writeln!(w, "|| Uncovered Lines:").unwrap();
117 for (key, value) in result.iter() {
118 let path = config.strip_base_dir(key);
119 let mut uncovered_lines = vec![];
120 for v in value.iter() {
121 if let CoverageStat::Line(0) = v.stats {
122 uncovered_lines.push(v.line);
123 }
124 }
125 uncovered_lines.sort_unstable();
126 let (groups, last_group) = uncovered_lines
127 .into_iter()
128 .fold((vec![], vec![]), accumulate_lines);
129 let (groups, _) = accumulate_lines((groups, last_group), u64::max_value());
130 if !groups.is_empty() {
131 writeln!(w, "|| {}: {}", path.display(), groups.join(", ")).unwrap();
132 }
133 }
134}
135
136fn get_previous_result(config: &Config) -> Option<TraceMap> {
137 let mut report_dir = config.target_dir();
139 report_dir.push("tarpaulin");
140 if report_dir.exists() {
141 report_dir.push(coverage_report_name(config));
143 let file = File::open(&report_dir).ok()?;
144 let reader = BufReader::new(file);
145 serde_json::from_reader(reader).ok()
146 } else {
147 create_dir_all(&report_dir)
149 .unwrap_or_else(|e| error!("Failed to create report directory: {}", e));
150 None
151 }
152}
153
154fn print_summary(config: &Config, result: &TraceMap) {
155 let mut w: Box<dyn Write> = if config.stderr {
156 Box::new(io::stderr().lock())
157 } else {
158 Box::new(io::stdout().lock())
159 };
160 let last = match get_previous_result(config) {
161 Some(l) => l,
162 None => TraceMap::new(),
163 };
164 writeln!(w, "|| Tested/Total Lines:").unwrap();
166 for file in result.files() {
167 if result.coverable_in_path(file) == 0 {
168 continue;
169 }
170 let path = config.strip_base_dir(file);
171 if last.contains_file(file) && last.coverable_in_path(file) > 0 {
172 let last_percent = coverage_percentage(last.get_child_traces(file));
173 let current_percent = coverage_percentage(result.get_child_traces(file));
174 let delta = 100.0f64 * (current_percent - last_percent);
175 writeln!(
176 w,
177 "|| {}: {}/{} {:+.2}%",
178 path.display(),
179 result.covered_in_path(file),
180 result.coverable_in_path(file),
181 delta
182 )
183 .unwrap();
184 } else {
185 writeln!(
186 w,
187 "|| {}: {}/{}",
188 path.display(),
189 result.covered_in_path(file),
190 result.coverable_in_path(file)
191 )
192 .unwrap();
193 }
194 }
195 let percent = result.coverage_percentage() * 100.0f64;
196 if result.total_coverable() == 0 {
197 writeln!(w, "No coverable lines found").unwrap();
198 } else if last.is_empty() {
199 writeln!(
200 w,
201 "|| \n{:.2}% coverage, {}/{} lines covered",
202 percent,
203 result.total_covered(),
204 result.total_coverable()
205 )
206 .unwrap();
207 } else {
208 let delta = percent - 100.0f64 * last.coverage_percentage();
209 writeln!(
210 w,
211 "|| \n{:.2}% coverage, {}/{} lines covered, {:+.2}% change in coverage",
212 percent,
213 result.total_covered(),
214 result.total_coverable(),
215 delta
216 )
217 .unwrap();
218 }
219}
220
221fn accumulate_lines(
222 (mut acc, mut group): (Vec<String>, Vec<u64>),
223 next: u64,
224) -> (Vec<String>, Vec<u64>) {
225 if let Some(last) = group.last().cloned() {
226 if next == last + 1 {
227 group.push(next);
228 (acc, group)
229 } else {
230 match (group.first(), group.last()) {
231 (Some(first), Some(last)) if first == last => {
232 acc.push(format!("{first}"));
233 }
234 (Some(first), Some(last)) => {
235 acc.push(format!("{first}-{last}"));
236 }
237 (Some(_), None) | (None, _) => (),
238 };
239 (acc, vec![next])
240 }
241 } else {
242 group.push(next);
243 (acc, group)
244 }
245}