junit_report/
reports.rs

1/*
2 * Copyright (c) 2018 Pascal Bach
3 * Copyright (c) 2021 Siemens Mobility GmbH
4 *
5 * SPDX-License-Identifier:     MIT
6 */
7
8use std::io::Write;
9
10use derive_getters::Getters;
11use quick_xml::events::BytesDecl;
12use quick_xml::{
13    events::{BytesCData, Event},
14    ElementWriter, Result, Writer,
15};
16use time::format_description::well_known::Rfc3339;
17
18use crate::{TestCase, TestResult, TestSuite};
19
20/// Root element of a JUnit report
21#[derive(Default, Debug, Clone, Getters)]
22pub struct Report {
23    testsuites: Vec<TestSuite>,
24}
25
26impl Report {
27    /// Create a new empty Report
28    pub fn new() -> Report {
29        Report {
30            testsuites: Vec::new(),
31        }
32    }
33
34    /// Add a [`TestSuite`](struct.TestSuite.html) to this report.
35    ///
36    /// The function takes ownership of the supplied [`TestSuite`](struct.TestSuite.html).
37    pub fn add_testsuite(&mut self, testsuite: TestSuite) {
38        self.testsuites.push(testsuite);
39    }
40
41    /// Add multiple[`TestSuite`s](struct.TestSuite.html) from an iterator.
42    pub fn add_testsuites(&mut self, testsuites: impl IntoIterator<Item = TestSuite>) {
43        self.testsuites.extend(testsuites);
44    }
45
46    /// Write the XML version of the Report to the given `Writer`.
47    pub fn write_xml<W: Write>(&self, sink: W) -> Result<()> {
48        let mut writer = Writer::new(sink);
49
50        writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("utf-8"), None)))?;
51
52        writer
53            .create_element("testsuites")
54            .write_empty_or_inner(
55                |_| self.testsuites.is_empty(),
56                |w| {
57                    w.write_iter(self.testsuites.iter().enumerate(), |w, (id, ts)| {
58                        w.create_element("testsuite")
59                            .with_attributes([
60                                ("id", id.to_string().as_str()),
61                                ("name", &ts.name),
62                                ("package", &ts.package),
63                                ("tests", &ts.tests().to_string()),
64                                ("errors", &ts.errors().to_string()),
65                                ("failures", &ts.failures().to_string()),
66                                ("hostname", &ts.hostname),
67                                ("timestamp", &ts.timestamp.format(&Rfc3339).unwrap()),
68                                ("time", &ts.time().as_seconds_f64().to_string()),
69                            ])
70                            .write_empty_or_inner(
71                                |_| {
72                                    ts.testcases.is_empty()
73                                        && ts.system_out.is_none()
74                                        && ts.system_err.is_none()
75                                },
76                                |w| {
77                                    w.write_iter(ts.testcases.iter(), |w, tc| tc.write_xml(w))?
78                                        .write_opt(ts.system_out.as_ref(), |writer, out| {
79                                            writer
80                                                .create_element("system-out")
81                                                .write_cdata_content(BytesCData::new(out))
82                                        })?
83                                        .write_opt(ts.system_err.as_ref(), |writer, err| {
84                                            writer
85                                                .create_element("system-err")
86                                                .write_cdata_content(BytesCData::new(err))
87                                        })
88                                        .map(drop)
89                                },
90                            )
91                    })
92                    .map(drop)
93                },
94            )
95            .map(drop)
96    }
97}
98
99impl TestCase {
100    /// Write the XML version of the [`TestCase`] to the given [`Writer`].
101    fn write_xml<'a, W: Write>(&self, w: &'a mut Writer<W>) -> Result<&'a mut Writer<W>> {
102        let time = self.time.as_seconds_f64().to_string();
103        w.create_element("testcase")
104            .with_attributes(
105                [
106                    Some(("name", self.name.as_str())),
107                    Some(("time", time.as_str())),
108                    self.classname.as_ref().map(|cl| ("classname", cl.as_str())),
109                    self.filepath.as_ref().map(|f| ("file", f.as_str())),
110                ]
111                .into_iter()
112                .flatten(),
113            )
114            .write_empty_or_inner(
115                |_| {
116                    matches!(self.result, TestResult::Success)
117                        && self.system_out.is_none()
118                        && self.system_err.is_none()
119                },
120                |w| {
121                    match self.result {
122                        TestResult::Success => w
123                            .write_opt(self.system_out.as_ref(), |w, out| {
124                                w.create_element("system-out")
125                                    .write_cdata_content(BytesCData::new(out.as_str()))
126                            })?
127                            .write_opt(self.system_err.as_ref(), |w, err| {
128                                w.create_element("system-err")
129                                    .write_cdata_content(BytesCData::new(err.as_str()))
130                            }),
131                        TestResult::Error {
132                            ref type_,
133                            ref message,
134                        } => w
135                            .create_element("error")
136                            .with_attributes([
137                                ("type", type_.as_str()),
138                                ("message", message.as_str()),
139                            ])
140                            .write_empty_or_inner(
141                                |_| self.system_out.is_none() && self.system_err.is_none(),
142                                |w| {
143                                    w.write_opt(self.system_out.as_ref(), |w, stdout| {
144                                        let data = strip_ansi_escapes::strip(stdout);
145                                        w.write_event(Event::CData(BytesCData::new(
146                                            String::from_utf8_lossy(&data),
147                                        )))
148                                        .map(|_| w)
149                                    })?
150                                    .write_opt(self.system_err.as_ref(), |w, stderr| {
151                                        let data = strip_ansi_escapes::strip(stderr);
152                                        w.write_event(Event::CData(BytesCData::new(
153                                            String::from_utf8_lossy(&data),
154                                        )))
155                                        .map(|_| w)
156                                    })
157                                    .map(drop)
158                                },
159                            ),
160                        TestResult::Failure {
161                            ref type_,
162                            ref message,
163                        } => w
164                            .create_element("failure")
165                            .with_attributes([
166                                ("type", type_.as_str()),
167                                ("message", message.as_str()),
168                            ])
169                            .write_empty_or_inner(
170                                |_| self.system_out.is_none() && self.system_err.is_none(),
171                                |w| {
172                                    w.write_opt(self.system_out.as_ref(), |w, stdout| {
173                                        let data = strip_ansi_escapes::strip(stdout);
174                                        w.write_event(Event::CData(BytesCData::new(
175                                            String::from_utf8_lossy(&data),
176                                        )))
177                                        .map(|_| w)
178                                    })?
179                                    .write_opt(self.system_err.as_ref(), |w, stderr| {
180                                        let data = strip_ansi_escapes::strip(stderr);
181                                        w.write_event(Event::CData(BytesCData::new(
182                                            String::from_utf8_lossy(&data),
183                                        )))
184                                        .map(|_| w)
185                                    })
186                                    .map(drop)
187                                },
188                            ),
189                        TestResult::Skipped => w.create_element("skipped").write_empty(),
190                    }
191                    .map(drop)
192                },
193            )
194    }
195}
196
197/// Builder for JUnit [`Report`](struct.Report.html) objects
198#[derive(Default, Debug, Clone, Getters)]
199pub struct ReportBuilder {
200    report: Report,
201}
202
203impl ReportBuilder {
204    /// Create a new empty ReportBuilder
205    pub fn new() -> ReportBuilder {
206        ReportBuilder {
207            report: Report::new(),
208        }
209    }
210
211    /// Add a [`TestSuite`](struct.TestSuite.html) to this report builder.
212    ///
213    /// The function takes ownership of the supplied [`TestSuite`](struct.TestSuite.html).
214    pub fn add_testsuite(&mut self, testsuite: TestSuite) -> &mut Self {
215        self.report.testsuites.push(testsuite);
216        self
217    }
218
219    /// Add multiple[`TestSuite`s](struct.TestSuite.html) from an iterator.
220    pub fn add_testsuites(&mut self, testsuites: impl IntoIterator<Item = TestSuite>) -> &mut Self {
221        self.report.testsuites.extend(testsuites);
222        self
223    }
224
225    /// Build and return a [`Report`](struct.Report.html) object based on the data stored in this ReportBuilder object.
226    pub fn build(&self) -> Report {
227        self.report.clone()
228    }
229}
230
231/// [`Writer`] extension.
232trait WriterExt {
233    /// [`Write`]s in case `val` is [`Some`] or does nothing otherwise.
234    fn write_opt<T>(
235        &mut self,
236        val: Option<T>,
237        inner: impl FnOnce(&mut Self, T) -> Result<&mut Self>,
238    ) -> Result<&mut Self>;
239
240    /// [`Write`]s every item of the [`Iterator`].
241    fn write_iter<T, I>(
242        &mut self,
243        val: I,
244        inner: impl FnMut(&mut Self, T) -> Result<&mut Self>,
245    ) -> Result<&mut Self>
246    where
247        I: IntoIterator<Item = T>;
248}
249
250impl<W: Write> WriterExt for Writer<W> {
251    fn write_opt<T>(
252        &mut self,
253        val: Option<T>,
254        inner: impl FnOnce(&mut Self, T) -> Result<&mut Self>,
255    ) -> Result<&mut Self> {
256        if let Some(val) = val {
257            inner(self, val)
258        } else {
259            Ok(self)
260        }
261    }
262
263    fn write_iter<T, I>(
264        &mut self,
265        iter: I,
266        inner: impl FnMut(&mut Self, T) -> Result<&mut Self>,
267    ) -> Result<&mut Self>
268    where
269        I: IntoIterator<Item = T>,
270    {
271        iter.into_iter().try_fold(self, inner)
272    }
273}
274
275/// [`ElementWriter`] extension.
276trait ElementWriterExt<'a, W: Write> {
277    /// [`Writes`] with `inner` in case `is_empty` resolves to [`false`] or
278    /// [`Write`]s with [`ElementWriter::write_empty`] otherwise.
279    fn write_empty_or_inner<Inner>(
280        self,
281        is_empty: impl FnOnce(&mut Self) -> bool,
282        inner: Inner,
283    ) -> Result<&'a mut Writer<W>>
284    where
285        Inner: Fn(&mut Writer<W>) -> Result<()>;
286}
287
288impl<'a, W: Write> ElementWriterExt<'a, W> for ElementWriter<'a, W> {
289    fn write_empty_or_inner<Inner>(
290        mut self,
291        is_empty: impl FnOnce(&mut Self) -> bool,
292        inner: Inner,
293    ) -> Result<&'a mut Writer<W>>
294    where
295        Inner: Fn(&mut Writer<W>) -> Result<()>,
296    {
297        if is_empty(&mut self) {
298            self.write_empty()
299        } else {
300            self.write_inner_content(inner)
301        }
302    }
303}