1use serde::Serialize;
25
26use super::xacli as xacli_report;
27
28#[derive(Debug, Clone)]
32pub enum TestCaseStatus {
33 Passed,
35 Failed { failure: TestFailure },
37 Error { error: TestError },
39 Skipped { skipped: TestSkipped },
41}
42
43#[derive(Debug, Clone, Serialize)]
45pub struct TestFailure {
46 #[serde(rename = "@message")]
47 pub message: String,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 #[serde(rename = "@type")]
50 pub failure_type: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 #[serde(rename = "$text")]
53 pub content: Option<String>,
54}
55
56#[derive(Debug, Clone, Serialize)]
58pub struct TestError {
59 #[serde(rename = "@message")]
60 pub message: String,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 #[serde(rename = "@type")]
63 pub error_type: Option<String>,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 #[serde(rename = "$text")]
66 pub content: Option<String>,
67}
68
69#[derive(Debug, Clone, Serialize)]
71pub struct TestSkipped {
72 #[serde(skip_serializing_if = "Option::is_none")]
73 #[serde(rename = "@message")]
74 pub message: Option<String>,
75}
76
77#[derive(Debug, Clone, Serialize, Default)]
81#[serde(rename = "testsuites")]
82pub struct TestSuites {
83 #[serde(skip_serializing_if = "Option::is_none")]
85 #[serde(rename = "@name")]
86 pub name: Option<String>,
87
88 #[serde(rename = "@tests")]
90 pub tests: u32,
91
92 #[serde(rename = "@failures")]
94 pub failures: u32,
95
96 #[serde(rename = "@errors")]
98 pub errors: u32,
99
100 #[serde(rename = "@skipped")]
102 pub skipped: u32,
103
104 #[serde(rename = "@time")]
106 pub time: f64,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
110 #[serde(rename = "@timestamp")]
111 pub timestamp: Option<String>,
112
113 #[serde(rename = "testsuite")]
115 pub suites: Vec<TestSuite>,
116}
117
118impl TestSuites {
119 pub fn new() -> Self {
121 Self::default()
122 }
123
124 pub fn with_name(name: impl Into<String>) -> Self {
126 Self {
127 name: Some(name.into()),
128 ..Default::default()
129 }
130 }
131
132 pub fn add_suite(&mut self, suite: TestSuite) {
134 self.tests += suite.tests;
135 self.failures += suite.failures;
136 self.errors += suite.errors;
137 self.skipped += suite.skipped;
138 self.time += suite.time;
139 self.suites.push(suite);
140 }
141
142 pub fn with_timestamp(mut self, timestamp: impl Into<String>) -> Self {
144 self.timestamp = Some(timestamp.into());
145 self
146 }
147
148 pub fn to_xml(&self) -> Result<String, quick_xml::SeError> {
150 let mut xml = String::from(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
151 xml.push('\n');
152 xml.push_str(&quick_xml::se::to_string(self)?);
153 Ok(xml)
154 }
155
156 pub fn to_xml_pretty(&self) -> Result<String, quick_xml::SeError> {
158 let mut xml = String::from(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
159 xml.push('\n');
160
161 let mut buffer = String::new();
162 let mut serializer = quick_xml::se::Serializer::new(&mut buffer);
163 serializer.indent(' ', 2);
164 serde::Serialize::serialize(self, serializer)?;
165 xml.push_str(&buffer);
166 Ok(xml)
167 }
168}
169
170#[derive(Debug, Clone, Serialize, Default)]
174#[serde(rename = "testsuite")]
175pub struct TestSuite {
176 #[serde(rename = "@name")]
178 pub name: String,
179
180 #[serde(rename = "@tests")]
182 pub tests: u32,
183
184 #[serde(rename = "@failures")]
186 pub failures: u32,
187
188 #[serde(rename = "@errors")]
190 pub errors: u32,
191
192 #[serde(rename = "@skipped")]
194 pub skipped: u32,
195
196 #[serde(rename = "@time")]
198 pub time: f64,
199
200 #[serde(skip_serializing_if = "Option::is_none")]
202 #[serde(rename = "@timestamp")]
203 pub timestamp: Option<String>,
204
205 #[serde(skip_serializing_if = "Option::is_none")]
207 #[serde(rename = "@hostname")]
208 pub hostname: Option<String>,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
212 #[serde(rename = "@package")]
213 pub package: Option<String>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub properties: Option<Properties>,
218
219 #[serde(rename = "testcase")]
221 pub test_cases: Vec<TestCase>,
222
223 #[serde(skip_serializing_if = "Option::is_none")]
225 #[serde(rename = "system-out")]
226 pub system_out: Option<SystemOutput>,
227
228 #[serde(skip_serializing_if = "Option::is_none")]
230 #[serde(rename = "system-err")]
231 pub system_err: Option<SystemOutput>,
232}
233
234impl TestSuite {
235 pub fn new(name: impl Into<String>) -> Self {
237 Self {
238 name: name.into(),
239 ..Default::default()
240 }
241 }
242
243 pub fn add_test_case(&mut self, test_case: TestCase) {
245 self.tests += 1;
246 self.time += test_case.time;
247
248 match &test_case.status {
249 TestCaseStatus::Passed => {}
250 TestCaseStatus::Failed { .. } => self.failures += 1,
251 TestCaseStatus::Error { .. } => self.errors += 1,
252 TestCaseStatus::Skipped { .. } => self.skipped += 1,
253 }
254
255 self.test_cases.push(test_case);
256 }
257
258 pub fn with_timestamp(mut self, timestamp: impl Into<String>) -> Self {
260 self.timestamp = Some(timestamp.into());
261 self
262 }
263
264 pub fn with_hostname(mut self, hostname: impl Into<String>) -> Self {
266 self.hostname = Some(hostname.into());
267 self
268 }
269
270 pub fn with_package(mut self, package: impl Into<String>) -> Self {
272 self.package = Some(package.into());
273 self
274 }
275
276 pub fn with_system_out(mut self, output: impl Into<String>) -> Self {
278 self.system_out = Some(SystemOutput(output.into()));
279 self
280 }
281
282 pub fn with_system_err(mut self, output: impl Into<String>) -> Self {
284 self.system_err = Some(SystemOutput(output.into()));
285 self
286 }
287
288 pub fn add_property(&mut self, name: impl Into<String>, value: impl Into<String>) {
290 let property = Property {
291 name: name.into(),
292 value: value.into(),
293 };
294 if let Some(props) = &mut self.properties {
295 props.properties.push(property);
296 } else {
297 self.properties = Some(Properties {
298 properties: vec![property],
299 });
300 }
301 }
302}
303
304#[derive(Debug, Clone, Serialize, Default)]
306pub struct Properties {
307 #[serde(rename = "property")]
308 pub properties: Vec<Property>,
309}
310
311#[derive(Debug, Clone, Serialize)]
313pub struct Property {
314 #[serde(rename = "@name")]
315 pub name: String,
316 #[serde(rename = "@value")]
317 pub value: String,
318}
319
320#[derive(Debug, Clone, Serialize)]
322pub struct SystemOutput(#[serde(rename = "$text")] pub String);
323
324#[derive(Debug, Clone)]
328pub struct TestCase {
329 pub name: String,
331
332 pub classname: Option<String>,
334
335 pub time: f64,
337
338 pub status: TestCaseStatus,
340
341 pub system_out: Option<SystemOutput>,
343
344 pub system_err: Option<SystemOutput>,
346}
347
348impl serde::Serialize for TestCase {
349 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
350 where
351 S: serde::Serializer,
352 {
353 use serde::ser::SerializeMap;
354
355 let mut field_count = 2; if self.classname.is_some() {
358 field_count += 1;
359 }
360 match &self.status {
361 TestCaseStatus::Passed => {}
362 TestCaseStatus::Failed { .. } => field_count += 1,
363 TestCaseStatus::Error { .. } => field_count += 1,
364 TestCaseStatus::Skipped { .. } => field_count += 1,
365 }
366 if self.system_out.is_some() {
367 field_count += 1;
368 }
369 if self.system_err.is_some() {
370 field_count += 1;
371 }
372
373 let mut map = serializer.serialize_map(Some(field_count))?;
374 map.serialize_entry("@name", &self.name)?;
375 if let Some(ref classname) = self.classname {
376 map.serialize_entry("@classname", classname)?;
377 }
378 map.serialize_entry("@time", &self.time)?;
379
380 match &self.status {
381 TestCaseStatus::Passed => {
382 }
384 TestCaseStatus::Failed { failure } => {
385 map.serialize_entry("failure", failure)?;
386 }
387 TestCaseStatus::Error { error } => {
388 map.serialize_entry("error", error)?;
389 }
390 TestCaseStatus::Skipped { skipped } => {
391 map.serialize_entry("skipped", skipped)?;
392 }
393 }
394
395 if let Some(ref stdout) = self.system_out {
396 map.serialize_entry("system-out", stdout)?;
397 }
398 if let Some(ref stderr) = self.system_err {
399 map.serialize_entry("system-err", stderr)?;
400 }
401
402 map.end()
403 }
404}
405
406impl TestCase {
407 pub fn passed(name: impl Into<String>, time: f64) -> Self {
409 Self {
410 name: name.into(),
411 classname: None,
412 time,
413 status: TestCaseStatus::Passed,
414 system_out: None,
415 system_err: None,
416 }
417 }
418
419 pub fn failed(
421 name: impl Into<String>,
422 time: f64,
423 message: impl Into<String>,
424 details: impl Into<String>,
425 ) -> Self {
426 Self {
427 name: name.into(),
428 classname: None,
429 time,
430 status: TestCaseStatus::Failed {
431 failure: TestFailure {
432 message: message.into(),
433 failure_type: None,
434 content: Some(details.into()),
435 },
436 },
437 system_out: None,
438 system_err: None,
439 }
440 }
441
442 pub fn error(
444 name: impl Into<String>,
445 time: f64,
446 message: impl Into<String>,
447 details: impl Into<String>,
448 ) -> Self {
449 Self {
450 name: name.into(),
451 classname: None,
452 time,
453 status: TestCaseStatus::Error {
454 error: TestError {
455 message: message.into(),
456 error_type: None,
457 content: Some(details.into()),
458 },
459 },
460 system_out: None,
461 system_err: None,
462 }
463 }
464
465 pub fn skipped(name: impl Into<String>, message: impl Into<String>) -> Self {
467 Self {
468 name: name.into(),
469 classname: None,
470 time: 0.0,
471 status: TestCaseStatus::Skipped {
472 skipped: TestSkipped {
473 message: Some(message.into()),
474 },
475 },
476 system_out: None,
477 system_err: None,
478 }
479 }
480
481 pub fn with_classname(mut self, classname: impl Into<String>) -> Self {
483 self.classname = Some(classname.into());
484 self
485 }
486
487 pub fn with_system_out(mut self, output: impl Into<String>) -> Self {
489 self.system_out = Some(SystemOutput(output.into()));
490 self
491 }
492
493 pub fn with_system_err(mut self, output: impl Into<String>) -> Self {
495 self.system_err = Some(SystemOutput(output.into()));
496 self
497 }
498}
499
500impl From<&xacli_report::TestSuitesResult> for TestSuites {
505 fn from(results: &xacli_report::TestSuitesResult) -> Self {
506 let mut suites = TestSuites::new();
507 for (suite_name, suite_result) in &results.suites {
508 suites.add_suite(TestSuite::from((suite_name.as_str(), suite_result)));
509 }
510 suites
511 }
512}
513
514impl From<xacli_report::TestSuitesResult> for TestSuites {
515 fn from(results: xacli_report::TestSuitesResult) -> Self {
516 TestSuites::from(&results)
517 }
518}
519
520impl From<(&str, &xacli_report::TestSuiteResult)> for TestSuite {
521 fn from((name, result): (&str, &xacli_report::TestSuiteResult)) -> Self {
522 let mut suite = TestSuite::new(name);
523 for (test_name, test_result) in &result.tests {
524 suite.add_test_case(TestCase::from((test_name.as_str(), test_result)));
525 }
526 suite
527 }
528}
529
530impl From<(&str, &xacli_report::TestCaseResult)> for TestCase {
531 fn from((name, result): (&str, &xacli_report::TestCaseResult)) -> Self {
532 let duration_secs = result.duration.as_secs_f64();
533
534 match &result.status {
535 xacli_report::TestCaseStatus::Passed => TestCase::passed(name, duration_secs),
536 xacli_report::TestCaseStatus::Failed { failure } => {
537 TestCase::failed(name, duration_secs, &failure.message, "")
538 }
539 xacli_report::TestCaseStatus::Error { error } => {
540 TestCase::error(name, duration_secs, &error.message, "")
541 }
542 xacli_report::TestCaseStatus::Skipped { skipped } => {
543 TestCase::skipped(name, skipped.message.as_deref().unwrap_or(""))
544 }
545 }
546 .with_system_out(&result.stdout)
547 .with_system_err(&result.stderr)
548 }
549}