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