mockforge_bench/conformance/
report.rs1use super::spec::ConformanceFeature;
4use crate::error::{BenchError, Result};
5use colored::*;
6use std::collections::HashMap;
7use std::path::Path;
8
9#[derive(Debug, Clone, Default)]
11pub struct CategoryResult {
12 pub passed: usize,
13 pub failed: usize,
14}
15
16impl CategoryResult {
17 pub fn total(&self) -> usize {
18 self.passed + self.failed
19 }
20
21 pub fn rate(&self) -> f64 {
22 if self.total() == 0 {
23 0.0
24 } else {
25 (self.passed as f64 / self.total() as f64) * 100.0
26 }
27 }
28}
29
30pub struct ConformanceReport {
32 check_results: HashMap<String, (u64, u64)>,
34}
35
36impl ConformanceReport {
37 pub fn from_file(path: &Path) -> Result<Self> {
39 let content = std::fs::read_to_string(path)
40 .map_err(|e| BenchError::Other(format!("Failed to read conformance report: {}", e)))?;
41 Self::from_json(&content)
42 }
43
44 pub fn from_json(json_str: &str) -> Result<Self> {
46 let json: serde_json::Value = serde_json::from_str(json_str)
47 .map_err(|e| BenchError::Other(format!("Failed to parse conformance JSON: {}", e)))?;
48
49 let mut check_results = HashMap::new();
50
51 if let Some(checks) = json.get("checks").and_then(|c| c.as_object()) {
52 for (name, result) in checks {
53 let passes = result.get("passes").and_then(|v| v.as_u64()).unwrap_or(0);
54 let fails = result.get("fails").and_then(|v| v.as_u64()).unwrap_or(0);
55 check_results.insert(name.clone(), (passes, fails));
56 }
57 }
58
59 Ok(Self { check_results })
60 }
61
62 pub fn by_category(&self) -> HashMap<&'static str, CategoryResult> {
64 let mut categories: HashMap<&'static str, CategoryResult> = HashMap::new();
65
66 for cat in ConformanceFeature::categories() {
68 categories.insert(cat, CategoryResult::default());
69 }
70
71 for feature in ConformanceFeature::all() {
76 let check_name = feature.check_name();
77 let category = feature.category();
78
79 let entry = categories.entry(category).or_default();
80
81 if let Some((passes, fails)) = self.check_results.get(check_name) {
83 if *fails == 0 && *passes > 0 {
84 entry.passed += 1;
85 } else {
86 entry.failed += 1;
87 }
88 } else {
89 let prefix = format!("{}:", check_name);
91 for (name, (passes, fails)) in &self.check_results {
92 if name.starts_with(&prefix) {
93 if *fails == 0 && *passes > 0 {
94 entry.passed += 1;
95 } else {
96 entry.failed += 1;
97 }
98 }
99 }
100 }
102 }
103
104 categories
105 }
106
107 pub fn print_report(&self) {
109 self.print_report_with_options(false);
110 }
111
112 pub fn print_report_with_options(&self, all_operations: bool) {
114 let categories = self.by_category();
115
116 println!("\n{}", "OpenAPI 3.0.0 Conformance Report".bold());
117 println!("{}", "=".repeat(64).bright_green());
118
119 println!(
120 "{:<20} {:>8} {:>8} {:>8} {:>8}",
121 "Category".bold(),
122 "Passed".green().bold(),
123 "Failed".red().bold(),
124 "Total".bold(),
125 "Rate".bold()
126 );
127 println!("{}", "-".repeat(64));
128
129 let mut total_passed = 0usize;
130 let mut total_failed = 0usize;
131
132 for cat_name in ConformanceFeature::categories() {
133 if let Some(result) = categories.get(cat_name) {
134 let total = result.total();
135 if total == 0 {
136 continue;
137 }
138 total_passed += result.passed;
139 total_failed += result.failed;
140
141 let rate_str = format!("{:.0}%", result.rate());
142 let rate_colored = if result.rate() >= 100.0 {
143 rate_str.green()
144 } else if result.rate() >= 80.0 {
145 rate_str.yellow()
146 } else {
147 rate_str.red()
148 };
149
150 println!(
151 "{:<20} {:>8} {:>8} {:>8} {:>8}",
152 cat_name,
153 result.passed.to_string().green(),
154 result.failed.to_string().red(),
155 total,
156 rate_colored
157 );
158 }
159 }
160
161 println!("{}", "=".repeat(64).bright_green());
162
163 let grand_total = total_passed + total_failed;
164 let overall_rate = if grand_total > 0 {
165 (total_passed as f64 / grand_total as f64) * 100.0
166 } else {
167 0.0
168 };
169 let rate_str = format!("{:.0}%", overall_rate);
170 let rate_colored = if overall_rate >= 100.0 {
171 rate_str.green()
172 } else if overall_rate >= 80.0 {
173 rate_str.yellow()
174 } else {
175 rate_str.red()
176 };
177
178 println!(
179 "{:<20} {:>8} {:>8} {:>8} {:>8}",
180 "Total:".bold(),
181 total_passed.to_string().green(),
182 total_failed.to_string().red(),
183 grand_total,
184 rate_colored
185 );
186
187 let failed_checks: Vec<_> =
189 self.check_results.iter().filter(|(_, (_, fails))| *fails > 0).collect();
190
191 if !failed_checks.is_empty() {
192 println!();
193 println!("{}", "Failed Checks:".red().bold());
194 let mut sorted_failures: Vec<_> = failed_checks.into_iter().collect();
195 sorted_failures.sort_by_key(|(name, _)| (*name).clone());
196 for (name, (passes, fails)) in sorted_failures {
197 println!(
198 " {} ({} passed, {} failed)",
199 name.red(),
200 passes.to_string().green(),
201 fails.to_string().red()
202 );
203 }
204
205 if !all_operations {
206 println!();
207 println!(
208 "{}",
209 "Tip: Use --conformance-all-operations to see which specific endpoints failed."
210 .yellow()
211 );
212 }
213 }
214
215 println!();
216 }
217
218 pub fn raw_check_results(&self) -> &HashMap<String, (u64, u64)> {
220 &self.check_results
221 }
222
223 pub fn overall_rate(&self) -> f64 {
225 let categories = self.by_category();
226 let total_passed: usize = categories.values().map(|r| r.passed).sum();
227 let total: usize = categories.values().map(|r| r.total()).sum();
228 if total == 0 {
229 0.0
230 } else {
231 (total_passed as f64 / total as f64) * 100.0
232 }
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn test_parse_conformance_report() {
242 let json = r#"{
243 "checks": {
244 "param:path:string": { "passes": 1, "fails": 0 },
245 "param:path:integer": { "passes": 1, "fails": 0 },
246 "body:json": { "passes": 0, "fails": 1 },
247 "method:GET": { "passes": 1, "fails": 0 }
248 },
249 "overall": { "overall_pass_rate": 0.75 }
250 }"#;
251
252 let report = ConformanceReport::from_json(json).unwrap();
253 let categories = report.by_category();
254
255 let params = categories.get("Parameters").unwrap();
256 assert_eq!(params.passed, 2);
257
258 let bodies = categories.get("Request Bodies").unwrap();
259 assert_eq!(bodies.failed, 1);
260 }
261
262 #[test]
263 fn test_empty_report() {
264 let json = r#"{ "checks": {} }"#;
265 let report = ConformanceReport::from_json(json).unwrap();
266 assert_eq!(report.overall_rate(), 0.0);
267 }
268}