ephemeris_report/
junit.rs1use roxmltree::{Document, Node};
4
5use crate::{ConvertError, Converted, Details, Outcome, TestItem, TestSummary};
6
7pub(crate) fn convert(bytes: &[u8]) -> Result<Converted, ConvertError> {
19 let text = str::from_utf8(bytes)?;
20 let document = Document::parse(text).map_err(|source| ConvertError::Xml { source })?;
21 let mut items: Vec<TestItem> = document
22 .descendants()
23 .filter(|node| node.has_tag_name("testcase"))
24 .map(|node| item(&node))
25 .collect();
26 items.sort_by(|left, right| left.name.cmp(&right.name));
27 let count = |outcome| items.iter().filter(|item| item.outcome == outcome).count() as u64;
28 Ok(Converted {
29 details: Details::Tests {
30 summary: TestSummary {
31 passed: count(Outcome::Pass),
32 failed: count(Outcome::Fail),
33 skipped: count(Outcome::Skip),
34 },
35 items,
36 },
37 terminal: text.to_owned(),
38 })
39}
40
41fn item(case: &Node<'_, '_>) -> TestItem {
42 let name = case.attribute("name").unwrap_or_default();
43 let name = match case.attribute("classname") {
44 Some(class) if !class.is_empty() => format!("{class}::{name}"),
45 _ => name.to_owned(),
46 };
47 let child = |tag| case.children().find(|node| node.has_tag_name(tag));
48 if let Some(failure) = child("failure").or_else(|| child("error")) {
49 let message = failure
50 .attribute("message")
51 .map(str::to_owned)
52 .or_else(|| failure.text().map(|text| text.trim().to_owned()))
53 .filter(|message| !message.is_empty());
54 return TestItem {
55 name,
56 outcome: Outcome::Fail,
57 message,
58 };
59 }
60 let outcome = if child("skipped").is_some() {
61 Outcome::Skip
62 } else {
63 Outcome::Pass
64 };
65 TestItem {
66 name,
67 outcome,
68 message: None,
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::convert;
75 use crate::{ConvertError, Details, Outcome};
76
77 const SAMPLE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
78<testsuites name="nextest-run" tests="4" failures="1" errors="0">
79 <testsuite name="pkg" tests="4" disabled="0" errors="0" failures="1">
80 <testcase name="a::passes" classname="pkg" time="0.01"></testcase>
81 <testcase name="a::breaks" classname="pkg" time="0.02">
82 <failure message="assertion failed" type="test failure">stack</failure>
83 </testcase>
84 <testcase name="a::ignored" classname="pkg"><skipped/></testcase>
85 <testcase name="bare"><error>panicked</error></testcase>
86 </testsuite>
87</testsuites>"#;
88
89 #[test]
90 fn an_empty_suite_passes_with_zero_counts() {
91 let converted = convert(br#"<testsuites name="nextest-run"></testsuites>"#).unwrap();
92 let Details::Tests { summary, items } = converted.details else {
93 panic!("expected tests");
94 };
95 assert_eq!(items.len(), 0);
96 assert_eq!((summary.passed, summary.failed, summary.skipped), (0, 0, 0));
97 }
98
99 #[test]
100 fn cases_become_items() {
101 let Details::Tests { summary, items } = convert(SAMPLE.as_bytes()).unwrap().details else {
102 panic!("expected tests");
103 };
104 assert_eq!((summary.passed, summary.failed, summary.skipped), (1, 2, 1));
105 let names: Vec<_> = items.iter().map(|item| item.name.as_str()).collect();
106 assert_eq!(
107 names,
108 [
109 "bare",
110 "pkg::a::breaks",
111 "pkg::a::ignored",
112 "pkg::a::passes"
113 ]
114 );
115 assert_eq!(items[1].outcome, Outcome::Fail);
116 assert_eq!(items[1].message.as_deref(), Some("assertion failed"));
117 assert_eq!(items[2].outcome, Outcome::Skip);
118 assert_eq!(items[0].message.as_deref(), Some("panicked"));
119 assert_eq!(items[3].message, None);
120 }
121
122 #[test]
123 fn malformed_input_is_an_error() {
124 assert!(matches!(
125 convert(b"<testsuites>"),
126 Err(ConvertError::Xml { source: _ })
127 ));
128 assert!(matches!(convert(b"\xff"), Err(ConvertError::Utf8(_))));
129 }
130}