clnrm_core/reporting/
junit.rs1use crate::error::{CleanroomError, Result};
6use crate::validation::ValidationReport;
7use std::path::Path;
8
9pub struct JunitReporter;
11
12impl JunitReporter {
13 pub fn write(path: &Path, report: &ValidationReport) -> Result<()> {
25 let xml = Self::generate_xml(report);
26 Self::write_file(path, &xml)
27 }
28
29 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 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 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 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 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 fn append_testsuite_close(xml: &mut String) {
90 xml.push_str("</testsuite>\n");
91 }
92
93 fn escape_xml(s: &str) -> String {
95 s.replace('&', "&")
96 .replace('<', "<")
97 .replace('>', ">")
98 .replace('"', """)
99 .replace('\'', "'")
100 }
101
102 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}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::validation::ValidationReport;
113 use tempfile::TempDir;
114
115 #[test]
116 fn test_junit_reporter_all_pass() -> Result<()> {
117 let temp_dir = TempDir::new()
119 .map_err(|e| CleanroomError::io_error(format!("Failed to create temp dir: {}", e)))?;
120 let xml_path = temp_dir.path().join("junit.xml");
121
122 let mut report = ValidationReport::new();
123 report.add_pass("test1");
124 report.add_pass("test2");
125
126 JunitReporter::write(&xml_path, &report)?;
128
129 let content = std::fs::read_to_string(&xml_path)
131 .map_err(|e| CleanroomError::io_error(format!("Failed to read file: {}", e)))?;
132
133 assert!(content.contains(r#"<?xml version="1.0" encoding="UTF-8"?>"#));
134 assert!(content.contains(r#"<testsuite name="clnrm" tests="2" failures="0""#));
135 assert!(content.contains(r#"<testcase name="test1" />"#));
136 assert!(content.contains(r#"<testcase name="test2" />"#));
137 assert!(content.contains(r#"</testsuite>"#));
138
139 Ok(())
140 }
141
142 #[test]
143 fn test_junit_reporter_with_failures() -> Result<()> {
144 let temp_dir = TempDir::new()
146 .map_err(|e| CleanroomError::io_error(format!("Failed to create temp dir: {}", e)))?;
147 let xml_path = temp_dir.path().join("junit.xml");
148
149 let mut report = ValidationReport::new();
150 report.add_pass("test1");
151 report.add_fail("test2", "Expected 2 but got 1".to_string());
152 report.add_fail("test3", "Missing span".to_string());
153
154 JunitReporter::write(&xml_path, &report)?;
156
157 let content = std::fs::read_to_string(&xml_path)
159 .map_err(|e| CleanroomError::io_error(format!("Failed to read file: {}", e)))?;
160
161 assert!(content.contains(r#"tests="3" failures="2""#));
162 assert!(content.contains(r#"<testcase name="test1" />"#));
163 assert!(content.contains(r#"<testcase name="test2">"#));
164 assert!(content.contains(r#"<failure message="Expected 2 but got 1" />"#));
165 assert!(content.contains(r#"<testcase name="test3">"#));
166 assert!(content.contains(r#"<failure message="Missing span" />"#));
167
168 Ok(())
169 }
170
171 #[test]
172 fn test_junit_reporter_empty_report() -> Result<()> {
173 let temp_dir = TempDir::new()
175 .map_err(|e| CleanroomError::io_error(format!("Failed to create temp dir: {}", e)))?;
176 let xml_path = temp_dir.path().join("junit.xml");
177
178 let report = ValidationReport::new();
179
180 JunitReporter::write(&xml_path, &report)?;
182
183 let content = std::fs::read_to_string(&xml_path)
185 .map_err(|e| CleanroomError::io_error(format!("Failed to read file: {}", e)))?;
186
187 assert!(content.contains(r#"tests="0" failures="0""#));
188
189 Ok(())
190 }
191
192 #[test]
193 fn test_junit_reporter_xml_escaping() -> Result<()> {
194 let temp_dir = TempDir::new()
196 .map_err(|e| CleanroomError::io_error(format!("Failed to create temp dir: {}", e)))?;
197 let xml_path = temp_dir.path().join("junit.xml");
198
199 let mut report = ValidationReport::new();
200 report.add_fail(
201 "test_with_<>",
202 r#"Error: "Value" & 'Expected' < 10 > 5"#.to_string(),
203 );
204
205 JunitReporter::write(&xml_path, &report)?;
207
208 let content = std::fs::read_to_string(&xml_path)
210 .map_err(|e| CleanroomError::io_error(format!("Failed to read file: {}", e)))?;
211
212 assert!(content.contains("<"));
213 assert!(content.contains(">"));
214 assert!(content.contains("&"));
215 assert!(content.contains("""));
216 assert!(content.contains("'"));
217 assert!(!content.contains("Error: \"Value\""));
219
220 Ok(())
221 }
222
223 #[test]
224 fn test_escape_xml_all_special_chars() {
225 let input = r#"<test>"value"&'data'"#;
227
228 let escaped = JunitReporter::escape_xml(input);
230
231 assert_eq!(
233 escaped,
234 "<test>"value"&'data'"
235 );
236 }
237
238 #[test]
239 fn test_escape_xml_no_special_chars() {
240 let input = "test_value_123";
242
243 let escaped = JunitReporter::escape_xml(input);
245
246 assert_eq!(escaped, "test_value_123");
248 }
249}