Skip to main content

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