quick_junit/
report.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4#[cfg(any(test, feature = "proptest"))]
5use crate::proptest_impls::{
6    datetime_strategy, duration_strategy, test_name_strategy, text_node_strategy,
7    xml_attr_index_map_strategy,
8};
9use crate::{serialize::serialize_report, SerializeError};
10use chrono::{DateTime, FixedOffset};
11use indexmap::map::IndexMap;
12use newtype_uuid::{GenericUuid, TypedUuid, TypedUuidKind, TypedUuidTag};
13#[cfg(any(test, feature = "proptest"))]
14use proptest::{collection, option, prelude::*};
15use std::{borrow::Borrow, hash::Hash, io, iter, ops::Deref, time::Duration};
16use uuid::Uuid;
17
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(any(test, feature = "proptest"), derive(test_strategy::Arbitrary))]
303#[non_exhaustive]
304pub struct TestCase {
305    /// The name of the test case.
306    #[cfg_attr(any(test, 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(
322        any(test, feature = "proptest"),
323        strategy(option::of(datetime_strategy()))
324    )]
325    pub timestamp: Option<DateTime<FixedOffset>>,
326
327    /// The time it took to execute this test case.
328    #[cfg_attr(
329        any(test, feature = "proptest"),
330        strategy(option::of(duration_strategy()))
331    )]
332    pub time: Option<Duration>,
333
334    /// The status of this test.
335    pub status: TestCaseStatus,
336
337    /// Data written to standard output while the test case was executed.
338    pub system_out: Option<XmlString>,
339
340    /// Data written to standard error while the test case was executed.
341    pub system_err: Option<XmlString>,
342
343    /// Other fields that may be set as attributes, such as "classname".
344    #[cfg_attr(
345        any(test, feature = "proptest"),
346        strategy(xml_attr_index_map_strategy())
347    )]
348    pub extra: IndexMap<XmlString, XmlString>,
349
350    /// Custom properties set during test execution, e.g. steps.
351    #[cfg_attr(any(test, feature = "proptest"), strategy(collection::vec(any::<Property>(), 0..3)))]
352    pub properties: Vec<Property>,
353}
354
355impl TestCase {
356    /// Creates a new test case.
357    pub fn new(name: impl Into<XmlString>, status: TestCaseStatus) -> Self {
358        Self {
359            name: name.into(),
360            classname: None,
361            assertions: None,
362            timestamp: None,
363            time: None,
364            status,
365            system_out: None,
366            system_err: None,
367            extra: IndexMap::new(),
368            properties: vec![],
369        }
370    }
371
372    /// Sets the classname of the test.
373    pub fn set_classname(&mut self, classname: impl Into<XmlString>) -> &mut Self {
374        self.classname = Some(classname.into());
375        self
376    }
377
378    /// Sets the number of assertions in the test case.
379    pub fn set_assertions(&mut self, assertions: usize) -> &mut Self {
380        self.assertions = Some(assertions);
381        self
382    }
383
384    /// Sets the start timestamp for the test case.
385    pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
386        self.timestamp = Some(timestamp.into());
387        self
388    }
389
390    /// Sets the time taken for the test case.
391    pub fn set_time(&mut self, time: Duration) -> &mut Self {
392        self.time = Some(time);
393        self
394    }
395
396    /// Sets standard output.
397    pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
398        self.system_out = Some(system_out.into());
399        self
400    }
401
402    /// Sets standard output from a `Vec<u8>`.
403    ///
404    /// The output is converted to a string, lossily.
405    pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
406        self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
407    }
408
409    /// Sets standard error.
410    pub fn set_system_err(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
411        self.system_err = Some(system_out.into());
412        self
413    }
414
415    /// Sets standard error from a `Vec<u8>`.
416    ///
417    /// The output is converted to a string, lossily.
418    pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
419        self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
420    }
421
422    /// Adds a property to this TestCase.
423    pub fn add_property(&mut self, property: impl Into<Property>) -> &mut Self {
424        self.properties.push(property.into());
425        self
426    }
427
428    /// Adds several properties to this TestCase.
429    pub fn add_properties(
430        &mut self,
431        properties: impl IntoIterator<Item = impl Into<Property>>,
432    ) -> &mut Self {
433        for property in properties {
434            self.add_property(property);
435        }
436        self
437    }
438}
439
440/// Represents the success or failure of a test case.
441#[derive(Clone, Debug, PartialEq, Eq)]
442#[cfg_attr(any(test, feature = "proptest"), derive(test_strategy::Arbitrary))]
443pub enum TestCaseStatus {
444    /// This test case passed.
445    Success {
446        /// Prior runs of the test. These are represented as `flakyFailure` or `flakyError` in the
447        /// JUnit XML.
448        flaky_runs: Vec<TestRerun>,
449    },
450
451    /// This test case did not pass.
452    NonSuccess {
453        /// Whether this test case failed in an expected way (failure) or an unexpected way (error).
454        kind: NonSuccessKind,
455
456        /// The failure message.
457        message: Option<XmlString>,
458
459        /// The "type" of failure that occurred.
460        ty: Option<XmlString>,
461
462        /// The description of the failure.
463        ///
464        /// This is serialized and deserialized from the text node of the element.
465        #[cfg_attr(
466            any(test, feature = "proptest"),
467            strategy(option::of(text_node_strategy()))
468        )]
469        description: Option<XmlString>,
470
471        /// Test reruns. These are represented as `rerunFailure` or `rerunError` in the JUnit XML.
472        reruns: Vec<TestRerun>,
473    },
474
475    /// This test case was not run.
476    Skipped {
477        /// The skip message.
478        message: Option<XmlString>,
479
480        /// The "type" of skip that occurred.
481        ty: Option<XmlString>,
482
483        /// The description of the skip.
484        ///
485        /// This is serialized and deserialized from the text node of the element.
486        #[cfg_attr(
487            any(test, feature = "proptest"),
488            strategy(option::of(text_node_strategy()))
489        )]
490        description: Option<XmlString>,
491    },
492}
493
494impl TestCaseStatus {
495    /// Creates a new `TestCaseStatus` that represents a successful test.
496    pub fn success() -> Self {
497        TestCaseStatus::Success { flaky_runs: vec![] }
498    }
499
500    /// Creates a new `TestCaseStatus` that represents an unsuccessful test.
501    pub fn non_success(kind: NonSuccessKind) -> Self {
502        TestCaseStatus::NonSuccess {
503            kind,
504            message: None,
505            ty: None,
506            description: None,
507            reruns: vec![],
508        }
509    }
510
511    /// Creates a new `TestCaseStatus` that represents a skipped test.
512    pub fn skipped() -> Self {
513        TestCaseStatus::Skipped {
514            message: None,
515            ty: None,
516            description: None,
517        }
518    }
519
520    /// Sets the message. No-op if this is a success case.
521    pub fn set_message(&mut self, message: impl Into<XmlString>) -> &mut Self {
522        let message_mut = match self {
523            TestCaseStatus::Success { .. } => return self,
524            TestCaseStatus::NonSuccess { message, .. } => message,
525            TestCaseStatus::Skipped { message, .. } => message,
526        };
527        *message_mut = Some(message.into());
528        self
529    }
530
531    /// Sets the type. No-op if this is a success case.
532    pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &mut Self {
533        let ty_mut = match self {
534            TestCaseStatus::Success { .. } => return self,
535            TestCaseStatus::NonSuccess { ty, .. } => ty,
536            TestCaseStatus::Skipped { ty, .. } => ty,
537        };
538        *ty_mut = Some(ty.into());
539        self
540    }
541
542    /// Sets the description (text node). No-op if this is a success case.
543    pub fn set_description(&mut self, description: impl Into<XmlString>) -> &mut Self {
544        let description_mut = match self {
545            TestCaseStatus::Success { .. } => return self,
546            TestCaseStatus::NonSuccess { description, .. } => description,
547            TestCaseStatus::Skipped { description, .. } => description,
548        };
549        *description_mut = Some(description.into());
550        self
551    }
552
553    /// Adds a rerun or flaky run. No-op if this test was skipped.
554    pub fn add_rerun(&mut self, rerun: TestRerun) -> &mut Self {
555        self.add_reruns(iter::once(rerun))
556    }
557
558    /// Adds reruns or flaky runs. No-op if this test was skipped.
559    pub fn add_reruns(&mut self, reruns: impl IntoIterator<Item = TestRerun>) -> &mut Self {
560        let reruns_mut = match self {
561            TestCaseStatus::Success { flaky_runs } => flaky_runs,
562            TestCaseStatus::NonSuccess { reruns, .. } => reruns,
563            TestCaseStatus::Skipped { .. } => return self,
564        };
565        reruns_mut.extend(reruns);
566        self
567    }
568}
569
570/// A rerun of a test.
571///
572/// This is serialized as `flakyFailure` or `flakyError` for successes, and as `rerunFailure` or
573/// `rerunError` for failures/errors.
574#[derive(Clone, Debug, PartialEq, Eq)]
575#[cfg_attr(any(test, feature = "proptest"), derive(test_strategy::Arbitrary))]
576pub struct TestRerun {
577    /// The failure kind: error or failure.
578    pub kind: NonSuccessKind,
579
580    /// The time at which this rerun began execution.
581    ///
582    /// This is not part of the JUnit spec, but may be useful for some tools.
583    #[cfg_attr(
584        any(test, feature = "proptest"),
585        strategy(option::of(datetime_strategy()))
586    )]
587    pub timestamp: Option<DateTime<FixedOffset>>,
588
589    /// The time it took to execute this rerun.
590    ///
591    /// This is not part of the JUnit spec, but may be useful for some tools.
592    #[cfg_attr(
593        any(test, feature = "proptest"),
594        strategy(option::of(duration_strategy()))
595    )]
596    pub time: Option<Duration>,
597
598    /// The failure message.
599    pub message: Option<XmlString>,
600
601    /// The "type" of failure that occurred.
602    pub ty: Option<XmlString>,
603
604    /// The stack trace, if any.
605    pub stack_trace: Option<XmlString>,
606
607    /// Data written to standard output while the test rerun was executed.
608    pub system_out: Option<XmlString>,
609
610    /// Data written to standard error while the test rerun was executed.
611    pub system_err: Option<XmlString>,
612
613    /// The description of the failure.
614    ///
615    /// This is serialized and deserialized from the text node of the element.
616    #[cfg_attr(
617        any(test, feature = "proptest"),
618        strategy(option::of(text_node_strategy()))
619    )]
620    pub description: Option<XmlString>,
621}
622
623impl TestRerun {
624    /// Creates a new `TestRerun` of the given kind.
625    pub fn new(kind: NonSuccessKind) -> Self {
626        TestRerun {
627            kind,
628            timestamp: None,
629            time: None,
630            message: None,
631            ty: None,
632            stack_trace: None,
633            system_out: None,
634            system_err: None,
635            description: None,
636        }
637    }
638
639    /// Sets the start timestamp for this rerun.
640    pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
641        self.timestamp = Some(timestamp.into());
642        self
643    }
644
645    /// Sets the time taken for this rerun.
646    pub fn set_time(&mut self, time: Duration) -> &mut Self {
647        self.time = Some(time);
648        self
649    }
650
651    /// Sets the message.
652    pub fn set_message(&mut self, message: impl Into<XmlString>) -> &mut Self {
653        self.message = Some(message.into());
654        self
655    }
656
657    /// Sets the type.
658    pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &mut Self {
659        self.ty = Some(ty.into());
660        self
661    }
662
663    /// Sets the stack trace.
664    pub fn set_stack_trace(&mut self, stack_trace: impl Into<XmlString>) -> &mut Self {
665        self.stack_trace = Some(stack_trace.into());
666        self
667    }
668
669    /// Sets standard output.
670    pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
671        self.system_out = Some(system_out.into());
672        self
673    }
674
675    /// Sets standard output from a `Vec<u8>`.
676    ///
677    /// The output is converted to a string, lossily.
678    pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
679        self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
680    }
681
682    /// Sets standard error.
683    pub fn set_system_err(&mut self, system_err: impl Into<XmlString>) -> &mut Self {
684        self.system_err = Some(system_err.into());
685        self
686    }
687
688    /// Sets standard error from a `Vec<u8>`.
689    ///
690    /// The output is converted to a string, lossily.
691    pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
692        self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
693    }
694
695    /// Sets the description of the failure.
696    pub fn set_description(&mut self, description: impl Into<XmlString>) -> &mut Self {
697        self.description = Some(description.into());
698        self
699    }
700}
701
702/// Whether a test failure is "expected" or not.
703///
704/// An expected test failure is generally one that is anticipated by the test or the harness, while
705/// an unexpected failure might be something like an external service being down or a failure to
706/// execute the binary.
707#[derive(Copy, Clone, Debug, Eq, PartialEq)]
708#[cfg_attr(any(test, feature = "proptest"), derive(test_strategy::Arbitrary))]
709pub enum NonSuccessKind {
710    /// This is an expected failure. Serialized as `failure`, `flakyFailure` or `rerunFailure`
711    /// depending on the context.
712    Failure,
713
714    /// This is an unexpected error. Serialized as `error`, `flakyError` or `rerunError` depending
715    /// on the context.
716    Error,
717}
718
719/// Custom properties set during test execution, e.g. environment variables.
720#[derive(Clone, Debug, PartialEq, Eq)]
721#[cfg_attr(any(test, feature = "proptest"), derive(test_strategy::Arbitrary))]
722pub struct Property {
723    /// The name of the property.
724    pub name: XmlString,
725
726    /// The value of the property.
727    pub value: XmlString,
728}
729
730impl Property {
731    /// Creates a new `Property` instance.
732    pub fn new(name: impl Into<XmlString>, value: impl Into<XmlString>) -> Self {
733        Self {
734            name: name.into(),
735            value: value.into(),
736        }
737    }
738}
739
740impl<T> From<(T, T)> for Property
741where
742    T: Into<XmlString>,
743{
744    fn from((k, v): (T, T)) -> Self {
745        Property::new(k, v)
746    }
747}
748
749/// An owned string suitable for inclusion in XML.
750///
751/// This type filters out invalid XML characters (e.g. ANSI escape codes), and is useful in places
752/// where those codes might be seen -- for example, standard output and standard error.
753///
754/// # Encoding
755///
756/// On Unix platforms, standard output and standard error are typically bytestrings (`Vec<u8>`).
757/// However, XUnit assumes that the output is valid Unicode, and this type definition reflects that.
758#[derive(Clone, Debug, PartialEq, Eq)]
759pub struct XmlString {
760    data: Box<str>,
761}
762
763impl XmlString {
764    /// Creates a new `XmlString`, removing any ANSI escapes and non-printable characters from it.
765    pub fn new(data: impl AsRef<str>) -> Self {
766        let data = data.as_ref();
767        let data = strip_ansi_escapes::strip_str(data);
768        let data = data
769            .replace(
770                |c| matches!(c, '\x00'..='\x08' | '\x0b' | '\x0c' | '\x0e'..='\x1f'),
771                "",
772            )
773            .into_boxed_str();
774        Self { data }
775    }
776
777    /// Returns the data as a string.
778    pub fn as_str(&self) -> &str {
779        &self.data
780    }
781
782    /// Converts self into a string.
783    pub fn into_string(self) -> String {
784        self.data.into_string()
785    }
786}
787
788impl<T: AsRef<str>> From<T> for XmlString {
789    fn from(s: T) -> Self {
790        XmlString::new(s)
791    }
792}
793
794impl From<XmlString> for String {
795    fn from(s: XmlString) -> Self {
796        s.into_string()
797    }
798}
799
800impl Deref for XmlString {
801    type Target = str;
802
803    fn deref(&self) -> &Self::Target {
804        &self.data
805    }
806}
807
808impl Borrow<str> for XmlString {
809    fn borrow(&self) -> &str {
810        &self.data
811    }
812}
813
814impl PartialOrd for XmlString {
815    #[inline]
816    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
817        Some(self.cmp(other))
818    }
819}
820
821impl Ord for XmlString {
822    #[inline]
823    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
824        self.data.cmp(&other.data)
825    }
826}
827
828impl Hash for XmlString {
829    #[inline]
830    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
831        // Need to hash the data as a `str` to obey the `Borrow<str>` invariant.
832        self.data.hash(state);
833    }
834}
835
836impl PartialEq<str> for XmlString {
837    fn eq(&self, other: &str) -> bool {
838        &*self.data == other
839    }
840}
841
842impl PartialEq<XmlString> for str {
843    fn eq(&self, other: &XmlString) -> bool {
844        self == &*other.data
845    }
846}
847
848impl PartialEq<String> for XmlString {
849    fn eq(&self, other: &String) -> bool {
850        &*self.data == other
851    }
852}
853
854#[cfg(test)]
855mod tests {
856    use super::*;
857    use proptest::prop_assume;
858    use std::hash::Hasher;
859    use test_strategy::proptest;
860
861    // Borrow requires Hash and Ord to be consistent -- use properties to ensure that.
862
863    #[proptest]
864    fn xml_string_hash(s: String) {
865        let xml_string = XmlString::new(&s);
866        // If the string has invalid XML characters, it will no longer be the same so reject those
867        // cases.
868        prop_assume!(xml_string == s);
869
870        let mut hasher1 = std::collections::hash_map::DefaultHasher::new();
871        let mut hasher2 = std::collections::hash_map::DefaultHasher::new();
872        s.as_str().hash(&mut hasher1);
873        xml_string.hash(&mut hasher2);
874        assert_eq!(hasher1.finish(), hasher2.finish());
875    }
876
877    #[proptest]
878    fn xml_string_ord(s1: String, s2: String) {
879        let xml_string1 = XmlString::new(&s1);
880        let xml_string2 = XmlString::new(&s2);
881        // If the string has invalid XML characters, it will no longer be the same so reject those
882        // cases.
883        prop_assume!(xml_string1 == s1 && xml_string2 == s2);
884
885        assert_eq!(s1.as_str().cmp(s2.as_str()), xml_string1.cmp(&xml_string2));
886    }
887}