speccoder_runtime/verify/
junit.rs1use quick_xml::events::{BytesStart, Event};
5use quick_xml::{Reader, XmlVersion};
6use speccoder_core::verify::{TestOutcome, TestResult};
7
8pub fn parse(xml: &str) -> Result<Vec<TestResult>, String> {
10 let mut reader = Reader::from_str(xml);
11 let mut tests = Vec::new();
12 let mut open: Option<TestResult> = None;
14 loop {
15 match reader
16 .read_event()
17 .map_err(|e| format!("invalid JUnit XML: {e}"))?
18 {
19 Event::Start(e) if e.name().as_ref() == "testcase" => {
20 open = Some(TestResult {
21 id: test_id(&e)?,
22 outcome: TestOutcome::Passed,
23 });
24 }
25 Event::Empty(e) if e.name().as_ref() == "testcase" => tests.push(TestResult {
26 id: test_id(&e)?,
27 outcome: TestOutcome::Passed,
28 }),
29 Event::Start(e) | Event::Empty(e) => {
30 if let Some(t) = open.as_mut() {
31 match e.name().as_ref() {
32 "failure" => t.outcome = TestOutcome::Failed,
33 "error" if t.outcome != TestOutcome::Failed => {
35 t.outcome = TestOutcome::Error;
36 }
37 "skipped" if !t.outcome.is_failing() => {
38 t.outcome = TestOutcome::Skipped;
39 }
40 _ => {}
41 }
42 }
43 }
44 Event::End(e) if e.name().as_ref() == "testcase" => {
45 tests.extend(open.take());
46 }
47 Event::Eof => break,
48 _ => {}
49 }
50 }
51 Ok(tests)
52}
53
54fn test_id(e: &BytesStart<'_>) -> Result<String, String> {
55 let attr = |key: &str| -> Result<Option<String>, String> {
56 match e
57 .try_get_attribute(key)
58 .map_err(|err| format!("invalid JUnit attribute: {err}"))?
59 {
60 Some(a) => a
61 .normalized_value(XmlVersion::default())
62 .map(|v| Some(v.into_owned()))
63 .map_err(|err| format!("invalid JUnit attribute: {err}")),
64 None => Ok(None),
65 }
66 };
67 let name = attr("name")?.ok_or("a <testcase> has no name")?;
68 Ok(match attr("classname")? {
69 Some(class) if !class.is_empty() => format!("{class}::{name}"),
70 _ => name,
71 })
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn reads_nextest_style_reports() {
80 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
81<testsuites name="nextest-run" tests="3" failures="1" errors="0">
82 <testsuite name="speccoder-core" tests="3" failures="1">
83 <testcase name="graph::tests::ok" classname="speccoder-core" timestamp="x" time="0.01"/>
84 <testcase name="graph::tests::bad" classname="speccoder-core" time="0.01">
85 <failure type="test failure">assertion failed & more</failure>
86 <system-out>out</system-out>
87 </testcase>
88 <testcase name="graph::tests::later" classname="speccoder-core"><skipped/></testcase>
89 </testsuite>
90</testsuites>"#;
91 let tests = parse(xml).unwrap();
92 let got: Vec<_> = tests.iter().map(|t| (t.id.as_str(), t.outcome)).collect();
93 assert_eq!(
94 got,
95 [
96 ("speccoder-core::graph::tests::ok", TestOutcome::Passed),
97 ("speccoder-core::graph::tests::bad", TestOutcome::Failed),
98 ("speccoder-core::graph::tests::later", TestOutcome::Skipped),
99 ]
100 );
101 }
102
103 #[test]
104 fn reads_pytest_style_reports_with_errors() {
105 let xml = r#"<testsuites><testsuite name="pytest"><testcase classname="tests.test_api" name="test_login"><error message="boom"/></testcase><testcase name="test_bare"/></testsuite></testsuites>"#;
106 let tests = parse(xml).unwrap();
107 assert_eq!(tests[0].id, "tests.test_api::test_login");
108 assert_eq!(tests[0].outcome, TestOutcome::Error);
109 assert_eq!(tests[1].id, "test_bare");
110 }
111
112 #[test]
113 fn a_failure_beside_an_error_is_a_failure() {
114 let xml = r#"<testsuite><testcase name="a"><error/><failure/></testcase><testcase name="b"><failure/><error/></testcase></testsuite>"#;
115 let tests = parse(xml).unwrap();
116 assert!(tests.iter().all(|t| t.outcome == TestOutcome::Failed));
117 }
118
119 #[test]
120 fn rejects_malformed_xml() {
121 assert!(parse("<testsuite><testcase name=\"a\"></testsuite>").is_err());
122 }
123}