Skip to main content

quick_junit/
deserialize.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5    DeserializeError, DeserializeErrorKind, FlakyOrRerun, NonSuccessKind, NonSuccessReruns,
6    PathElement, Property, Report, ReportUuid, TestCase, TestCaseStatus, TestRerun, TestSuite,
7    XmlString,
8};
9use chrono::{DateTime, FixedOffset};
10use indexmap::IndexMap;
11use newtype_uuid::GenericUuid;
12use quick_xml::{
13    escape::{resolve_xml_entity, unescape_with},
14    events::{BytesStart, Event},
15    Reader,
16};
17use std::{io::BufRead, time::Duration};
18
19impl Report {
20    /// **Experimental**: Deserializes a JUnit XML report from a reader.
21    ///
22    /// The deserializer should work with JUnit reports generated by the
23    /// `quick-junit` crate, but might not work with JUnit reports generated by
24    /// other tools. Patches to fix this are welcome.
25    ///
26    /// # Errors
27    ///
28    /// Returns an error if the XML is malformed, or if required attributes are
29    /// missing.
30    pub fn deserialize<R: BufRead>(reader: R) -> Result<Self, DeserializeError> {
31        let mut xml_reader = Reader::from_reader(reader);
32        xml_reader.config_mut().trim_text(false);
33        deserialize_report(&mut xml_reader)
34    }
35
36    /// Deserializes a JUnit XML report from a string.
37    ///
38    /// # Errors
39    ///
40    /// Returns an error if the XML is malformed, or if required attributes are
41    /// missing.
42    ///
43    /// # Examples
44    ///
45    /// ```rust
46    /// use quick_junit::Report;
47    ///
48    /// let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
49    /// <testsuites name="my-test-run" tests="1" failures="0" errors="0">
50    ///     <testsuite name="my-test-suite" tests="1" skipped="0" errors="0" failures="0">
51    ///         <testcase name="success-case"/>
52    ///     </testsuite>
53    /// </testsuites>
54    /// "#;
55    ///
56    /// let report = Report::deserialize_from_str(xml).unwrap();
57    /// assert_eq!(report.name.as_str(), "my-test-run");
58    /// assert_eq!(report.tests, 1);
59    /// ```
60    pub fn deserialize_from_str(xml: &str) -> Result<Self, DeserializeError> {
61        Self::deserialize(xml.as_bytes())
62    }
63}
64
65/// Deserializes a Report from XML.
66fn deserialize_report<R: BufRead>(reader: &mut Reader<R>) -> Result<Report, DeserializeError> {
67    let mut buf = Vec::new();
68    let mut report: Option<Report> = None;
69    let mut properly_closed = false;
70    let root_path = vec![PathElement::TestSuites];
71
72    loop {
73        match reader.read_event_into(&mut buf) {
74            Ok(Event::Start(e)) if e.name().as_ref() == b"testsuites" => {
75                report = Some(parse_testsuites_element(&e, &root_path)?);
76            }
77            Ok(Event::Empty(e)) if e.name().as_ref() == b"testsuites" => {
78                report = Some(parse_testsuites_element(&e, &root_path)?);
79                properly_closed = true; // Empty elements are self-closing
80            }
81            Ok(Event::Start(e)) if e.name().as_ref() == b"testsuite" => {
82                if let Some(ref mut report) = report {
83                    let suite_index = report.test_suites.len();
84                    let test_suite =
85                        deserialize_test_suite(reader, &e, false, &root_path, suite_index)?;
86                    report.test_suites.push(test_suite);
87                }
88            }
89            Ok(Event::Empty(e)) if e.name().as_ref() == b"testsuite" => {
90                if let Some(ref mut report) = report {
91                    let suite_index = report.test_suites.len();
92                    let test_suite =
93                        deserialize_test_suite(reader, &e, true, &root_path, suite_index)?;
94                    report.test_suites.push(test_suite);
95                }
96            }
97            Ok(Event::End(e)) if e.name().as_ref() == b"testsuites" => {
98                properly_closed = true;
99                break;
100            }
101            Ok(Event::Eof) => break,
102            Ok(_) => {}
103            Err(e) => {
104                return Err(DeserializeError::new(
105                    DeserializeErrorKind::XmlError(e),
106                    root_path.clone(),
107                ))
108            }
109        }
110        buf.clear();
111    }
112
113    if !properly_closed && report.is_some() {
114        return Err(DeserializeError::new(
115            DeserializeErrorKind::InvalidStructure(
116                "unexpected EOF, <testsuites> not properly closed".to_string(),
117            ),
118            root_path,
119        ));
120    }
121
122    report.ok_or_else(|| {
123        DeserializeError::new(
124            DeserializeErrorKind::InvalidStructure("missing <testsuites> element".to_string()),
125            Vec::new(),
126        )
127    })
128}
129
130/// Parses the attributes of a `<testsuites>` element into a `Report`
131/// (with an empty `test_suites` vec).
132fn parse_testsuites_element(
133    element: &BytesStart<'_>,
134    path: &[PathElement],
135) -> Result<Report, DeserializeError> {
136    let mut name = None;
137    let mut uuid = None;
138    let mut timestamp = None;
139    let mut time = None;
140    let mut tests = 0;
141    let mut failures = 0;
142    let mut errors = 0;
143    let mut skipped = 0;
144    let mut disabled: Option<usize> = None;
145
146    for attr in element.attributes() {
147        let attr = attr.map_err(|e| {
148            DeserializeError::new(DeserializeErrorKind::AttrError(e), path.to_vec())
149        })?;
150        let mut attr_path = path.to_vec();
151        match attr.key.as_ref() {
152            b"name" => {
153                attr_path.push(PathElement::Attribute("name".to_string()));
154                name = Some(parse_xml_string(&attr.value, &attr_path)?);
155            }
156            b"uuid" => {
157                attr_path.push(PathElement::Attribute("uuid".to_string()));
158                uuid = Some(parse_uuid(&attr.value, &attr_path)?);
159            }
160            b"timestamp" => {
161                attr_path.push(PathElement::Attribute("timestamp".to_string()));
162                timestamp = Some(parse_timestamp(&attr.value, &attr_path)?);
163            }
164            b"time" => {
165                attr_path.push(PathElement::Attribute("time".to_string()));
166                time = Some(parse_duration(&attr.value, &attr_path)?);
167            }
168            b"tests" => {
169                attr_path.push(PathElement::Attribute("tests".to_string()));
170                tests = parse_usize(&attr.value, &attr_path)?;
171            }
172            b"failures" => {
173                attr_path.push(PathElement::Attribute("failures".to_string()));
174                failures = parse_usize(&attr.value, &attr_path)?;
175            }
176            b"errors" => {
177                attr_path.push(PathElement::Attribute("errors".to_string()));
178                errors = parse_usize(&attr.value, &attr_path)?;
179            }
180            b"skipped" => {
181                attr_path.push(PathElement::Attribute("skipped".to_string()));
182                skipped = parse_usize(&attr.value, &attr_path)?;
183            }
184            b"disabled" => {
185                attr_path.push(PathElement::Attribute("disabled".to_string()));
186                disabled = Some(parse_usize(&attr.value, &attr_path)?);
187            }
188            _ => {} // Ignore unknown attributes.
189        }
190    }
191
192    let name = require_attribute(name, "name", path)?;
193
194    Ok(Report {
195        name,
196        uuid,
197        timestamp,
198        time,
199        tests,
200        failures,
201        errors,
202        skipped,
203        disabled,
204        test_suites: Vec::new(),
205    })
206}
207
208/// Deserializes a TestSuite from XML.
209///
210/// Handles both `<testsuite>` start tags (with child elements) and
211/// `<testsuite/>` self-closing tags.
212fn deserialize_test_suite<R: BufRead>(
213    reader: &mut Reader<R>,
214    element: &BytesStart<'_>,
215    is_empty: bool,
216    path: &[PathElement],
217    suite_index: usize,
218) -> Result<TestSuite, DeserializeError> {
219    let mut name = None;
220    let mut tests = 0;
221    let mut skipped = 0;
222    let mut disabled = None;
223    let mut errors = 0;
224    let mut failures = 0;
225    let mut timestamp = None;
226    let mut time = None;
227    let mut extra = IndexMap::new();
228
229    // Build element path (before name is known) for error reporting.
230    let mut element_path = path.to_vec();
231    element_path.push(PathElement::TestSuite(suite_index, None));
232
233    for attr in element.attributes() {
234        let attr = attr.map_err(|e| {
235            DeserializeError::new(DeserializeErrorKind::AttrError(e), element_path.clone())
236        })?;
237        let mut attr_path = element_path.clone();
238        match attr.key.as_ref() {
239            b"name" => {
240                attr_path.push(PathElement::Attribute("name".to_string()));
241                name = Some(parse_xml_string(&attr.value, &attr_path)?);
242            }
243            b"tests" => {
244                attr_path.push(PathElement::Attribute("tests".to_string()));
245                tests = parse_usize(&attr.value, &attr_path)?;
246            }
247            b"skipped" => {
248                attr_path.push(PathElement::Attribute("skipped".to_string()));
249                skipped = parse_usize(&attr.value, &attr_path)?;
250            }
251            b"disabled" => {
252                attr_path.push(PathElement::Attribute("disabled".to_string()));
253                disabled = Some(parse_usize(&attr.value, &attr_path)?);
254            }
255            b"errors" => {
256                attr_path.push(PathElement::Attribute("errors".to_string()));
257                errors = parse_usize(&attr.value, &attr_path)?;
258            }
259            b"failures" => {
260                attr_path.push(PathElement::Attribute("failures".to_string()));
261                failures = parse_usize(&attr.value, &attr_path)?;
262            }
263            b"timestamp" => {
264                attr_path.push(PathElement::Attribute("timestamp".to_string()));
265                timestamp = Some(parse_timestamp(&attr.value, &attr_path)?);
266            }
267            b"time" => {
268                attr_path.push(PathElement::Attribute("time".to_string()));
269                time = Some(parse_duration(&attr.value, &attr_path)?);
270            }
271            _ => {
272                // Store unknown attributes in extra.
273                let key = parse_xml_string(attr.key.as_ref(), &attr_path)?;
274                let value = parse_xml_string(&attr.value, &attr_path)?;
275                extra.insert(key, value);
276            }
277        }
278    }
279
280    let name = require_attribute(name, "name", &element_path)?;
281
282    // For self-closing tags, there are no children to parse.
283    if is_empty {
284        return Ok(TestSuite {
285            name,
286            tests,
287            skipped,
288            disabled,
289            errors,
290            failures,
291            timestamp,
292            time,
293            test_cases: Vec::new(),
294            properties: Vec::new(),
295            system_out: None,
296            system_err: None,
297            extra,
298        });
299    }
300
301    // Build the suite path with the name for child element error reporting.
302    let mut suite_path = path.to_vec();
303    suite_path.push(PathElement::TestSuite(
304        suite_index,
305        Some(name.as_str().to_string()),
306    ));
307
308    let mut test_cases = Vec::new();
309    let mut properties = Vec::new();
310    let mut system_out = None;
311    let mut system_err = None;
312    let mut buf = Vec::new();
313
314    loop {
315        match reader.read_event_into(&mut buf) {
316            Ok(Event::Start(ref e)) => {
317                let element_name = e.name().as_ref().to_vec();
318                if &element_name == b"testcase" {
319                    let test_case =
320                        deserialize_test_case(reader, e, false, &suite_path, test_cases.len())?;
321                    test_cases.push(test_case);
322                } else if &element_name == b"properties" {
323                    properties = deserialize_properties(reader, &suite_path)?;
324                } else if &element_name == b"system-out" {
325                    let mut child_path = suite_path.clone();
326                    child_path.push(PathElement::SystemOut);
327                    system_out = Some(read_text_content(reader, b"system-out", &child_path)?);
328                } else if &element_name == b"system-err" {
329                    let mut child_path = suite_path.clone();
330                    child_path.push(PathElement::SystemErr);
331                    system_err = Some(read_text_content(reader, b"system-err", &child_path)?);
332                } else {
333                    // Skip unknown elements.
334                    let tag_name = e.name().to_owned();
335                    reader
336                        .read_to_end_into(tag_name, &mut Vec::new())
337                        .map_err(|e| {
338                            DeserializeError::new(
339                                DeserializeErrorKind::XmlError(e),
340                                suite_path.clone(),
341                            )
342                        })?;
343                }
344            }
345            Ok(Event::Empty(ref e)) => {
346                if e.name().as_ref() == b"testcase" {
347                    let test_case =
348                        deserialize_test_case(reader, e, true, &suite_path, test_cases.len())?;
349                    test_cases.push(test_case);
350                }
351            }
352            Ok(Event::End(ref e)) if e.name().as_ref() == b"testsuite" => break,
353            Ok(Event::Eof) => {
354                return Err(DeserializeError::new(
355                    DeserializeErrorKind::InvalidStructure(
356                        "unexpected EOF in <testsuite>".to_string(),
357                    ),
358                    suite_path,
359                ))
360            }
361            Ok(_) => {}
362            Err(e) => {
363                return Err(DeserializeError::new(
364                    DeserializeErrorKind::XmlError(e),
365                    suite_path,
366                ))
367            }
368        }
369        buf.clear();
370    }
371
372    Ok(TestSuite {
373        name,
374        tests,
375        skipped,
376        disabled,
377        errors,
378        failures,
379        timestamp,
380        time,
381        test_cases,
382        properties,
383        system_out,
384        system_err,
385        extra,
386    })
387}
388
389/// Deserializes a TestCase from XML.
390///
391/// Handles both `<testcase>` start tags (with child elements) and
392/// `<testcase/>` self-closing tags.
393fn deserialize_test_case<R: BufRead>(
394    reader: &mut Reader<R>,
395    element: &BytesStart<'_>,
396    is_empty: bool,
397    path: &[PathElement],
398    case_index: usize,
399) -> Result<TestCase, DeserializeError> {
400    let mut name = None;
401    let mut classname = None;
402    let mut assertions = None;
403    let mut timestamp = None;
404    let mut time = None;
405    let mut extra = IndexMap::new();
406
407    // Build element path (before name is known) for error reporting.
408    let mut element_path = path.to_vec();
409    element_path.push(PathElement::TestCase(case_index, None));
410
411    for attr in element.attributes() {
412        let attr = attr.map_err(|e| {
413            DeserializeError::new(DeserializeErrorKind::AttrError(e), element_path.clone())
414        })?;
415        let mut attr_path = element_path.clone();
416        match attr.key.as_ref() {
417            b"name" => {
418                attr_path.push(PathElement::Attribute("name".to_string()));
419                name = Some(parse_xml_string(&attr.value, &attr_path)?);
420            }
421            b"classname" => {
422                attr_path.push(PathElement::Attribute("classname".to_string()));
423                classname = Some(parse_xml_string(&attr.value, &attr_path)?);
424            }
425            b"assertions" => {
426                attr_path.push(PathElement::Attribute("assertions".to_string()));
427                assertions = Some(parse_usize(&attr.value, &attr_path)?);
428            }
429            b"timestamp" => {
430                attr_path.push(PathElement::Attribute("timestamp".to_string()));
431                timestamp = Some(parse_timestamp(&attr.value, &attr_path)?);
432            }
433            b"time" => {
434                attr_path.push(PathElement::Attribute("time".to_string()));
435                time = Some(parse_duration(&attr.value, &attr_path)?);
436            }
437            _ => {
438                let key = parse_xml_string(attr.key.as_ref(), &attr_path)?;
439                let value = parse_xml_string(&attr.value, &attr_path)?;
440                extra.insert(key, value);
441            }
442        }
443    }
444
445    let name = require_attribute(name, "name", &element_path)?;
446
447    // For self-closing tags, there are no children to parse.
448    if is_empty {
449        return Ok(TestCase {
450            name,
451            classname,
452            assertions,
453            timestamp,
454            time,
455            status: TestCaseStatus::success(),
456            system_out: None,
457            system_err: None,
458            extra,
459            properties: Vec::new(),
460        });
461    }
462
463    // Build the test case path with the name for child element error reporting.
464    let mut case_path = path.to_vec();
465    case_path.push(PathElement::TestCase(
466        case_index,
467        Some(name.as_str().to_string()),
468    ));
469
470    let mut properties = Vec::new();
471    let mut system_out = None;
472    let mut system_err = None;
473    let mut status_elements = Vec::new();
474    let mut buf = Vec::new();
475
476    loop {
477        match reader.read_event_into(&mut buf) {
478            Ok(Event::Start(ref e)) => {
479                let element_name = e.name().as_ref().to_vec();
480
481                if is_status_element(&element_name) {
482                    let status_element = deserialize_status_element(reader, e, false, &case_path)?;
483                    status_elements.push(status_element);
484                } else if &element_name == b"properties" {
485                    properties = deserialize_properties(reader, &case_path)?;
486                } else if &element_name == b"system-out" {
487                    let mut child_path = case_path.clone();
488                    child_path.push(PathElement::SystemOut);
489                    system_out = Some(read_text_content(reader, b"system-out", &child_path)?);
490                } else if &element_name == b"system-err" {
491                    let mut child_path = case_path.clone();
492                    child_path.push(PathElement::SystemErr);
493                    system_err = Some(read_text_content(reader, b"system-err", &child_path)?);
494                } else {
495                    // Skip unknown elements.
496                    let tag_name = e.name().to_owned();
497                    reader
498                        .read_to_end_into(tag_name, &mut Vec::new())
499                        .map_err(|e| {
500                            DeserializeError::new(
501                                DeserializeErrorKind::XmlError(e),
502                                case_path.clone(),
503                            )
504                        })?;
505                }
506            }
507            Ok(Event::Empty(ref e)) => {
508                let element_name = e.name().as_ref().to_vec();
509
510                if is_status_element(&element_name) {
511                    let status_element = deserialize_status_element(reader, e, true, &case_path)?;
512                    status_elements.push(status_element);
513                }
514                // Empty elements don't need special handling for properties,
515                // system-out, or system-err.
516            }
517            Ok(Event::End(ref e)) if e.name().as_ref() == b"testcase" => break,
518            Ok(Event::Eof) => {
519                return Err(DeserializeError::new(
520                    DeserializeErrorKind::InvalidStructure(
521                        "unexpected EOF in <testcase>".to_string(),
522                    ),
523                    case_path,
524                ))
525            }
526            Ok(_) => {}
527            Err(e) => {
528                return Err(DeserializeError::new(
529                    DeserializeErrorKind::XmlError(e),
530                    case_path,
531                ))
532            }
533        }
534        buf.clear();
535    }
536
537    let status = build_test_case_status(status_elements, &case_path)?;
538
539    Ok(TestCase {
540        name,
541        classname,
542        assertions,
543        timestamp,
544        time,
545        status,
546        system_out,
547        system_err,
548        extra,
549        properties,
550    })
551}
552
553/// Represents a parsed status element (failure, error, skipped, etc.)
554#[derive(Debug)]
555/// Common data for all status elements
556struct StatusElementData {
557    message: Option<XmlString>,
558    ty: Option<XmlString>,
559    description: Option<XmlString>,
560    stack_trace: Option<XmlString>,
561    system_out: Option<XmlString>,
562    system_err: Option<XmlString>,
563    timestamp: Option<DateTime<FixedOffset>>,
564    time: Option<Duration>,
565}
566
567/// Main status element kind (failure, error, or skipped)
568#[derive(Debug, PartialEq, Eq, Clone, Copy)]
569enum MainStatusKind {
570    Failure,
571    Error,
572    Skipped,
573}
574
575impl MainStatusKind {
576    /// Converts to `NonSuccessKind`. Panics if called on `Skipped`.
577    fn to_non_success_kind(self) -> NonSuccessKind {
578        match self {
579            MainStatusKind::Failure => NonSuccessKind::Failure,
580            MainStatusKind::Error => NonSuccessKind::Error,
581            MainStatusKind::Skipped => {
582                panic!("to_non_success_kind called on Skipped")
583            }
584        }
585    }
586}
587
588/// Main status element
589struct MainStatusElement {
590    kind: MainStatusKind,
591    data: StatusElementData,
592}
593
594/// Rerun/flaky status element kind (failure or error)
595#[derive(Debug, PartialEq, Eq, Clone, Copy)]
596enum RerunStatusKind {
597    Failure,
598    Error,
599}
600
601/// Rerun or flaky status element
602struct RerunStatusElement {
603    kind: RerunStatusKind,
604    data: StatusElementData,
605}
606
607/// Categorized status element
608enum StatusElement {
609    Main(MainStatusElement),
610    Flaky(RerunStatusElement),
611    Rerun(RerunStatusElement),
612}
613
614enum StatusCategory {
615    Main(MainStatusKind),
616    Flaky(RerunStatusKind),
617    Rerun(RerunStatusKind),
618}
619
620/// Deserializes a status element (failure, error, skipped, flaky*, rerun*).
621fn deserialize_status_element<R: BufRead>(
622    reader: &mut Reader<R>,
623    element: &BytesStart<'_>,
624    is_empty: bool,
625    path: &[PathElement],
626) -> Result<StatusElement, DeserializeError> {
627    let (category, status_path_elem) = match element.name().as_ref() {
628        b"failure" => (
629            StatusCategory::Main(MainStatusKind::Failure),
630            PathElement::Failure,
631        ),
632        b"error" => (
633            StatusCategory::Main(MainStatusKind::Error),
634            PathElement::Error,
635        ),
636        b"skipped" => (
637            StatusCategory::Main(MainStatusKind::Skipped),
638            PathElement::Skipped,
639        ),
640        b"flakyFailure" => (
641            StatusCategory::Flaky(RerunStatusKind::Failure),
642            PathElement::FlakyFailure,
643        ),
644        b"flakyError" => (
645            StatusCategory::Flaky(RerunStatusKind::Error),
646            PathElement::FlakyError,
647        ),
648        b"rerunFailure" => (
649            StatusCategory::Rerun(RerunStatusKind::Failure),
650            PathElement::RerunFailure,
651        ),
652        b"rerunError" => (
653            StatusCategory::Rerun(RerunStatusKind::Error),
654            PathElement::RerunError,
655        ),
656        _ => {
657            return Err(DeserializeError::new(
658                DeserializeErrorKind::UnexpectedElement(
659                    String::from_utf8_lossy(element.name().as_ref()).to_string(),
660                ),
661                path.to_vec(),
662            ))
663        }
664    };
665
666    let mut status_path = path.to_vec();
667    status_path.push(status_path_elem);
668
669    let mut message = None;
670    let mut ty = None;
671    let mut timestamp = None;
672    let mut time = None;
673
674    for attr in element.attributes() {
675        let attr = attr.map_err(|e| {
676            DeserializeError::new(DeserializeErrorKind::AttrError(e), status_path.clone())
677        })?;
678        let mut attr_path = status_path.clone();
679        match attr.key.as_ref() {
680            b"message" => {
681                attr_path.push(PathElement::Attribute("message".to_string()));
682                message = Some(parse_xml_string(&attr.value, &attr_path)?);
683            }
684            b"type" => {
685                attr_path.push(PathElement::Attribute("type".to_string()));
686                ty = Some(parse_xml_string(&attr.value, &attr_path)?);
687            }
688            b"timestamp" => {
689                attr_path.push(PathElement::Attribute("timestamp".to_string()));
690                timestamp = Some(parse_timestamp(&attr.value, &attr_path)?);
691            }
692            b"time" => {
693                attr_path.push(PathElement::Attribute("time".to_string()));
694                time = Some(parse_duration(&attr.value, &attr_path)?);
695            }
696            _ => {} // Ignore unknown attributes
697        }
698    }
699
700    let mut description_text = String::new();
701    let mut stack_trace = None;
702    let mut system_out = None;
703    let mut system_err = None;
704
705    // Only read child content if this is not an empty element.
706    if !is_empty {
707        let mut buf = Vec::new();
708        loop {
709            match reader.read_event_into(&mut buf) {
710                Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
711                    let element_name = e.name().as_ref().to_vec();
712                    if &element_name == b"stackTrace" {
713                        let mut child_path = status_path.clone();
714                        child_path.push(PathElement::Attribute("stackTrace".to_string()));
715                        stack_trace = Some(read_text_content(reader, b"stackTrace", &child_path)?);
716                    } else if &element_name == b"system-out" {
717                        let mut child_path = status_path.clone();
718                        child_path.push(PathElement::SystemOut);
719                        system_out = Some(read_text_content(reader, b"system-out", &child_path)?);
720                    } else if &element_name == b"system-err" {
721                        let mut child_path = status_path.clone();
722                        child_path.push(PathElement::SystemErr);
723                        system_err = Some(read_text_content(reader, b"system-err", &child_path)?);
724                    } else {
725                        // Skip unknown Start elements
726                        let tag_name = e.name().to_owned();
727                        reader
728                            .read_to_end_into(tag_name, &mut Vec::new())
729                            .map_err(|e| {
730                                DeserializeError::new(
731                                    DeserializeErrorKind::XmlError(e),
732                                    status_path.clone(),
733                                )
734                            })?;
735                    }
736                }
737                Ok(Event::Text(ref e)) => {
738                    let text = std::str::from_utf8(e.as_ref()).map_err(|e| {
739                        DeserializeError::new(
740                            DeserializeErrorKind::Utf8Error(e),
741                            status_path.clone(),
742                        )
743                    })?;
744                    // Unescape XML entities in the text content and accumulate
745                    let unescaped = unescape_with(text, resolve_xml_entity).map_err(|e| {
746                        DeserializeError::new(
747                            DeserializeErrorKind::EscapeError(e),
748                            status_path.clone(),
749                        )
750                    })?;
751                    description_text.push_str(&unescaped);
752                }
753                Ok(Event::CData(ref e)) => {
754                    // CDATA sections are already unescaped, just accumulate
755                    let text = std::str::from_utf8(e.as_ref()).map_err(|e| {
756                        DeserializeError::new(
757                            DeserializeErrorKind::Utf8Error(e),
758                            status_path.clone(),
759                        )
760                    })?;
761                    description_text.push_str(text);
762                }
763                Ok(Event::GeneralRef(ref e)) => {
764                    // Handle entity references like &quot;, &amp;, etc.
765                    let entity_name = std::str::from_utf8(e.as_ref()).map_err(|e| {
766                        DeserializeError::new(
767                            DeserializeErrorKind::Utf8Error(e),
768                            status_path.clone(),
769                        )
770                    })?;
771                    let unescaped = resolve_xml_entity(entity_name).ok_or_else(|| {
772                        DeserializeError::new(
773                            DeserializeErrorKind::InvalidStructure(format!(
774                                "unrecognized entity: {entity_name}",
775                            )),
776                            status_path.clone(),
777                        )
778                    })?;
779                    description_text.push_str(unescaped);
780                }
781                Ok(Event::End(ref e)) if is_status_element(e.name().as_ref()) => {
782                    break;
783                }
784                Ok(Event::Eof) => {
785                    return Err(DeserializeError::new(
786                        DeserializeErrorKind::InvalidStructure(
787                            "unexpected EOF in status element".to_string(),
788                        ),
789                        status_path,
790                    ))
791                }
792                Ok(_) => {}
793                Err(e) => {
794                    return Err(DeserializeError::new(
795                        DeserializeErrorKind::XmlError(e),
796                        status_path,
797                    ))
798                }
799            }
800            buf.clear();
801        }
802    }
803
804    // Convert accumulated text to final description, trimming whitespace
805    let description = if !description_text.trim().is_empty() {
806        Some(XmlString::new(description_text.trim()))
807    } else {
808        None
809    };
810
811    let data = StatusElementData {
812        message,
813        ty,
814        description,
815        stack_trace,
816        system_out,
817        system_err,
818        timestamp,
819        time,
820    };
821
822    Ok(match category {
823        StatusCategory::Main(kind) => StatusElement::Main(MainStatusElement { kind, data }),
824        StatusCategory::Flaky(kind) => StatusElement::Flaky(RerunStatusElement { kind, data }),
825        StatusCategory::Rerun(kind) => StatusElement::Rerun(RerunStatusElement { kind, data }),
826    })
827}
828
829/// Builds a TestCaseStatus from parsed status elements.
830fn build_test_case_status(
831    status_elements: Vec<StatusElement>,
832    path: &[PathElement],
833) -> Result<TestCaseStatus, DeserializeError> {
834    // Separate the main status from reruns and flaky runs.
835    let mut main_status: Option<&MainStatusElement> = None;
836    let mut flaky_runs = Vec::new();
837    let mut reruns = Vec::new();
838
839    for element in &status_elements {
840        match element {
841            StatusElement::Main(main) => {
842                if main_status.is_some() {
843                    return Err(DeserializeError::new(
844                        DeserializeErrorKind::InvalidStructure(
845                            "multiple main status elements (failure/error/skipped) are not allowed"
846                                .to_string(),
847                        ),
848                        path.to_vec(),
849                    ));
850                }
851                main_status = Some(main);
852            }
853            StatusElement::Flaky(flaky) => {
854                flaky_runs.push(flaky);
855            }
856            StatusElement::Rerun(rerun) => {
857                reruns.push(rerun);
858            }
859        }
860    }
861
862    // Build the status from the combination of main status, flaky runs, and
863    // reruns. Each arm corresponds to a row in the decision table:
864    //
865    // main_status  has_flaky  has_reruns   |         result
866    //
867    //    None        false      false      |   Success (empty)
868    //    None        true       false      |   Success (flaky_runs)
869    //    None        false      true       |   Error: reruns without main
870    //   Skipped      true         *        |   Error: skipped + reruns
871    //   Skipped        *        true       |   Error: skipped + reruns
872    //   Skipped      false      false      |   Skipped
873    //  Fail/Error    true       false      |   NonSuccess (flaky reruns)
874    //  Fail/Error    false        *        |   NonSuccess (rerun reruns)
875    //      *         true       true       |   Error: mixed flaky + rerun
876
877    let main_with_kind = main_status.map(|m| (m, m.kind));
878    let has_flaky = !flaky_runs.is_empty();
879    let has_reruns = !reruns.is_empty();
880
881    match (main_with_kind, has_flaky, has_reruns) {
882        // No main status, no reruns/flaky: success.
883        (None, false, false) => Ok(TestCaseStatus::success()),
884
885        // No main status + flaky runs: success with prior flaky failures.
886        (None, true, false) => {
887            let flaky_runs = flaky_runs.into_iter().map(build_test_rerun).collect();
888            Ok(TestCaseStatus::Success { flaky_runs })
889        }
890
891        // No main status + rerun elements: invalid (reruns require a main
892        // failure/error).
893        (None, false, true) => Err(DeserializeError::new(
894            DeserializeErrorKind::InvalidStructure(
895                "found rerunFailure/rerunError elements without a corresponding \
896                 failure or error element"
897                    .to_string(),
898            ),
899            path.to_vec(),
900        )),
901
902        // Skipped + any reruns/flaky: invalid.
903        (Some((_, MainStatusKind::Skipped)), true, _)
904        | (Some((_, MainStatusKind::Skipped)), _, true) => Err(DeserializeError::new(
905            DeserializeErrorKind::InvalidStructure(
906                "skipped test case cannot have flakyFailure, flakyError, \
907                 rerunFailure, or rerunError elements"
908                    .to_string(),
909            ),
910            path.to_vec(),
911        )),
912
913        // Skipped with no reruns/flaky.
914        (Some((main, MainStatusKind::Skipped)), false, false) => Ok(TestCaseStatus::Skipped {
915            message: main.data.message.clone(),
916            ty: main.data.ty.clone(),
917            description: main.data.description.clone(),
918        }),
919
920        // Failure/error: build NonSuccess status. If flaky runs are present,
921        // they become the reruns (FlakyOrRerun::Flaky); otherwise any rerun
922        // elements are used (FlakyOrRerun::Rerun, possibly empty).
923        (Some((main, MainStatusKind::Failure | MainStatusKind::Error)), _, false)
924        | (Some((main, MainStatusKind::Failure | MainStatusKind::Error)), false, _) => {
925            let kind = main.kind.to_non_success_kind();
926            let (rerun_kind, runs) = if has_flaky {
927                (FlakyOrRerun::Flaky, flaky_runs)
928            } else {
929                (FlakyOrRerun::Rerun, reruns)
930            };
931            Ok(TestCaseStatus::NonSuccess {
932                kind,
933                message: main.data.message.clone(),
934                ty: main.data.ty.clone(),
935                description: main.data.description.clone(),
936                reruns: NonSuccessReruns {
937                    kind: rerun_kind,
938                    runs: runs.into_iter().map(build_test_rerun).collect(),
939                },
940            })
941        }
942
943        // Mixed flaky + rerun elements: the data model uses a single
944        // FlakyOrRerun kind for all reruns, so this cannot be represented.
945        (_, true, true) => Err(DeserializeError::new(
946            DeserializeErrorKind::InvalidStructure(
947                "test case has both flakyFailure/flakyError and \
948                 rerunFailure/rerunError elements, which is not supported"
949                    .to_string(),
950            ),
951            path.to_vec(),
952        )),
953    }
954}
955
956/// Builds a TestRerun from a rerun status element.
957///
958/// The type system ensures only flaky/rerun elements can be passed here.
959fn build_test_rerun(element: &RerunStatusElement) -> TestRerun {
960    let kind = match element.kind {
961        RerunStatusKind::Failure => NonSuccessKind::Failure,
962        RerunStatusKind::Error => NonSuccessKind::Error,
963    };
964
965    TestRerun {
966        kind,
967        timestamp: element.data.timestamp,
968        time: element.data.time,
969        message: element.data.message.clone(),
970        ty: element.data.ty.clone(),
971        stack_trace: element.data.stack_trace.clone(),
972        system_out: element.data.system_out.clone(),
973        system_err: element.data.system_err.clone(),
974        description: element.data.description.clone(),
975    }
976}
977
978/// Returns true if the element name is a test case status element.
979fn is_status_element(name: &[u8]) -> bool {
980    matches!(
981        name,
982        b"failure"
983            | b"error"
984            | b"skipped"
985            | b"flakyFailure"
986            | b"flakyError"
987            | b"rerunFailure"
988            | b"rerunError"
989    )
990}
991
992/// Deserializes properties from XML.
993fn deserialize_properties<R: BufRead>(
994    reader: &mut Reader<R>,
995    path: &[PathElement],
996) -> Result<Vec<Property>, DeserializeError> {
997    let mut properties = Vec::new();
998    let mut buf = Vec::new();
999    let mut prop_path = path.to_vec();
1000    prop_path.push(PathElement::Properties);
1001
1002    loop {
1003        match reader.read_event_into(&mut buf) {
1004            Ok(Event::Empty(e)) if e.name().as_ref() == b"property" => {
1005                let mut elem_path = prop_path.clone();
1006                elem_path.push(PathElement::Property(properties.len()));
1007                let property = deserialize_property(&e, &elem_path)?;
1008                properties.push(property);
1009            }
1010            Ok(Event::End(e)) if e.name().as_ref() == b"properties" => break,
1011            Ok(Event::Eof) => {
1012                return Err(DeserializeError::new(
1013                    DeserializeErrorKind::InvalidStructure(
1014                        "unexpected EOF in <properties>".to_string(),
1015                    ),
1016                    prop_path,
1017                ))
1018            }
1019            Ok(_) => {}
1020            Err(e) => {
1021                return Err(DeserializeError::new(
1022                    DeserializeErrorKind::XmlError(e),
1023                    prop_path,
1024                ))
1025            }
1026        }
1027        buf.clear();
1028    }
1029
1030    Ok(properties)
1031}
1032
1033/// Deserializes a single property.
1034fn deserialize_property(
1035    element: &BytesStart<'_>,
1036    path: &[PathElement],
1037) -> Result<Property, DeserializeError> {
1038    let mut name = None;
1039    let mut value = None;
1040
1041    for attr in element.attributes() {
1042        let attr = attr.map_err(|e| {
1043            DeserializeError::new(DeserializeErrorKind::AttrError(e), path.to_vec())
1044        })?;
1045        let mut attr_path = path.to_vec();
1046        match attr.key.as_ref() {
1047            b"name" => {
1048                attr_path.push(PathElement::Attribute("name".to_string()));
1049                name = Some(parse_xml_string(&attr.value, &attr_path)?);
1050            }
1051            b"value" => {
1052                attr_path.push(PathElement::Attribute("value".to_string()));
1053                value = Some(parse_xml_string(&attr.value, &attr_path)?);
1054            }
1055            _ => {} // Ignore unknown attributes
1056        }
1057    }
1058
1059    let name = name.ok_or_else(|| {
1060        let mut attr_path = path.to_vec();
1061        attr_path.push(PathElement::Attribute("name".to_string()));
1062        DeserializeError::new(
1063            DeserializeErrorKind::MissingAttribute("name".to_string()),
1064            attr_path,
1065        )
1066    })?;
1067    let value = value.ok_or_else(|| {
1068        let mut attr_path = path.to_vec();
1069        attr_path.push(PathElement::Attribute("value".to_string()));
1070        DeserializeError::new(
1071            DeserializeErrorKind::MissingAttribute("value".to_string()),
1072            attr_path,
1073        )
1074    })?;
1075
1076    Ok(Property { name, value })
1077}
1078
1079/// Reads text content from an element.
1080fn read_text_content<R: BufRead>(
1081    reader: &mut Reader<R>,
1082    element_name: &[u8],
1083    path: &[PathElement],
1084) -> Result<XmlString, DeserializeError> {
1085    let mut text = String::new();
1086    let mut buf = Vec::new();
1087
1088    loop {
1089        match reader.read_event_into(&mut buf) {
1090            Ok(Event::Text(e)) => {
1091                let s = std::str::from_utf8(e.as_ref()).map_err(|e| {
1092                    DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec())
1093                })?;
1094                let unescaped = unescape_with(s, resolve_xml_entity).map_err(|e| {
1095                    DeserializeError::new(DeserializeErrorKind::EscapeError(e), path.to_vec())
1096                })?;
1097                text.push_str(&unescaped);
1098            }
1099            Ok(Event::CData(e)) => {
1100                // CDATA sections are already unescaped, just convert to UTF-8.
1101                let s = std::str::from_utf8(e.as_ref()).map_err(|e| {
1102                    DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec())
1103                })?;
1104                text.push_str(s);
1105            }
1106            Ok(Event::GeneralRef(e)) => {
1107                let entity_name = std::str::from_utf8(e.as_ref()).map_err(|e| {
1108                    DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec())
1109                })?;
1110                let unescaped = resolve_xml_entity(entity_name).ok_or_else(|| {
1111                    DeserializeError::new(
1112                        DeserializeErrorKind::InvalidStructure(format!(
1113                            "unrecognized entity: {entity_name}",
1114                        )),
1115                        path.to_vec(),
1116                    )
1117                })?;
1118                text.push_str(unescaped);
1119            }
1120            Ok(Event::End(e)) if e.name().as_ref() == element_name => break,
1121            Ok(Event::Eof) => {
1122                return Err(DeserializeError::new(
1123                    DeserializeErrorKind::InvalidStructure(format!(
1124                        "unexpected EOF in <{}>",
1125                        String::from_utf8_lossy(element_name)
1126                    )),
1127                    path.to_vec(),
1128                ))
1129            }
1130            Ok(_) => {}
1131            Err(e) => {
1132                return Err(DeserializeError::new(
1133                    DeserializeErrorKind::XmlError(e),
1134                    path.to_vec(),
1135                ))
1136            }
1137        }
1138        buf.clear();
1139    }
1140
1141    // Trim leading and trailing whitespace from the text content.
1142    Ok(XmlString::new(text.trim()))
1143}
1144
1145// ---
1146// Helper functions
1147// ---
1148
1149/// Requires that an attribute was present, returning a `MissingAttribute`
1150/// error if it was `None`.
1151fn require_attribute<T>(
1152    value: Option<T>,
1153    attr_name: &str,
1154    path: &[PathElement],
1155) -> Result<T, DeserializeError> {
1156    value.ok_or_else(|| {
1157        let mut attr_path = path.to_vec();
1158        attr_path.push(PathElement::Attribute(attr_name.to_string()));
1159        DeserializeError::new(
1160            DeserializeErrorKind::MissingAttribute(attr_name.to_string()),
1161            attr_path,
1162        )
1163    })
1164}
1165
1166fn parse_xml_string(bytes: &[u8], path: &[PathElement]) -> Result<XmlString, DeserializeError> {
1167    let s = std::str::from_utf8(bytes)
1168        .map_err(|e| DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec()))?;
1169    let unescaped = unescape_with(s, resolve_xml_entity)
1170        .map_err(|e| DeserializeError::new(DeserializeErrorKind::EscapeError(e), path.to_vec()))?;
1171    Ok(XmlString::new(unescaped.as_ref()))
1172}
1173
1174fn parse_usize(bytes: &[u8], path: &[PathElement]) -> Result<usize, DeserializeError> {
1175    let s = std::str::from_utf8(bytes)
1176        .map_err(|e| DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec()))?;
1177    s.parse()
1178        .map_err(|e| DeserializeError::new(DeserializeErrorKind::ParseIntError(e), path.to_vec()))
1179}
1180
1181fn parse_duration(bytes: &[u8], path: &[PathElement]) -> Result<Duration, DeserializeError> {
1182    let s = std::str::from_utf8(bytes)
1183        .map_err(|e| DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec()))?;
1184    let seconds: f64 = s.parse().map_err(|_| {
1185        DeserializeError::new(
1186            DeserializeErrorKind::ParseDurationError(s.to_string()),
1187            path.to_vec(),
1188        )
1189    })?;
1190
1191    Duration::try_from_secs_f64(seconds).map_err(|_| {
1192        DeserializeError::new(
1193            DeserializeErrorKind::ParseDurationError(s.to_string()),
1194            path.to_vec(),
1195        )
1196    })
1197}
1198
1199fn parse_timestamp(
1200    bytes: &[u8],
1201    path: &[PathElement],
1202) -> Result<DateTime<FixedOffset>, DeserializeError> {
1203    let s = std::str::from_utf8(bytes)
1204        .map_err(|e| DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec()))?;
1205    DateTime::parse_from_rfc3339(s).map_err(|_| {
1206        DeserializeError::new(
1207            DeserializeErrorKind::ParseTimestampError(s.to_string()),
1208            path.to_vec(),
1209        )
1210    })
1211}
1212
1213fn parse_uuid(bytes: &[u8], path: &[PathElement]) -> Result<ReportUuid, DeserializeError> {
1214    let s = std::str::from_utf8(bytes)
1215        .map_err(|e| DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec()))?;
1216    let uuid = s.parse().map_err(|e| {
1217        DeserializeError::new(DeserializeErrorKind::ParseUuidError(e), path.to_vec())
1218    })?;
1219    Ok(ReportUuid::from_untyped_uuid(uuid))
1220}
1221
1222#[cfg(test)]
1223mod tests {
1224    use super::*;
1225
1226    #[test]
1227    fn test_parse_simple_report() {
1228        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1229<testsuites name="my-test-run" tests="1" failures="0" errors="0">
1230    <testsuite name="my-test-suite" tests="1" disabled="0" errors="0" failures="0">
1231        <testcase name="success-case"/>
1232    </testsuite>
1233</testsuites>
1234"#;
1235
1236        let report = Report::deserialize_from_str(xml).unwrap();
1237        assert_eq!(report.name.as_str(), "my-test-run");
1238        assert_eq!(report.tests, 1);
1239        assert_eq!(report.failures, 0);
1240        assert_eq!(report.errors, 0);
1241        assert_eq!(report.test_suites.len(), 1);
1242
1243        let suite = &report.test_suites[0];
1244        assert_eq!(suite.name.as_str(), "my-test-suite");
1245        assert_eq!(suite.test_cases.len(), 1);
1246
1247        let case = &suite.test_cases[0];
1248        assert_eq!(case.name.as_str(), "success-case");
1249        assert!(matches!(case.status, TestCaseStatus::Success { .. }));
1250    }
1251
1252    #[test]
1253    fn test_parse_report_with_failure() {
1254        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1255<testsuites name="test-run" tests="1" failures="1" errors="0">
1256    <testsuite name="suite" tests="1" disabled="0" errors="0" failures="1">
1257        <testcase name="failing-test">
1258            <failure message="assertion failed">Expected true but got false</failure>
1259        </testcase>
1260    </testsuite>
1261</testsuites>
1262"#;
1263
1264        let report = Report::deserialize_from_str(xml).unwrap();
1265        let case = &report.test_suites[0].test_cases[0];
1266
1267        match &case.status {
1268            TestCaseStatus::NonSuccess {
1269                kind,
1270                message,
1271                description,
1272                ..
1273            } => {
1274                assert_eq!(*kind, NonSuccessKind::Failure);
1275                assert_eq!(message.as_ref().unwrap().as_str(), "assertion failed");
1276                assert_eq!(
1277                    description.as_ref().unwrap().as_str(),
1278                    "Expected true but got false"
1279                );
1280            }
1281            _ => panic!("Expected NonSuccess status"),
1282        }
1283    }
1284
1285    #[test]
1286    fn test_parse_report_with_properties() {
1287        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1288<testsuites name="test-run" tests="1" failures="0" errors="0">
1289    <testsuite name="suite" tests="1" disabled="0" errors="0" failures="0">
1290        <properties>
1291            <property name="env" value="test"/>
1292            <property name="platform" value="linux"/>
1293        </properties>
1294        <testcase name="test"/>
1295    </testsuite>
1296</testsuites>
1297"#;
1298
1299        let report = Report::deserialize_from_str(xml).unwrap();
1300        let suite = &report.test_suites[0];
1301
1302        assert_eq!(suite.properties.len(), 2);
1303        assert_eq!(suite.properties[0].name.as_str(), "env");
1304        assert_eq!(suite.properties[0].value.as_str(), "test");
1305        assert_eq!(suite.properties[1].name.as_str(), "platform");
1306        assert_eq!(suite.properties[1].value.as_str(), "linux");
1307    }
1308}