Skip to main content

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)]
33#[non_exhaustive]
34pub struct Report {
35    /// The name of this report.
36    pub name: Option<XmlString>,
37
38    /// A unique identifier associated with this report.
39    ///
40    /// This is an extension to the spec that's used by nextest.
41    pub uuid: Option<ReportUuid>,
42
43    /// The time at which the first test in this report began execution.
44    ///
45    /// This is not part of the JUnit spec, but may be useful for some tools.
46    pub timestamp: Option<DateTime<FixedOffset>>,
47
48    /// The overall time taken by the test suite.
49    ///
50    /// This is serialized as the number of seconds.
51    pub time: Option<Duration>,
52
53    /// The total number of tests from all TestSuites.
54    pub tests: usize,
55
56    /// The total number of failures from all TestSuites.
57    pub failures: usize,
58
59    /// The total number of errors from all TestSuites.
60    pub errors: usize,
61
62    /// The total number of tests skipped at runtime across all TestSuites.
63    ///
64    /// This is an extension to the spec that's used by nextest.
65    pub skipped: usize,
66
67    /// The total number of tests disabled by design across all TestSuites.
68    ///
69    /// Unlike [`skipped`](Self::skipped), which counts tests skipped at runtime,
70    /// `disabled` counts tests disabled by design -- for example, googletest's
71    /// `DISABLED_` prefix. `None` means no child suite carried a `disabled`
72    /// count.
73    ///
74    /// [`Self::add_test_suite`] aggregates the `disabled` counts of suites
75    /// which carry that field, but `quick-junit` itself has no notion of
76    /// disabled tests. Suite-level `disabled` counts can be populated either by
77    /// the deserializer or through manual assignment.
78    pub disabled: Option<usize>,
79
80    /// The test suites contained in this report.
81    pub test_suites: Vec<TestSuite>,
82}
83
84impl Report {
85    /// Creates a new `Report` with the given name.
86    pub fn new(name: impl Into<XmlString>) -> Self {
87        let mut report = Self::unnamed();
88        report.name = Some(name.into());
89        report
90    }
91
92    /// Creates a new `Report` without a name.
93    ///
94    /// The `name` attribute on the root `<testsuites>` element is optional in
95    /// the JUnit schema, and some tools omit it. A report created this way
96    /// serializes without a `name` attribute. To set a name later, assign to
97    /// the `name` field.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use quick_junit::Report;
103    ///
104    /// let report = Report::unnamed();
105    /// assert!(report.name.is_none());
106    ///
107    /// let xml = report.to_string().unwrap();
108    /// assert!(xml.contains(r#"<testsuites tests="0""#));
109    /// ```
110    pub fn unnamed() -> Self {
111        Self {
112            name: None,
113            uuid: None,
114            timestamp: None,
115            time: None,
116            tests: 0,
117            failures: 0,
118            errors: 0,
119            skipped: 0,
120            disabled: None,
121            test_suites: vec![],
122        }
123    }
124
125    /// Sets a unique ID for this `Report`.
126    ///
127    /// This is an extension that's used by nextest.
128    pub fn set_report_uuid(&mut self, uuid: ReportUuid) -> &mut Self {
129        self.uuid = Some(uuid);
130        self
131    }
132
133    /// Sets a unique ID for this `Report` from an untyped [`Uuid`].
134    ///
135    /// This is an extension that's used by nextest.
136    pub fn set_uuid(&mut self, uuid: Uuid) -> &mut Self {
137        self.uuid = Some(ReportUuid::from_untyped_uuid(uuid));
138        self
139    }
140
141    /// Sets the start timestamp for the report.
142    pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
143        self.timestamp = Some(timestamp.into());
144        self
145    }
146
147    /// Sets the time taken for overall execution.
148    pub fn set_time(&mut self, time: Duration) -> &mut Self {
149        self.time = Some(time);
150        self
151    }
152
153    /// Adds a new TestSuite and updates the aggregate counts.
154    ///
155    /// This updates the `tests`, `skipped`, `failures`, and `errors` counts. If
156    /// the suite carries a `disabled` count, it is added into `disabled` as
157    /// well, initializing that field from `None` to `Some(0)` if necessary; a
158    /// suite with no `disabled` count leaves `disabled` untouched.
159    ///
160    /// When generating a new report, use of this method is recommended over adding to
161    /// `self.TestSuites` directly.
162    pub fn add_test_suite(&mut self, test_suite: TestSuite) -> &mut Self {
163        self.tests += test_suite.tests;
164        self.failures += test_suite.failures;
165        self.errors += test_suite.errors;
166        self.skipped += test_suite.skipped;
167        if let Some(disabled) = test_suite.disabled {
168            *self.disabled.get_or_insert(0) += disabled;
169        }
170        self.test_suites.push(test_suite);
171        self
172    }
173
174    /// Adds several [`TestSuite`]s and updates the aggregate counts.
175    ///
176    /// This updates the `tests`, `skipped`, `failures`, and `errors` counts, and
177    /// the `disabled` count for any suites that carry one. See
178    /// [`add_test_suite`](Self::add_test_suite) for the details of `disabled`
179    /// aggregation.
180    ///
181    /// When generating a new report, use of this method is recommended over adding to
182    /// `self.TestSuites` directly.
183    pub fn add_test_suites(
184        &mut self,
185        test_suites: impl IntoIterator<Item = TestSuite>,
186    ) -> &mut Self {
187        for test_suite in test_suites {
188            self.add_test_suite(test_suite);
189        }
190        self
191    }
192
193    /// Serialize this report to the given writer.
194    pub fn serialize(&self, writer: impl io::Write) -> Result<(), SerializeError> {
195        serialize_report(self, writer)
196    }
197
198    /// Serialize this report to a string.
199    pub fn to_string(&self) -> Result<String, SerializeError> {
200        let mut buf: Vec<u8> = vec![];
201        self.serialize(&mut buf)?;
202        String::from_utf8(buf).map_err(|utf8_err| {
203            quick_xml::encoding::EncodingError::from(utf8_err.utf8_error()).into()
204        })
205    }
206}
207
208/// Represents a single TestSuite.
209///
210/// A `TestSuite` groups together several `TestCase` instances.
211#[derive(Clone, Debug, PartialEq, Eq)]
212#[non_exhaustive]
213pub struct TestSuite {
214    /// The name of this TestSuite.
215    pub name: XmlString,
216
217    /// The total number of tests in this TestSuite.
218    pub tests: usize,
219
220    /// The total number of tests skipped at runtime in this TestSuite.
221    pub skipped: usize,
222
223    /// The number of tests in this TestSuite disabled by design.
224    ///
225    /// Unlike [`skipped`](Self::skipped), which counts tests skipped at
226    /// runtime, `disabled` counts tests disabled by design -- for example,
227    /// googletest's `DISABLED_` prefix. `None` means the `<testsuite>` element
228    /// carried no `disabled` attribute.
229    ///
230    /// `quick-junit` itself has no notion of disabled tests. This field is
231    /// populated by the deserializer or set manually.
232    pub disabled: Option<usize>,
233
234    /// The total number of tests in this suite that errored.
235    ///
236    /// An "error" is usually some sort of *unexpected* issue in a test.
237    pub errors: usize,
238
239    /// The total number of tests in this suite that failed.
240    ///
241    /// A "failure" is usually some sort of *expected* issue in a test.
242    pub failures: usize,
243
244    /// The time at which the TestSuite began execution.
245    pub timestamp: Option<DateTime<FixedOffset>>,
246
247    /// The overall time taken by the TestSuite.
248    pub time: Option<Duration>,
249
250    /// The test cases that form this TestSuite.
251    pub test_cases: Vec<TestCase>,
252
253    /// Custom properties set during test execution, e.g. environment variables.
254    pub properties: Vec<Property>,
255
256    /// Data written to standard output while the TestSuite was executed.
257    pub system_out: Option<XmlString>,
258
259    /// Data written to standard error while the TestSuite was executed.
260    pub system_err: Option<XmlString>,
261
262    /// Other fields that may be set as attributes, such as "hostname" or "package".
263    pub extra: IndexMap<XmlString, XmlString>,
264}
265
266impl TestSuite {
267    /// Creates a new `TestSuite`.
268    pub fn new(name: impl Into<XmlString>) -> Self {
269        Self {
270            name: name.into(),
271            time: None,
272            timestamp: None,
273            tests: 0,
274            skipped: 0,
275            disabled: None,
276            errors: 0,
277            failures: 0,
278            test_cases: vec![],
279            properties: vec![],
280            system_out: None,
281            system_err: None,
282            extra: IndexMap::new(),
283        }
284    }
285
286    /// Sets the start timestamp for the TestSuite.
287    pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
288        self.timestamp = Some(timestamp.into());
289        self
290    }
291
292    /// Sets the time taken for the TestSuite.
293    pub fn set_time(&mut self, time: Duration) -> &mut Self {
294        self.time = Some(time);
295        self
296    }
297
298    /// Adds a property to this TestSuite.
299    pub fn add_property(&mut self, property: impl Into<Property>) -> &mut Self {
300        self.properties.push(property.into());
301        self
302    }
303
304    /// Adds several properties to this TestSuite.
305    pub fn add_properties(
306        &mut self,
307        properties: impl IntoIterator<Item = impl Into<Property>>,
308    ) -> &mut Self {
309        for property in properties {
310            self.add_property(property);
311        }
312        self
313    }
314
315    /// Adds a [`TestCase`] to this TestSuite and updates counts.
316    ///
317    /// When generating a new report, use of this method is recommended over adding to
318    /// `self.test_cases` directly.
319    pub fn add_test_case(&mut self, test_case: TestCase) -> &mut Self {
320        self.tests += 1;
321        match &test_case.status {
322            TestCaseStatus::Success { .. } => {}
323            TestCaseStatus::NonSuccess { kind, .. } => match kind {
324                NonSuccessKind::Failure => self.failures += 1,
325                NonSuccessKind::Error => self.errors += 1,
326            },
327            TestCaseStatus::Skipped { .. } => self.skipped += 1,
328        }
329        self.test_cases.push(test_case);
330        self
331    }
332
333    /// Adds several [`TestCase`]s to this TestSuite and updates counts.
334    ///
335    /// When generating a new report, use of this method is recommended over adding to
336    /// `self.test_cases` directly.
337    pub fn add_test_cases(&mut self, test_cases: impl IntoIterator<Item = TestCase>) -> &mut Self {
338        for test_case in test_cases {
339            self.add_test_case(test_case);
340        }
341        self
342    }
343
344    /// Sets standard output.
345    pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
346        self.system_out = Some(system_out.into());
347        self
348    }
349
350    /// Sets standard output from a `Vec<u8>`.
351    ///
352    /// The output is converted to a string, lossily.
353    pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
354        self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
355    }
356
357    /// Sets standard error.
358    pub fn set_system_err(&mut self, system_err: impl Into<XmlString>) -> &mut Self {
359        self.system_err = Some(system_err.into());
360        self
361    }
362
363    /// Sets standard error from a `Vec<u8>`.
364    ///
365    /// The output is converted to a string, lossily.
366    pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
367        self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
368    }
369}
370
371/// Represents a single test case.
372#[derive(Clone, Debug, PartialEq, Eq)]
373#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
374#[non_exhaustive]
375pub struct TestCase {
376    /// The name of the test case.
377    #[cfg_attr(feature = "proptest", strategy(test_name_strategy()))]
378    pub name: XmlString,
379
380    /// The "classname" of the test case.
381    ///
382    /// Typically, this represents the fully qualified path to the test. In other words,
383    /// `classname` + `name` together should uniquely identify and locate a test.
384    pub classname: Option<XmlString>,
385
386    /// The number of assertions in the test case.
387    pub assertions: Option<usize>,
388
389    /// The time at which this test case began execution.
390    ///
391    /// This is not part of the JUnit spec, but may be useful for some tools.
392    #[cfg_attr(feature = "proptest", strategy(option::of(datetime_strategy())))]
393    pub timestamp: Option<DateTime<FixedOffset>>,
394
395    /// The time it took to execute this test case.
396    #[cfg_attr(feature = "proptest", strategy(option::of(duration_strategy())))]
397    pub time: Option<Duration>,
398
399    /// The status of this test.
400    pub status: TestCaseStatus,
401
402    /// Data written to standard output while the test case was executed.
403    pub system_out: Option<XmlString>,
404
405    /// Data written to standard error while the test case was executed.
406    pub system_err: Option<XmlString>,
407
408    /// Other fields that may be set as attributes, such as "classname".
409    #[cfg_attr(feature = "proptest", strategy(xml_attr_index_map_strategy()))]
410    pub extra: IndexMap<XmlString, XmlString>,
411
412    /// Custom properties set during test execution, e.g. steps.
413    #[cfg_attr(feature = "proptest", strategy(collection::vec(any::<Property>(), 0..3)))]
414    pub properties: Vec<Property>,
415}
416
417impl TestCase {
418    /// Creates a new test case.
419    pub fn new(name: impl Into<XmlString>, status: TestCaseStatus) -> Self {
420        Self {
421            name: name.into(),
422            classname: None,
423            assertions: None,
424            timestamp: None,
425            time: None,
426            status,
427            system_out: None,
428            system_err: None,
429            extra: IndexMap::new(),
430            properties: vec![],
431        }
432    }
433
434    /// Sets the classname of the test.
435    pub fn set_classname(&mut self, classname: impl Into<XmlString>) -> &mut Self {
436        self.classname = Some(classname.into());
437        self
438    }
439
440    /// Sets the number of assertions in the test case.
441    pub fn set_assertions(&mut self, assertions: usize) -> &mut Self {
442        self.assertions = Some(assertions);
443        self
444    }
445
446    /// Sets the start timestamp for the test case.
447    pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
448        self.timestamp = Some(timestamp.into());
449        self
450    }
451
452    /// Sets the time taken for the test case.
453    pub fn set_time(&mut self, time: Duration) -> &mut Self {
454        self.time = Some(time);
455        self
456    }
457
458    /// Sets standard output.
459    pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
460        self.system_out = Some(system_out.into());
461        self
462    }
463
464    /// Sets standard output from a `Vec<u8>`.
465    ///
466    /// The output is converted to a string, lossily.
467    pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
468        self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
469    }
470
471    /// Sets standard error.
472    pub fn set_system_err(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
473        self.system_err = Some(system_out.into());
474        self
475    }
476
477    /// Sets standard error from a `Vec<u8>`.
478    ///
479    /// The output is converted to a string, lossily.
480    pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
481        self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
482    }
483
484    /// Adds a property to this TestCase.
485    pub fn add_property(&mut self, property: impl Into<Property>) -> &mut Self {
486        self.properties.push(property.into());
487        self
488    }
489
490    /// Adds several properties to this TestCase.
491    pub fn add_properties(
492        &mut self,
493        properties: impl IntoIterator<Item = impl Into<Property>>,
494    ) -> &mut Self {
495        for property in properties {
496            self.add_property(property);
497        }
498        self
499    }
500}
501
502/// Represents the success or failure of a test case.
503#[derive(Clone, Debug, PartialEq, Eq)]
504#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
505pub enum TestCaseStatus {
506    /// This test case passed.
507    Success {
508        /// Prior runs of the test. These are represented as `flakyFailure` or `flakyError` in the
509        /// JUnit XML.
510        #[cfg_attr(
511            feature = "proptest",
512            strategy(collection::vec(any::<TestRerun>(), 0..5))
513        )]
514        flaky_runs: Vec<TestRerun>,
515    },
516
517    /// This test case did not pass.
518    NonSuccess {
519        /// Whether this test case failed in an expected way (failure) or an unexpected way (error).
520        kind: NonSuccessKind,
521
522        /// The failure message.
523        message: Option<XmlString>,
524
525        /// The "type" of failure that occurred.
526        ty: Option<XmlString>,
527
528        /// The description of the failure.
529        ///
530        /// This is serialized and deserialized from the text node of the element.
531        #[cfg_attr(feature = "proptest", strategy(option::of(text_node_strategy())))]
532        description: Option<XmlString>,
533
534        /// Test reruns and how they are serialized.
535        ///
536        /// See [`NonSuccessReruns`] for details.
537        reruns: NonSuccessReruns,
538    },
539
540    /// This test case was not run.
541    Skipped {
542        /// The skip message.
543        message: Option<XmlString>,
544
545        /// The "type" of skip that occurred.
546        ty: Option<XmlString>,
547
548        /// The description of the skip.
549        ///
550        /// This is serialized and deserialized from the text node of the element.
551        #[cfg_attr(feature = "proptest", strategy(option::of(text_node_strategy())))]
552        description: Option<XmlString>,
553    },
554}
555
556impl TestCaseStatus {
557    /// Creates a new `TestCaseStatus` that represents a successful test.
558    pub fn success() -> Self {
559        TestCaseStatus::Success { flaky_runs: vec![] }
560    }
561
562    /// Creates a new `TestCaseStatus` that represents an unsuccessful test.
563    pub fn non_success(kind: NonSuccessKind) -> Self {
564        TestCaseStatus::NonSuccess {
565            kind,
566            message: None,
567            ty: None,
568            description: None,
569            reruns: NonSuccessReruns::default(),
570        }
571    }
572
573    /// Creates a new `TestCaseStatus` that represents a skipped test.
574    pub fn skipped() -> Self {
575        TestCaseStatus::Skipped {
576            message: None,
577            ty: None,
578            description: None,
579        }
580    }
581
582    /// Sets the message. No-op if this is a success case.
583    pub fn set_message(&mut self, message: impl Into<XmlString>) -> &mut Self {
584        let message_mut = match self {
585            TestCaseStatus::Success { .. } => return self,
586            TestCaseStatus::NonSuccess { message, .. } => message,
587            TestCaseStatus::Skipped { message, .. } => message,
588        };
589        *message_mut = Some(message.into());
590        self
591    }
592
593    /// Sets the type. No-op if this is a success case.
594    pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &mut Self {
595        let ty_mut = match self {
596            TestCaseStatus::Success { .. } => return self,
597            TestCaseStatus::NonSuccess { ty, .. } => ty,
598            TestCaseStatus::Skipped { ty, .. } => ty,
599        };
600        *ty_mut = Some(ty.into());
601        self
602    }
603
604    /// Sets the description (text node). No-op if this is a success case.
605    pub fn set_description(&mut self, description: impl Into<XmlString>) -> &mut Self {
606        let description_mut = match self {
607            TestCaseStatus::Success { .. } => return self,
608            TestCaseStatus::NonSuccess { description, .. } => description,
609            TestCaseStatus::Skipped { description, .. } => description,
610        };
611        *description_mut = Some(description.into());
612        self
613    }
614
615    /// Adds a rerun or flaky run. No-op if this test was skipped.
616    ///
617    /// For `Success`, reruns are always serialized as `<flakyFailure>`/`<flakyError>`.
618    /// For `NonSuccess`, the rerun is added to the existing [`NonSuccessReruns`] variant.
619    pub fn add_rerun(&mut self, rerun: TestRerun) -> &mut Self {
620        self.add_reruns(iter::once(rerun))
621    }
622
623    /// Adds reruns or flaky runs. No-op if this test was skipped.
624    ///
625    /// For `Success`, reruns are always serialized as `<flakyFailure>`/`<flakyError>`.
626    /// For `NonSuccess`, reruns are added to the existing [`NonSuccessReruns`] variant.
627    pub fn add_reruns(&mut self, new_reruns: impl IntoIterator<Item = TestRerun>) -> &mut Self {
628        match self {
629            TestCaseStatus::Success { flaky_runs } => {
630                flaky_runs.extend(new_reruns);
631            }
632            TestCaseStatus::NonSuccess { reruns, .. } => {
633                reruns.runs.extend(new_reruns);
634            }
635            TestCaseStatus::Skipped { .. } => {}
636        }
637        self
638    }
639
640    /// Sets the rerun kind for `NonSuccess` statuses.
641    ///
642    /// This controls how reruns are serialized in JUnit XML. Use
643    /// [`FlakyOrRerun::Flaky`] for `<flakyFailure>`/`<flakyError>` (the test exhibited
644    /// flakiness), or [`FlakyOrRerun::Rerun`] for `<rerunFailure>`/`<rerunError>` (the
645    /// default).
646    ///
647    /// This is a no-op for `Success` (in which case reruns are always
648    /// serialized as flaky) and `Skipped` (no reruns).
649    pub fn set_rerun_kind(&mut self, kind: FlakyOrRerun) -> &mut Self {
650        if let TestCaseStatus::NonSuccess { reruns, .. } = self {
651            reruns.kind = kind;
652        }
653        self
654    }
655}
656
657/// A rerun of a test.
658///
659/// The XML element name depends on context:
660///
661/// - For [`TestCaseStatus::Success`], reruns are always serialized as `<flakyFailure>` or
662///   `<flakyError>`.
663/// - For [`TestCaseStatus::NonSuccess`], the element name is controlled by
664///   [`NonSuccessReruns::kind`].
665#[derive(Clone, Debug, PartialEq, Eq)]
666#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
667pub struct TestRerun {
668    /// The failure kind: error or failure.
669    pub kind: NonSuccessKind,
670
671    /// The time at which this rerun began execution.
672    ///
673    /// This is not part of the JUnit spec, but may be useful for some tools.
674    #[cfg_attr(feature = "proptest", strategy(option::of(datetime_strategy())))]
675    pub timestamp: Option<DateTime<FixedOffset>>,
676
677    /// The time it took to execute this rerun.
678    ///
679    /// This is not part of the JUnit spec, but may be useful for some tools.
680    #[cfg_attr(feature = "proptest", strategy(option::of(duration_strategy())))]
681    pub time: Option<Duration>,
682
683    /// The failure message.
684    pub message: Option<XmlString>,
685
686    /// The "type" of failure that occurred.
687    pub ty: Option<XmlString>,
688
689    /// The stack trace, if any.
690    pub stack_trace: Option<XmlString>,
691
692    /// Data written to standard output while the test rerun was executed.
693    pub system_out: Option<XmlString>,
694
695    /// Data written to standard error while the test rerun was executed.
696    pub system_err: Option<XmlString>,
697
698    /// The description of the failure.
699    ///
700    /// This is serialized and deserialized from the text node of the element.
701    #[cfg_attr(feature = "proptest", strategy(option::of(text_node_strategy())))]
702    pub description: Option<XmlString>,
703}
704
705impl TestRerun {
706    /// Creates a new `TestRerun` of the given kind.
707    pub fn new(kind: NonSuccessKind) -> Self {
708        TestRerun {
709            kind,
710            timestamp: None,
711            time: None,
712            message: None,
713            ty: None,
714            stack_trace: None,
715            system_out: None,
716            system_err: None,
717            description: None,
718        }
719    }
720
721    /// Sets the start timestamp for this rerun.
722    pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
723        self.timestamp = Some(timestamp.into());
724        self
725    }
726
727    /// Sets the time taken for this rerun.
728    pub fn set_time(&mut self, time: Duration) -> &mut Self {
729        self.time = Some(time);
730        self
731    }
732
733    /// Sets the message.
734    pub fn set_message(&mut self, message: impl Into<XmlString>) -> &mut Self {
735        self.message = Some(message.into());
736        self
737    }
738
739    /// Sets the type.
740    pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &mut Self {
741        self.ty = Some(ty.into());
742        self
743    }
744
745    /// Sets the stack trace.
746    pub fn set_stack_trace(&mut self, stack_trace: impl Into<XmlString>) -> &mut Self {
747        self.stack_trace = Some(stack_trace.into());
748        self
749    }
750
751    /// Sets standard output.
752    pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
753        self.system_out = Some(system_out.into());
754        self
755    }
756
757    /// Sets standard output from a `Vec<u8>`.
758    ///
759    /// The output is converted to a string, lossily.
760    pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
761        self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
762    }
763
764    /// Sets standard error.
765    pub fn set_system_err(&mut self, system_err: impl Into<XmlString>) -> &mut Self {
766        self.system_err = Some(system_err.into());
767        self
768    }
769
770    /// Sets standard error from a `Vec<u8>`.
771    ///
772    /// The output is converted to a string, lossily.
773    pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
774        self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
775    }
776
777    /// Sets the description of the failure.
778    pub fn set_description(&mut self, description: impl Into<XmlString>) -> &mut Self {
779        self.description = Some(description.into());
780        self
781    }
782}
783
784/// Whether a test failure is "expected" or not.
785///
786/// An expected test failure is generally one that is anticipated by the test or the harness, while
787/// an unexpected failure might be something like an external service being down or a failure to
788/// execute the binary.
789#[derive(Copy, Clone, Debug, Eq, PartialEq)]
790#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
791pub enum NonSuccessKind {
792    /// This is an expected failure. Serialized as `failure`, `flakyFailure` or `rerunFailure`
793    /// depending on the context.
794    Failure,
795
796    /// This is an unexpected error. Serialized as `error`, `flakyError` or `rerunError` depending
797    /// on the context.
798    Error,
799}
800
801/// Reruns for a [`TestCaseStatus::NonSuccess`] test case.
802///
803/// This type bundles the list of reruns together with how they should be serialized
804/// (`<flakyFailure>`/`<flakyError>` vs `<rerunFailure>`/`<rerunError>`).
805///
806/// For [`TestCaseStatus::Success`], reruns are always serialized as `<flakyFailure>` or
807/// `<flakyError>` and are stored directly in the `flaky_runs` field.
808#[derive(Clone, Debug, PartialEq, Eq)]
809pub struct NonSuccessReruns {
810    /// How reruns are serialized in JUnit XML.
811    ///
812    /// The default is [`FlakyOrRerun::Rerun`] (`<rerunFailure>`/`<rerunError>`).
813    /// Set to [`FlakyOrRerun::Flaky`] for `<flakyFailure>`/`<flakyError>`.
814    ///
815    /// When `runs` is empty, no XML elements are emitted regardless of this value, so the
816    /// `kind` is unobservable and will not be preserved through a serialization roundtrip.
817    pub kind: FlakyOrRerun,
818
819    /// The list of reruns.
820    pub runs: Vec<TestRerun>,
821}
822
823impl Default for NonSuccessReruns {
824    fn default() -> Self {
825        Self {
826            kind: FlakyOrRerun::Rerun,
827            runs: vec![],
828        }
829    }
830}
831
832/// Controls how reruns in [`TestCaseStatus::NonSuccess`] are represented in JUnit XML.
833///
834/// [`TestCaseStatus::Success`] does not use this type; its reruns are always serialized as
835/// `<flakyFailure>` or `<flakyError>`.
836///
837/// See [`NonSuccessReruns`] for the bundled representation and
838/// [`TestCaseStatus::set_rerun_kind`] for setting the kind.
839#[derive(Copy, Clone, Debug, Eq, PartialEq)]
840#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
841pub enum FlakyOrRerun {
842    /// Reruns represent flaky behavior: the test eventually passed, but these runs failed.
843    /// Serialized as `<flakyFailure>` or `<flakyError>`.
844    Flaky,
845
846    /// Reruns represent retries: the test was retried but ultimately still failed.
847    /// Serialized as `<rerunFailure>` or `<rerunError>`.
848    Rerun,
849}
850
851/// Custom properties set during test execution, e.g. environment variables.
852#[derive(Clone, Debug, PartialEq, Eq)]
853#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
854pub struct Property {
855    /// The name of the property.
856    pub name: XmlString,
857
858    /// The value of the property.
859    pub value: XmlString,
860}
861
862impl Property {
863    /// Creates a new `Property` instance.
864    pub fn new(name: impl Into<XmlString>, value: impl Into<XmlString>) -> Self {
865        Self {
866            name: name.into(),
867            value: value.into(),
868        }
869    }
870}
871
872impl<T> From<(T, T)> for Property
873where
874    T: Into<XmlString>,
875{
876    fn from((k, v): (T, T)) -> Self {
877        Property::new(k, v)
878    }
879}
880
881/// An owned string suitable for inclusion in XML.
882///
883/// This type filters out invalid XML characters (e.g. ANSI escape codes), and is useful in places
884/// where those codes might be seen -- for example, standard output and standard error.
885///
886/// # Encoding
887///
888/// On Unix platforms, standard output and standard error are typically bytestrings (`Vec<u8>`).
889/// However, XUnit assumes that the output is valid Unicode, and this type definition reflects that.
890#[derive(Clone, Debug, PartialEq, Eq)]
891pub struct XmlString {
892    data: Box<str>,
893}
894
895impl XmlString {
896    /// Creates a new `XmlString`, removing any ANSI escapes and non-printable characters from it.
897    pub fn new(data: impl AsRef<str>) -> Self {
898        let data = data.as_ref();
899        let data = strip_ansi_escapes::strip_str(data);
900        let data = data
901            .replace(
902                |c| matches!(c, '\x00'..='\x08' | '\x0b' | '\x0c' | '\x0e'..='\x1f'),
903                "",
904            )
905            .into_boxed_str();
906        Self { data }
907    }
908
909    /// Returns the data as a string.
910    pub fn as_str(&self) -> &str {
911        &self.data
912    }
913
914    /// Converts self into a string.
915    pub fn into_string(self) -> String {
916        self.data.into_string()
917    }
918}
919
920impl<T: AsRef<str>> From<T> for XmlString {
921    fn from(s: T) -> Self {
922        XmlString::new(s)
923    }
924}
925
926impl From<XmlString> for String {
927    fn from(s: XmlString) -> Self {
928        s.into_string()
929    }
930}
931
932impl Deref for XmlString {
933    type Target = str;
934
935    fn deref(&self) -> &Self::Target {
936        &self.data
937    }
938}
939
940impl Borrow<str> for XmlString {
941    fn borrow(&self) -> &str {
942        &self.data
943    }
944}
945
946impl PartialOrd for XmlString {
947    #[inline]
948    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
949        Some(self.cmp(other))
950    }
951}
952
953impl Ord for XmlString {
954    #[inline]
955    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
956        self.data.cmp(&other.data)
957    }
958}
959
960impl Hash for XmlString {
961    #[inline]
962    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
963        // Need to hash the data as a `str` to obey the `Borrow<str>` invariant.
964        self.data.hash(state);
965    }
966}
967
968impl PartialEq<str> for XmlString {
969    fn eq(&self, other: &str) -> bool {
970        &*self.data == other
971    }
972}
973
974impl PartialEq<XmlString> for str {
975    fn eq(&self, other: &XmlString) -> bool {
976        self == &*other.data
977    }
978}
979
980impl PartialEq<String> for XmlString {
981    fn eq(&self, other: &String) -> bool {
982        &*self.data == other
983    }
984}
985
986#[cfg(test)]
987mod tests {
988    use super::*;
989    use proptest::prop_assume;
990    use std::hash::Hasher;
991    use test_strategy::proptest;
992
993    // Borrow requires Hash and Ord to be consistent -- use properties to ensure that.
994
995    #[proptest]
996    fn xml_string_hash(s: String) {
997        let xml_string = XmlString::new(&s);
998        // If the string has invalid XML characters, it will no longer be the same so reject those
999        // cases.
1000        prop_assume!(xml_string == s);
1001
1002        let mut hasher1 = std::collections::hash_map::DefaultHasher::new();
1003        let mut hasher2 = std::collections::hash_map::DefaultHasher::new();
1004        s.as_str().hash(&mut hasher1);
1005        xml_string.hash(&mut hasher2);
1006        assert_eq!(hasher1.finish(), hasher2.finish());
1007    }
1008
1009    #[proptest]
1010    fn xml_string_ord(s1: String, s2: String) {
1011        let xml_string1 = XmlString::new(&s1);
1012        let xml_string2 = XmlString::new(&s2);
1013        // If the string has invalid XML characters, it will no longer be the same so reject those
1014        // cases.
1015        prop_assume!(xml_string1 == s1 && xml_string2 == s2);
1016
1017        assert_eq!(s1.as_str().cmp(s2.as_str()), xml_string1.cmp(&xml_string2));
1018    }
1019}