1#[cfg(any(test, 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(any(test, 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(any(test, feature = "proptest"), derive(test_strategy::Arbitrary))]
303#[non_exhaustive]
304pub struct TestCase {
305 #[cfg_attr(any(test, 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(
322 any(test, feature = "proptest"),
323 strategy(option::of(datetime_strategy()))
324 )]
325 pub timestamp: Option<DateTime<FixedOffset>>,
326
327 #[cfg_attr(
329 any(test, feature = "proptest"),
330 strategy(option::of(duration_strategy()))
331 )]
332 pub time: Option<Duration>,
333
334 pub status: TestCaseStatus,
336
337 pub system_out: Option<XmlString>,
339
340 pub system_err: Option<XmlString>,
342
343 #[cfg_attr(
345 any(test, feature = "proptest"),
346 strategy(xml_attr_index_map_strategy())
347 )]
348 pub extra: IndexMap<XmlString, XmlString>,
349
350 #[cfg_attr(any(test, feature = "proptest"), strategy(collection::vec(any::<Property>(), 0..3)))]
352 pub properties: Vec<Property>,
353}
354
355impl TestCase {
356 pub fn new(name: impl Into<XmlString>, status: TestCaseStatus) -> Self {
358 Self {
359 name: name.into(),
360 classname: None,
361 assertions: None,
362 timestamp: None,
363 time: None,
364 status,
365 system_out: None,
366 system_err: None,
367 extra: IndexMap::new(),
368 properties: vec![],
369 }
370 }
371
372 pub fn set_classname(&mut self, classname: impl Into<XmlString>) -> &mut Self {
374 self.classname = Some(classname.into());
375 self
376 }
377
378 pub fn set_assertions(&mut self, assertions: usize) -> &mut Self {
380 self.assertions = Some(assertions);
381 self
382 }
383
384 pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
386 self.timestamp = Some(timestamp.into());
387 self
388 }
389
390 pub fn set_time(&mut self, time: Duration) -> &mut Self {
392 self.time = Some(time);
393 self
394 }
395
396 pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
398 self.system_out = Some(system_out.into());
399 self
400 }
401
402 pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
406 self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
407 }
408
409 pub fn set_system_err(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
411 self.system_err = Some(system_out.into());
412 self
413 }
414
415 pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
419 self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
420 }
421
422 pub fn add_property(&mut self, property: impl Into<Property>) -> &mut Self {
424 self.properties.push(property.into());
425 self
426 }
427
428 pub fn add_properties(
430 &mut self,
431 properties: impl IntoIterator<Item = impl Into<Property>>,
432 ) -> &mut Self {
433 for property in properties {
434 self.add_property(property);
435 }
436 self
437 }
438}
439
440#[derive(Clone, Debug, PartialEq, Eq)]
442#[cfg_attr(any(test, feature = "proptest"), derive(test_strategy::Arbitrary))]
443pub enum TestCaseStatus {
444 Success {
446 flaky_runs: Vec<TestRerun>,
449 },
450
451 NonSuccess {
453 kind: NonSuccessKind,
455
456 message: Option<XmlString>,
458
459 ty: Option<XmlString>,
461
462 #[cfg_attr(
466 any(test, feature = "proptest"),
467 strategy(option::of(text_node_strategy()))
468 )]
469 description: Option<XmlString>,
470
471 reruns: Vec<TestRerun>,
473 },
474
475 Skipped {
477 message: Option<XmlString>,
479
480 ty: Option<XmlString>,
482
483 #[cfg_attr(
487 any(test, feature = "proptest"),
488 strategy(option::of(text_node_strategy()))
489 )]
490 description: Option<XmlString>,
491 },
492}
493
494impl TestCaseStatus {
495 pub fn success() -> Self {
497 TestCaseStatus::Success { flaky_runs: vec![] }
498 }
499
500 pub fn non_success(kind: NonSuccessKind) -> Self {
502 TestCaseStatus::NonSuccess {
503 kind,
504 message: None,
505 ty: None,
506 description: None,
507 reruns: vec![],
508 }
509 }
510
511 pub fn skipped() -> Self {
513 TestCaseStatus::Skipped {
514 message: None,
515 ty: None,
516 description: None,
517 }
518 }
519
520 pub fn set_message(&mut self, message: impl Into<XmlString>) -> &mut Self {
522 let message_mut = match self {
523 TestCaseStatus::Success { .. } => return self,
524 TestCaseStatus::NonSuccess { message, .. } => message,
525 TestCaseStatus::Skipped { message, .. } => message,
526 };
527 *message_mut = Some(message.into());
528 self
529 }
530
531 pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &mut Self {
533 let ty_mut = match self {
534 TestCaseStatus::Success { .. } => return self,
535 TestCaseStatus::NonSuccess { ty, .. } => ty,
536 TestCaseStatus::Skipped { ty, .. } => ty,
537 };
538 *ty_mut = Some(ty.into());
539 self
540 }
541
542 pub fn set_description(&mut self, description: impl Into<XmlString>) -> &mut Self {
544 let description_mut = match self {
545 TestCaseStatus::Success { .. } => return self,
546 TestCaseStatus::NonSuccess { description, .. } => description,
547 TestCaseStatus::Skipped { description, .. } => description,
548 };
549 *description_mut = Some(description.into());
550 self
551 }
552
553 pub fn add_rerun(&mut self, rerun: TestRerun) -> &mut Self {
555 self.add_reruns(iter::once(rerun))
556 }
557
558 pub fn add_reruns(&mut self, reruns: impl IntoIterator<Item = TestRerun>) -> &mut Self {
560 let reruns_mut = match self {
561 TestCaseStatus::Success { flaky_runs } => flaky_runs,
562 TestCaseStatus::NonSuccess { reruns, .. } => reruns,
563 TestCaseStatus::Skipped { .. } => return self,
564 };
565 reruns_mut.extend(reruns);
566 self
567 }
568}
569
570#[derive(Clone, Debug, PartialEq, Eq)]
575#[cfg_attr(any(test, feature = "proptest"), derive(test_strategy::Arbitrary))]
576pub struct TestRerun {
577 pub kind: NonSuccessKind,
579
580 #[cfg_attr(
584 any(test, feature = "proptest"),
585 strategy(option::of(datetime_strategy()))
586 )]
587 pub timestamp: Option<DateTime<FixedOffset>>,
588
589 #[cfg_attr(
593 any(test, feature = "proptest"),
594 strategy(option::of(duration_strategy()))
595 )]
596 pub time: Option<Duration>,
597
598 pub message: Option<XmlString>,
600
601 pub ty: Option<XmlString>,
603
604 pub stack_trace: Option<XmlString>,
606
607 pub system_out: Option<XmlString>,
609
610 pub system_err: Option<XmlString>,
612
613 #[cfg_attr(
617 any(test, feature = "proptest"),
618 strategy(option::of(text_node_strategy()))
619 )]
620 pub description: Option<XmlString>,
621}
622
623impl TestRerun {
624 pub fn new(kind: NonSuccessKind) -> Self {
626 TestRerun {
627 kind,
628 timestamp: None,
629 time: None,
630 message: None,
631 ty: None,
632 stack_trace: None,
633 system_out: None,
634 system_err: None,
635 description: None,
636 }
637 }
638
639 pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
641 self.timestamp = Some(timestamp.into());
642 self
643 }
644
645 pub fn set_time(&mut self, time: Duration) -> &mut Self {
647 self.time = Some(time);
648 self
649 }
650
651 pub fn set_message(&mut self, message: impl Into<XmlString>) -> &mut Self {
653 self.message = Some(message.into());
654 self
655 }
656
657 pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &mut Self {
659 self.ty = Some(ty.into());
660 self
661 }
662
663 pub fn set_stack_trace(&mut self, stack_trace: impl Into<XmlString>) -> &mut Self {
665 self.stack_trace = Some(stack_trace.into());
666 self
667 }
668
669 pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
671 self.system_out = Some(system_out.into());
672 self
673 }
674
675 pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
679 self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
680 }
681
682 pub fn set_system_err(&mut self, system_err: impl Into<XmlString>) -> &mut Self {
684 self.system_err = Some(system_err.into());
685 self
686 }
687
688 pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
692 self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
693 }
694
695 pub fn set_description(&mut self, description: impl Into<XmlString>) -> &mut Self {
697 self.description = Some(description.into());
698 self
699 }
700}
701
702#[derive(Copy, Clone, Debug, Eq, PartialEq)]
708#[cfg_attr(any(test, feature = "proptest"), derive(test_strategy::Arbitrary))]
709pub enum NonSuccessKind {
710 Failure,
713
714 Error,
717}
718
719#[derive(Clone, Debug, PartialEq, Eq)]
721#[cfg_attr(any(test, feature = "proptest"), derive(test_strategy::Arbitrary))]
722pub struct Property {
723 pub name: XmlString,
725
726 pub value: XmlString,
728}
729
730impl Property {
731 pub fn new(name: impl Into<XmlString>, value: impl Into<XmlString>) -> Self {
733 Self {
734 name: name.into(),
735 value: value.into(),
736 }
737 }
738}
739
740impl<T> From<(T, T)> for Property
741where
742 T: Into<XmlString>,
743{
744 fn from((k, v): (T, T)) -> Self {
745 Property::new(k, v)
746 }
747}
748
749#[derive(Clone, Debug, PartialEq, Eq)]
759pub struct XmlString {
760 data: Box<str>,
761}
762
763impl XmlString {
764 pub fn new(data: impl AsRef<str>) -> Self {
766 let data = data.as_ref();
767 let data = strip_ansi_escapes::strip_str(data);
768 let data = data
769 .replace(
770 |c| matches!(c, '\x00'..='\x08' | '\x0b' | '\x0c' | '\x0e'..='\x1f'),
771 "",
772 )
773 .into_boxed_str();
774 Self { data }
775 }
776
777 pub fn as_str(&self) -> &str {
779 &self.data
780 }
781
782 pub fn into_string(self) -> String {
784 self.data.into_string()
785 }
786}
787
788impl<T: AsRef<str>> From<T> for XmlString {
789 fn from(s: T) -> Self {
790 XmlString::new(s)
791 }
792}
793
794impl From<XmlString> for String {
795 fn from(s: XmlString) -> Self {
796 s.into_string()
797 }
798}
799
800impl Deref for XmlString {
801 type Target = str;
802
803 fn deref(&self) -> &Self::Target {
804 &self.data
805 }
806}
807
808impl Borrow<str> for XmlString {
809 fn borrow(&self) -> &str {
810 &self.data
811 }
812}
813
814impl PartialOrd for XmlString {
815 #[inline]
816 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
817 Some(self.cmp(other))
818 }
819}
820
821impl Ord for XmlString {
822 #[inline]
823 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
824 self.data.cmp(&other.data)
825 }
826}
827
828impl Hash for XmlString {
829 #[inline]
830 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
831 self.data.hash(state);
833 }
834}
835
836impl PartialEq<str> for XmlString {
837 fn eq(&self, other: &str) -> bool {
838 &*self.data == other
839 }
840}
841
842impl PartialEq<XmlString> for str {
843 fn eq(&self, other: &XmlString) -> bool {
844 self == &*other.data
845 }
846}
847
848impl PartialEq<String> for XmlString {
849 fn eq(&self, other: &String) -> bool {
850 &*self.data == other
851 }
852}
853
854#[cfg(test)]
855mod tests {
856 use super::*;
857 use proptest::prop_assume;
858 use std::hash::Hasher;
859 use test_strategy::proptest;
860
861 #[proptest]
864 fn xml_string_hash(s: String) {
865 let xml_string = XmlString::new(&s);
866 prop_assume!(xml_string == s);
869
870 let mut hasher1 = std::collections::hash_map::DefaultHasher::new();
871 let mut hasher2 = std::collections::hash_map::DefaultHasher::new();
872 s.as_str().hash(&mut hasher1);
873 xml_string.hash(&mut hasher2);
874 assert_eq!(hasher1.finish(), hasher2.finish());
875 }
876
877 #[proptest]
878 fn xml_string_ord(s1: String, s2: String) {
879 let xml_string1 = XmlString::new(&s1);
880 let xml_string2 = XmlString::new(&s2);
881 prop_assume!(xml_string1 == s1 && xml_string2 == s2);
884
885 assert_eq!(s1.as_str().cmp(s2.as_str()), xml_string1.cmp(&xml_string2));
886 }
887}