clnrm_core/reporting/
junit.rs

1//! JUnit XML report format
2//!
3//! Generates JUnit-compatible XML reports for CI/CD integration.
4
5use crate::error::{CleanroomError, Result};
6use crate::validation::ValidationReport;
7use std::path::Path;
8
9/// JUnit XML report generator
10pub struct JunitReporter;
11
12impl JunitReporter {
13    /// Write JUnit XML report to file
14    ///
15    /// # Arguments
16    /// * `path` - File path for XML output
17    /// * `report` - Validation report to convert
18    ///
19    /// # Returns
20    /// * `Result<()>` - Success or error
21    ///
22    /// # Errors
23    /// Returns error if file write fails
24    pub fn write(path: &Path, report: &ValidationReport) -> Result<()> {
25        let xml = Self::generate_xml(report);
26        Self::write_file(path, &xml)
27    }
28
29    /// Generate complete JUnit XML document
30    fn generate_xml(report: &ValidationReport) -> String {
31        let mut xml = String::new();
32
33        Self::append_xml_header(&mut xml);
34        Self::append_testsuite_open(&mut xml, report);
35        Self::append_passed_tests(&mut xml, report);
36        Self::append_failed_tests(&mut xml, report);
37        Self::append_testsuite_close(&mut xml);
38
39        xml
40    }
41
42    /// Append XML header
43    fn append_xml_header(xml: &mut String) {
44        xml.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
45        xml.push('\n');
46    }
47
48    /// Append testsuite opening tag
49    fn append_testsuite_open(xml: &mut String, report: &ValidationReport) {
50        let total = report.passes().len() + report.failures().len();
51        xml.push_str(&format!(
52            r#"<testsuite name="clnrm" tests="{}" failures="{}" errors="0">"#,
53            total,
54            report.failures().len()
55        ));
56        xml.push('\n');
57    }
58
59    /// Append passed test cases
60    fn append_passed_tests(xml: &mut String, report: &ValidationReport) {
61        for pass_name in report.passes() {
62            xml.push_str(&format!(
63                r#"  <testcase name="{}" />"#,
64                Self::escape_xml(pass_name)
65            ));
66            xml.push('\n');
67        }
68    }
69
70    /// Append failed test cases
71    fn append_failed_tests(xml: &mut String, report: &ValidationReport) {
72        for (fail_name, error) in report.failures() {
73            xml.push_str(&format!(
74                r#"  <testcase name="{}">"#,
75                Self::escape_xml(fail_name)
76            ));
77            xml.push('\n');
78            xml.push_str(&format!(
79                r#"    <failure message="{}" />"#,
80                Self::escape_xml(error)
81            ));
82            xml.push('\n');
83            xml.push_str(r#"  </testcase>"#);
84            xml.push('\n');
85        }
86    }
87
88    /// Append testsuite closing tag
89    fn append_testsuite_close(xml: &mut String) {
90        xml.push_str("</testsuite>\n");
91    }
92
93    /// Escape XML special characters
94    fn escape_xml(s: &str) -> String {
95        s.replace('&', "&amp;")
96            .replace('<', "&lt;")
97            .replace('>', "&gt;")
98            .replace('"', "&quot;")
99            .replace('\'', "&apos;")
100    }
101
102    /// Write XML string to file
103    fn write_file(path: &Path, content: &str) -> Result<()> {
104        std::fs::write(path, content)
105            .map_err(|e| CleanroomError::report_error(format!("Failed to write JUnit XML: {}", e)))
106    }
107}