clnrm_core/coverage/
report.rs1use crate::coverage::BehaviorCoverageReport;
4use crate::error::{CleanroomError, Result};
5use std::path::Path;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ReportFormat {
10 Text,
12 Json,
14 Html,
16 Markdown,
18}
19
20impl ReportFormat {
21 #[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
37pub struct ReportGenerator;
39
40impl ReportGenerator {
41 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 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 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 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 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 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 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 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 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 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}