Skip to main content

ferrous_forge/test_coverage/
utils.rs

1//! Test coverage utility functions
2
3use super::types::FileCoverage;
4use crate::{Error, Result};
5use serde::Deserialize;
6use std::collections::HashMap;
7
8/// Tarpaulin JSON output structure (tarpaulin >= 0.18 format).
9///
10/// tarpaulin --out Json produces:
11/// ```json
12/// { "files": [ { "path": [...], "covered": N, "coverable": N, ... } ],
13///   "coverage": 82.6, "covered": 2121, "coverable": 2567 }
14/// ```
15#[derive(Deserialize)]
16pub struct TarpaulinOutput {
17    /// Line coverage percentage (0–100)
18    pub coverage: f64,
19    /// Number of lines covered across all files
20    pub covered: u32,
21    /// Total coverable lines across all files
22    pub coverable: u32,
23    /// Per-file coverage entries
24    pub files: Vec<TarpaulinFile>,
25    /// Unused — branches not reported in this format
26    #[serde(skip)]
27    pub branches_covered: Option<u32>,
28    /// Unused — branches not reported in this format
29    #[serde(skip)]
30    pub branches_total: Option<u32>,
31    /// Convenience alias used by the rest of the codebase
32    #[serde(skip)]
33    pub lines_covered: u32,
34    /// Convenience alias used by the rest of the codebase
35    #[serde(skip)]
36    pub lines_total: u32,
37    /// Convenience alias used by the rest of the codebase
38    #[serde(skip)]
39    pub line_coverage: f64,
40}
41
42/// Tarpaulin per-file coverage data (tarpaulin >= 0.18 format).
43#[derive(Deserialize)]
44pub struct TarpaulinFile {
45    /// File path split into components (e.g. `["/", "src", "lib.rs"]`)
46    pub path: Vec<String>,
47    /// Number of covered lines in this file
48    pub covered: u32,
49    /// Total coverable lines in this file
50    pub coverable: u32,
51}
52
53/// Function coverage statistics
54pub struct FunctionStats {
55    /// Function coverage percentage
56    pub coverage: f64,
57    /// Number of functions tested
58    pub tested: u32,
59    /// Total number of functions
60    pub total: u32,
61}
62
63/// Parse tarpaulin JSON output
64///
65/// Supports the tarpaulin >= 0.18 array-of-files format:
66/// `{ "files": [...], "coverage": f64, "covered": u32, "coverable": u32 }`
67///
68/// # Errors
69///
70/// Returns an error if the output string is not valid JSON or does not
71/// match the expected tarpaulin format.
72pub fn parse_tarpaulin_json(output: &str) -> Result<TarpaulinOutput> {
73    let mut data: TarpaulinOutput = serde_json::from_str(output)
74        .map_err(|e| Error::process(format!("Failed to parse tarpaulin output: {}", e)))?;
75
76    // Populate convenience aliases so callers that reference the old field names still work.
77    data.line_coverage = data.coverage;
78    data.lines_covered = data.covered;
79    data.lines_total = data.coverable;
80
81    Ok(data)
82}
83
84/// Process file coverage data
85pub fn process_file_coverage(
86    files: &[TarpaulinFile],
87) -> (HashMap<String, FileCoverage>, FunctionStats) {
88    let mut file_coverage = HashMap::new();
89    let mut total_functions_tested = 0;
90    let mut total_functions = 0;
91
92    for file_data in files {
93        // Reconstruct the file path from its component array, skipping the leading "/".
94        let file_path = file_data
95            .path
96            .join("/")
97            .trim_start_matches("//")
98            .to_string();
99        let (estimated_functions, estimated_functions_tested) =
100            estimate_function_coverage(file_data);
101
102        total_functions += estimated_functions;
103        total_functions_tested += estimated_functions_tested;
104
105        let coverage = create_file_coverage(
106            &file_path,
107            file_data,
108            estimated_functions,
109            estimated_functions_tested,
110        );
111        file_coverage.insert(file_path, coverage);
112    }
113
114    let function_coverage =
115        calculate_function_coverage_percentage(total_functions_tested, total_functions);
116
117    (
118        file_coverage,
119        FunctionStats {
120            coverage: function_coverage,
121            tested: total_functions_tested,
122            total: total_functions,
123        },
124    )
125}
126
127/// Estimate function coverage from line coverage data
128fn estimate_function_coverage(file_data: &TarpaulinFile) -> (u32, u32) {
129    let estimated_functions = (file_data.coverable / 10).max(1);
130    let line_coverage_pct = if file_data.coverable == 0 {
131        0.0
132    } else {
133        file_data.covered as f64 / file_data.coverable as f64
134    };
135    let estimated_functions_tested = (line_coverage_pct * estimated_functions as f64) as u32;
136    (estimated_functions, estimated_functions_tested)
137}
138
139/// Create file coverage object
140fn create_file_coverage(
141    file_path: &str,
142    file_data: &TarpaulinFile,
143    estimated_functions: u32,
144    estimated_functions_tested: u32,
145) -> FileCoverage {
146    let line_coverage = if file_data.coverable == 0 {
147        0.0
148    } else {
149        (file_data.covered as f64 / file_data.coverable as f64) * 100.0
150    };
151    FileCoverage {
152        file_path: file_path.to_string(),
153        line_coverage,
154        function_coverage: calculate_function_coverage_percentage(
155            estimated_functions_tested,
156            estimated_functions,
157        ),
158        lines_tested: file_data.covered,
159        total_lines: file_data.coverable,
160        functions_tested: estimated_functions_tested,
161        total_functions: estimated_functions,
162    }
163}
164
165/// Calculate function coverage percentage
166pub fn calculate_function_coverage_percentage(tested: u32, total: u32) -> f64 {
167    if total == 0 {
168        0.0
169    } else {
170        (tested as f64 / total as f64) * 100.0
171    }
172}
173
174/// Calculate branch coverage
175pub fn calculate_branch_coverage(data: &TarpaulinOutput) -> f64 {
176    match (data.branches_covered, data.branches_total) {
177        (Some(covered), Some(total)) if total > 0 => (covered as f64 / total as f64) * 100.0,
178        _ => 0.0,
179    }
180}