git_perf/parsers/
junit_xml.rs1use crate::parsers::types::{ParsedMeasurement, Parser, TestMeasurement, TestStatus};
2use anyhow::Result;
3use quick_xml::de::from_str;
4use serde::Deserialize;
5use std::collections::HashMap;
6use std::time::Duration;
7
8pub struct JunitXmlParser;
10
11impl Parser for JunitXmlParser {
12 fn parse(&self, input: &str) -> Result<Vec<ParsedMeasurement>> {
13 let trimmed = input.trim();
15 let is_testsuites = trimmed.contains("<testsuites");
16
17 if is_testsuites {
18 if let Ok(testsuites) = from_str::<TestSuites>(input) {
20 return Ok(testsuites.into_measurements());
21 }
22 } else {
23 if let Ok(testsuite) = from_str::<TestSuite>(input) {
25 return Ok(testsuite.into_measurements());
26 }
27 }
28
29 anyhow::bail!("Failed to parse JUnit XML: input is neither <testsuites> nor <testsuite>")
30 }
31}
32
33#[derive(Debug, Deserialize)]
35struct TestSuites {
36 #[serde(rename = "$value", default)]
37 testsuite: Vec<TestSuite>,
38}
39
40impl TestSuites {
41 fn into_measurements(self) -> Vec<ParsedMeasurement> {
42 self.testsuite
43 .into_iter()
44 .flat_map(|suite| suite.into_measurements())
45 .collect()
46 }
47}
48
49#[derive(Debug, Deserialize)]
51struct TestSuite {
52 #[serde(rename = "@name", default)]
53 name: String,
54 #[serde(rename = "$value", default)]
55 testcase: Vec<TestCase>,
56}
57
58impl TestSuite {
59 fn into_measurements(self) -> Vec<ParsedMeasurement> {
60 self.testcase
61 .into_iter()
62 .map(|tc| tc.into_measurement(&self.name))
63 .collect()
64 }
65}
66
67#[derive(Debug, Deserialize)]
69struct TestCase {
70 #[serde(rename = "@name")]
71 name: String,
72 #[serde(rename = "@classname", default)]
73 classname: String,
74 #[serde(rename = "@time")]
75 time: Option<f64>,
76 failure: Option<Failure>,
77 error: Option<Error>,
78 skipped: Option<Skipped>,
79}
80
81#[derive(Debug, Deserialize)]
82#[allow(dead_code)]
83struct Failure {
84 #[serde(default)]
85 message: String,
86 #[serde(rename = "type", default)]
87 failure_type: String,
88}
89
90#[derive(Debug, Deserialize)]
91#[allow(dead_code)]
92struct Error {
93 #[serde(default)]
94 message: String,
95 #[serde(rename = "type", default)]
96 error_type: String,
97}
98
99#[derive(Debug, Deserialize)]
100struct Skipped {}
101
102impl TestCase {
103 fn into_measurement(self, suite_name: &str) -> ParsedMeasurement {
104 let status = if self.skipped.is_some() {
105 TestStatus::Skipped
106 } else if self.error.is_some() {
107 TestStatus::Error
108 } else if self.failure.is_some() {
109 TestStatus::Failed
110 } else {
111 TestStatus::Passed
112 };
113
114 let duration = self.time.map(Duration::from_secs_f64);
115
116 let mut metadata = HashMap::new();
117 metadata.insert("type".to_string(), "test".to_string());
118 if !self.classname.is_empty() {
119 metadata.insert("classname".to_string(), self.classname);
120 }
121 if !suite_name.is_empty() {
122 metadata.insert("suite".to_string(), suite_name.to_string());
123 }
124 metadata.insert("status".to_string(), status.as_str().to_string());
125
126 ParsedMeasurement::Test(TestMeasurement {
127 name: self.name,
128 duration,
129 status,
130 metadata,
131 })
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn test_parse_single_testsuite() {
141 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
142<testsuite name="my_tests" tests="2" failures="0" errors="0" skipped="0" time="3.5">
143 <testcase name="test_one" classname="module::tests" time="1.5"/>
144 <testcase name="test_two" classname="module::tests" time="2.0"/>
145</testsuite>"#;
146
147 let parser = JunitXmlParser;
148 let result = parser.parse(xml).unwrap();
149
150 assert_eq!(result.len(), 2);
151
152 if let ParsedMeasurement::Test(test) = &result[0] {
153 assert_eq!(test.name, "test_one");
154 assert_eq!(test.duration, Some(Duration::from_secs_f64(1.5)));
155 assert_eq!(test.status, TestStatus::Passed);
156 assert_eq!(test.metadata.get("classname").unwrap(), "module::tests");
157 assert_eq!(test.metadata.get("status").unwrap(), "passed");
158 } else {
159 panic!("Expected Test measurement");
160 }
161 }
162
163 #[test]
164 fn test_parse_testsuites() {
165 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
166<testsuites tests="3" failures="1" errors="0" skipped="1" time="5.2">
167 <testsuite name="suite_one" tests="2" failures="0" time="3.5">
168 <testcase name="test_one" classname="module::tests" time="1.5"/>
169 <testcase name="test_two" classname="module::tests" time="2.0"/>
170 </testsuite>
171 <testsuite name="suite_two" tests="1" failures="1" time="1.7">
172 <testcase name="test_three" classname="other::tests" time="1.7">
173 <failure message="assertion failed" type="AssertionError"/>
174 </testcase>
175 </testsuite>
176</testsuites>"#;
177
178 let parser = JunitXmlParser;
179 let result = parser.parse(xml).unwrap();
180
181 assert_eq!(result.len(), 3);
182
183 if let ParsedMeasurement::Test(test) = &result[2] {
184 assert_eq!(test.name, "test_three");
185 assert_eq!(test.status, TestStatus::Failed);
186 assert_eq!(test.metadata.get("suite").unwrap(), "suite_two");
187 } else {
188 panic!("Expected Test measurement");
189 }
190 }
191
192 #[test]
193 fn test_parse_skipped_test() {
194 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
195<testsuite name="my_tests" tests="1" skipped="1">
196 <testcase name="test_skip" classname="module::tests" time="0.0">
197 <skipped/>
198 </testcase>
199</testsuite>"#;
200
201 let parser = JunitXmlParser;
202 let result = parser.parse(xml).unwrap();
203
204 if let ParsedMeasurement::Test(test) = &result[0] {
205 assert_eq!(test.status, TestStatus::Skipped);
206 } else {
207 panic!("Expected Test measurement");
208 }
209 }
210
211 #[test]
212 fn test_parse_error_test() {
213 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
214<testsuite name="my_tests" tests="1" errors="1">
215 <testcase name="test_error" classname="module::tests" time="0.5">
216 <error message="runtime error" type="RuntimeError"/>
217 </testcase>
218</testsuite>"#;
219
220 let parser = JunitXmlParser;
221 let result = parser.parse(xml).unwrap();
222
223 if let ParsedMeasurement::Test(test) = &result[0] {
224 assert_eq!(test.status, TestStatus::Error);
225 } else {
226 panic!("Expected Test measurement");
227 }
228 }
229
230 #[test]
231 fn test_parse_missing_time() {
232 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
233<testsuite name="my_tests" tests="1">
234 <testcase name="test_no_time" classname="module::tests"/>
235</testsuite>"#;
236
237 let parser = JunitXmlParser;
238 let result = parser.parse(xml).unwrap();
239
240 if let ParsedMeasurement::Test(test) = &result[0] {
241 assert_eq!(test.duration, None);
242 } else {
243 panic!("Expected Test measurement");
244 }
245 }
246
247 #[test]
248 fn test_parse_invalid_xml() {
249 let xml = "not valid xml";
250 let parser = JunitXmlParser;
251 assert!(parser.parse(xml).is_err());
252 }
253}