1use 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#[derive(Default, Debug, Clone, Getters)]
24pub struct Report {
25 testsuites: Vec<TestSuite>,
26}
27
28impl Report {
29 pub fn new() -> Report {
31 Report {
32 testsuites: Vec::new(),
33 }
34 }
35
36 pub fn add_testsuite(&mut self, testsuite: TestSuite) {
40 self.testsuites.push(testsuite);
41 }
42
43 pub fn add_testsuites(&mut self, testsuites: impl IntoIterator<Item = TestSuite>) {
45 self.testsuites.extend(testsuites);
46 }
47
48 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 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#[derive(Default, Debug, Clone, Getters)]
228pub struct ReportBuilder {
229 report: Report,
230}
231
232impl ReportBuilder {
233 pub fn new() -> ReportBuilder {
235 ReportBuilder {
236 report: Report::new(),
237 }
238 }
239
240 pub fn add_testsuite(&mut self, testsuite: TestSuite) -> &mut Self {
244 self.report.testsuites.push(testsuite);
245 self
246 }
247
248 pub fn add_testsuites(&mut self, testsuites: impl IntoIterator<Item = TestSuite>) -> &mut Self {
250 self.report.testsuites.extend(testsuites);
251 self
252 }
253
254 pub fn build(&self) -> Report {
256 self.report.clone()
257 }
258}
259
260trait WriterExt {
262 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 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
304trait ElementWriterExt<'a, W: Write> {
306 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}