cargo_tarpaulin/report/
mod.rs

1#![allow(unreachable_patterns)] // We may want to add more warnings and keep error logs stable
2use 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;
19/// Trait for report formats to implement.
20/// Currently reports must be serializable using serde
21pub trait Report<Out: Serialize> {
22    /// Export coverage report
23    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
35/// Reports the test coverage using the users preferred method. See config.rs
36/// or help text for details.
37pub 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                // Already reported the missing lines
94                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    // We always want to report the short summary
106    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    // Check for previous report
138    let mut report_dir = config.target_dir();
139    report_dir.push("tarpaulin");
140    if report_dir.exists() {
141        // is report there?
142        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        // make directory
148        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    // All the `writeln` unwraps are fine, it's basically what the `println` macro does
165    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}