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