cargo_tarpaulin/report/
html.rs

1use crate::config::Config;
2use crate::errors::*;
3use crate::report::{get_previous_result, safe_json};
4use crate::traces::{Trace, TraceMap};
5use serde::Serialize;
6use std::fs::{read_to_string, File};
7use std::io::{self, Write};
8
9#[derive(Serialize)]
10struct SourceFile {
11    pub path: Vec<String>,
12    pub content: String,
13    pub traces: Vec<Trace>,
14    pub covered: usize,
15    pub coverable: usize,
16}
17
18#[derive(Serialize)]
19struct CoverageReport {
20    pub files: Vec<SourceFile>,
21}
22
23#[derive(PartialEq)]
24enum Context {
25    CurrentResults,
26    PreviousResults,
27}
28
29fn get_json(coverage_data: &TraceMap, context: Context) -> Result<String, RunError> {
30    let mut report = CoverageReport { files: Vec::new() };
31
32    for (path, traces) in coverage_data.iter() {
33        let content = match read_to_string(path) {
34            Ok(k) => k,
35            Err(e) => {
36                if context == Context::PreviousResults && e.kind() == io::ErrorKind::NotFound {
37                    // Assume the file has been deleted since the last run.
38                    continue;
39                }
40
41                return Err(RunError::Html(format!(
42                    "Unable to read source file to string: {e}"
43                )));
44            }
45        };
46
47        report.files.push(SourceFile {
48            path: path
49                .components()
50                .map(|c| c.as_os_str().to_string_lossy().to_string())
51                .collect(),
52            content,
53            traces: traces.clone(),
54            covered: coverage_data.covered_in_path(path),
55            coverable: coverage_data.coverable_in_path(path),
56        });
57    }
58
59    safe_json::to_string_safe(&report)
60        .map_err(|e| RunError::Html(format!("Report isn't serializable: {e}")))
61}
62
63pub fn export(coverage_data: &TraceMap, config: &Config) -> Result<(), RunError> {
64    let file_path = config.output_dir().join("tarpaulin-report.html");
65    let mut file = match File::create(file_path) {
66        Ok(k) => k,
67        Err(e) => return Err(RunError::Html(format!("File is not writeable: {e}"))),
68    };
69
70    let report_json = get_json(coverage_data, Context::CurrentResults)?;
71    let previous_report_json = match get_previous_result(config) {
72        Some(result) => get_json(&result, Context::PreviousResults)?,
73        None => String::from("null"),
74    };
75
76    match write!(
77        file,
78        r##"<!doctype html>
79<html>
80<head>
81    <meta charset="utf-8">
82    <style>{}</style>
83</head>
84<body>
85    <div id="root"></div>
86    <script>
87        var data = {};
88        var previousData = {};
89    </script>
90    <script crossorigin>{}</script>
91    <script crossorigin>{}</script>
92    <script>{}</script>
93</body>
94</html>"##,
95        include_str!("report_viewer.css"),
96        report_json,
97        previous_report_json,
98        include_str!("react.production.min.js"),
99        include_str!("react-dom.production.min.js"),
100        include_str!("report_viewer.js"),
101    ) {
102        Ok(_) => (),
103        Err(e) => return Err(RunError::Html(e.to_string())),
104    };
105
106    Ok(())
107}