1use crate::{serialize::serialize_report, SerializeError};
5use chrono::{DateTime, FixedOffset};
6use indexmap::map::IndexMap;
7use newtype_uuid::{GenericUuid, TypedUuid, TypedUuidKind, TypedUuidTag};
8use std::{borrow::Borrow, hash::Hash, io, iter, ops::Deref, time::Duration};
9use uuid::Uuid;
10
11pub enum ReportKind {}
13
14impl TypedUuidKind for ReportKind {
15 fn tag() -> TypedUuidTag {
16 const TAG: TypedUuidTag = TypedUuidTag::new("quick-junit-report");
17 TAG
18 }
19}
20
21pub type ReportUuid = TypedUuid<ReportKind>;
23
24#[derive(Clone, Debug)]
26pub struct Report {
27 pub name: XmlString,
29
30 pub uuid: Option<ReportUuid>,
34
35 pub timestamp: Option<DateTime<FixedOffset>>,
39
40 pub time: Option<Duration>,
44
45 pub tests: usize,
47
48 pub failures: usize,
50
51 pub errors: usize,
53
54 pub test_suites: Vec<TestSuite>,
56}
57
58impl Report {
59 pub fn new(name: impl Into<XmlString>) -> Self {
61 Self {
62 name: name.into(),
63 uuid: None,
64 timestamp: None,
65 time: None,
66 tests: 0,
67 failures: 0,
68 errors: 0,
69 test_suites: vec![],
70 }
71 }
72
73 pub fn set_report_uuid(&mut self, uuid: ReportUuid) -> &mut Self {
77 self.uuid = Some(uuid);
78 self
79 }
80
81 pub fn set_uuid(&mut self, uuid: Uuid) -> &mut Self {
85 self.uuid = Some(ReportUuid::from_untyped_uuid(uuid));
86 self
87 }
88
89 pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
91 self.timestamp = Some(timestamp.into());
92 self
93 }
94
95 pub fn set_time(&mut self, time: Duration) -> &mut Self {
97 self.time = Some(time);
98 self
99 }
100
101 pub fn add_test_suite(&mut self, test_suite: TestSuite) -> &mut Self {
106 self.tests += test_suite.tests;
107 self.failures += test_suite.failures;
108 self.errors += test_suite.errors;
109 self.test_suites.push(test_suite);
110 self
111 }
112
113 pub fn add_test_suites(
118 &mut self,
119 test_suites: impl IntoIterator<Item = TestSuite>,
120 ) -> &mut Self {
121 for test_suite in test_suites {
122 self.add_test_suite(test_suite);
123 }
124 self
125 }
126
127 pub fn serialize(&self, writer: impl io::Write) -> Result<(), SerializeError> {
129 serialize_report(self, writer)
130 }
131
132 pub fn to_string(&self) -> Result<String, SerializeError> {
134 let mut buf: Vec<u8> = vec![];
135 self.serialize(&mut buf)?;
136 String::from_utf8(buf).map_err(|utf8_err| {
137 quick_xml::encoding::EncodingError::from(utf8_err.utf8_error()).into()
138 })
139 }
140}
141
142#[derive(Clone, Debug)]
146#[non_exhaustive]
147pub struct TestSuite {
148 pub name: XmlString,
150
151 pub tests: usize,
153
154 pub disabled: usize,
156
157 pub errors: usize,
161
162 pub failures: usize,
166
167 pub timestamp: Option<DateTime<FixedOffset>>,
169
170 pub time: Option<Duration>,
172
173 pub test_cases: Vec<TestCase>,
175
176 pub properties: Vec<Property>,
178
179 pub system_out: Option<XmlString>,
181
182 pub system_err: Option<XmlString>,
184
185 pub extra: IndexMap<XmlString, XmlString>,
187}
188
189impl TestSuite {
190 pub fn new(name: impl Into<XmlString>) -> Self {
192 Self {
193 name: name.into(),
194 time: None,
195 timestamp: None,
196 tests: 0,
197 disabled: 0,
198 errors: 0,
199 failures: 0,
200 test_cases: vec![],
201 properties: vec![],
202 system_out: None,
203 system_err: None,
204 extra: IndexMap::new(),
205 }
206 }
207
208 pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
210 self.timestamp = Some(timestamp.into());
211 self
212 }
213
214 pub fn set_time(&mut self, time: Duration) -> &mut Self {
216 self.time = Some(time);
217 self
218 }
219
220 pub fn add_property(&mut self, property: impl Into<Property>) -> &mut Self {
222 self.properties.push(property.into());
223 self
224 }
225
226 pub fn add_properties(
228 &mut self,
229 properties: impl IntoIterator<Item = impl Into<Property>>,
230 ) -> &mut Self {
231 for property in properties {
232 self.add_property(property);
233 }
234 self
235 }
236
237 pub fn add_test_case(&mut self, test_case: TestCase) -> &mut Self {
242 self.tests += 1;
243 match &test_case.status {
244 TestCaseStatus::Success { .. } => {}
245 TestCaseStatus::NonSuccess { kind, .. } => match kind {
246 NonSuccessKind::Failure => self.failures += 1,
247 NonSuccessKind::Error => self.errors += 1,
248 },
249 TestCaseStatus::Skipped { .. } => self.disabled += 1,
250 }
251 self.test_cases.push(test_case);
252 self
253 }
254
255 pub fn add_test_cases(&mut self, test_cases: impl IntoIterator<Item = TestCase>) -> &mut Self {
260 for test_case in test_cases {
261 self.add_test_case(test_case);
262 }
263 self
264 }
265
266 pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
268 self.system_out = Some(system_out.into());
269 self
270 }
271
272 pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
276 self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
277 }
278
279 pub fn set_system_err(&mut self, system_err: impl Into<XmlString>) -> &mut Self {
281 self.system_err = Some(system_err.into());
282 self
283 }
284
285 pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
289 self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
290 }
291}
292
293#[derive(Clone, Debug)]
295#[non_exhaustive]
296pub struct TestCase {
297 pub name: XmlString,
299
300 pub classname: Option<XmlString>,
305
306 pub assertions: Option<usize>,
308
309 pub timestamp: Option<DateTime<FixedOffset>>,
313
314 pub time: Option<Duration>,
316
317 pub status: TestCaseStatus,
319
320 pub system_out: Option<XmlString>,
322
323 pub system_err: Option<XmlString>,
325
326 pub extra: IndexMap<XmlString, XmlString>,
328
329 pub properties: Vec<Property>,
331}
332
333impl TestCase {
334 pub fn new(name: impl Into<XmlString>, status: TestCaseStatus) -> Self {
336 Self {
337 name: name.into(),
338 classname: None,
339 assertions: None,
340 timestamp: None,
341 time: None,
342 status,
343 system_out: None,
344 system_err: None,
345 extra: IndexMap::new(),
346 properties: vec![],
347 }
348 }
349
350 pub fn set_classname(&mut self, classname: impl Into<XmlString>) -> &mut Self {
352 self.classname = Some(classname.into());
353 self
354 }
355
356 pub fn set_assertions(&mut self, assertions: usize) -> &mut Self {
358 self.assertions = Some(assertions);
359 self
360 }
361
362 pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
364 self.timestamp = Some(timestamp.into());
365 self
366 }
367
368 pub fn set_time(&mut self, time: Duration) -> &mut Self {
370 self.time = Some(time);
371 self
372 }
373
374 pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
376 self.system_out = Some(system_out.into());
377 self
378 }
379
380 pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
384 self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
385 }
386
387 pub fn set_system_err(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
389 self.system_err = Some(system_out.into());
390 self
391 }
392
393 pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
397 self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
398 }
399
400 pub fn add_property(&mut self, property: impl Into<Property>) -> &mut Self {
402 self.properties.push(property.into());
403 self
404 }
405
406 pub fn add_properties(
408 &mut self,
409 properties: impl IntoIterator<Item = impl Into<Property>>,
410 ) -> &mut Self {
411 for property in properties {
412 self.add_property(property);
413 }
414 self
415 }
416}
417
418#[derive(Clone, Debug)]
420pub enum TestCaseStatus {
421 Success {
423 flaky_runs: Vec<TestRerun>,
426 },
427
428 NonSuccess {
430 kind: NonSuccessKind,
432
433 message: Option<XmlString>,
435
436 ty: Option<XmlString>,
438
439 description: Option<XmlString>,
443
444 reruns: Vec<TestRerun>,
446 },
447
448 Skipped {
450 message: Option<XmlString>,
452
453 ty: Option<XmlString>,
455
456 description: Option<XmlString>,
460 },
461}
462
463impl TestCaseStatus {
464 pub fn success() -> Self {
466 TestCaseStatus::Success { flaky_runs: vec![] }
467 }
468
469 pub fn non_success(kind: NonSuccessKind) -> Self {
471 TestCaseStatus::NonSuccess {
472 kind,
473 message: None,
474 ty: None,
475 description: None,
476 reruns: vec![],
477 }
478 }
479
480 pub fn skipped() -> Self {
482 TestCaseStatus::Skipped {
483 message: None,
484 ty: None,
485 description: None,
486 }
487 }
488
489 pub fn set_message(&mut self, message: impl Into<XmlString>) -> &mut Self {
491 let message_mut = match self {
492 TestCaseStatus::Success { .. } => return self,
493 TestCaseStatus::NonSuccess { message, .. } => message,
494 TestCaseStatus::Skipped { message, .. } => message,
495 };
496 *message_mut = Some(message.into());
497 self
498 }
499
500 pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &mut Self {
502 let ty_mut = match self {
503 TestCaseStatus::Success { .. } => return self,
504 TestCaseStatus::NonSuccess { ty, .. } => ty,
505 TestCaseStatus::Skipped { ty, .. } => ty,
506 };
507 *ty_mut = Some(ty.into());
508 self
509 }
510
511 pub fn set_description(&mut self, description: impl Into<XmlString>) -> &mut Self {
513 let description_mut = match self {
514 TestCaseStatus::Success { .. } => return self,
515 TestCaseStatus::NonSuccess { description, .. } => description,
516 TestCaseStatus::Skipped { description, .. } => description,
517 };
518 *description_mut = Some(description.into());
519 self
520 }
521
522 pub fn add_rerun(&mut self, rerun: TestRerun) -> &mut Self {
524 self.add_reruns(iter::once(rerun))
525 }
526
527 pub fn add_reruns(&mut self, reruns: impl IntoIterator<Item = TestRerun>) -> &mut Self {
529 let reruns_mut = match self {
530 TestCaseStatus::Success { flaky_runs } => flaky_runs,
531 TestCaseStatus::NonSuccess { reruns, .. } => reruns,
532 TestCaseStatus::Skipped { .. } => return self,
533 };
534 reruns_mut.extend(reruns);
535 self
536 }
537}
538
539#[derive(Clone, Debug)]
544pub struct TestRerun {
545 pub kind: NonSuccessKind,
547
548 pub timestamp: Option<DateTime<FixedOffset>>,
552
553 pub time: Option<Duration>,
557
558 pub message: Option<XmlString>,
560
561 pub ty: Option<XmlString>,
563
564 pub stack_trace: Option<XmlString>,
566
567 pub system_out: Option<XmlString>,
569
570 pub system_err: Option<XmlString>,
572
573 pub description: Option<XmlString>,
577}
578
579impl TestRerun {
580 pub fn new(kind: NonSuccessKind) -> Self {
582 TestRerun {
583 kind,
584 timestamp: None,
585 time: None,
586 message: None,
587 ty: None,
588 stack_trace: None,
589 system_out: None,
590 system_err: None,
591 description: None,
592 }
593 }
594
595 pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
597 self.timestamp = Some(timestamp.into());
598 self
599 }
600
601 pub fn set_time(&mut self, time: Duration) -> &mut Self {
603 self.time = Some(time);
604 self
605 }
606
607 pub fn set_message(&mut self, message: impl Into<XmlString>) -> &mut Self {
609 self.message = Some(message.into());
610 self
611 }
612
613 pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &mut Self {
615 self.ty = Some(ty.into());
616 self
617 }
618
619 pub fn set_stack_trace(&mut self, stack_trace: impl Into<XmlString>) -> &mut Self {
621 self.stack_trace = Some(stack_trace.into());
622 self
623 }
624
625 pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
627 self.system_out = Some(system_out.into());
628 self
629 }
630
631 pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
635 self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
636 }
637
638 pub fn set_system_err(&mut self, system_err: impl Into<XmlString>) -> &mut Self {
640 self.system_err = Some(system_err.into());
641 self
642 }
643
644 pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
648 self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
649 }
650
651 pub fn set_description(&mut self, description: impl Into<XmlString>) -> &mut Self {
653 self.description = Some(description.into());
654 self
655 }
656}
657
658#[derive(Copy, Clone, Debug, Eq, PartialEq)]
664pub enum NonSuccessKind {
665 Failure,
668
669 Error,
672}
673
674#[derive(Clone, Debug)]
676pub struct Property {
677 pub name: XmlString,
679
680 pub value: XmlString,
682}
683
684impl Property {
685 pub fn new(name: impl Into<XmlString>, value: impl Into<XmlString>) -> Self {
687 Self {
688 name: name.into(),
689 value: value.into(),
690 }
691 }
692}
693
694impl<T> From<(T, T)> for Property
695where
696 T: Into<XmlString>,
697{
698 fn from((k, v): (T, T)) -> Self {
699 Property::new(k, v)
700 }
701}
702
703#[derive(Clone, Debug, PartialEq, Eq)]
713pub struct XmlString {
714 data: Box<str>,
715}
716
717impl XmlString {
718 pub fn new(data: impl AsRef<str>) -> Self {
720 let data = data.as_ref();
721 let data = strip_ansi_escapes::strip_str(data);
722 let data = data
723 .replace(
724 |c| matches!(c, '\x00'..='\x08' | '\x0b' | '\x0c' | '\x0e'..='\x1f'),
725 "",
726 )
727 .into_boxed_str();
728 Self { data }
729 }
730
731 pub fn as_str(&self) -> &str {
733 &self.data
734 }
735
736 pub fn into_string(self) -> String {
738 self.data.into_string()
739 }
740}
741
742impl<T: AsRef<str>> From<T> for XmlString {
743 fn from(s: T) -> Self {
744 XmlString::new(s)
745 }
746}
747
748impl From<XmlString> for String {
749 fn from(s: XmlString) -> Self {
750 s.into_string()
751 }
752}
753
754impl Deref for XmlString {
755 type Target = str;
756
757 fn deref(&self) -> &Self::Target {
758 &self.data
759 }
760}
761
762impl Borrow<str> for XmlString {
763 fn borrow(&self) -> &str {
764 &self.data
765 }
766}
767
768impl PartialOrd for XmlString {
769 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
770 Some(self.data.cmp(&other.data))
771 }
772}
773
774impl Ord for XmlString {
775 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
776 self.data.cmp(&other.data)
777 }
778}
779
780impl Hash for XmlString {
781 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
782 self.data.hash(state);
784 }
785}
786
787impl PartialEq<str> for XmlString {
788 fn eq(&self, other: &str) -> bool {
789 &*self.data == other
790 }
791}
792
793impl PartialEq<XmlString> for str {
794 fn eq(&self, other: &XmlString) -> bool {
795 self == &*other.data
796 }
797}
798
799impl PartialEq<String> for XmlString {
800 fn eq(&self, other: &String) -> bool {
801 &*self.data == other
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use super::*;
808 use proptest::prop_assume;
809 use std::hash::Hasher;
810 use test_strategy::proptest;
811
812 #[proptest]
815 fn xml_string_hash(s: String) {
816 let xml_string = XmlString::new(&s);
817 prop_assume!(xml_string == s);
820
821 let mut hasher1 = std::collections::hash_map::DefaultHasher::new();
822 let mut hasher2 = std::collections::hash_map::DefaultHasher::new();
823 s.as_str().hash(&mut hasher1);
824 xml_string.hash(&mut hasher2);
825 assert_eq!(hasher1.finish(), hasher2.finish());
826 }
827
828 #[proptest]
829 fn xml_string_ord(s1: String, s2: String) {
830 let xml_string1 = XmlString::new(&s1);
831 let xml_string2 = XmlString::new(&s2);
832 prop_assume!(xml_string1 == s1 && xml_string2 == s2);
835
836 assert_eq!(s1.as_str().cmp(s2.as_str()), xml_string1.cmp(&xml_string2));
837 }
838}