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