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        let categories = self.by_category();
94
95        println!("\n{}", "OpenAPI 3.0.0 Conformance Report".bold());
96        println!("{}", "=".repeat(64).bright_green());
97
98        println!(
99            "{:<20} {:>8} {:>8} {:>8} {:>8}",
100            "Category".bold(),
101            "Passed".green().bold(),
102            "Failed".red().bold(),
103            "Total".bold(),
104            "Rate".bold()
105        );
106        println!("{}", "-".repeat(64));
107
108        let mut total_passed = 0usize;
109        let mut total_failed = 0usize;
110
111        for cat_name in ConformanceFeature::categories() {
112            if let Some(result) = categories.get(cat_name) {
113                let total = result.total();
114                if total == 0 {
115                    continue;
116                }
117                total_passed += result.passed;
118                total_failed += result.failed;
119
120                let rate_str = format!("{:.0}%", result.rate());
121                let rate_colored = if result.rate() >= 100.0 {
122                    rate_str.green()
123                } else if result.rate() >= 80.0 {
124                    rate_str.yellow()
125                } else {
126                    rate_str.red()
127                };
128
129                println!(
130                    "{:<20} {:>8} {:>8} {:>8} {:>8}",
131                    cat_name,
132                    result.passed.to_string().green(),
133                    result.failed.to_string().red(),
134                    total,
135                    rate_colored
136                );
137            }
138        }
139
140        println!("{}", "=".repeat(64).bright_green());
141
142        let grand_total = total_passed + total_failed;
143        let overall_rate = if grand_total > 0 {
144            (total_passed as f64 / grand_total as f64) * 100.0
145        } else {
146            0.0
147        };
148        let rate_str = format!("{:.0}%", overall_rate);
149        let rate_colored = if overall_rate >= 100.0 {
150            rate_str.green()
151        } else if overall_rate >= 80.0 {
152            rate_str.yellow()
153        } else {
154            rate_str.red()
155        };
156
157        println!(
158            "{:<20} {:>8} {:>8} {:>8} {:>8}",
159            "Total:".bold(),
160            total_passed.to_string().green(),
161            total_failed.to_string().red(),
162            grand_total,
163            rate_colored
164        );
165        println!();
166    }
167
168    /// Get raw per-check results (for SARIF conversion)
169    pub fn raw_check_results(&self) -> &HashMap<String, (u64, u64)> {
170        &self.check_results
171    }
172
173    /// Overall pass rate (0.0 - 100.0)
174    pub fn overall_rate(&self) -> f64 {
175        let categories = self.by_category();
176        let total_passed: usize = categories.values().map(|r| r.passed).sum();
177        let total: usize = categories.values().map(|r| r.total()).sum();
178        if total == 0 {
179            0.0
180        } else {
181            (total_passed as f64 / total as f64) * 100.0
182        }
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_parse_conformance_report() {
192        let json = r#"{
193            "checks": {
194                "param:path:string": { "passes": 1, "fails": 0 },
195                "param:path:integer": { "passes": 1, "fails": 0 },
196                "body:json": { "passes": 0, "fails": 1 },
197                "method:GET": { "passes": 1, "fails": 0 }
198            },
199            "overall": { "overall_pass_rate": 0.75 }
200        }"#;
201
202        let report = ConformanceReport::from_json(json).unwrap();
203        let categories = report.by_category();
204
205        let params = categories.get("Parameters").unwrap();
206        assert_eq!(params.passed, 2);
207
208        let bodies = categories.get("Request Bodies").unwrap();
209        assert_eq!(bodies.failed, 1);
210    }
211
212    #[test]
213    fn test_empty_report() {
214        let json = r#"{ "checks": {} }"#;
215        let report = ConformanceReport::from_json(json).unwrap();
216        assert_eq!(report.overall_rate(), 0.0);
217    }
218}