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)]
33pub struct Report {
34 pub name: XmlString,
36
37 pub uuid: Option<ReportUuid>,
41
42 pub timestamp: Option<DateTime<FixedOffset>>,
46
47 pub time: Option<Duration>,
51
52 pub tests: usize,
54
55 pub failures: usize,
57
58 pub errors: usize,
60
61 pub test_suites: Vec<TestSuite>,
63}
64
65impl Report {
66 pub fn new(name: impl Into<XmlString>) -> Self {
68 Self {
69 name: name.into(),
70 uuid: None,
71 timestamp: None,
72 time: None,
73 tests: 0,
74 failures: 0,
75 errors: 0,
76 test_suites: vec![],
77 }
78 }
79
80 pub fn set_report_uuid(&mut self, uuid: ReportUuid) -> &mut Self {
84 self.uuid = Some(uuid);
85 self
86 }
87
88 pub fn set_uuid(&mut self, uuid: Uuid) -> &mut Self {
92 self.uuid = Some(ReportUuid::from_untyped_uuid(uuid));
93 self
94 }
95
96 pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
98 self.timestamp = Some(timestamp.into());
99 self
100 }
101
102 pub fn set_time(&mut self, time: Duration) -> &mut Self {
104 self.time = Some(time);
105 self
106 }
107
108 pub fn add_test_suite(&mut self, test_suite: TestSuite) -> &mut Self {
113 self.tests += test_suite.tests;
114 self.failures += test_suite.failures;
115 self.errors += test_suite.errors;
116 self.test_suites.push(test_suite);
117 self
118 }
119
120 pub fn add_test_suites(
125 &mut self,
126 test_suites: impl IntoIterator<Item = TestSuite>,
127 ) -> &mut Self {
128 for test_suite in test_suites {
129 self.add_test_suite(test_suite);
130 }
131 self
132 }
133
134 pub fn serialize(&self, writer: impl io::Write) -> Result<(), SerializeError> {
136 serialize_report(self, writer)
137 }
138
139 pub fn to_string(&self) -> Result<String, SerializeError> {
141 let mut buf: Vec<u8> = vec![];
142 self.serialize(&mut buf)?;
143 String::from_utf8(buf).map_err(|utf8_err| {
144 quick_xml::encoding::EncodingError::from(utf8_err.utf8_error()).into()
145 })
146 }
147}
148
149#[derive(Clone, Debug, PartialEq, Eq)]
153#[non_exhaustive]
154pub struct TestSuite {
155 pub name: XmlString,
157
158 pub tests: usize,
160
161 pub disabled: usize,
163
164 pub errors: usize,
168
169 pub failures: usize,
173
174 pub timestamp: Option<DateTime<FixedOffset>>,
176
177 pub time: Option<Duration>,
179
180 pub test_cases: Vec<TestCase>,
182
183 pub properties: Vec<Property>,
185
186 pub system_out: Option<XmlString>,
188
189 pub system_err: Option<XmlString>,
191
192 pub extra: IndexMap<XmlString, XmlString>,
194}
195
196impl TestSuite {
197 pub fn new(name: impl Into<XmlString>) -> Self {
199 Self {
200 name: name.into(),
201 time: None,
202 timestamp: None,
203 tests: 0,
204 disabled: 0,
205 errors: 0,
206 failures: 0,
207 test_cases: vec![],
208 properties: vec![],
209 system_out: None,
210 system_err: None,
211 extra: IndexMap::new(),
212 }
213 }
214
215 pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
217 self.timestamp = Some(timestamp.into());
218 self
219 }
220
221 pub fn set_time(&mut self, time: Duration) -> &mut Self {
223 self.time = Some(time);
224 self
225 }
226
227 pub fn add_property(&mut self, property: impl Into<Property>) -> &mut Self {
229 self.properties.push(property.into());
230 self
231 }
232
233 pub fn add_properties(
235 &mut self,
236 properties: impl IntoIterator<Item = impl Into<Property>>,
237 ) -> &mut Self {
238 for property in properties {
239 self.add_property(property);
240 }
241 self
242 }
243
244 pub fn add_test_case(&mut self, test_case: TestCase) -> &mut Self {
249 self.tests += 1;
250 match &test_case.status {
251 TestCaseStatus::Success { .. } => {}
252 TestCaseStatus::NonSuccess { kind, .. } => match kind {
253 NonSuccessKind::Failure => self.failures += 1,
254 NonSuccessKind::Error => self.errors += 1,
255 },
256 TestCaseStatus::Skipped { .. } => self.disabled += 1,
257 }
258 self.test_cases.push(test_case);
259 self
260 }
261
262 pub fn add_test_cases(&mut self, test_cases: impl IntoIterator<Item = TestCase>) -> &mut Self {
267 for test_case in test_cases {
268 self.add_test_case(test_case);
269 }
270 self
271 }
272
273 pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
275 self.system_out = Some(system_out.into());
276 self
277 }
278
279 pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
283 self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
284 }
285
286 pub fn set_system_err(&mut self, system_err: impl Into<XmlString>) -> &mut Self {
288 self.system_err = Some(system_err.into());
289 self
290 }
291
292 pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
296 self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
297 }
298}
299
300#[derive(Clone, Debug, PartialEq, Eq)]
302#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
303#[non_exhaustive]
304pub struct TestCase {
305 #[cfg_attr(feature = "proptest", strategy(test_name_strategy()))]
307 pub name: XmlString,
308
309 pub classname: Option<XmlString>,
314
315 pub assertions: Option<usize>,
317
318 #[cfg_attr(feature = "proptest", strategy(option::of(datetime_strategy())))]
322 pub timestamp: Option<DateTime<FixedOffset>>,
323
324 #[cfg_attr(feature = "proptest", strategy(option::of(duration_strategy())))]
326 pub time: Option<Duration>,
327
328 pub status: TestCaseStatus,
330
331 pub system_out: Option<XmlString>,
333
334 pub system_err: Option<XmlString>,
336
337 #[cfg_attr(feature = "proptest", strategy(xml_attr_index_map_strategy()))]
339 pub extra: IndexMap<XmlString, XmlString>,
340
341 #[cfg_attr(feature = "proptest", strategy(collection::vec(any::<Property>(), 0..3)))]
343 pub properties: Vec<Property>,
344}
345
346impl TestCase {
347 pub fn new(name: impl Into<XmlString>, status: TestCaseStatus) -> Self {
349 Self {
350 name: name.into(),
351 classname: None,
352 assertions: None,
353 timestamp: None,
354 time: None,
355 status,
356 system_out: None,
357 system_err: None,
358 extra: IndexMap::new(),
359 properties: vec![],
360 }
361 }
362
363 pub fn set_classname(&mut self, classname: impl Into<XmlString>) -> &mut Self {
365 self.classname = Some(classname.into());
366 self
367 }
368
369 pub fn set_assertions(&mut self, assertions: usize) -> &mut Self {
371 self.assertions = Some(assertions);
372 self
373 }
374
375 pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
377 self.timestamp = Some(timestamp.into());
378 self
379 }
380
381 pub fn set_time(&mut self, time: Duration) -> &mut Self {
383 self.time = Some(time);
384 self
385 }
386
387 pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
389 self.system_out = Some(system_out.into());
390 self
391 }
392
393 pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
397 self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
398 }
399
400 pub fn set_system_err(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
402 self.system_err = Some(system_out.into());
403 self
404 }
405
406 pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
410 self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
411 }
412
413 pub fn add_property(&mut self, property: impl Into<Property>) -> &mut Self {
415 self.properties.push(property.into());
416 self
417 }
418
419 pub fn add_properties(
421 &mut self,
422 properties: impl IntoIterator<Item = impl Into<Property>>,
423 ) -> &mut Self {
424 for property in properties {
425 self.add_property(property);
426 }
427 self
428 }
429}
430
431#[derive(Clone, Debug, PartialEq, Eq)]
433#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
434pub enum TestCaseStatus {
435 Success {
437 flaky_runs: Vec<TestRerun>,
440 },
441
442 NonSuccess {
444 kind: NonSuccessKind,
446
447 message: Option<XmlString>,
449
450 ty: Option<XmlString>,
452
453 #[cfg_attr(feature = "proptest", strategy(option::of(text_node_strategy())))]
457 description: Option<XmlString>,
458
459 reruns: NonSuccessReruns,
463 },
464
465 Skipped {
467 message: Option<XmlString>,
469
470 ty: Option<XmlString>,
472
473 #[cfg_attr(feature = "proptest", strategy(option::of(text_node_strategy())))]
477 description: Option<XmlString>,
478 },
479}
480
481impl TestCaseStatus {
482 pub fn success() -> Self {
484 TestCaseStatus::Success { flaky_runs: vec![] }
485 }
486
487 pub fn non_success(kind: NonSuccessKind) -> Self {
489 TestCaseStatus::NonSuccess {
490 kind,
491 message: None,
492 ty: None,
493 description: None,
494 reruns: NonSuccessReruns::default(),
495 }
496 }
497
498 pub fn skipped() -> Self {
500 TestCaseStatus::Skipped {
501 message: None,
502 ty: None,
503 description: None,
504 }
505 }
506
507 pub fn set_message(&mut self, message: impl Into<XmlString>) -> &mut Self {
509 let message_mut = match self {
510 TestCaseStatus::Success { .. } => return self,
511 TestCaseStatus::NonSuccess { message, .. } => message,
512 TestCaseStatus::Skipped { message, .. } => message,
513 };
514 *message_mut = Some(message.into());
515 self
516 }
517
518 pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &mut Self {
520 let ty_mut = match self {
521 TestCaseStatus::Success { .. } => return self,
522 TestCaseStatus::NonSuccess { ty, .. } => ty,
523 TestCaseStatus::Skipped { ty, .. } => ty,
524 };
525 *ty_mut = Some(ty.into());
526 self
527 }
528
529 pub fn set_description(&mut self, description: impl Into<XmlString>) -> &mut Self {
531 let description_mut = match self {
532 TestCaseStatus::Success { .. } => return self,
533 TestCaseStatus::NonSuccess { description, .. } => description,
534 TestCaseStatus::Skipped { description, .. } => description,
535 };
536 *description_mut = Some(description.into());
537 self
538 }
539
540 pub fn add_rerun(&mut self, rerun: TestRerun) -> &mut Self {
545 self.add_reruns(iter::once(rerun))
546 }
547
548 pub fn add_reruns(&mut self, new_reruns: impl IntoIterator<Item = TestRerun>) -> &mut Self {
553 match self {
554 TestCaseStatus::Success { flaky_runs } => {
555 flaky_runs.extend(new_reruns);
556 }
557 TestCaseStatus::NonSuccess { reruns, .. } => {
558 reruns.runs.extend(new_reruns);
559 }
560 TestCaseStatus::Skipped { .. } => {}
561 }
562 self
563 }
564
565 pub fn set_rerun_kind(&mut self, kind: FlakyOrRerun) -> &mut Self {
575 if let TestCaseStatus::NonSuccess { reruns, .. } = self {
576 reruns.kind = kind;
577 }
578 self
579 }
580}
581
582#[derive(Clone, Debug, PartialEq, Eq)]
591#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
592pub struct TestRerun {
593 pub kind: NonSuccessKind,
595
596 #[cfg_attr(feature = "proptest", strategy(option::of(datetime_strategy())))]
600 pub timestamp: Option<DateTime<FixedOffset>>,
601
602 #[cfg_attr(feature = "proptest", strategy(option::of(duration_strategy())))]
606 pub time: Option<Duration>,
607
608 pub message: Option<XmlString>,
610
611 pub ty: Option<XmlString>,
613
614 pub stack_trace: Option<XmlString>,
616
617 pub system_out: Option<XmlString>,
619
620 pub system_err: Option<XmlString>,
622
623 #[cfg_attr(feature = "proptest", strategy(option::of(text_node_strategy())))]
627 pub description: Option<XmlString>,
628}
629
630impl TestRerun {
631 pub fn new(kind: NonSuccessKind) -> Self {
633 TestRerun {
634 kind,
635 timestamp: None,
636 time: None,
637 message: None,
638 ty: None,
639 stack_trace: None,
640 system_out: None,
641 system_err: None,
642 description: None,
643 }
644 }
645
646 pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
648 self.timestamp = Some(timestamp.into());
649 self
650 }
651
652 pub fn set_time(&mut self, time: Duration) -> &mut Self {
654 self.time = Some(time);
655 self
656 }
657
658 pub fn set_message(&mut self, message: impl Into<XmlString>) -> &mut Self {
660 self.message = Some(message.into());
661 self
662 }
663
664 pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &mut Self {
666 self.ty = Some(ty.into());
667 self
668 }
669
670 pub fn set_stack_trace(&mut self, stack_trace: impl Into<XmlString>) -> &mut Self {
672 self.stack_trace = Some(stack_trace.into());
673 self
674 }
675
676 pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
678 self.system_out = Some(system_out.into());
679 self
680 }
681
682 pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
686 self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
687 }
688
689 pub fn set_system_err(&mut self, system_err: impl Into<XmlString>) -> &mut Self {
691 self.system_err = Some(system_err.into());
692 self
693 }
694
695 pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
699 self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
700 }
701
702 pub fn set_description(&mut self, description: impl Into<XmlString>) -> &mut Self {
704 self.description = Some(description.into());
705 self
706 }
707}
708
709#[derive(Copy, Clone, Debug, Eq, PartialEq)]
715#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
716pub enum NonSuccessKind {
717 Failure,
720
721 Error,
724}
725
726#[derive(Clone, Debug, PartialEq, Eq)]
734pub struct NonSuccessReruns {
735 pub kind: FlakyOrRerun,
743
744 pub runs: Vec<TestRerun>,
746}
747
748impl Default for NonSuccessReruns {
749 fn default() -> Self {
750 Self {
751 kind: FlakyOrRerun::Rerun,
752 runs: vec![],
753 }
754 }
755}
756
757#[derive(Copy, Clone, Debug, Eq, PartialEq)]
765#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
766pub enum FlakyOrRerun {
767 Flaky,
770
771 Rerun,
774}
775
776#[derive(Clone, Debug, PartialEq, Eq)]
778#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
779pub struct Property {
780 pub name: XmlString,
782
783 pub value: XmlString,
785}
786
787impl Property {
788 pub fn new(name: impl Into<XmlString>, value: impl Into<XmlString>) -> Self {
790 Self {
791 name: name.into(),
792 value: value.into(),
793 }
794 }
795}
796
797impl<T> From<(T, T)> for Property
798where
799 T: Into<XmlString>,
800{
801 fn from((k, v): (T, T)) -> Self {
802 Property::new(k, v)
803 }
804}
805
806#[derive(Clone, Debug, PartialEq, Eq)]
816pub struct XmlString {
817 data: Box<str>,
818}
819
820impl XmlString {
821 pub fn new(data: impl AsRef<str>) -> Self {
823 let data = data.as_ref();
824 let data = strip_ansi_escapes::strip_str(data);
825 let data = data
826 .replace(
827 |c| matches!(c, '\x00'..='\x08' | '\x0b' | '\x0c' | '\x0e'..='\x1f'),
828 "",
829 )
830 .into_boxed_str();
831 Self { data }
832 }
833
834 pub fn as_str(&self) -> &str {
836 &self.data
837 }
838
839 pub fn into_string(self) -> String {
841 self.data.into_string()
842 }
843}
844
845impl<T: AsRef<str>> From<T> for XmlString {
846 fn from(s: T) -> Self {
847 XmlString::new(s)
848 }
849}
850
851impl From<XmlString> for String {
852 fn from(s: XmlString) -> Self {
853 s.into_string()
854 }
855}
856
857impl Deref for XmlString {
858 type Target = str;
859
860 fn deref(&self) -> &Self::Target {
861 &self.data
862 }
863}
864
865impl Borrow<str> for XmlString {
866 fn borrow(&self) -> &str {
867 &self.data
868 }
869}
870
871impl PartialOrd for XmlString {
872 #[inline]
873 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
874 Some(self.cmp(other))
875 }
876}
877
878impl Ord for XmlString {
879 #[inline]
880 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
881 self.data.cmp(&other.data)
882 }
883}
884
885impl Hash for XmlString {
886 #[inline]
887 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
888 self.data.hash(state);
890 }
891}
892
893impl PartialEq<str> for XmlString {
894 fn eq(&self, other: &str) -> bool {
895 &*self.data == other
896 }
897}
898
899impl PartialEq<XmlString> for str {
900 fn eq(&self, other: &XmlString) -> bool {
901 self == &*other.data
902 }
903}
904
905impl PartialEq<String> for XmlString {
906 fn eq(&self, other: &String) -> bool {
907 &*self.data == other
908 }
909}
910
911#[cfg(test)]
912mod tests {
913 use super::*;
914 use proptest::prop_assume;
915 use std::hash::Hasher;
916 use test_strategy::proptest;
917
918 #[proptest]
921 fn xml_string_hash(s: String) {
922 let xml_string = XmlString::new(&s);
923 prop_assume!(xml_string == s);
926
927 let mut hasher1 = std::collections::hash_map::DefaultHasher::new();
928 let mut hasher2 = std::collections::hash_map::DefaultHasher::new();
929 s.as_str().hash(&mut hasher1);
930 xml_string.hash(&mut hasher2);
931 assert_eq!(hasher1.finish(), hasher2.finish());
932 }
933
934 #[proptest]
935 fn xml_string_ord(s1: String, s2: String) {
936 let xml_string1 = XmlString::new(&s1);
937 let xml_string2 = XmlString::new(&s2);
938 prop_assume!(xml_string1 == s1 && xml_string2 == s2);
941
942 assert_eq!(s1.as_str().cmp(s2.as_str()), xml_string1.cmp(&xml_string2));
943 }
944}