1#[cfg(feature = "proptest")]
5use crate::proptest_impls::{
6 datetime_strategy, duration_strategy, test_name_strategy, text_node_strategy,
7 xml_attr_index_map_strategy,
8};
9use crate::{serialize::serialize_report, SerializeError};
10use chrono::{DateTime, FixedOffset};
11use indexmap::map::IndexMap;
12use newtype_uuid::{GenericUuid, TypedUuid, TypedUuidKind, TypedUuidTag};
13#[cfg(feature = "proptest")]
14use proptest::{collection, option, prelude::*};
15use std::{borrow::Borrow, hash::Hash, io, iter, ops::Deref, time::Duration};
16use uuid::Uuid;
17
18pub enum ReportKind {}
20
21impl TypedUuidKind for ReportKind {
22 fn tag() -> TypedUuidTag {
23 const TAG: TypedUuidTag = TypedUuidTag::new("quick-junit-report");
24 TAG
25 }
26}
27
28pub type ReportUuid = TypedUuid<ReportKind>;
30
31#[derive(Clone, Debug, PartialEq, Eq)]
33#[non_exhaustive]
34pub struct Report {
35 pub name: XmlString,
37
38 pub uuid: Option<ReportUuid>,
42
43 pub timestamp: Option<DateTime<FixedOffset>>,
47
48 pub time: Option<Duration>,
52
53 pub tests: usize,
55
56 pub failures: usize,
58
59 pub errors: usize,
61
62 pub skipped: usize,
66
67 pub disabled: Option<usize>,
79
80 pub test_suites: Vec<TestSuite>,
82}
83
84impl Report {
85 pub fn new(name: impl Into<XmlString>) -> Self {
87 Self {
88 name: name.into(),
89 uuid: None,
90 timestamp: None,
91 time: None,
92 tests: 0,
93 failures: 0,
94 errors: 0,
95 skipped: 0,
96 disabled: None,
97 test_suites: vec![],
98 }
99 }
100
101 pub fn set_report_uuid(&mut self, uuid: ReportUuid) -> &mut Self {
105 self.uuid = Some(uuid);
106 self
107 }
108
109 pub fn set_uuid(&mut self, uuid: Uuid) -> &mut Self {
113 self.uuid = Some(ReportUuid::from_untyped_uuid(uuid));
114 self
115 }
116
117 pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
119 self.timestamp = Some(timestamp.into());
120 self
121 }
122
123 pub fn set_time(&mut self, time: Duration) -> &mut Self {
125 self.time = Some(time);
126 self
127 }
128
129 pub fn add_test_suite(&mut self, test_suite: TestSuite) -> &mut Self {
139 self.tests += test_suite.tests;
140 self.failures += test_suite.failures;
141 self.errors += test_suite.errors;
142 self.skipped += test_suite.skipped;
143 if let Some(disabled) = test_suite.disabled {
144 *self.disabled.get_or_insert(0) += disabled;
145 }
146 self.test_suites.push(test_suite);
147 self
148 }
149
150 pub fn add_test_suites(
160 &mut self,
161 test_suites: impl IntoIterator<Item = TestSuite>,
162 ) -> &mut Self {
163 for test_suite in test_suites {
164 self.add_test_suite(test_suite);
165 }
166 self
167 }
168
169 pub fn serialize(&self, writer: impl io::Write) -> Result<(), SerializeError> {
171 serialize_report(self, writer)
172 }
173
174 pub fn to_string(&self) -> Result<String, SerializeError> {
176 let mut buf: Vec<u8> = vec![];
177 self.serialize(&mut buf)?;
178 String::from_utf8(buf).map_err(|utf8_err| {
179 quick_xml::encoding::EncodingError::from(utf8_err.utf8_error()).into()
180 })
181 }
182}
183
184#[derive(Clone, Debug, PartialEq, Eq)]
188#[non_exhaustive]
189pub struct TestSuite {
190 pub name: XmlString,
192
193 pub tests: usize,
195
196 pub skipped: usize,
198
199 pub disabled: Option<usize>,
209
210 pub errors: usize,
214
215 pub failures: usize,
219
220 pub timestamp: Option<DateTime<FixedOffset>>,
222
223 pub time: Option<Duration>,
225
226 pub test_cases: Vec<TestCase>,
228
229 pub properties: Vec<Property>,
231
232 pub system_out: Option<XmlString>,
234
235 pub system_err: Option<XmlString>,
237
238 pub extra: IndexMap<XmlString, XmlString>,
240}
241
242impl TestSuite {
243 pub fn new(name: impl Into<XmlString>) -> Self {
245 Self {
246 name: name.into(),
247 time: None,
248 timestamp: None,
249 tests: 0,
250 skipped: 0,
251 disabled: None,
252 errors: 0,
253 failures: 0,
254 test_cases: vec![],
255 properties: vec![],
256 system_out: None,
257 system_err: None,
258 extra: IndexMap::new(),
259 }
260 }
261
262 pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
264 self.timestamp = Some(timestamp.into());
265 self
266 }
267
268 pub fn set_time(&mut self, time: Duration) -> &mut Self {
270 self.time = Some(time);
271 self
272 }
273
274 pub fn add_property(&mut self, property: impl Into<Property>) -> &mut Self {
276 self.properties.push(property.into());
277 self
278 }
279
280 pub fn add_properties(
282 &mut self,
283 properties: impl IntoIterator<Item = impl Into<Property>>,
284 ) -> &mut Self {
285 for property in properties {
286 self.add_property(property);
287 }
288 self
289 }
290
291 pub fn add_test_case(&mut self, test_case: TestCase) -> &mut Self {
296 self.tests += 1;
297 match &test_case.status {
298 TestCaseStatus::Success { .. } => {}
299 TestCaseStatus::NonSuccess { kind, .. } => match kind {
300 NonSuccessKind::Failure => self.failures += 1,
301 NonSuccessKind::Error => self.errors += 1,
302 },
303 TestCaseStatus::Skipped { .. } => self.skipped += 1,
304 }
305 self.test_cases.push(test_case);
306 self
307 }
308
309 pub fn add_test_cases(&mut self, test_cases: impl IntoIterator<Item = TestCase>) -> &mut Self {
314 for test_case in test_cases {
315 self.add_test_case(test_case);
316 }
317 self
318 }
319
320 pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
322 self.system_out = Some(system_out.into());
323 self
324 }
325
326 pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
330 self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
331 }
332
333 pub fn set_system_err(&mut self, system_err: impl Into<XmlString>) -> &mut Self {
335 self.system_err = Some(system_err.into());
336 self
337 }
338
339 pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
343 self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
344 }
345}
346
347#[derive(Clone, Debug, PartialEq, Eq)]
349#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
350#[non_exhaustive]
351pub struct TestCase {
352 #[cfg_attr(feature = "proptest", strategy(test_name_strategy()))]
354 pub name: XmlString,
355
356 pub classname: Option<XmlString>,
361
362 pub assertions: Option<usize>,
364
365 #[cfg_attr(feature = "proptest", strategy(option::of(datetime_strategy())))]
369 pub timestamp: Option<DateTime<FixedOffset>>,
370
371 #[cfg_attr(feature = "proptest", strategy(option::of(duration_strategy())))]
373 pub time: Option<Duration>,
374
375 pub status: TestCaseStatus,
377
378 pub system_out: Option<XmlString>,
380
381 pub system_err: Option<XmlString>,
383
384 #[cfg_attr(feature = "proptest", strategy(xml_attr_index_map_strategy()))]
386 pub extra: IndexMap<XmlString, XmlString>,
387
388 #[cfg_attr(feature = "proptest", strategy(collection::vec(any::<Property>(), 0..3)))]
390 pub properties: Vec<Property>,
391}
392
393impl TestCase {
394 pub fn new(name: impl Into<XmlString>, status: TestCaseStatus) -> Self {
396 Self {
397 name: name.into(),
398 classname: None,
399 assertions: None,
400 timestamp: None,
401 time: None,
402 status,
403 system_out: None,
404 system_err: None,
405 extra: IndexMap::new(),
406 properties: vec![],
407 }
408 }
409
410 pub fn set_classname(&mut self, classname: impl Into<XmlString>) -> &mut Self {
412 self.classname = Some(classname.into());
413 self
414 }
415
416 pub fn set_assertions(&mut self, assertions: usize) -> &mut Self {
418 self.assertions = Some(assertions);
419 self
420 }
421
422 pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
424 self.timestamp = Some(timestamp.into());
425 self
426 }
427
428 pub fn set_time(&mut self, time: Duration) -> &mut Self {
430 self.time = Some(time);
431 self
432 }
433
434 pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
436 self.system_out = Some(system_out.into());
437 self
438 }
439
440 pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
444 self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
445 }
446
447 pub fn set_system_err(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
449 self.system_err = Some(system_out.into());
450 self
451 }
452
453 pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
457 self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
458 }
459
460 pub fn add_property(&mut self, property: impl Into<Property>) -> &mut Self {
462 self.properties.push(property.into());
463 self
464 }
465
466 pub fn add_properties(
468 &mut self,
469 properties: impl IntoIterator<Item = impl Into<Property>>,
470 ) -> &mut Self {
471 for property in properties {
472 self.add_property(property);
473 }
474 self
475 }
476}
477
478#[derive(Clone, Debug, PartialEq, Eq)]
480#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
481pub enum TestCaseStatus {
482 Success {
484 #[cfg_attr(
487 feature = "proptest",
488 strategy(collection::vec(any::<TestRerun>(), 0..5))
489 )]
490 flaky_runs: Vec<TestRerun>,
491 },
492
493 NonSuccess {
495 kind: NonSuccessKind,
497
498 message: Option<XmlString>,
500
501 ty: Option<XmlString>,
503
504 #[cfg_attr(feature = "proptest", strategy(option::of(text_node_strategy())))]
508 description: Option<XmlString>,
509
510 reruns: NonSuccessReruns,
514 },
515
516 Skipped {
518 message: Option<XmlString>,
520
521 ty: Option<XmlString>,
523
524 #[cfg_attr(feature = "proptest", strategy(option::of(text_node_strategy())))]
528 description: Option<XmlString>,
529 },
530}
531
532impl TestCaseStatus {
533 pub fn success() -> Self {
535 TestCaseStatus::Success { flaky_runs: vec![] }
536 }
537
538 pub fn non_success(kind: NonSuccessKind) -> Self {
540 TestCaseStatus::NonSuccess {
541 kind,
542 message: None,
543 ty: None,
544 description: None,
545 reruns: NonSuccessReruns::default(),
546 }
547 }
548
549 pub fn skipped() -> Self {
551 TestCaseStatus::Skipped {
552 message: None,
553 ty: None,
554 description: None,
555 }
556 }
557
558 pub fn set_message(&mut self, message: impl Into<XmlString>) -> &mut Self {
560 let message_mut = match self {
561 TestCaseStatus::Success { .. } => return self,
562 TestCaseStatus::NonSuccess { message, .. } => message,
563 TestCaseStatus::Skipped { message, .. } => message,
564 };
565 *message_mut = Some(message.into());
566 self
567 }
568
569 pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &mut Self {
571 let ty_mut = match self {
572 TestCaseStatus::Success { .. } => return self,
573 TestCaseStatus::NonSuccess { ty, .. } => ty,
574 TestCaseStatus::Skipped { ty, .. } => ty,
575 };
576 *ty_mut = Some(ty.into());
577 self
578 }
579
580 pub fn set_description(&mut self, description: impl Into<XmlString>) -> &mut Self {
582 let description_mut = match self {
583 TestCaseStatus::Success { .. } => return self,
584 TestCaseStatus::NonSuccess { description, .. } => description,
585 TestCaseStatus::Skipped { description, .. } => description,
586 };
587 *description_mut = Some(description.into());
588 self
589 }
590
591 pub fn add_rerun(&mut self, rerun: TestRerun) -> &mut Self {
596 self.add_reruns(iter::once(rerun))
597 }
598
599 pub fn add_reruns(&mut self, new_reruns: impl IntoIterator<Item = TestRerun>) -> &mut Self {
604 match self {
605 TestCaseStatus::Success { flaky_runs } => {
606 flaky_runs.extend(new_reruns);
607 }
608 TestCaseStatus::NonSuccess { reruns, .. } => {
609 reruns.runs.extend(new_reruns);
610 }
611 TestCaseStatus::Skipped { .. } => {}
612 }
613 self
614 }
615
616 pub fn set_rerun_kind(&mut self, kind: FlakyOrRerun) -> &mut Self {
626 if let TestCaseStatus::NonSuccess { reruns, .. } = self {
627 reruns.kind = kind;
628 }
629 self
630 }
631}
632
633#[derive(Clone, Debug, PartialEq, Eq)]
642#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
643pub struct TestRerun {
644 pub kind: NonSuccessKind,
646
647 #[cfg_attr(feature = "proptest", strategy(option::of(datetime_strategy())))]
651 pub timestamp: Option<DateTime<FixedOffset>>,
652
653 #[cfg_attr(feature = "proptest", strategy(option::of(duration_strategy())))]
657 pub time: Option<Duration>,
658
659 pub message: Option<XmlString>,
661
662 pub ty: Option<XmlString>,
664
665 pub stack_trace: Option<XmlString>,
667
668 pub system_out: Option<XmlString>,
670
671 pub system_err: Option<XmlString>,
673
674 #[cfg_attr(feature = "proptest", strategy(option::of(text_node_strategy())))]
678 pub description: Option<XmlString>,
679}
680
681impl TestRerun {
682 pub fn new(kind: NonSuccessKind) -> Self {
684 TestRerun {
685 kind,
686 timestamp: None,
687 time: None,
688 message: None,
689 ty: None,
690 stack_trace: None,
691 system_out: None,
692 system_err: None,
693 description: None,
694 }
695 }
696
697 pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
699 self.timestamp = Some(timestamp.into());
700 self
701 }
702
703 pub fn set_time(&mut self, time: Duration) -> &mut Self {
705 self.time = Some(time);
706 self
707 }
708
709 pub fn set_message(&mut self, message: impl Into<XmlString>) -> &mut Self {
711 self.message = Some(message.into());
712 self
713 }
714
715 pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &mut Self {
717 self.ty = Some(ty.into());
718 self
719 }
720
721 pub fn set_stack_trace(&mut self, stack_trace: impl Into<XmlString>) -> &mut Self {
723 self.stack_trace = Some(stack_trace.into());
724 self
725 }
726
727 pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
729 self.system_out = Some(system_out.into());
730 self
731 }
732
733 pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
737 self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
738 }
739
740 pub fn set_system_err(&mut self, system_err: impl Into<XmlString>) -> &mut Self {
742 self.system_err = Some(system_err.into());
743 self
744 }
745
746 pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
750 self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
751 }
752
753 pub fn set_description(&mut self, description: impl Into<XmlString>) -> &mut Self {
755 self.description = Some(description.into());
756 self
757 }
758}
759
760#[derive(Copy, Clone, Debug, Eq, PartialEq)]
766#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
767pub enum NonSuccessKind {
768 Failure,
771
772 Error,
775}
776
777#[derive(Clone, Debug, PartialEq, Eq)]
785pub struct NonSuccessReruns {
786 pub kind: FlakyOrRerun,
794
795 pub runs: Vec<TestRerun>,
797}
798
799impl Default for NonSuccessReruns {
800 fn default() -> Self {
801 Self {
802 kind: FlakyOrRerun::Rerun,
803 runs: vec![],
804 }
805 }
806}
807
808#[derive(Copy, Clone, Debug, Eq, PartialEq)]
816#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
817pub enum FlakyOrRerun {
818 Flaky,
821
822 Rerun,
825}
826
827#[derive(Clone, Debug, PartialEq, Eq)]
829#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
830pub struct Property {
831 pub name: XmlString,
833
834 pub value: XmlString,
836}
837
838impl Property {
839 pub fn new(name: impl Into<XmlString>, value: impl Into<XmlString>) -> Self {
841 Self {
842 name: name.into(),
843 value: value.into(),
844 }
845 }
846}
847
848impl<T> From<(T, T)> for Property
849where
850 T: Into<XmlString>,
851{
852 fn from((k, v): (T, T)) -> Self {
853 Property::new(k, v)
854 }
855}
856
857#[derive(Clone, Debug, PartialEq, Eq)]
867pub struct XmlString {
868 data: Box<str>,
869}
870
871impl XmlString {
872 pub fn new(data: impl AsRef<str>) -> Self {
874 let data = data.as_ref();
875 let data = strip_ansi_escapes::strip_str(data);
876 let data = data
877 .replace(
878 |c| matches!(c, '\x00'..='\x08' | '\x0b' | '\x0c' | '\x0e'..='\x1f'),
879 "",
880 )
881 .into_boxed_str();
882 Self { data }
883 }
884
885 pub fn as_str(&self) -> &str {
887 &self.data
888 }
889
890 pub fn into_string(self) -> String {
892 self.data.into_string()
893 }
894}
895
896impl<T: AsRef<str>> From<T> for XmlString {
897 fn from(s: T) -> Self {
898 XmlString::new(s)
899 }
900}
901
902impl From<XmlString> for String {
903 fn from(s: XmlString) -> Self {
904 s.into_string()
905 }
906}
907
908impl Deref for XmlString {
909 type Target = str;
910
911 fn deref(&self) -> &Self::Target {
912 &self.data
913 }
914}
915
916impl Borrow<str> for XmlString {
917 fn borrow(&self) -> &str {
918 &self.data
919 }
920}
921
922impl PartialOrd for XmlString {
923 #[inline]
924 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
925 Some(self.cmp(other))
926 }
927}
928
929impl Ord for XmlString {
930 #[inline]
931 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
932 self.data.cmp(&other.data)
933 }
934}
935
936impl Hash for XmlString {
937 #[inline]
938 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
939 self.data.hash(state);
941 }
942}
943
944impl PartialEq<str> for XmlString {
945 fn eq(&self, other: &str) -> bool {
946 &*self.data == other
947 }
948}
949
950impl PartialEq<XmlString> for str {
951 fn eq(&self, other: &XmlString) -> bool {
952 self == &*other.data
953 }
954}
955
956impl PartialEq<String> for XmlString {
957 fn eq(&self, other: &String) -> bool {
958 &*self.data == other
959 }
960}
961
962#[cfg(test)]
963mod tests {
964 use super::*;
965 use proptest::prop_assume;
966 use std::hash::Hasher;
967 use test_strategy::proptest;
968
969 #[proptest]
972 fn xml_string_hash(s: String) {
973 let xml_string = XmlString::new(&s);
974 prop_assume!(xml_string == s);
977
978 let mut hasher1 = std::collections::hash_map::DefaultHasher::new();
979 let mut hasher2 = std::collections::hash_map::DefaultHasher::new();
980 s.as_str().hash(&mut hasher1);
981 xml_string.hash(&mut hasher2);
982 assert_eq!(hasher1.finish(), hasher2.finish());
983 }
984
985 #[proptest]
986 fn xml_string_ord(s1: String, s2: String) {
987 let xml_string1 = XmlString::new(&s1);
988 let xml_string2 = XmlString::new(&s2);
989 prop_assume!(xml_string1 == s1 && xml_string2 == s2);
992
993 assert_eq!(s1.as_str().cmp(s2.as_str()), xml_string1.cmp(&xml_string2));
994 }
995}