clnrm_core/coverage/
report.rs

1//! Coverage report generation in various formats
2
3use crate::coverage::BehaviorCoverageReport;
4use crate::error::{CleanroomError, Result};
5use std::path::Path;
6
7/// Report format options
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ReportFormat {
10    /// Plain text output
11    Text,
12    /// JSON format
13    Json,
14    /// HTML format
15    Html,
16    /// Markdown format
17    Markdown,
18}
19
20impl ReportFormat {
21    /// Parse format from string (not implementing FromStr trait to keep simple API)
22    #[allow(clippy::should_implement_trait)]
23    pub fn from_str(s: &str) -> Result<Self> {
24        match s.to_lowercase().as_str() {
25            "text" | "txt" => Ok(Self::Text),
26            "json" => Ok(Self::Json),
27            "html" => Ok(Self::Html),
28            "markdown" | "md" => Ok(Self::Markdown),
29            _ => Err(CleanroomError::validation_error(format!(
30                "Unknown report format: {}. Valid formats: text, json, html, markdown",
31                s
32            ))),
33        }
34    }
35}
36
37/// Coverage report generator
38pub struct ReportGenerator;
39
40impl ReportGenerator {
41    /// Generate report in specified format
42    pub fn generate(report: &BehaviorCoverageReport, format: ReportFormat) -> Result<String> {
43        match format {
44            ReportFormat::Text => Ok(report.format_text()),
45            ReportFormat::Json => Self::generate_json(report),
46            ReportFormat::Html => Self::generate_html(report),
47            ReportFormat::Markdown => Self::generate_markdown(report),
48        }
49    }
50
51    /// Generate JSON report
52    fn generate_json(report: &BehaviorCoverageReport) -> Result<String> {
53        serde_json::to_string_pretty(report).map_err(|e| {
54            CleanroomError::validation_error(format!("Failed to serialize report to JSON: {}", e))
55        })
56    }
57
58    /// Generate HTML report
59    fn generate_html(report: &BehaviorCoverageReport) -> Result<String> {
60        let mut html = String::new();
61
62        html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
63        html.push_str("  <meta charset=\"UTF-8\">\n");
64        html.push_str(
65            "  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n",
66        );
67        html.push_str("  <title>Behavior Coverage Report</title>\n");
68        html.push_str("  <style>\n");
69        html.push_str("    body { font-family: Arial, sans-serif; margin: 20px; }\n");
70        html.push_str("    h1 { color: #333; }\n");
71        html.push_str("    .summary { background: #f5f5f5; padding: 20px; border-radius: 8px; margin: 20px 0; }\n");
72        html.push_str("    .grade { font-size: 48px; font-weight: bold; }\n");
73        html.push_str("    .grade-a { color: #4caf50; }\n");
74        html.push_str("    .grade-b { color: #8bc34a; }\n");
75        html.push_str("    .grade-c { color: #ffc107; }\n");
76        html.push_str("    .grade-d { color: #ff9800; }\n");
77        html.push_str("    .grade-f { color: #f44336; }\n");
78        html.push_str("    table { width: 100%; border-collapse: collapse; margin: 20px 0; }\n");
79        html.push_str(
80            "    th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }\n",
81        );
82        html.push_str("    th { background-color: #4caf50; color: white; }\n");
83        html.push_str("    .progress-bar { width: 100%; height: 20px; background: #f0f0f0; border-radius: 10px; overflow: hidden; }\n");
84        html.push_str(
85            "    .progress-fill { height: 100%; background: #4caf50; transition: width 0.3s; }\n",
86        );
87        html.push_str("    .uncovered { background: #fff3cd; padding: 10px; margin: 10px 0; border-left: 4px solid #ffc107; }\n");
88        html.push_str("  </style>\n");
89        html.push_str("</head>\n<body>\n");
90
91        // Title and summary
92        html.push_str("  <h1>Behavior Coverage Report</h1>\n");
93        html.push_str("  <div class=\"summary\">\n");
94        html.push_str(&format!(
95            "    <div class=\"grade grade-{}\">{} {}</div>\n",
96            report.grade().to_lowercase(),
97            report.emoji(),
98            report.grade()
99        ));
100        html.push_str(&format!(
101            "    <p><strong>Overall Coverage:</strong> {:.1}%</p>\n",
102            report.total_coverage
103        ));
104        html.push_str(&format!(
105            "    <p><strong>Behaviors Covered:</strong> {} / {} ({:.1}%)</p>\n",
106            report.covered_behaviors,
107            report.total_behaviors,
108            (report.covered_behaviors as f64 / report.total_behaviors.max(1) as f64) * 100.0
109        ));
110        html.push_str("    <div class=\"progress-bar\">\n");
111        html.push_str(&format!(
112            "      <div class=\"progress-fill\" style=\"width: {:.1}%\"></div>\n",
113            report.total_coverage
114        ));
115        html.push_str("    </div>\n");
116        html.push_str("  </div>\n");
117
118        // Dimensions table
119        html.push_str("  <h2>Dimension Breakdown</h2>\n");
120        html.push_str("  <table>\n");
121        html.push_str("    <tr><th>Dimension</th><th>Coverage</th><th>Weight</th><th>Score</th><th>Covered/Total</th></tr>\n");
122        for dim in &report.dimensions {
123            html.push_str(&format!(
124                "    <tr><td>{}</td><td>{:.1}%</td><td>{:.0}%</td><td>{:.2}%</td><td>{}/{}</td></tr>\n",
125                dim.name,
126                dim.coverage * 100.0,
127                dim.weight * 100.0,
128                dim.weighted_score * 100.0,
129                dim.covered,
130                dim.total
131            ));
132        }
133        html.push_str("  </table>\n");
134
135        // Uncovered behaviors
136        if !report.uncovered_behaviors.is_empty() {
137            html.push_str("  <h2>Uncovered Behaviors</h2>\n");
138            for behavior in report.uncovered_behaviors.top_priority(10) {
139                html.push_str(&format!(
140                    "  <div class=\"uncovered\"><strong>{}:</strong> {}</div>\n",
141                    behavior.dimension, behavior.name
142                ));
143            }
144        }
145
146        html.push_str("</body>\n</html>");
147
148        Ok(html)
149    }
150
151    /// Generate Markdown report
152    fn generate_markdown(report: &BehaviorCoverageReport) -> Result<String> {
153        let mut md = String::new();
154
155        md.push_str("# Behavior Coverage Report\n\n");
156        md.push_str(&format!(
157            "**Overall Coverage:** {:.1}% {} (Grade: {})\n\n",
158            report.total_coverage,
159            report.emoji(),
160            report.grade()
161        ));
162
163        md.push_str(&format!(
164            "**Behaviors Covered:** {} / {}\n\n",
165            report.covered_behaviors, report.total_behaviors
166        ));
167
168        // Dimensions table
169        md.push_str("## Dimension Breakdown\n\n");
170        md.push_str("| Dimension | Coverage | Weight | Score | Covered/Total |\n");
171        md.push_str("|-----------|----------|--------|-------|---------------|\n");
172        for dim in &report.dimensions {
173            md.push_str(&format!(
174                "| {} | {:.1}% | {:.0}% | {:.2}% | {}/{} |\n",
175                dim.name,
176                dim.coverage * 100.0,
177                dim.weight * 100.0,
178                dim.weighted_score * 100.0,
179                dim.covered,
180                dim.total
181            ));
182        }
183        md.push('\n');
184
185        // Uncovered behaviors
186        if !report.uncovered_behaviors.is_empty() {
187            md.push_str("## Uncovered Behaviors\n\n");
188            for (i, behavior) in report
189                .uncovered_behaviors
190                .top_priority(10)
191                .iter()
192                .enumerate()
193            {
194                md.push_str(&format!(
195                    "{}. **{}**: {}\n",
196                    i + 1,
197                    behavior.dimension,
198                    behavior.name
199                ));
200            }
201        }
202
203        Ok(md)
204    }
205
206    /// Save report to file
207    pub fn save(
208        report: &BehaviorCoverageReport,
209        path: impl AsRef<Path>,
210        format: ReportFormat,
211    ) -> Result<()> {
212        let content = Self::generate(report, format)?;
213        std::fs::write(path.as_ref(), content).map_err(|e| {
214            CleanroomError::io_error(format!(
215                "Failed to write report to {}: {}",
216                path.as_ref().display(),
217                e
218            ))
219        })
220    }
221}