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