quick_junit/
report.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4#[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
18/// A tag indicating the kind of report.
19pub 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
28/// A unique identifier associated with a report.
29pub type ReportUuid = TypedUuid<ReportKind>;
30
31/// The root element of a JUnit report.
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct Report {
34    /// The name of this report.
35    pub name: XmlString,
36
37    /// A unique identifier associated with this report.
38    ///
39    /// This is an extension to the spec that's used by nextest.
40    pub uuid: Option<ReportUuid>,
41
42    /// The time at which the first test in this report began execution.
43    ///
44    /// This is not part of the JUnit spec, but may be useful for some tools.
45    pub timestamp: Option<DateTime<FixedOffset>>,
46
47    /// The overall time taken by the test suite.
48    ///
49    /// This is serialized as the number of seconds.
50    pub time: Option<Duration>,
51
52    /// The total number of tests from all TestSuites.
53    pub tests: usize,
54
55    /// The total number of failures from all TestSuites.
56    pub failures: usize,
57
58    /// The total number of errors from all TestSuites.
59    pub errors: usize,
60
61    /// The test suites contained in this report.
62    pub test_suites: Vec<TestSuite>,
63}
64
65impl Report {
66    /// Creates a new `Report` with the given name.
67    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    /// Sets a unique ID for this `Report`.
81    ///
82    /// This is an extension that's used by nextest.
83    pub fn set_report_uuid(&mut self, uuid: ReportUuid) -> &mut Self {
84        self.uuid = Some(uuid);
85        self
86    }
87
88    /// Sets a unique ID for this `Report` from an untyped [`Uuid`].
89    ///
90    /// This is an extension that's used by nextest.
91    pub fn set_uuid(&mut self, uuid: Uuid) -> &mut Self {
92        self.uuid = Some(ReportUuid::from_untyped_uuid(uuid));
93        self
94    }
95
96    /// Sets the start timestamp for the report.
97    pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
98        self.timestamp = Some(timestamp.into());
99        self
100    }
101
102    /// Sets the time taken for overall execution.
103    pub fn set_time(&mut self, time: Duration) -> &mut Self {
104        self.time = Some(time);
105        self
106    }
107
108    /// Adds a new TestSuite and updates the `tests`, `failures` and `errors` counts.
109    ///
110    /// When generating a new report, use of this method is recommended over adding to
111    /// `self.TestSuites` directly.
112    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    /// Adds several [`TestSuite`]s and updates the `tests`, `failures` and `errors` counts.
121    ///
122    /// When generating a new report, use of this method is recommended over adding to
123    /// `self.TestSuites` directly.
124    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    /// Serialize this report to the given writer.
135    pub fn serialize(&self, writer: impl io::Write) -> Result<(), SerializeError> {
136        serialize_report(self, writer)
137    }
138
139    /// Serialize this report to a string.
140    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/// Represents a single TestSuite.
150///
151/// A `TestSuite` groups together several `TestCase` instances.
152#[derive(Clone, Debug, PartialEq, Eq)]
153#[non_exhaustive]
154pub struct TestSuite {
155    /// The name of this TestSuite.
156    pub name: XmlString,
157
158    /// The total number of tests in this TestSuite.
159    pub tests: usize,
160
161    /// The total number of disabled tests in this TestSuite.
162    pub disabled: usize,
163
164    /// The total number of tests in this suite that errored.
165    ///
166    /// An "error" is usually some sort of *unexpected* issue in a test.
167    pub errors: usize,
168
169    /// The total number of tests in this suite that failed.
170    ///
171    /// A "failure" is usually some sort of *expected* issue in a test.
172    pub failures: usize,
173
174    /// The time at which the TestSuite began execution.
175    pub timestamp: Option<DateTime<FixedOffset>>,
176
177    /// The overall time taken by the TestSuite.
178    pub time: Option<Duration>,
179
180    /// The test cases that form this TestSuite.
181    pub test_cases: Vec<TestCase>,
182
183    /// Custom properties set during test execution, e.g. environment variables.
184    pub properties: Vec<Property>,
185
186    /// Data written to standard output while the TestSuite was executed.
187    pub system_out: Option<XmlString>,
188
189    /// Data written to standard error while the TestSuite was executed.
190    pub system_err: Option<XmlString>,
191
192    /// Other fields that may be set as attributes, such as "hostname" or "package".
193    pub extra: IndexMap<XmlString, XmlString>,
194}
195
196impl TestSuite {
197    /// Creates a new `TestSuite`.
198    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    /// Sets the start timestamp for the TestSuite.
216    pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
217        self.timestamp = Some(timestamp.into());
218        self
219    }
220
221    /// Sets the time taken for the TestSuite.
222    pub fn set_time(&mut self, time: Duration) -> &mut Self {
223        self.time = Some(time);
224        self
225    }
226
227    /// Adds a property to this TestSuite.
228    pub fn add_property(&mut self, property: impl Into<Property>) -> &mut Self {
229        self.properties.push(property.into());
230        self
231    }
232
233    /// Adds several properties to this TestSuite.
234    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    /// Adds a [`TestCase`] to this TestSuite and updates counts.
245    ///
246    /// When generating a new report, use of this method is recommended over adding to
247    /// `self.test_cases` directly.
248    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    /// Adds several [`TestCase`]s to this TestSuite and updates counts.
263    ///
264    /// When generating a new report, use of this method is recommended over adding to
265    /// `self.test_cases` directly.
266    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    /// Sets standard output.
274    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    /// Sets standard output from a `Vec<u8>`.
280    ///
281    /// The output is converted to a string, lossily.
282    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    /// Sets standard error.
287    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    /// Sets standard error from a `Vec<u8>`.
293    ///
294    /// The output is converted to a string, lossily.
295    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/// Represents a single test case.
301#[derive(Clone, Debug, PartialEq, Eq)]
302#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
303#[non_exhaustive]
304pub struct TestCase {
305    /// The name of the test case.
306    #[cfg_attr(feature = "proptest", strategy(test_name_strategy()))]
307    pub name: XmlString,
308
309    /// The "classname" of the test case.
310    ///
311    /// Typically, this represents the fully qualified path to the test. In other words,
312    /// `classname` + `name` together should uniquely identify and locate a test.
313    pub classname: Option<XmlString>,
314
315    /// The number of assertions in the test case.
316    pub assertions: Option<usize>,
317
318    /// The time at which this test case began execution.
319    ///
320    /// This is not part of the JUnit spec, but may be useful for some tools.
321    #[cfg_attr(feature = "proptest", strategy(option::of(datetime_strategy())))]
322    pub timestamp: Option<DateTime<FixedOffset>>,
323
324    /// The time it took to execute this test case.
325    #[cfg_attr(feature = "proptest", strategy(option::of(duration_strategy())))]
326    pub time: Option<Duration>,
327
328    /// The status of this test.
329    pub status: TestCaseStatus,
330
331    /// Data written to standard output while the test case was executed.
332    pub system_out: Option<XmlString>,
333
334    /// Data written to standard error while the test case was executed.
335    pub system_err: Option<XmlString>,
336
337    /// Other fields that may be set as attributes, such as "classname".
338    #[cfg_attr(feature = "proptest", strategy(xml_attr_index_map_strategy()))]
339    pub extra: IndexMap<XmlString, XmlString>,
340
341    /// Custom properties set during test execution, e.g. steps.
342    #[cfg_attr(feature = "proptest", strategy(collection::vec(any::<Property>(), 0..3)))]
343    pub properties: Vec<Property>,
344}
345
346impl TestCase {
347    /// Creates a new test case.
348    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    /// Sets the classname of the test.
364    pub fn set_classname(&mut self, classname: impl Into<XmlString>) -> &mut Self {
365        self.classname = Some(classname.into());
366        self
367    }
368
369    /// Sets the number of assertions in the test case.
370    pub fn set_assertions(&mut self, assertions: usize) -> &mut Self {
371        self.assertions = Some(assertions);
372        self
373    }
374
375    /// Sets the start timestamp for the test case.
376    pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
377        self.timestamp = Some(timestamp.into());
378        self
379    }
380
381    /// Sets the time taken for the test case.
382    pub fn set_time(&mut self, time: Duration) -> &mut Self {
383        self.time = Some(time);
384        self
385    }
386
387    /// Sets standard output.
388    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    /// Sets standard output from a `Vec<u8>`.
394    ///
395    /// The output is converted to a string, lossily.
396    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    /// Sets standard error.
401    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    /// Sets standard error from a `Vec<u8>`.
407    ///
408    /// The output is converted to a string, lossily.
409    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    /// Adds a property to this TestCase.
414    pub fn add_property(&mut self, property: impl Into<Property>) -> &mut Self {
415        self.properties.push(property.into());
416        self
417    }
418
419    /// Adds several properties to this TestCase.
420    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/// Represents the success or failure of a test case.
432#[derive(Clone, Debug, PartialEq, Eq)]
433#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
434pub enum TestCaseStatus {
435    /// This test case passed.
436    Success {
437        /// Prior runs of the test. These are represented as `flakyFailure` or `flakyError` in the
438        /// JUnit XML.
439        flaky_runs: Vec<TestRerun>,
440    },
441
442    /// This test case did not pass.
443    NonSuccess {
444        /// Whether this test case failed in an expected way (failure) or an unexpected way (error).
445        kind: NonSuccessKind,
446
447        /// The failure message.
448        message: Option<XmlString>,
449
450        /// The "type" of failure that occurred.
451        ty: Option<XmlString>,
452
453        /// The description of the failure.
454        ///
455        /// This is serialized and deserialized from the text node of the element.
456        #[cfg_attr(feature = "proptest", strategy(option::of(text_node_strategy())))]
457        description: Option<XmlString>,
458
459        /// Test reruns and how they are serialized.
460        ///
461        /// See [`NonSuccessReruns`] for details.
462        reruns: NonSuccessReruns,
463    },
464
465    /// This test case was not run.
466    Skipped {
467        /// The skip message.
468        message: Option<XmlString>,
469
470        /// The "type" of skip that occurred.
471        ty: Option<XmlString>,
472
473        /// The description of the skip.
474        ///
475        /// This is serialized and deserialized from the text node of the element.
476        #[cfg_attr(feature = "proptest", strategy(option::of(text_node_strategy())))]
477        description: Option<XmlString>,
478    },
479}
480
481impl TestCaseStatus {
482    /// Creates a new `TestCaseStatus` that represents a successful test.
483    pub fn success() -> Self {
484        TestCaseStatus::Success { flaky_runs: vec![] }
485    }
486
487    /// Creates a new `TestCaseStatus` that represents an unsuccessful test.
488    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    /// Creates a new `TestCaseStatus` that represents a skipped test.
499    pub fn skipped() -> Self {
500        TestCaseStatus::Skipped {
501            message: None,
502            ty: None,
503            description: None,
504        }
505    }
506
507    /// Sets the message. No-op if this is a success case.
508    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    /// Sets the type. No-op if this is a success case.
519    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    /// Sets the description (text node). No-op if this is a success case.
530    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    /// Adds a rerun or flaky run. No-op if this test was skipped.
541    ///
542    /// For `Success`, reruns are always serialized as `<flakyFailure>`/`<flakyError>`.
543    /// For `NonSuccess`, the rerun is added to the existing [`NonSuccessReruns`] variant.
544    pub fn add_rerun(&mut self, rerun: TestRerun) -> &mut Self {
545        self.add_reruns(iter::once(rerun))
546    }
547
548    /// Adds reruns or flaky runs. No-op if this test was skipped.
549    ///
550    /// For `Success`, reruns are always serialized as `<flakyFailure>`/`<flakyError>`.
551    /// For `NonSuccess`, reruns are added to the existing [`NonSuccessReruns`] variant.
552    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    /// Sets the rerun kind for `NonSuccess` statuses.
566    ///
567    /// This controls how reruns are serialized in JUnit XML. Use
568    /// [`FlakyOrRerun::Flaky`] for `<flakyFailure>`/`<flakyError>` (the test exhibited
569    /// flakiness), or [`FlakyOrRerun::Rerun`] for `<rerunFailure>`/`<rerunError>` (the
570    /// default).
571    ///
572    /// This is a no-op for `Success` (in which case reruns are always
573    /// serialized as flaky) and `Skipped` (no reruns).
574    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/// A rerun of a test.
583///
584/// The XML element name depends on context:
585///
586/// - For [`TestCaseStatus::Success`], reruns are always serialized as `<flakyFailure>` or
587///   `<flakyError>`.
588/// - For [`TestCaseStatus::NonSuccess`], the element name is controlled by
589///   [`NonSuccessReruns::kind`].
590#[derive(Clone, Debug, PartialEq, Eq)]
591#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
592pub struct TestRerun {
593    /// The failure kind: error or failure.
594    pub kind: NonSuccessKind,
595
596    /// The time at which this rerun began execution.
597    ///
598    /// This is not part of the JUnit spec, but may be useful for some tools.
599    #[cfg_attr(feature = "proptest", strategy(option::of(datetime_strategy())))]
600    pub timestamp: Option<DateTime<FixedOffset>>,
601
602    /// The time it took to execute this rerun.
603    ///
604    /// This is not part of the JUnit spec, but may be useful for some tools.
605    #[cfg_attr(feature = "proptest", strategy(option::of(duration_strategy())))]
606    pub time: Option<Duration>,
607
608    /// The failure message.
609    pub message: Option<XmlString>,
610
611    /// The "type" of failure that occurred.
612    pub ty: Option<XmlString>,
613
614    /// The stack trace, if any.
615    pub stack_trace: Option<XmlString>,
616
617    /// Data written to standard output while the test rerun was executed.
618    pub system_out: Option<XmlString>,
619
620    /// Data written to standard error while the test rerun was executed.
621    pub system_err: Option<XmlString>,
622
623    /// The description of the failure.
624    ///
625    /// This is serialized and deserialized from the text node of the element.
626    #[cfg_attr(feature = "proptest", strategy(option::of(text_node_strategy())))]
627    pub description: Option<XmlString>,
628}
629
630impl TestRerun {
631    /// Creates a new `TestRerun` of the given kind.
632    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    /// Sets the start timestamp for this rerun.
647    pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
648        self.timestamp = Some(timestamp.into());
649        self
650    }
651
652    /// Sets the time taken for this rerun.
653    pub fn set_time(&mut self, time: Duration) -> &mut Self {
654        self.time = Some(time);
655        self
656    }
657
658    /// Sets the message.
659    pub fn set_message(&mut self, message: impl Into<XmlString>) -> &mut Self {
660        self.message = Some(message.into());
661        self
662    }
663
664    /// Sets the type.
665    pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &mut Self {
666        self.ty = Some(ty.into());
667        self
668    }
669
670    /// Sets the stack trace.
671    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    /// Sets standard output.
677    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    /// Sets standard output from a `Vec<u8>`.
683    ///
684    /// The output is converted to a string, lossily.
685    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    /// Sets standard error.
690    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    /// Sets standard error from a `Vec<u8>`.
696    ///
697    /// The output is converted to a string, lossily.
698    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    /// Sets the description of the failure.
703    pub fn set_description(&mut self, description: impl Into<XmlString>) -> &mut Self {
704        self.description = Some(description.into());
705        self
706    }
707}
708
709/// Whether a test failure is "expected" or not.
710///
711/// An expected test failure is generally one that is anticipated by the test or the harness, while
712/// an unexpected failure might be something like an external service being down or a failure to
713/// execute the binary.
714#[derive(Copy, Clone, Debug, Eq, PartialEq)]
715#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
716pub enum NonSuccessKind {
717    /// This is an expected failure. Serialized as `failure`, `flakyFailure` or `rerunFailure`
718    /// depending on the context.
719    Failure,
720
721    /// This is an unexpected error. Serialized as `error`, `flakyError` or `rerunError` depending
722    /// on the context.
723    Error,
724}
725
726/// Reruns for a [`TestCaseStatus::NonSuccess`] test case.
727///
728/// This type bundles the list of reruns together with how they should be serialized
729/// (`<flakyFailure>`/`<flakyError>` vs `<rerunFailure>`/`<rerunError>`).
730///
731/// For [`TestCaseStatus::Success`], reruns are always serialized as `<flakyFailure>` or
732/// `<flakyError>` and are stored directly in the `flaky_runs` field.
733#[derive(Clone, Debug, PartialEq, Eq)]
734pub struct NonSuccessReruns {
735    /// How reruns are serialized in JUnit XML.
736    ///
737    /// The default is [`FlakyOrRerun::Rerun`] (`<rerunFailure>`/`<rerunError>`).
738    /// Set to [`FlakyOrRerun::Flaky`] for `<flakyFailure>`/`<flakyError>`.
739    ///
740    /// When `runs` is empty, no XML elements are emitted regardless of this value, so the
741    /// `kind` is unobservable and will not be preserved through a serialization roundtrip.
742    pub kind: FlakyOrRerun,
743
744    /// The list of reruns.
745    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/// Controls how reruns in [`TestCaseStatus::NonSuccess`] are represented in JUnit XML.
758///
759/// [`TestCaseStatus::Success`] does not use this type; its reruns are always serialized as
760/// `<flakyFailure>` or `<flakyError>`.
761///
762/// See [`NonSuccessReruns`] for the bundled representation and
763/// [`TestCaseStatus::set_rerun_kind`] for setting the kind.
764#[derive(Copy, Clone, Debug, Eq, PartialEq)]
765#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
766pub enum FlakyOrRerun {
767    /// Reruns represent flaky behavior: the test eventually passed, but these runs failed.
768    /// Serialized as `<flakyFailure>` or `<flakyError>`.
769    Flaky,
770
771    /// Reruns represent retries: the test was retried but ultimately still failed.
772    /// Serialized as `<rerunFailure>` or `<rerunError>`.
773    Rerun,
774}
775
776/// Custom properties set during test execution, e.g. environment variables.
777#[derive(Clone, Debug, PartialEq, Eq)]
778#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
779pub struct Property {
780    /// The name of the property.
781    pub name: XmlString,
782
783    /// The value of the property.
784    pub value: XmlString,
785}
786
787impl Property {
788    /// Creates a new `Property` instance.
789    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/// An owned string suitable for inclusion in XML.
807///
808/// This type filters out invalid XML characters (e.g. ANSI escape codes), and is useful in places
809/// where those codes might be seen -- for example, standard output and standard error.
810///
811/// # Encoding
812///
813/// On Unix platforms, standard output and standard error are typically bytestrings (`Vec<u8>`).
814/// However, XUnit assumes that the output is valid Unicode, and this type definition reflects that.
815#[derive(Clone, Debug, PartialEq, Eq)]
816pub struct XmlString {
817    data: Box<str>,
818}
819
820impl XmlString {
821    /// Creates a new `XmlString`, removing any ANSI escapes and non-printable characters from it.
822    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    /// Returns the data as a string.
835    pub fn as_str(&self) -> &str {
836        &self.data
837    }
838
839    /// Converts self into a string.
840    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        // Need to hash the data as a `str` to obey the `Borrow<str>` invariant.
889        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    // Borrow requires Hash and Ord to be consistent -- use properties to ensure that.
919
920    #[proptest]
921    fn xml_string_hash(s: String) {
922        let xml_string = XmlString::new(&s);
923        // If the string has invalid XML characters, it will no longer be the same so reject those
924        // cases.
925        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        // If the string has invalid XML characters, it will no longer be the same so reject those
939        // cases.
940        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}