clnrm_core/reporting/
json.rs

1//! JSON report format
2//!
3//! Generates structured JSON reports for test results with pass/fail details.
4
5use crate::error::{CleanroomError, Result};
6use crate::validation::ValidationReport;
7use serde::Serialize;
8use std::path::Path;
9
10/// JSON report structure
11#[derive(Debug, Serialize)]
12pub struct JsonReport {
13    /// Overall test success status
14    pub passed: bool,
15    /// Total number of passing validations
16    pub total_passes: usize,
17    /// Total number of failing validations
18    pub total_failures: usize,
19    /// List of validation names that passed
20    pub passes: Vec<String>,
21    /// List of failures with details
22    pub failures: Vec<FailureDetail>,
23}
24
25/// Detailed failure information
26#[derive(Debug, Serialize)]
27pub struct FailureDetail {
28    /// Name of the failing validation
29    pub name: String,
30    /// Error message describing the failure
31    pub error: String,
32}
33
34/// JSON report generator
35pub struct JsonReporter;
36
37impl JsonReporter {
38    /// Write JSON report to file
39    ///
40    /// # Arguments
41    /// * `path` - File path for JSON output
42    /// * `report` - Validation report to convert
43    ///
44    /// # Returns
45    /// * `Result<()>` - Success or error
46    ///
47    /// # Errors
48    /// Returns error if:
49    /// - JSON serialization fails
50    /// - File write fails
51    pub fn write(path: &Path, report: &ValidationReport) -> Result<()> {
52        let json_report = Self::convert_report(report);
53        let json_str = Self::serialize(&json_report)?;
54        Self::write_file(path, &json_str)
55    }
56
57    /// Convert ValidationReport to JsonReport
58    fn convert_report(report: &ValidationReport) -> JsonReport {
59        JsonReport {
60            passed: report.is_success(),
61            total_passes: report.passes().len(),
62            total_failures: report.failures().len(),
63            passes: report.passes().to_vec(),
64            failures: report
65                .failures()
66                .iter()
67                .map(|(name, error)| FailureDetail {
68                    name: name.clone(),
69                    error: error.clone(),
70                })
71                .collect(),
72        }
73    }
74
75    /// Serialize JsonReport to pretty-printed JSON string
76    fn serialize(json_report: &JsonReport) -> Result<String> {
77        serde_json::to_string_pretty(json_report).map_err(|e| {
78            CleanroomError::serialization_error(format!("JSON serialization failed: {}", e))
79        })
80    }
81
82    /// Write JSON string to file
83    fn write_file(path: &Path, content: &str) -> Result<()> {
84        std::fs::write(path, content).map_err(|e| {
85            CleanroomError::report_error(format!("Failed to write JSON report: {}", e))
86        })
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::validation::ValidationReport;
94    use tempfile::TempDir;
95
96    #[test]
97    fn test_json_reporter_all_pass() -> Result<()> {
98        // Arrange
99        let temp_dir = TempDir::new()
100            .map_err(|e| CleanroomError::io_error(format!("Failed to create temp dir: {}", e)))?;
101        let json_path = temp_dir.path().join("report.json");
102
103        let mut report = ValidationReport::new();
104        report.add_pass("test1");
105        report.add_pass("test2");
106
107        // Act
108        JsonReporter::write(&json_path, &report)?;
109
110        // Assert
111        let content = std::fs::read_to_string(&json_path)
112            .map_err(|e| CleanroomError::io_error(format!("Failed to read file: {}", e)))?;
113
114        assert!(content.contains(r#""passed": true"#));
115        assert!(content.contains(r#""total_passes": 2"#));
116        assert!(content.contains(r#""total_failures": 0"#));
117        assert!(content.contains(r#""test1""#));
118        assert!(content.contains(r#""test2""#));
119
120        Ok(())
121    }
122
123    #[test]
124    fn test_json_reporter_with_failures() -> Result<()> {
125        // Arrange
126        let temp_dir = TempDir::new()
127            .map_err(|e| CleanroomError::io_error(format!("Failed to create temp dir: {}", e)))?;
128        let json_path = temp_dir.path().join("report.json");
129
130        let mut report = ValidationReport::new();
131        report.add_pass("test1");
132        report.add_fail("test2", "Expected 2 but got 1".to_string());
133        report.add_fail("test3", "Missing span".to_string());
134
135        // Act
136        JsonReporter::write(&json_path, &report)?;
137
138        // Assert
139        let content = std::fs::read_to_string(&json_path)
140            .map_err(|e| CleanroomError::io_error(format!("Failed to read file: {}", e)))?;
141
142        assert!(content.contains(r#""passed": false"#));
143        assert!(content.contains(r#""total_passes": 1"#));
144        assert!(content.contains(r#""total_failures": 2"#));
145        assert!(content.contains(r#""test2""#));
146        assert!(content.contains(r#""Expected 2 but got 1""#));
147        assert!(content.contains(r#""test3""#));
148        assert!(content.contains(r#""Missing span""#));
149
150        Ok(())
151    }
152
153    #[test]
154    fn test_json_reporter_empty_report() -> Result<()> {
155        // Arrange
156        let temp_dir = TempDir::new()
157            .map_err(|e| CleanroomError::io_error(format!("Failed to create temp dir: {}", e)))?;
158        let json_path = temp_dir.path().join("report.json");
159
160        let report = ValidationReport::new();
161
162        // Act
163        JsonReporter::write(&json_path, &report)?;
164
165        // Assert
166        let content = std::fs::read_to_string(&json_path)
167            .map_err(|e| CleanroomError::io_error(format!("Failed to read file: {}", e)))?;
168
169        assert!(content.contains(r#""passed": true"#));
170        assert!(content.contains(r#""total_passes": 0"#));
171        assert!(content.contains(r#""total_failures": 0"#));
172
173        Ok(())
174    }
175
176    #[test]
177    fn test_json_reporter_special_characters() -> Result<()> {
178        // Arrange
179        let temp_dir = TempDir::new()
180            .map_err(|e| CleanroomError::io_error(format!("Failed to create temp dir: {}", e)))?;
181        let json_path = temp_dir.path().join("report.json");
182
183        let mut report = ValidationReport::new();
184        report.add_fail(
185            "test_with_quotes",
186            r#"Error: "Expected value" not found"#.to_string(),
187        );
188        report.add_fail("test_with_newlines", "Error:\nLine 1\nLine 2".to_string());
189
190        // Act
191        JsonReporter::write(&json_path, &report)?;
192
193        // Assert
194        let content = std::fs::read_to_string(&json_path)
195            .map_err(|e| CleanroomError::io_error(format!("Failed to read file: {}", e)))?;
196
197        // Verify JSON is valid by parsing it
198        let parsed: serde_json::Value = serde_json::from_str(&content).map_err(|e| {
199            CleanroomError::serialization_error(format!("Failed to parse JSON: {}", e))
200        })?;
201
202        assert!(parsed.is_object());
203
204        Ok(())
205    }
206}