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