1use 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#[derive(Default, Debug, Clone, Getters)]
22pub struct Report {
23 testsuites: Vec<TestSuite>,
24}
25
26impl Report {
27 pub fn new() -> Report {
29 Report {
30 testsuites: Vec::new(),
31 }
32 }
33
34 pub fn add_testsuite(&mut self, testsuite: TestSuite) {
38 self.testsuites.push(testsuite);
39 }
40
41 pub fn add_testsuites(&mut self, testsuites: impl IntoIterator<Item = TestSuite>) {
43 self.testsuites.extend(testsuites);
44 }
45
46 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 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#[derive(Default, Debug, Clone, Getters)]
199pub struct ReportBuilder {
200 report: Report,
201}
202
203impl ReportBuilder {
204 pub fn new() -> ReportBuilder {
206 ReportBuilder {
207 report: Report::new(),
208 }
209 }
210
211 pub fn add_testsuite(&mut self, testsuite: TestSuite) -> &mut Self {
215 self.report.testsuites.push(testsuite);
216 self
217 }
218
219 pub fn add_testsuites(&mut self, testsuites: impl IntoIterator<Item = TestSuite>) -> &mut Self {
221 self.report.testsuites.extend(testsuites);
222 self
223 }
224
225 pub fn build(&self) -> Report {
227 self.report.clone()
228 }
229}
230
231trait WriterExt {
233 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 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
275trait ElementWriterExt<'a, W: Write> {
277 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}