Skip to main content

mockforge_bench/conformance/
report.rs

1//! Conformance test report parsing and display
2
3use super::spec::ConformanceFeature;
4use crate::error::{BenchError, Result};
5use colored::*;
6use std::collections::HashMap;
7use std::path::Path;
8
9/// Per-category conformance result
10#[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
30/// Conformance test report
31pub struct ConformanceReport {
32    /// Per-check results: check_name -> (passes, fails)
33    check_results: HashMap<String, (u64, u64)>,
34}
35
36impl ConformanceReport {
37    /// Parse a conformance report from k6's handleSummary JSON output
38    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    /// Parse from JSON string
45    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    /// Get results grouped by category
63    pub fn by_category(&self) -> HashMap<&'static str, CategoryResult> {
64        let mut categories: HashMap<&'static str, CategoryResult> = HashMap::new();
65
66        // Initialize all categories
67        for cat in ConformanceFeature::categories() {
68            categories.insert(cat, CategoryResult::default());
69        }
70
71        // Map check results to features
72        for feature in ConformanceFeature::all() {
73            let check_name = feature.check_name();
74            let category = feature.category();
75
76            let entry = categories.entry(category).or_default();
77
78            if let Some((passes, fails)) = self.check_results.get(check_name) {
79                if *fails == 0 && *passes > 0 {
80                    entry.passed += 1;
81                } else {
82                    entry.failed += 1;
83                }
84            }
85            // Features not in results are not counted (not tested)
86        }
87
88        categories
89    }
90
91    /// Print the conformance report to stdout
92    pub fn print_report(&self) {
93        self.print_report_with_options(false);
94    }
95
96    /// Print the conformance report with options controlling detail level
97    pub fn print_report_with_options(&self, all_operations: bool) {
98        let categories = self.by_category();
99
100        println!("\n{}", "OpenAPI 3.0.0 Conformance Report".bold());
101        println!("{}", "=".repeat(64).bright_green());
102
103        println!(
104            "{:<20} {:>8} {:>8} {:>8} {:>8}",
105            "Category".bold(),
106            "Passed".green().bold(),
107            "Failed".red().bold(),
108            "Total".bold(),
109            "Rate".bold()
110        );
111        println!("{}", "-".repeat(64));
112
113        let mut total_passed = 0usize;
114        let mut total_failed = 0usize;
115
116        for cat_name in ConformanceFeature::categories() {
117            if let Some(result) = categories.get(cat_name) {
118                let total = result.total();
119                if total == 0 {
120                    continue;
121                }
122                total_passed += result.passed;
123                total_failed += result.failed;
124
125                let rate_str = format!("{:.0}%", result.rate());
126                let rate_colored = if result.rate() >= 100.0 {
127                    rate_str.green()
128                } else if result.rate() >= 80.0 {
129                    rate_str.yellow()
130                } else {
131                    rate_str.red()
132                };
133
134                println!(
135                    "{:<20} {:>8} {:>8} {:>8} {:>8}",
136                    cat_name,
137                    result.passed.to_string().green(),
138                    result.failed.to_string().red(),
139                    total,
140                    rate_colored
141                );
142            }
143        }
144
145        println!("{}", "=".repeat(64).bright_green());
146
147        let grand_total = total_passed + total_failed;
148        let overall_rate = if grand_total > 0 {
149            (total_passed as f64 / grand_total as f64) * 100.0
150        } else {
151            0.0
152        };
153        let rate_str = format!("{:.0}%", overall_rate);
154        let rate_colored = if overall_rate >= 100.0 {
155            rate_str.green()
156        } else if overall_rate >= 80.0 {
157            rate_str.yellow()
158        } else {
159            rate_str.red()
160        };
161
162        println!(
163            "{:<20} {:>8} {:>8} {:>8} {:>8}",
164            "Total:".bold(),
165            total_passed.to_string().green(),
166            total_failed.to_string().red(),
167            grand_total,
168            rate_colored
169        );
170
171        // Print failed checks detail section
172        let failed_checks: Vec<_> =
173            self.check_results.iter().filter(|(_, (_, fails))| *fails > 0).collect();
174
175        if !failed_checks.is_empty() {
176            println!();
177            println!("{}", "Failed Checks:".red().bold());
178            let mut sorted_failures: Vec<_> = failed_checks.into_iter().collect();
179            sorted_failures.sort_by_key(|(name, _)| (*name).clone());
180            for (name, (passes, fails)) in sorted_failures {
181                println!(
182                    "  {} ({} passed, {} failed)",
183                    name.red(),
184                    passes.to_string().green(),
185                    fails.to_string().red()
186                );
187            }
188
189            if !all_operations {
190                println!();
191                println!(
192                    "{}",
193                    "Tip: Use --conformance-all-operations to see which specific endpoints failed."
194                        .yellow()
195                );
196            }
197        }
198
199        println!();
200    }
201
202    /// Get raw per-check results (for SARIF conversion)
203    pub fn raw_check_results(&self) -> &HashMap<String, (u64, u64)> {
204        &self.check_results
205    }
206
207    /// Overall pass rate (0.0 - 100.0)
208    pub fn overall_rate(&self) -> f64 {
209        let categories = self.by_category();
210        let total_passed: usize = categories.values().map(|r| r.passed).sum();
211        let total: usize = categories.values().map(|r| r.total()).sum();
212        if total == 0 {
213            0.0
214        } else {
215            (total_passed as f64 / total as f64) * 100.0
216        }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_parse_conformance_report() {
226        let json = r#"{
227            "checks": {
228                "param:path:string": { "passes": 1, "fails": 0 },
229                "param:path:integer": { "passes": 1, "fails": 0 },
230                "body:json": { "passes": 0, "fails": 1 },
231                "method:GET": { "passes": 1, "fails": 0 }
232            },
233            "overall": { "overall_pass_rate": 0.75 }
234        }"#;
235
236        let report = ConformanceReport::from_json(json).unwrap();
237        let categories = report.by_category();
238
239        let params = categories.get("Parameters").unwrap();
240        assert_eq!(params.passed, 2);
241
242        let bodies = categories.get("Request Bodies").unwrap();
243        assert_eq!(bodies.failed, 1);
244    }
245
246    #[test]
247    fn test_empty_report() {
248        let json = r#"{ "checks": {} }"#;
249        let report = ConformanceReport::from_json(json).unwrap();
250        assert_eq!(report.overall_rate(), 0.0);
251    }
252}