junit_parser/
lib.rs

1//! Library to parse JUnit XML files
2//!
3//! # Example
4//!
5//! Parsing a JUnit content
6//!
7//! ```
8//! use std::io::Cursor;
9//!     let xml = r#"
10//! <testsuite tests="3" failures="1">
11//!   <testcase classname="foo1" name="ASuccessfulTest"/>
12//!   <testcase classname="foo2" name="AnotherSuccessfulTest"/>
13//!   <testcase classname="foo3" name="AFailingTest">
14//!     <failure type="NotEnoughFoo"> details about failure </failure>
15//!   </testcase>
16//! </testsuite>
17//! "#;
18//!     let cursor = Cursor::new(xml);
19//!     let r = junit_parser::from_reader(cursor);
20//!     assert!(r.is_ok());
21//!     let t = r.unwrap();
22//!     assert_eq!(t.suites.len(), 1);
23//!     let ts = &t.suites[0];
24//!     assert_eq!(ts.tests, 3);
25//!     assert_eq!(ts.failures, 1);
26//!     assert_eq!(ts.cases.len(), 3);
27//!     assert!(ts.cases[0].status.is_success());
28//!     assert!(ts.cases[2].status.is_failure());
29//! ```
30//!
31//! # Features
32#![cfg_attr(
33    feature = "document-features",
34    cfg_attr(doc, doc = ::document_features::document_features!())
35)]
36#![forbid(unsafe_code)]
37#![deny(missing_docs)]
38// Enable feature requirements in the docs from 1.57
39// See https://stackoverflow.com/questions/61417452
40#![cfg_attr(docsrs, feature(doc_auto_cfg))]
41
42/// Errors
43mod errors;
44
45pub use errors::Error;
46use quick_xml::escape::unescape;
47use quick_xml::events::BytesStart as XMLBytesStart;
48use quick_xml::events::Event as XMLEvent;
49use quick_xml::name::QName;
50use quick_xml::Reader as XMLReader;
51use std::borrow::Cow;
52#[cfg(feature = "properties_as_hashmap")]
53use std::collections::HashMap;
54use std::io::prelude::*;
55use std::str;
56use std::vec::Vec;
57
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59#[derive(Debug, Clone, Default)]
60/// Properties associated to a [`TestSuite`] or a [`TestCase`]
61pub struct Properties {
62    /// Hashmap of the properties
63    #[cfg(feature = "properties_as_hashmap")]
64    pub hashmap: HashMap<String, String>,
65    /// Vector of the properties
66    #[cfg(feature = "properties_as_vector")]
67    pub vec: Vec<(String, String)>,
68}
69
70/// Parse attributes of a `property` element
71fn parse_property<B: BufRead>(
72    e: &XMLBytesStart,
73    r: Option<&mut XMLReader<B>>,
74) -> Result<(String, String), Error> {
75    let mut k: Option<String> = None;
76    let mut v: Option<String> = None;
77    for a in e.attributes() {
78        let a = a?;
79        match a.key {
80            QName(b"name") => k = Some(try_from_attribute_value_string(a.value)?),
81            QName(b"value") => v = Some(try_from_attribute_value_string(a.value)?),
82            _ => {}
83        };
84    }
85    if let Some(r) = r {
86        loop {
87            let mut buf = Vec::new();
88            match r.read_event_into(&mut buf) {
89                Ok(XMLEvent::End(ref e)) if e.name() == QName(b"property") => break,
90                Ok(XMLEvent::Eof) => {
91                    return Err(Error::UnexpectedEndOfFile("property".to_string()));
92                }
93                Ok(XMLEvent::Text(e)) => {
94                    if v.is_none() {
95                        v = Some(e.unescape()?.trim().to_string());
96                    } else {
97                        v.as_mut().unwrap().push('\n');
98                        v.as_mut().unwrap().push_str(e.unescape()?.trim());
99                    }
100                }
101                Ok(XMLEvent::CData(e)) => {
102                    if v.is_none() {
103                        v = Some(str::from_utf8(&e)?.to_string());
104                    } else {
105                        v.as_mut().unwrap().push('\n');
106                        v.as_mut().unwrap().push_str(str::from_utf8(&e)?);
107                    }
108                }
109                Ok(XMLEvent::Start(ref e)) => {
110                    r.read_to_end_into(e.name(), &mut Vec::new())?;
111                }
112                Err(err) => return Err(err.into()),
113                _ => (),
114            }
115            buf.clear();
116        }
117    }
118    match (k, v) {
119        (Some(k), Some(v)) => Ok((k, v)),
120        (Some(k), None) => Ok((k, "".to_string())),
121        _ => Err(Error::MissingPropertyName),
122    }
123}
124
125impl Properties {
126    /// Create a [`Properties`] from a XML `properties` element
127    fn from_reader<B: BufRead>(r: &mut XMLReader<B>) -> Result<Self, Error> {
128        let mut p = Self::default();
129        loop {
130            let mut buf = Vec::new();
131            match r.read_event_into(&mut buf) {
132                Ok(XMLEvent::End(ref e)) if e.name() == QName(b"properties") => break,
133
134                Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"property") => {
135                    let (k, v) = parse_property::<B>(e, None)?;
136                    p.add_property(k, v);
137                }
138                Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"property") => {
139                    let (k, v) = parse_property(e, Some(r))?;
140                    p.add_property(k, v);
141                }
142                Ok(XMLEvent::Start(ref e)) => {
143                    r.read_to_end_into(e.name(), &mut Vec::new())?;
144                }
145                Ok(XMLEvent::Eof) => {
146                    return Err(Error::UnexpectedEndOfFile("properties".to_string()));
147                }
148                Err(err) => return Err(err.into()),
149                _ => (),
150            }
151            buf.clear();
152        }
153        Ok(p)
154    }
155
156    // `key` and `value` if no feature to store them
157    #[cfg_attr(
158        all(
159            not(feature = "properties_as_hashmap"),
160            not(feature = "properties_as_vector")
161        ),
162        allow(unused_variables)
163    )]
164    /// Add a property to the set of properties
165    fn add_property(&mut self, key: String, value: String) {
166        #[cfg(feature = "properties_as_hashmap")]
167        self.hashmap.insert(key.clone(), value.clone());
168        #[cfg(feature = "properties_as_vector")]
169        self.vec.push((key, value));
170    }
171}
172
173#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
174#[derive(Debug, Clone, Copy, Default)]
175/// The kind of rerun or flaky test result
176pub enum RerunOrFlakyKind {
177    /// Flaky failure
178    #[default]
179    FlakyFailure,
180    /// Flaky error
181    FlakyError,
182    /// Rerun failure
183    RerunFailure,
184    /// Rerun error
185    RerunError,
186}
187
188#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
189#[derive(Debug, Clone, Default)]
190/// Value from a `<flakyFailure />`, `<rerunFailure />`, `<flakyError />`, `<rerunError />` tag
191pub struct RerunOrFlaky {
192    /// The `timestamp` attribute
193    pub timestamp: Option<String>,
194    /// The `time` attribute
195    pub time: f64,
196    /// The `type` attribute
197    pub rerun_type: String,
198    /// The `message` attribute
199    pub message: String,
200    /// Body of the tag
201    pub text: String,
202    /// stdout output from the `system-out` element nested within
203    pub system_out: Option<String>,
204    /// stderr output from the `system-err` element nested within
205    pub system_err: Option<String>,
206    /// Stack trace of the flaky failure
207    pub stack_trace: Option<String>,
208    /// The kind of rerun
209    pub kind: RerunOrFlakyKind,
210}
211
212impl RerunOrFlaky {
213    /// Fill up `self` with attributes from the XML tag
214    fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
215        for a in e.attributes() {
216            let a = a?;
217            match a.key {
218                // The schema specifies 'type' attribute, mapping to rerun_type
219                QName(b"type") => self.rerun_type = try_from_attribute_value_string(a.value)?,
220                QName(b"time") => self.time = try_from_attribute_value_f64(a.value)?,
221                QName(b"timestamp") => {
222                    self.timestamp = Some(try_from_attribute_value_string(a.value)?)
223                }
224                QName(b"message") => self.message = try_from_attribute_value_string(a.value)?,
225                _ => {}
226            };
227        }
228        Ok(())
229    }
230
231    /// New [`RerunOrFlaky`] from empty XML tag, requires the kind of rerun/flaky based on the tag name.
232    fn new_empty(e: &XMLBytesStart, kind: RerunOrFlakyKind) -> Result<Self, Error> {
233        let mut rt = Self {
234            kind,
235            ..Default::default()
236        };
237        rt.parse_attributes(e)?;
238        Ok(rt)
239    }
240
241    /// New [`RerunOrFlaky`] from XML tree, requires the kind of rerun based on the tag name.
242    fn from_reader<B: BufRead>(
243        e: &XMLBytesStart,
244        r: &mut XMLReader<B>,
245        kind: RerunOrFlakyKind,
246    ) -> Result<Self, Error> {
247        let mut rt = Self {
248            kind,
249            system_out: None,
250            system_err: None,
251            stack_trace: None,
252            ..Default::default()
253        };
254        rt.parse_attributes(e)?;
255
256        let end_tag_name = e.name();
257
258        loop {
259            let mut buf = Vec::new();
260            match r.read_event_into(&mut buf) {
261                Ok(XMLEvent::End(ref end_event)) if end_event.name() == end_tag_name => break,
262                Ok(XMLEvent::Text(e)) => {
263                    if rt.text.is_empty() {
264                        rt.text = e.unescape()?.trim().to_string();
265                    } else {
266                        rt.text.push('\n');
267                        rt.text.push_str(e.unescape()?.trim());
268                    }
269                }
270                Ok(XMLEvent::CData(e)) => {
271                    if rt.text.is_empty() {
272                        rt.text = str::from_utf8(&e)?.to_string();
273                    } else {
274                        rt.text.push('\n');
275                        rt.text.push_str(str::from_utf8(&e)?);
276                    }
277                }
278                Ok(XMLEvent::Start(ref start_event)) => match start_event.name() {
279                    QName(b"system-out") => {
280                        if let Some(parsed_content) = parse_system(start_event, r)? {
281                            let current_out = rt.system_out.get_or_insert_with(String::new);
282                            if !current_out.is_empty() {
283                                current_out.push('\n');
284                            }
285                            current_out.push_str(&parsed_content);
286                        }
287                    }
288                    QName(b"system-err") => {
289                        if let Some(parsed_content) = parse_system(start_event, r)? {
290                            let current_err = rt.system_err.get_or_insert_with(String::new);
291                            if !current_err.is_empty() {
292                                current_err.push('\n');
293                            }
294                            current_err.push_str(&parsed_content);
295                        }
296                    }
297                    QName(b"stackTrace") => {
298                        // Overwrite stackTrace as multiple instances are unlikely/undefined
299                        rt.stack_trace = parse_system(start_event, r)?;
300                    }
301                    _ => {
302                        r.read_to_end_into(start_event.name(), &mut Vec::new())?;
303                    }
304                },
305                Ok(XMLEvent::Empty(ref empty_event)) => match empty_event.name() {
306                    QName(b"system-out") => {}
307                    QName(b"system-err") => {}
308                    QName(b"stackTrace") => {}
309                    _ => {}
310                },
311                Ok(XMLEvent::Eof) => {
312                    let tag_name = String::from_utf8_lossy(end_tag_name.as_ref()).to_string();
313                    return Err(Error::UnexpectedEndOfFile(tag_name));
314                }
315                Err(err) => return Err(err.into()),
316                _ => (),
317            }
318            buf.clear();
319        }
320        Ok(rt)
321    }
322}
323
324#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
325#[derive(Debug, Clone, Default)]
326/// Value from a `<failure />` tag
327pub struct TestFailure {
328    /// The `message` attribute
329    pub message: String,
330    /// Body of the `<failure />` tag
331    pub text: String,
332    /// The `type` attribute
333    pub failure_type: String,
334}
335impl TestFailure {
336    /// Fill up `self` with attributes from the XML tag
337    fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
338        for a in e.attributes() {
339            let a = a?;
340            match a.key {
341                QName(b"type") => self.failure_type = try_from_attribute_value_string(a.value)?,
342                QName(b"message") => self.message = try_from_attribute_value_string(a.value)?,
343                _ => {}
344            };
345        }
346        Ok(())
347    }
348
349    /// New [`TestFailure`] from empty XML tag
350    fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
351        let mut tf = Self::default();
352        tf.parse_attributes(e)?;
353        Ok(tf)
354    }
355
356    /// New [`TestFailure`] from XML tree
357    fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
358        let mut tf = Self::default();
359        tf.parse_attributes(e)?;
360        loop {
361            let mut buf = Vec::new();
362            match r.read_event_into(&mut buf) {
363                Ok(XMLEvent::End(ref e)) if e.name() == QName(b"failure") => break,
364                Ok(XMLEvent::Text(e)) => {
365                    if tf.text.is_empty() {
366                        tf.text = e.unescape()?.trim().to_string();
367                    } else {
368                        tf.text.push('\n');
369                        tf.text.push_str(e.unescape()?.trim());
370                    }
371                }
372                Ok(XMLEvent::Eof) => {
373                    return Err(Error::UnexpectedEndOfFile("failure".to_string()));
374                }
375                Ok(XMLEvent::CData(e)) => {
376                    if tf.text.is_empty() {
377                        tf.text = str::from_utf8(&e)?.to_string();
378                    } else {
379                        tf.text.push('\n');
380                        tf.text.push_str(str::from_utf8(&e)?);
381                    }
382                }
383                Ok(XMLEvent::Start(ref e)) => {
384                    r.read_to_end_into(e.name(), &mut Vec::new())?;
385                }
386                Err(err) => return Err(err.into()),
387                _ => (),
388            }
389            buf.clear();
390        }
391        Ok(tf)
392    }
393}
394
395#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
396#[derive(Debug, Clone, Default)]
397/// Value from an `<error />` tag
398pub struct TestError {
399    /// The `message` attribute
400    pub message: String,
401    /// Body of the `<error />` tag
402    pub text: String,
403    /// The `type` attribute
404    pub error_type: String,
405}
406impl TestError {
407    /// Fill up `self` with attributes from the XML tag
408    fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
409        for a in e.attributes() {
410            let a = a?;
411            match a.key {
412                QName(b"type") => self.error_type = try_from_attribute_value_string(a.value)?,
413                QName(b"message") => self.message = try_from_attribute_value_string(a.value)?,
414                _ => {}
415            };
416        }
417        Ok(())
418    }
419
420    /// New [`TestError`] from empty XML tag
421    fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
422        let mut te = Self::default();
423        te.parse_attributes(e)?;
424        Ok(te)
425    }
426
427    /// New [`TestError`] from XML tree
428    fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
429        let mut te = Self::default();
430        te.parse_attributes(e)?;
431        loop {
432            let mut buf = Vec::new();
433            match r.read_event_into(&mut buf) {
434                Ok(XMLEvent::End(ref e)) if e.name() == QName(b"error") => break,
435                Ok(XMLEvent::Text(e)) => {
436                    if te.text.is_empty() {
437                        te.text = e.unescape()?.trim().to_string();
438                    } else {
439                        te.text.push('\n');
440                        te.text.push_str(e.unescape()?.trim());
441                    }
442                }
443                Ok(XMLEvent::Eof) => {
444                    return Err(Error::UnexpectedEndOfFile("error".to_string()));
445                }
446                Ok(XMLEvent::CData(e)) => {
447                    if te.text.is_empty() {
448                        te.text = str::from_utf8(&e)?.to_string();
449                    } else {
450                        te.text.push('\n');
451                        te.text.push_str(str::from_utf8(&e)?);
452                    }
453                }
454                Ok(XMLEvent::Start(ref e)) => {
455                    r.read_to_end_into(e.name(), &mut Vec::new())?;
456                }
457                Err(err) => return Err(err.into()),
458                _ => (),
459            }
460            buf.clear();
461        }
462        Ok(te)
463    }
464}
465
466#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
467#[derive(Debug, Clone, Default)]
468/// Value from a `<skipped />` tag
469pub struct TestSkipped {
470    /// The `message` attribute
471    pub message: String,
472    /// Body of the `<skipped />` tag
473    pub text: String,
474    /// The `type` attribute
475    pub skipped_type: String,
476}
477impl TestSkipped {
478    /// Fill up `self` with attributes from the XML tag
479    fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
480        for a in e.attributes() {
481            let a = a?;
482            match a.key {
483                QName(b"type") => self.skipped_type = try_from_attribute_value_string(a.value)?,
484                QName(b"message") => self.message = try_from_attribute_value_string(a.value)?,
485                _ => {}
486            };
487        }
488        Ok(())
489    }
490
491    /// New [`TestSkipped`] from empty XML tag
492    fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
493        let mut ts = Self::default();
494        ts.parse_attributes(e)?;
495        Ok(ts)
496    }
497
498    /// New [`TestSkipped`] from XML tree
499    fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
500        let mut ts = Self::default();
501        ts.parse_attributes(e)?;
502        loop {
503            let mut buf = Vec::new();
504            match r.read_event_into(&mut buf) {
505                Ok(XMLEvent::End(ref e)) if e.name() == QName(b"skipped") => break,
506                Ok(XMLEvent::Text(e)) => {
507                    if ts.text.is_empty() {
508                        ts.text = e.unescape()?.trim().to_string();
509                    } else {
510                        ts.text.push('\n');
511                        ts.text.push_str(e.unescape()?.trim());
512                    }
513                }
514                Ok(XMLEvent::Eof) => {
515                    return Err(Error::UnexpectedEndOfFile("skipped".to_string()));
516                }
517                Ok(XMLEvent::CData(e)) => {
518                    if ts.text.is_empty() {
519                        ts.text = str::from_utf8(&e)?.to_string();
520                    } else {
521                        ts.text.push('\n');
522                        ts.text.push_str(str::from_utf8(&e)?);
523                    }
524                }
525                Ok(XMLEvent::Start(ref e)) => {
526                    r.read_to_end_into(e.name(), &mut Vec::new())?;
527                }
528                Err(err) => return Err(err.into()),
529                _ => (),
530            }
531            buf.clear();
532        }
533        Ok(ts)
534    }
535}
536
537#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
538#[derive(Debug, Clone, Default)]
539/// Status of a test case
540pub enum TestStatus {
541    /// Success
542    #[default]
543    Success,
544    /// Test case has a `<error />` tag
545    Error(TestError),
546    /// Test case has a `<failure />` tag
547    Failure(TestFailure),
548    /// Test case has a `<skipped />` tag
549    Skipped(TestSkipped),
550}
551impl TestStatus {
552    /// Returns `true` if the `TestStatus` is [`Success`](#variant.Success).
553    pub fn is_success(&self) -> bool {
554        matches!(self, TestStatus::Success)
555    }
556    /// Returns `true` if the `TestStatus` is [`Error(_)`](#variant.Error).
557    pub fn is_error(&self) -> bool {
558        matches!(self, TestStatus::Error(_))
559    }
560    /// Returns the contained [`Error(_)`](#variant.Error) value as a reference
561    ///
562    /// # Panics
563    ///
564    /// Panics if the value is not an [`Errror(_)`](#variant.Error)
565    pub fn error_as_ref(&self) -> &TestError {
566        if let TestStatus::Error(e) = self {
567            return e;
568        }
569        panic!("called `TestStatus::error()` on a value that is not TestStatus::Error(_)");
570    }
571
572    /// Returns `true` if the `TestStatus` is [`Failure(_)`](#variant.Failure).
573    pub fn is_failure(&self) -> bool {
574        matches!(self, TestStatus::Failure(_))
575    }
576
577    /// Returns the contained [`Failure(_)`](#variant.Failure) value as a reference
578    ///
579    /// # Panics
580    ///
581    /// Panics if the value is not a [`Failure(_)`](#variant.Failure)
582    pub fn failure_as_ref(&self) -> &TestFailure {
583        if let TestStatus::Failure(e) = self {
584            return e;
585        }
586        panic!("called `TestStatus::failure()` on a value that is not TestStatus::Failure(_)");
587    }
588
589    /// Returns `true` if the `TestStatus` is [`Skipped(_)`](#variant.Skipped).
590    pub fn is_skipped(&self) -> bool {
591        matches!(self, TestStatus::Skipped(_))
592    }
593
594    /// Returns the contained [`Skipped(_)`](#variant.Skipped) value as a reference
595    ///
596    /// # Panics
597    ///
598    /// Panics if the value is not a [`Skipped(_)`](#variant.Skipped)
599    pub fn skipped_as_ref(&self) -> &TestSkipped {
600        if let TestStatus::Skipped(e) = self {
601            return e;
602        }
603        panic!("called `TestStatus::skipped()` on a value that is not TestStatus::Skipped(_)");
604    }
605}
606
607#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
608#[derive(Debug, Clone, Default)]
609/// A test case
610pub struct TestCase {
611    /// How long the test case took to run, from the `time` attribute
612    pub time: f64,
613    /// Name of the test case, from the `name` attribute
614    /// If there is a `classname` attribute, store it as `classname::name`
615    /// Otherwise if there is a `group` attribute, store it as `group::name`
616    /// See [`TestCase::original_name`] for the original name
617    pub name: String,
618    /// Status of the test case
619    pub status: TestStatus,
620    /// Original name, from the `name` attribute
621    pub original_name: String,
622    /// Class name, from the `classname` attribute
623    pub classname: Option<String>,
624    /// Group name, from the `group` attribute
625    pub group: Option<String>,
626    /// File source code of the test
627    pub file: Option<String>,
628    /// Related line in the source code
629    pub line: Option<u64>,
630    /// stdout output from the `system-out` element
631    pub system_out: Option<String>,
632    /// stderr output from the `system-err` element
633    pub system_err: Option<String>,
634    /// Properties of the test case
635    pub properties: Properties,
636    /// Reruns of the test case
637    pub reruns: Vec<RerunOrFlaky>,
638}
639impl TestCase {
640    /// Fill up `self` with attributes from the XML tag
641    fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
642        for a in e.attributes() {
643            let a = a?;
644            match a.key {
645                QName(b"time") => self.time = try_from_attribute_value_f64(a.value)?,
646                QName(b"name") => self.original_name = try_from_attribute_value_string(a.value)?,
647                QName(b"classname") => {
648                    self.classname = Some(try_from_attribute_value_string(a.value)?)
649                }
650                QName(b"group") => self.group = Some(try_from_attribute_value_string(a.value)?),
651                QName(b"file") => self.file = Some(try_from_attribute_value_string(a.value)?),
652                QName(b"line") => self.line = Some(try_from_attribute_value_u64(a.value)?),
653                _ => {}
654            };
655        }
656        if let Some(cn) = self.classname.as_ref() {
657            self.name = format!("{}::{}", cn, self.original_name);
658        } else if let Some(gn) = self.group.as_ref() {
659            self.name = format!("{}::{}", gn, self.original_name);
660        } else {
661            self.name.clone_from(&self.original_name);
662        }
663        Ok(())
664    }
665
666    /// New [`TestCase`] from empty XML tag
667    fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
668        let mut tc = Self::default();
669        tc.parse_attributes(e)?;
670        Ok(tc)
671    }
672
673    /// New [`TestCase`] from XML tree
674    fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
675        let mut tc = Self {
676            system_out: None,
677            system_err: None,
678            ..Default::default()
679        };
680        tc.parse_attributes(e)?;
681        loop {
682            let mut buf = Vec::new();
683            match r.read_event_into(&mut buf) {
684                Ok(XMLEvent::End(ref e)) if e.name() == QName(b"testcase") => break,
685
686                Ok(XMLEvent::Empty(ref empty_event)) => match empty_event.name() {
687                    QName(b"system-out") => {
688                        if tc.system_out.is_none() {
689                            tc.system_out = Some(String::new());
690                        }
691                    }
692                    QName(b"system-err") => {
693                        if tc.system_err.is_none() {
694                            tc.system_err = Some(String::new());
695                        }
696                    }
697                    QName(b"flakyFailure") => {
698                        tc.reruns.push(RerunOrFlaky::new_empty(
699                            empty_event,
700                            RerunOrFlakyKind::FlakyFailure,
701                        )?);
702                    }
703                    QName(b"flakyError") => {
704                        tc.reruns.push(RerunOrFlaky::new_empty(
705                            empty_event,
706                            RerunOrFlakyKind::FlakyError,
707                        )?);
708                    }
709                    QName(b"rerunFailure") => {
710                        tc.reruns.push(RerunOrFlaky::new_empty(
711                            empty_event,
712                            RerunOrFlakyKind::RerunFailure,
713                        )?);
714                    }
715                    QName(b"rerunError") => {
716                        tc.reruns.push(RerunOrFlaky::new_empty(
717                            empty_event,
718                            RerunOrFlakyKind::RerunError,
719                        )?);
720                    }
721                    QName(b"skipped") => {
722                        tc.status = TestStatus::Skipped(TestSkipped::new_empty(empty_event)?);
723                    }
724                    QName(b"failure") => {
725                        tc.status = TestStatus::Failure(TestFailure::new_empty(empty_event)?);
726                    }
727                    QName(b"error") => {
728                        tc.status = TestStatus::Error(TestError::new_empty(empty_event)?);
729                    }
730                    _ => {}
731                },
732
733                Ok(XMLEvent::Start(ref start_event)) => match start_event.name() {
734                    QName(b"skipped") => {
735                        tc.status = TestStatus::Skipped(TestSkipped::from_reader(start_event, r)?);
736                    }
737                    QName(b"failure") => {
738                        tc.status = TestStatus::Failure(TestFailure::from_reader(start_event, r)?);
739                    }
740                    QName(b"error") => {
741                        tc.status = TestStatus::Error(TestError::from_reader(start_event, r)?);
742                    }
743                    QName(b"flakyFailure") => {
744                        tc.reruns.push(RerunOrFlaky::from_reader(
745                            start_event,
746                            r,
747                            RerunOrFlakyKind::FlakyFailure,
748                        )?);
749                    }
750                    QName(b"flakyError") => {
751                        tc.reruns.push(RerunOrFlaky::from_reader(
752                            start_event,
753                            r,
754                            RerunOrFlakyKind::FlakyError,
755                        )?);
756                    }
757                    QName(b"rerunFailure") => {
758                        tc.reruns.push(RerunOrFlaky::from_reader(
759                            start_event,
760                            r,
761                            RerunOrFlakyKind::RerunFailure,
762                        )?);
763                    }
764                    QName(b"rerunError") => {
765                        tc.reruns.push(RerunOrFlaky::from_reader(
766                            start_event,
767                            r,
768                            RerunOrFlakyKind::RerunError,
769                        )?);
770                    }
771                    QName(b"system-out") => {
772                        if let Some(parsed_content) = parse_system(start_event, r)? {
773                            let current_out = tc.system_out.get_or_insert_with(String::new);
774                            if !current_out.is_empty() {
775                                current_out.push('\n');
776                            }
777                            current_out.push_str(&parsed_content);
778                        }
779                    }
780                    QName(b"system-err") => {
781                        if let Some(parsed_content) = parse_system(start_event, r)? {
782                            let current_err = tc.system_err.get_or_insert_with(String::new);
783                            if !current_err.is_empty() {
784                                current_err.push('\n');
785                            }
786                            current_err.push_str(&parsed_content);
787                        }
788                    }
789                    QName(b"properties") => {
790                        tc.properties = Properties::from_reader(r)?;
791                    }
792                    _ => {
793                        r.read_to_end_into(start_event.name(), &mut Vec::new())?;
794                    }
795                },
796                Ok(XMLEvent::Eof) => {
797                    return Err(Error::UnexpectedEndOfFile("testcase".to_string()))
798                }
799                Err(err) => return Err(err.into()),
800                _ => (),
801            }
802            buf.clear();
803        }
804        Ok(tc)
805    }
806}
807
808#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
809#[derive(Debug, Clone, Default)]
810/// A test suite, containing test cases [`TestCase`](struct.TestCase.html)
811pub struct TestSuite {
812    /// List of status of tests represented by [`TestCase`]
813    pub cases: Vec<TestCase>,
814    /// List of tests suites represented by [`TestSuite`]
815    pub suites: Vec<TestSuite>,
816    /// How long the test suite took to run, from the `time` attribute
817    pub time: f64,
818    /// Number of tests in the test suite, from the `tests` attribute
819    pub tests: u64,
820    /// Number of tests in error in the test suite, from the `errors` attribute
821    pub errors: u64,
822    /// Number of tests in failure in the test suite, from the `failures` attribute
823    pub failures: u64,
824    /// Number of tests skipped in the test suites, from the `skipped` attribute
825    pub skipped: u64,
826    /// Number of assertions in the test suites, from the `assertions` attribute
827    pub assertions: Option<u64>,
828    /// Name of the test suite, from the `name` attribute
829    pub name: String,
830    /// Timestamp when the test suite was run, from the `timestamp` attribute
831    pub timestamp: Option<String>,
832    /// Hostname where the test suite was run, from the `hostname` attribute
833    pub hostname: Option<String>,
834    /// Identifier of the test suite, from the `id` attribute
835    pub id: Option<String>,
836    /// Package of the test suite, from the `package` attribute
837    pub package: Option<String>,
838    /// Source code file of the test suite, from the `file` attribute
839    pub file: Option<String>,
840    /// Logger of the test suite, from the `log` attribute
841    pub log: Option<String>,
842    /// URL of the test suite, from the `uri` attribute
843    pub url: Option<String>,
844    /// Version of the test suite, from the `version` attribute
845    pub version: Option<String>,
846    /// stdout output from the `system-out` element
847    pub system_out: Option<String>,
848    /// stderr output from the `system-err` element
849    pub system_err: Option<String>,
850    /// Properties of the test suite
851    pub properties: Properties,
852}
853impl TestSuite {
854    /// Fill up `self` with attributes from the XML tag
855    fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
856        for a in e.attributes() {
857            let a = a?;
858            match a.key {
859                QName(b"time") => self.time = try_from_attribute_value_f64(a.value)?,
860                QName(b"tests") => self.tests = try_from_attribute_value_u64(a.value)?,
861                QName(b"errors") => self.errors = try_from_attribute_value_u64(a.value)?,
862                QName(b"failures") => self.failures = try_from_attribute_value_u64(a.value)?,
863                QName(b"skipped") => self.skipped = try_from_attribute_value_u64(a.value)?,
864                QName(b"assertions") => {
865                    self.assertions = Some(try_from_attribute_value_u64(a.value)?)
866                }
867                QName(b"name") => self.name = try_from_attribute_value_string(a.value)?,
868                QName(b"timestamp") => {
869                    self.timestamp = Some(try_from_attribute_value_string(a.value)?)
870                }
871                QName(b"hostname") => {
872                    self.hostname = Some(try_from_attribute_value_string(a.value)?)
873                }
874                QName(b"id") => self.id = Some(try_from_attribute_value_string(a.value)?),
875                QName(b"package") => self.package = Some(try_from_attribute_value_string(a.value)?),
876                QName(b"file") => self.file = Some(try_from_attribute_value_string(a.value)?),
877                QName(b"log") => self.log = Some(try_from_attribute_value_string(a.value)?),
878                QName(b"url") => self.url = Some(try_from_attribute_value_string(a.value)?),
879                QName(b"version") => self.version = Some(try_from_attribute_value_string(a.value)?),
880                _ => {}
881            };
882        }
883        Ok(())
884    }
885
886    /// New [`TestSuite`] from empty XML tag
887    fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
888        let mut ts = Self::default();
889        ts.parse_attributes(e)?;
890        Ok(ts)
891    }
892
893    /// New [`TestSuite`] from XML tree
894    fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
895        let mut ts = Self::default();
896        ts.parse_attributes(e)?;
897        loop {
898            let mut buf = Vec::new();
899            match r.read_event_into(&mut buf) {
900                Ok(XMLEvent::End(ref e)) if e.name() == QName(b"testsuite") => break,
901                Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testsuite") => {
902                    ts.suites.push(TestSuite::from_reader(e, r)?);
903                }
904                Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testcase") => {
905                    ts.cases.push(TestCase::from_reader(e, r)?);
906                }
907                Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"testcase") => {
908                    ts.cases.push(TestCase::new_empty(e)?);
909                }
910                Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"system-out") => {}
911                Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"system-out") => {
912                    ts.system_out = parse_system(e, r)?;
913                }
914                Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"system-err") => {}
915                Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"system-err") => {
916                    ts.system_err = parse_system(e, r)?;
917                }
918                Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"properties") => {
919                    ts.properties = Properties::from_reader(r)?;
920                }
921                Ok(XMLEvent::Start(ref e)) => {
922                    r.read_to_end_into(e.name(), &mut Vec::new())?;
923                }
924                Ok(XMLEvent::Eof) => {
925                    return Err(Error::UnexpectedEndOfFile("testsuite".to_string()));
926                }
927                Err(err) => return Err(err.into()),
928                _ => (),
929            }
930            buf.clear();
931        }
932        Ok(ts)
933    }
934}
935
936#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
937#[derive(Debug, Clone, Default)]
938/// Struct representing a JUnit report, containing test suites [`TestSuite`](struct.TestSuite.html)
939pub struct TestSuites {
940    /// List of tests suites represented by [`TestSuite`]
941    pub suites: Vec<TestSuite>,
942    /// How long the test suites took to run, from the `time` attribute
943    pub time: f64,
944    /// Number of tests in the test suites, from the `tests` attribute
945    pub tests: u64,
946    /// Number of tests in error in the test suites, from the `errors` attribute
947    pub errors: u64,
948    /// Number of tests in failure in the test suites, from the `failures` attribute
949    pub failures: u64,
950    /// Number of tests skipped in the test suites, from the `skipped` attribute
951    pub skipped: u64,
952    /// Name of the test suites, from the `name` attribute
953    pub name: String,
954}
955impl TestSuites {
956    /// Fill up `self` with attributes from the XML tag
957    fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
958        for a in e.attributes() {
959            let a = a?;
960            match a.key {
961                QName(b"time") => self.time = try_from_attribute_value_f64(a.value)?,
962                QName(b"tests") => self.tests = try_from_attribute_value_u64(a.value)?,
963                QName(b"errors") => self.errors = try_from_attribute_value_u64(a.value)?,
964                QName(b"failures") => self.failures = try_from_attribute_value_u64(a.value)?,
965                QName(b"skipped") => self.skipped = try_from_attribute_value_u64(a.value)?,
966                QName(b"name") => self.name = try_from_attribute_value_string(a.value)?,
967                _ => {}
968            };
969        }
970        Ok(())
971    }
972
973    /// New [`TestSuites`] from empty XML tag
974    fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
975        let mut ts = Self::default();
976        ts.parse_attributes(e)?;
977        Ok(ts)
978    }
979
980    /// New [`TestSuites`] from XML tree
981    fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
982        let mut ts = Self::default();
983        ts.parse_attributes(e)?;
984        loop {
985            let mut buf = Vec::new();
986            match r.read_event_into(&mut buf) {
987                Ok(XMLEvent::End(ref e)) if e.name() == QName(b"testsuites") => break,
988                Ok(XMLEvent::End(ref e)) if e.name() == QName(b"testrun") => break,
989                Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testsuite") => {
990                    ts.suites.push(TestSuite::from_reader(e, r)?);
991                }
992                Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"testsuite") => {
993                    ts.suites.push(TestSuite::new_empty(e)?);
994                }
995                Ok(XMLEvent::Eof) => {
996                    return Err(Error::UnexpectedEndOfFile("testsuites".to_string()));
997                }
998                Ok(XMLEvent::Start(ref e)) => {
999                    r.read_to_end_into(e.name(), &mut Vec::new())?;
1000                }
1001                Err(err) => return Err(err.into()),
1002                _ => (),
1003            }
1004            buf.clear();
1005        }
1006        Ok(ts)
1007    }
1008}
1009
1010/// Try to decode attribute value as [`f64`]
1011fn try_from_attribute_value_f64(value: Cow<[u8]>) -> Result<f64, Error> {
1012    match str::from_utf8(&value)? {
1013        "" => Ok(f64::default()),
1014        s => Ok(s.parse::<f64>()?),
1015    }
1016}
1017
1018/// Try to decode attribute value as [`u64`]
1019fn try_from_attribute_value_u64(value: Cow<[u8]>) -> Result<u64, Error> {
1020    match str::from_utf8(&value)? {
1021        "" => Ok(u64::default()),
1022        s => Ok(s.parse::<u64>()?),
1023    }
1024}
1025
1026/// Try to decode and unescape attribute value as [`String`]
1027fn try_from_attribute_value_string(value: Cow<[u8]>) -> Result<String, Error> {
1028    let s = str::from_utf8(&value)?;
1029    let u = unescape(s)?;
1030    Ok(u.to_string())
1031}
1032
1033/// Parse a chunk of xml as system-out or system-err
1034fn parse_system<B: BufRead>(
1035    orig: &XMLBytesStart,
1036    r: &mut XMLReader<B>,
1037) -> Result<Option<String>, Error> {
1038    let mut res: Option<String> = Some(String::new());
1039    loop {
1040        let mut buf = Vec::new();
1041        match r.read_event_into(&mut buf) {
1042            Ok(XMLEvent::End(ref e)) if e.name() == orig.name() => break,
1043            Ok(XMLEvent::Text(e)) => {
1044                res.get_or_insert(String::new()).push_str(&e.unescape()?);
1045            }
1046            Ok(XMLEvent::CData(e)) => {
1047                res.get_or_insert(String::new())
1048                    .push_str(str::from_utf8(&e)?);
1049            }
1050            Ok(XMLEvent::Eof) => {
1051                return Err(Error::UnexpectedEndOfFile(format!("{:?}", orig.name())));
1052            }
1053            Ok(XMLEvent::Start(ref e)) => {
1054                r.read_to_end_into(e.name(), &mut Vec::new())?;
1055            }
1056            Err(err) => return Err(err.into()),
1057            _ => (),
1058        }
1059        buf.clear();
1060    }
1061    Ok(res)
1062}
1063
1064/// Creates a [`TestSuites`](struct.TestSuites.html) structure from a JUnit XML data read from `reader`
1065///
1066/// # Example
1067/// ```
1068/// use std::io::Cursor;
1069///     let xml = r#"
1070/// <testsuite tests="3" failures="1">
1071///   <testcase classname="foo1" name="ASuccessfulTest"/>
1072///   <testcase classname="foo2" name="AnotherSuccessfulTest"/>
1073///   <testcase classname="foo3" name="AFailingTest">
1074///     <failure type="NotEnoughFoo"> details about failure </failure>
1075///   </testcase>
1076/// </testsuite>
1077/// "#;
1078///     let cursor = Cursor::new(xml);
1079///     let r = junit_parser::from_reader(cursor);
1080///     assert!(r.is_ok());
1081/// ```
1082pub fn from_reader<B: BufRead>(reader: B) -> Result<TestSuites, Error> {
1083    let mut r = XMLReader::from_reader(reader);
1084    loop {
1085        let mut buf = Vec::new();
1086        match r.read_event_into(&mut buf) {
1087            Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"testsuites") => {
1088                return TestSuites::new_empty(e);
1089            }
1090            Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"testrun") => {
1091                return TestSuites::new_empty(e);
1092            }
1093            Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testsuites") => {
1094                return TestSuites::from_reader(e, &mut r);
1095            }
1096            Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testrun") => {
1097                return TestSuites::from_reader(e, &mut r);
1098            }
1099            Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"testsuite") => {
1100                let ts = TestSuite::new_empty(e)?;
1101                let mut suites = TestSuites::default();
1102                suites.suites.push(ts);
1103                return Ok(suites);
1104            }
1105            Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testsuite") => {
1106                let ts = TestSuite::from_reader(e, &mut r)?;
1107                let mut suites = TestSuites::default();
1108                suites.suites.push(ts);
1109                return Ok(suites);
1110            }
1111            Ok(XMLEvent::Start(ref e)) => {
1112                r.read_to_end_into(e.name(), &mut Vec::new())?;
1113            }
1114            Ok(XMLEvent::Eof) => {
1115                return Err(Error::UnexpectedEndOfFile("testsuites".to_string()));
1116            }
1117            Err(err) => return Err(err.into()),
1118            _ => (),
1119        }
1120        buf.clear();
1121    }
1122}