1use crate::evidence::HarnessResult;
9
10pub fn parse_junit_xml(xml: &str) -> Result<Vec<HarnessResult>, String> {
16 let mut results = Vec::new();
17
18 let mut search_pos = 0;
20 while let Some(start) = xml[search_pos..].find("<testsuite ") {
21 let abs_start = search_pos + start;
22 let tag_end = xml[abs_start..]
24 .find('>')
25 .ok_or_else(|| "Malformed XML: unclosed <testsuite> tag".to_string())?;
26 let tag = &xml[abs_start..abs_start + tag_end + 1];
27
28 let name = extract_attr(tag, "name").unwrap_or_default();
29 let tests: u32 = extract_attr(tag, "tests")
30 .and_then(|s| s.parse().ok())
31 .unwrap_or(0);
32 let failures: u32 = extract_attr(tag, "failures")
33 .and_then(|s| s.parse().ok())
34 .unwrap_or(0);
35 let errors: u32 = extract_attr(tag, "errors")
36 .and_then(|s| s.parse().ok())
37 .unwrap_or(0);
38 let skipped: u32 = extract_attr(tag, "skipped")
39 .and_then(|s| s.parse().ok())
40 .unwrap_or(0);
41 let time: Option<f64> = extract_attr(tag, "time").and_then(|s| s.parse().ok());
42
43 let failed_count = failures + errors;
44 let passed_count = tests.saturating_sub(failed_count + skipped);
45 let passed = failed_count == 0;
46
47 results.push(HarnessResult {
48 name: if name.is_empty() {
49 format!("testsuite-{}", results.len())
50 } else {
51 name
52 },
53 passed,
54 total: tests,
55 passed_count,
56 failed_count,
57 skipped_count: skipped,
58 duration_secs: time,
59 source_format: Some("junit-xml".to_string()),
60 });
61
62 search_pos = abs_start + tag_end + 1;
63 }
64
65 if results.is_empty() {
66 return Err("No <testsuite> elements found in XML".to_string());
67 }
68
69 Ok(results)
70}
71
72fn extract_attr(tag: &str, attr: &str) -> Option<String> {
75 let pattern = format!("{attr}=\"");
76 let start = tag.find(&pattern)?;
77 let value_start = start + pattern.len();
78 let value_end = tag[value_start..].find('"')?;
79 Some(tag[value_start..value_start + value_end].to_string())
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn parse_single_testsuite() {
88 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
89<testsuites>
90 <testsuite name="unit-tests" tests="42" failures="0" errors="0" skipped="2" time="1.234">
91 </testsuite>
92</testsuites>"#;
93 let results = parse_junit_xml(xml).unwrap();
94 assert_eq!(results.len(), 1);
95 assert_eq!(results[0].name, "unit-tests");
96 assert!(results[0].passed);
97 assert_eq!(results[0].total, 42);
98 assert_eq!(results[0].passed_count, 40);
99 assert_eq!(results[0].failed_count, 0);
100 assert_eq!(results[0].skipped_count, 2);
101 assert!((results[0].duration_secs.unwrap() - 1.234).abs() < 0.001);
102 assert_eq!(results[0].source_format.as_deref(), Some("junit-xml"));
103 }
104
105 #[test]
106 fn parse_multiple_testsuites() {
107 let xml = r#"<testsuites>
108 <testsuite name="unit" tests="10" failures="0" errors="0" time="0.5">
109 </testsuite>
110 <testsuite name="integration" tests="5" failures="2" errors="1" time="3.0">
111 </testsuite>
112</testsuites>"#;
113 let results = parse_junit_xml(xml).unwrap();
114 assert_eq!(results.len(), 2);
115 assert!(results[0].passed);
116 assert!(!results[1].passed);
117 assert_eq!(results[1].failed_count, 3); assert_eq!(results[1].passed_count, 2); }
120
121 #[test]
122 fn parse_with_failures() {
123 let xml = r#"<testsuite name="lint" tests="100" failures="3" errors="0" skipped="0">"#;
124 let results = parse_junit_xml(xml).unwrap();
125 assert_eq!(results.len(), 1);
126 assert!(!results[0].passed);
127 assert_eq!(results[0].failed_count, 3);
128 assert_eq!(results[0].passed_count, 97);
129 }
130
131 #[test]
132 fn parse_unwrapped_testsuite() {
133 let xml = r#"<testsuite name="mytest" tests="5" failures="0" errors="0" />"#;
134 let results = parse_junit_xml(xml).unwrap();
135 assert_eq!(results.len(), 1);
136 assert!(results[0].passed);
137 }
138
139 #[test]
140 fn error_on_empty_xml() {
141 let result = parse_junit_xml("<testsuites></testsuites>");
142 assert!(result.is_err());
143 assert!(result.unwrap_err().contains("No <testsuite> elements"));
144 }
145
146 #[test]
147 fn error_on_no_testsuite() {
148 let result = parse_junit_xml("not xml at all");
149 assert!(result.is_err());
150 }
151
152 #[test]
153 fn unnamed_testsuite_gets_default_name() {
154 let xml = r#"<testsuite tests="1" failures="0" errors="0" />"#;
155 let results = parse_junit_xml(xml).unwrap();
156 assert_eq!(results[0].name, "testsuite-0");
157 }
158
159 #[test]
160 fn missing_optional_attrs_default_to_zero() {
161 let xml = r#"<testsuite name="minimal" tests="5" failures="0" errors="0" />"#;
162 let results = parse_junit_xml(xml).unwrap();
163 assert_eq!(results[0].skipped_count, 0);
164 assert!(results[0].duration_secs.is_none());
165 }
166}