Skip to main content

ferrous_forge/test_coverage/
reporting.rs

1//! Coverage reporting and validation
2
3use super::analyzer::CoverageAnalyzer;
4use super::types::CoverageReport;
5use crate::{Error, Result};
6use std::path::Path;
7
8impl CoverageAnalyzer {
9    /// Validate coverage meets minimum thresholds
10    ///
11    /// # Errors
12    ///
13    /// Returns a validation error if any coverage metric falls below its
14    /// configured minimum threshold and `fail_on_low_coverage` is enabled.
15    pub fn validate_coverage(&self, report: &CoverageReport) -> Result<()> {
16        let mut violations = Vec::new();
17
18        if report.line_coverage < self.config().min_line_coverage {
19            violations.push(format!(
20                "Line coverage {:.1}% is below minimum {:.1}%",
21                report.line_coverage,
22                self.config().min_line_coverage
23            ));
24        }
25
26        if report.function_coverage < self.config().min_function_coverage {
27            violations.push(format!(
28                "Function coverage {:.1}% is below minimum {:.1}%",
29                report.function_coverage,
30                self.config().min_function_coverage
31            ));
32        }
33
34        if report.branch_coverage < self.config().min_branch_coverage {
35            violations.push(format!(
36                "Branch coverage {:.1}% is below minimum {:.1}%",
37                report.branch_coverage,
38                self.config().min_branch_coverage
39            ));
40        }
41
42        if !violations.is_empty() {
43            let message = format!("Coverage violations:\n  โ€ข {}", violations.join("\n  โ€ข "));
44
45            if self.config().fail_on_low_coverage {
46                return Err(Error::validation(message));
47            }
48            tracing::warn!("{}", message);
49        }
50
51        Ok(())
52    }
53
54    /// Generate a human-readable coverage report
55    pub fn format_coverage_report(&self, report: &CoverageReport) -> String {
56        let mut output = String::new();
57
58        self.add_report_header(&mut output);
59        self.add_overall_coverage(&mut output, report);
60        self.add_threshold_status(&mut output, report);
61        self.add_low_coverage_files(&mut output, report);
62        self.add_improvement_suggestions(&mut output);
63
64        output
65    }
66
67    /// Add report header section
68    fn add_report_header(&self, output: &mut String) {
69        output.push_str("๐Ÿ“Š Test Coverage Report\n");
70        output.push_str("=".repeat(39).as_str());
71        output.push_str("\n\n");
72    }
73
74    /// Add overall coverage statistics
75    fn add_overall_coverage(&self, output: &mut String, report: &CoverageReport) {
76        output.push_str(&format!("๐Ÿ“ˆ Overall Coverage:\n"));
77        output.push_str(&format!(
78            "  โ€ข Lines:     {:.1}% ({}/{})\n",
79            report.line_coverage, report.lines_tested, report.total_lines
80        ));
81        output.push_str(&format!(
82            "  โ€ข Functions: {:.1}% ({}/{})\n",
83            report.function_coverage, report.functions_tested, report.total_functions
84        ));
85        output.push_str(&format!(
86            "  โ€ข Branches:  {:.1}% ({}/{})\n\n",
87            report.branch_coverage, report.branches_tested, report.total_branches
88        ));
89    }
90
91    /// Add threshold status section
92    fn add_threshold_status(&self, output: &mut String, report: &CoverageReport) {
93        let line_status = if report.line_coverage >= self.config().min_line_coverage {
94            "โœ…"
95        } else {
96            "โŒ"
97        };
98        let func_status = if report.function_coverage >= self.config().min_function_coverage {
99            "โœ…"
100        } else {
101            "โŒ"
102        };
103        let branch_status = if report.branch_coverage >= self.config().min_branch_coverage {
104            "โœ…"
105        } else {
106            "โŒ"
107        };
108
109        output.push_str("๐ŸŽฏ Threshold Status:\n");
110        output.push_str(&format!(
111            "  {} Lines:     {:.1}% (min: {:.1}%)\n",
112            line_status,
113            report.line_coverage,
114            self.config().min_line_coverage
115        ));
116        output.push_str(&format!(
117            "  {} Functions: {:.1}% (min: {:.1}%)\n",
118            func_status,
119            report.function_coverage,
120            self.config().min_function_coverage
121        ));
122        output.push_str(&format!(
123            "  {} Branches:  {:.1}% (min: {:.1}%)\n\n",
124            branch_status,
125            report.branch_coverage,
126            self.config().min_branch_coverage
127        ));
128    }
129
130    /// Add low coverage files section
131    fn add_low_coverage_files(&self, output: &mut String, report: &CoverageReport) {
132        let mut low_coverage_files: Vec<_> = report
133            .file_coverage
134            .values()
135            .filter(|file| file.line_coverage < self.config().min_line_coverage)
136            .collect();
137        low_coverage_files.sort_by(|a, b| {
138            a.line_coverage
139                .partial_cmp(&b.line_coverage)
140                .unwrap_or(std::cmp::Ordering::Equal)
141        });
142
143        if !low_coverage_files.is_empty() {
144            output.push_str("โš ๏ธ  Files Below Threshold:\n");
145            for file in low_coverage_files.iter().take(5) {
146                output.push_str(&format!(
147                    "  โ€ข {}: {:.1}%\n",
148                    file.file_path, file.line_coverage
149                ));
150            }
151            if low_coverage_files.len() > 5 {
152                output.push_str(&format!(
153                    "  ... and {} more files\n",
154                    low_coverage_files.len() - 5
155                ));
156            }
157            output.push('\n');
158        }
159    }
160
161    /// Add improvement suggestions section
162    fn add_improvement_suggestions(&self, output: &mut String) {
163        output.push_str("๐Ÿ’ก To improve coverage:\n");
164        output.push_str("  โ€ข Add tests for uncovered code paths\n");
165        output.push_str("  โ€ข Remove dead code\n");
166        output.push_str("  โ€ข Test error conditions and edge cases\n");
167        output.push_str("  โ€ข Use property-based testing\n");
168    }
169
170    /// Check coverage for a project
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if the coverage run fails or coverage is below
175    /// the configured minimum thresholds.
176    pub async fn check_project_coverage(&self, project_path: &Path) -> Result<()> {
177        println!("๐Ÿงช Checking test coverage...");
178
179        let report = self.run_coverage(project_path).await?;
180
181        println!("{}", self.format_coverage_report(&report));
182
183        self.validate_coverage(&report)?;
184
185        println!("โœ… Coverage check completed successfully");
186        Ok(())
187    }
188}