quick_junit/
report.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{serialize::serialize_report, SerializeError};
5use chrono::{DateTime, FixedOffset};
6use indexmap::map::IndexMap;
7use newtype_uuid::{GenericUuid, TypedUuid, TypedUuidKind, TypedUuidTag};
8use std::{borrow::Borrow, hash::Hash, io, iter, ops::Deref, time::Duration};
9use uuid::Uuid;
10
11/// A tag indicating the kind of report.
12pub enum ReportKind {}
13
14impl TypedUuidKind for ReportKind {
15    fn tag() -> TypedUuidTag {
16        const TAG: TypedUuidTag = TypedUuidTag::new("quick-junit-report");
17        TAG
18    }
19}
20
21/// A unique identifier associated with a report.
22pub type ReportUuid = TypedUuid<ReportKind>;
23
24/// The root element of a JUnit report.
25#[derive(Clone, Debug)]
26pub struct Report {
27    /// The name of this report.
28    pub name: XmlString,
29
30    /// A unique identifier associated with this report.
31    ///
32    /// This is an extension to the spec that's used by nextest.
33    pub uuid: Option<ReportUuid>,
34
35    /// The time at which the first test in this report began execution.
36    ///
37    /// This is not part of the JUnit spec, but may be useful for some tools.
38    pub timestamp: Option<DateTime<FixedOffset>>,
39
40    /// The overall time taken by the test suite.
41    ///
42    /// This is serialized as the number of seconds.
43    pub time: Option<Duration>,
44
45    /// The total number of tests from all TestSuites.
46    pub tests: usize,
47
48    /// The total number of failures from all TestSuites.
49    pub failures: usize,
50
51    /// The total number of errors from all TestSuites.
52    pub errors: usize,
53
54    /// The test suites contained in this report.
55    pub test_suites: Vec<TestSuite>,
56}
57
58impl Report {
59    /// Creates a new `Report` with the given name.
60    pub fn new(name: impl Into<XmlString>) -> Self {
61        Self {
62            name: name.into(),
63            uuid: None,
64            timestamp: None,
65            time: None,
66            tests: 0,
67            failures: 0,
68            errors: 0,
69            test_suites: vec![],
70        }
71    }
72
73    /// Sets a unique ID for this `Report`.
74    ///
75    /// This is an extension that's used by nextest.
76    pub fn set_report_uuid(&mut self, uuid: ReportUuid) -> &mut Self {
77        self.uuid = Some(uuid);
78        self
79    }
80
81    /// Sets a unique ID for this `Report` from an untyped [`Uuid`].
82    ///
83    /// This is an extension that's used by nextest.
84    pub fn set_uuid(&mut self, uuid: Uuid) -> &mut Self {
85        self.uuid = Some(ReportUuid::from_untyped_uuid(uuid));
86        self
87    }
88
89    /// Sets the start timestamp for the report.
90    pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
91        self.timestamp = Some(timestamp.into());
92        self
93    }
94
95    /// Sets the time taken for overall execution.
96    pub fn set_time(&mut self, time: Duration) -> &mut Self {
97        self.time = Some(time);
98        self
99    }
100
101    /// Adds a new TestSuite and updates the `tests`, `failures` and `errors` counts.
102    ///
103    /// When generating a new report, use of this method is recommended over adding to
104    /// `self.TestSuites` directly.
105    pub fn add_test_suite(&mut self, test_suite: TestSuite) -> &mut Self {
106        self.tests += test_suite.tests;
107        self.failures += test_suite.failures;
108        self.errors += test_suite.errors;
109        self.test_suites.push(test_suite);
110        self
111    }
112
113    /// Adds several [`TestSuite`]s and updates the `tests`, `failures` and `errors` counts.
114    ///
115    /// When generating a new report, use of this method is recommended over adding to
116    /// `self.TestSuites` directly.
117    pub fn add_test_suites(
118        &mut self,
119        test_suites: impl IntoIterator<Item = TestSuite>,
120    ) -> &mut Self {
121        for test_suite in test_suites {
122            self.add_test_suite(test_suite);
123        }
124        self
125    }
126
127    /// Serialize this report to the given writer.
128    pub fn serialize(&self, writer: impl io::Write) -> Result<(), SerializeError> {
129        serialize_report(self, writer)
130    }
131
132    /// Serialize this report to a string.
133    pub fn to_string(&self) -> Result<String, SerializeError> {
134        let mut buf: Vec<u8> = vec![];
135        self.serialize(&mut buf)?;
136        String::from_utf8(buf).map_err(|utf8_err| {
137            quick_xml::encoding::EncodingError::from(utf8_err.utf8_error()).into()
138        })
139    }
140}
141
142/// Represents a single TestSuite.
143///
144/// A `TestSuite` groups together several `TestCase` instances.
145#[derive(Clone, Debug)]
146#[non_exhaustive]
147pub struct TestSuite {
148    /// The name of this TestSuite.
149    pub name: XmlString,
150
151    /// The total number of tests in this TestSuite.
152    pub tests: usize,
153
154    /// The total number of disabled tests in this TestSuite.
155    pub disabled: usize,
156
157    /// The total number of tests in this suite that errored.
158    ///
159    /// An "error" is usually some sort of *unexpected* issue in a test.
160    pub errors: usize,
161
162    /// The total number of tests in this suite that failed.
163    ///
164    /// A "failure" is usually some sort of *expected* issue in a test.
165    pub failures: usize,
166
167    /// The time at which the TestSuite began execution.
168    pub timestamp: Option<DateTime<FixedOffset>>,
169
170    /// The overall time taken by the TestSuite.
171    pub time: Option<Duration>,
172
173    /// The test cases that form this TestSuite.
174    pub test_cases: Vec<TestCase>,
175
176    /// Custom properties set during test execution, e.g. environment variables.
177    pub properties: Vec<Property>,
178
179    /// Data written to standard output while the TestSuite was executed.
180    pub system_out: Option<XmlString>,
181
182    /// Data written to standard error while the TestSuite was executed.
183    pub system_err: Option<XmlString>,
184
185    /// Other fields that may be set as attributes, such as "hostname" or "package".
186    pub extra: IndexMap<XmlString, XmlString>,
187}
188
189impl TestSuite {
190    /// Creates a new `TestSuite`.
191    pub fn new(name: impl Into<XmlString>) -> Self {
192        Self {
193            name: name.into(),
194            time: None,
195            timestamp: None,
196            tests: 0,
197            disabled: 0,
198            errors: 0,
199            failures: 0,
200            test_cases: vec![],
201            properties: vec![],
202            system_out: None,
203            system_err: None,
204            extra: IndexMap::new(),
205        }
206    }
207
208    /// Sets the start timestamp for the TestSuite.
209    pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
210        self.timestamp = Some(timestamp.into());
211        self
212    }
213
214    /// Sets the time taken for the TestSuite.
215    pub fn set_time(&mut self, time: Duration) -> &mut Self {
216        self.time = Some(time);
217        self
218    }
219
220    /// Adds a property to this TestSuite.
221    pub fn add_property(&mut self, property: impl Into<Property>) -> &mut Self {
222        self.properties.push(property.into());
223        self
224    }
225
226    /// Adds several properties to this TestSuite.
227    pub fn add_properties(
228        &mut self,
229        properties: impl IntoIterator<Item = impl Into<Property>>,
230    ) -> &mut Self {
231        for property in properties {
232            self.add_property(property);
233        }
234        self
235    }
236
237    /// Adds a [`TestCase`] to this TestSuite and updates counts.
238    ///
239    /// When generating a new report, use of this method is recommended over adding to
240    /// `self.test_cases` directly.
241    pub fn add_test_case(&mut self, test_case: TestCase) -> &mut Self {
242        self.tests += 1;
243        match &test_case.status {
244            TestCaseStatus::Success { .. } => {}
245            TestCaseStatus::NonSuccess { kind, .. } => match kind {
246                NonSuccessKind::Failure => self.failures += 1,
247                NonSuccessKind::Error => self.errors += 1,
248            },
249            TestCaseStatus::Skipped { .. } => self.disabled += 1,
250        }
251        self.test_cases.push(test_case);
252        self
253    }
254
255    /// Adds several [`TestCase`]s to this TestSuite and updates counts.
256    ///
257    /// When generating a new report, use of this method is recommended over adding to
258    /// `self.test_cases` directly.
259    pub fn add_test_cases(&mut self, test_cases: impl IntoIterator<Item = TestCase>) -> &mut Self {
260        for test_case in test_cases {
261            self.add_test_case(test_case);
262        }
263        self
264    }
265
266    /// Sets standard output.
267    pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
268        self.system_out = Some(system_out.into());
269        self
270    }
271
272    /// Sets standard output from a `Vec<u8>`.
273    ///
274    /// The output is converted to a string, lossily.
275    pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
276        self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
277    }
278
279    /// Sets standard error.
280    pub fn set_system_err(&mut self, system_err: impl Into<XmlString>) -> &mut Self {
281        self.system_err = Some(system_err.into());
282        self
283    }
284
285    /// Sets standard error from a `Vec<u8>`.
286    ///
287    /// The output is converted to a string, lossily.
288    pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
289        self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
290    }
291}
292
293/// Represents a single test case.
294#[derive(Clone, Debug)]
295#[non_exhaustive]
296pub struct TestCase {
297    /// The name of the test case.
298    pub name: XmlString,
299
300    /// The "classname" of the test case.
301    ///
302    /// Typically, this represents the fully qualified path to the test. In other words,
303    /// `classname` + `name` together should uniquely identify and locate a test.
304    pub classname: Option<XmlString>,
305
306    /// The number of assertions in the test case.
307    pub assertions: Option<usize>,
308
309    /// The time at which this test case began execution.
310    ///
311    /// This is not part of the JUnit spec, but may be useful for some tools.
312    pub timestamp: Option<DateTime<FixedOffset>>,
313
314    /// The time it took to execute this test case.
315    pub time: Option<Duration>,
316
317    /// The status of this test.
318    pub status: TestCaseStatus,
319
320    /// Data written to standard output while the test case was executed.
321    pub system_out: Option<XmlString>,
322
323    /// Data written to standard error while the test case was executed.
324    pub system_err: Option<XmlString>,
325
326    /// Other fields that may be set as attributes, such as "classname".
327    pub extra: IndexMap<XmlString, XmlString>,
328
329    /// Custom properties set during test execution, e.g. steps.
330    pub properties: Vec<Property>,
331}
332
333impl TestCase {
334    /// Creates a new test case.
335    pub fn new(name: impl Into<XmlString>, status: TestCaseStatus) -> Self {
336        Self {
337            name: name.into(),
338            classname: None,
339            assertions: None,
340            timestamp: None,
341            time: None,
342            status,
343            system_out: None,
344            system_err: None,
345            extra: IndexMap::new(),
346            properties: vec![],
347        }
348    }
349
350    /// Sets the classname of the test.
351    pub fn set_classname(&mut self, classname: impl Into<XmlString>) -> &mut Self {
352        self.classname = Some(classname.into());
353        self
354    }
355
356    /// Sets the number of assertions in the test case.
357    pub fn set_assertions(&mut self, assertions: usize) -> &mut Self {
358        self.assertions = Some(assertions);
359        self
360    }
361
362    /// Sets the start timestamp for the test case.
363    pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
364        self.timestamp = Some(timestamp.into());
365        self
366    }
367
368    /// Sets the time taken for the test case.
369    pub fn set_time(&mut self, time: Duration) -> &mut Self {
370        self.time = Some(time);
371        self
372    }
373
374    /// Sets standard output.
375    pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
376        self.system_out = Some(system_out.into());
377        self
378    }
379
380    /// Sets standard output from a `Vec<u8>`.
381    ///
382    /// The output is converted to a string, lossily.
383    pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
384        self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
385    }
386
387    /// Sets standard error.
388    pub fn set_system_err(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
389        self.system_err = Some(system_out.into());
390        self
391    }
392
393    /// Sets standard error from a `Vec<u8>`.
394    ///
395    /// The output is converted to a string, lossily.
396    pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
397        self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
398    }
399
400    /// Adds a property to this TestCase.
401    pub fn add_property(&mut self, property: impl Into<Property>) -> &mut Self {
402        self.properties.push(property.into());
403        self
404    }
405
406    /// Adds several properties to this TestCase.
407    pub fn add_properties(
408        &mut self,
409        properties: impl IntoIterator<Item = impl Into<Property>>,
410    ) -> &mut Self {
411        for property in properties {
412            self.add_property(property);
413        }
414        self
415    }
416}
417
418/// Represents the success or failure of a test case.
419#[derive(Clone, Debug)]
420pub enum TestCaseStatus {
421    /// This test case passed.
422    Success {
423        /// Prior runs of the test. These are represented as `flakyFailure` or `flakyError` in the
424        /// JUnit XML.
425        flaky_runs: Vec<TestRerun>,
426    },
427
428    /// This test case did not pass.
429    NonSuccess {
430        /// Whether this test case failed in an expected way (failure) or an unexpected way (error).
431        kind: NonSuccessKind,
432
433        /// The failure message.
434        message: Option<XmlString>,
435
436        /// The "type" of failure that occurred.
437        ty: Option<XmlString>,
438
439        /// The description of the failure.
440        ///
441        /// This is serialized and deserialized from the text node of the element.
442        description: Option<XmlString>,
443
444        /// Test reruns. These are represented as `rerunFailure` or `rerunError` in the JUnit XML.
445        reruns: Vec<TestRerun>,
446    },
447
448    /// This test case was not run.
449    Skipped {
450        /// The skip message.
451        message: Option<XmlString>,
452
453        /// The "type" of skip that occurred.
454        ty: Option<XmlString>,
455
456        /// The description of the skip.
457        ///
458        /// This is serialized and deserialized from the text node of the element.
459        description: Option<XmlString>,
460    },
461}
462
463impl TestCaseStatus {
464    /// Creates a new `TestCaseStatus` that represents a successful test.
465    pub fn success() -> Self {
466        TestCaseStatus::Success { flaky_runs: vec![] }
467    }
468
469    /// Creates a new `TestCaseStatus` that represents an unsuccessful test.
470    pub fn non_success(kind: NonSuccessKind) -> Self {
471        TestCaseStatus::NonSuccess {
472            kind,
473            message: None,
474            ty: None,
475            description: None,
476            reruns: vec![],
477        }
478    }
479
480    /// Creates a new `TestCaseStatus` that represents a skipped test.
481    pub fn skipped() -> Self {
482        TestCaseStatus::Skipped {
483            message: None,
484            ty: None,
485            description: None,
486        }
487    }
488
489    /// Sets the message. No-op if this is a success case.
490    pub fn set_message(&mut self, message: impl Into<XmlString>) -> &mut Self {
491        let message_mut = match self {
492            TestCaseStatus::Success { .. } => return self,
493            TestCaseStatus::NonSuccess { message, .. } => message,
494            TestCaseStatus::Skipped { message, .. } => message,
495        };
496        *message_mut = Some(message.into());
497        self
498    }
499
500    /// Sets the type. No-op if this is a success case.
501    pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &mut Self {
502        let ty_mut = match self {
503            TestCaseStatus::Success { .. } => return self,
504            TestCaseStatus::NonSuccess { ty, .. } => ty,
505            TestCaseStatus::Skipped { ty, .. } => ty,
506        };
507        *ty_mut = Some(ty.into());
508        self
509    }
510
511    /// Sets the description (text node). No-op if this is a success case.
512    pub fn set_description(&mut self, description: impl Into<XmlString>) -> &mut Self {
513        let description_mut = match self {
514            TestCaseStatus::Success { .. } => return self,
515            TestCaseStatus::NonSuccess { description, .. } => description,
516            TestCaseStatus::Skipped { description, .. } => description,
517        };
518        *description_mut = Some(description.into());
519        self
520    }
521
522    /// Adds a rerun or flaky run. No-op if this test was skipped.
523    pub fn add_rerun(&mut self, rerun: TestRerun) -> &mut Self {
524        self.add_reruns(iter::once(rerun))
525    }
526
527    /// Adds reruns or flaky runs. No-op if this test was skipped.
528    pub fn add_reruns(&mut self, reruns: impl IntoIterator<Item = TestRerun>) -> &mut Self {
529        let reruns_mut = match self {
530            TestCaseStatus::Success { flaky_runs } => flaky_runs,
531            TestCaseStatus::NonSuccess { reruns, .. } => reruns,
532            TestCaseStatus::Skipped { .. } => return self,
533        };
534        reruns_mut.extend(reruns);
535        self
536    }
537}
538
539/// A rerun of a test.
540///
541/// This is serialized as `flakyFailure` or `flakyError` for successes, and as `rerunFailure` or
542/// `rerunError` for failures/errors.
543#[derive(Clone, Debug)]
544pub struct TestRerun {
545    /// The failure kind: error or failure.
546    pub kind: NonSuccessKind,
547
548    /// The time at which this rerun began execution.
549    ///
550    /// This is not part of the JUnit spec, but may be useful for some tools.
551    pub timestamp: Option<DateTime<FixedOffset>>,
552
553    /// The time it took to execute this rerun.
554    ///
555    /// This is not part of the JUnit spec, but may be useful for some tools.
556    pub time: Option<Duration>,
557
558    /// The failure message.
559    pub message: Option<XmlString>,
560
561    /// The "type" of failure that occurred.
562    pub ty: Option<XmlString>,
563
564    /// The stack trace, if any.
565    pub stack_trace: Option<XmlString>,
566
567    /// Data written to standard output while the test rerun was executed.
568    pub system_out: Option<XmlString>,
569
570    /// Data written to standard error while the test rerun was executed.
571    pub system_err: Option<XmlString>,
572
573    /// The description of the failure.
574    ///
575    /// This is serialized and deserialized from the text node of the element.
576    pub description: Option<XmlString>,
577}
578
579impl TestRerun {
580    /// Creates a new `TestRerun` of the given kind.
581    pub fn new(kind: NonSuccessKind) -> Self {
582        TestRerun {
583            kind,
584            timestamp: None,
585            time: None,
586            message: None,
587            ty: None,
588            stack_trace: None,
589            system_out: None,
590            system_err: None,
591            description: None,
592        }
593    }
594
595    /// Sets the start timestamp for this rerun.
596    pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
597        self.timestamp = Some(timestamp.into());
598        self
599    }
600
601    /// Sets the time taken for this rerun.
602    pub fn set_time(&mut self, time: Duration) -> &mut Self {
603        self.time = Some(time);
604        self
605    }
606
607    /// Sets the message.
608    pub fn set_message(&mut self, message: impl Into<XmlString>) -> &mut Self {
609        self.message = Some(message.into());
610        self
611    }
612
613    /// Sets the type.
614    pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &mut Self {
615        self.ty = Some(ty.into());
616        self
617    }
618
619    /// Sets the stack trace.
620    pub fn set_stack_trace(&mut self, stack_trace: impl Into<XmlString>) -> &mut Self {
621        self.stack_trace = Some(stack_trace.into());
622        self
623    }
624
625    /// Sets standard output.
626    pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
627        self.system_out = Some(system_out.into());
628        self
629    }
630
631    /// Sets standard output from a `Vec<u8>`.
632    ///
633    /// The output is converted to a string, lossily.
634    pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self {
635        self.set_system_out(String::from_utf8_lossy(system_out.as_ref()))
636    }
637
638    /// Sets standard error.
639    pub fn set_system_err(&mut self, system_err: impl Into<XmlString>) -> &mut Self {
640        self.system_err = Some(system_err.into());
641        self
642    }
643
644    /// Sets standard error from a `Vec<u8>`.
645    ///
646    /// The output is converted to a string, lossily.
647    pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self {
648        self.set_system_err(String::from_utf8_lossy(system_err.as_ref()))
649    }
650
651    /// Sets the description of the failure.
652    pub fn set_description(&mut self, description: impl Into<XmlString>) -> &mut Self {
653        self.description = Some(description.into());
654        self
655    }
656}
657
658/// Whether a test failure is "expected" or not.
659///
660/// An expected test failure is generally one that is anticipated by the test or the harness, while
661/// an unexpected failure might be something like an external service being down or a failure to
662/// execute the binary.
663#[derive(Copy, Clone, Debug, Eq, PartialEq)]
664pub enum NonSuccessKind {
665    /// This is an expected failure. Serialized as `failure`, `flakyFailure` or `rerunFailure`
666    /// depending on the context.
667    Failure,
668
669    /// This is an unexpected error. Serialized as `error`, `flakyError` or `rerunError` depending
670    /// on the context.
671    Error,
672}
673
674/// Custom properties set during test execution, e.g. environment variables.
675#[derive(Clone, Debug)]
676pub struct Property {
677    /// The name of the property.
678    pub name: XmlString,
679
680    /// The value of the property.
681    pub value: XmlString,
682}
683
684impl Property {
685    /// Creates a new `Property` instance.
686    pub fn new(name: impl Into<XmlString>, value: impl Into<XmlString>) -> Self {
687        Self {
688            name: name.into(),
689            value: value.into(),
690        }
691    }
692}
693
694impl<T> From<(T, T)> for Property
695where
696    T: Into<XmlString>,
697{
698    fn from((k, v): (T, T)) -> Self {
699        Property::new(k, v)
700    }
701}
702
703/// An owned string suitable for inclusion in XML.
704///
705/// This type filters out invalid XML characters (e.g. ANSI escape codes), and is useful in places
706/// where those codes might be seen -- for example, standard output and standard error.
707///
708/// # Encoding
709///
710/// On Unix platforms, standard output and standard error are typically bytestrings (`Vec<u8>`).
711/// However, XUnit assumes that the output is valid Unicode, and this type definition reflects that.
712#[derive(Clone, Debug, PartialEq, Eq)]
713pub struct XmlString {
714    data: Box<str>,
715}
716
717impl XmlString {
718    /// Creates a new `XmlString`, removing any ANSI escapes and non-printable characters from it.
719    pub fn new(data: impl AsRef<str>) -> Self {
720        let data = data.as_ref();
721        let data = strip_ansi_escapes::strip_str(data);
722        let data = data
723            .replace(
724                |c| matches!(c, '\x00'..='\x08' | '\x0b' | '\x0c' | '\x0e'..='\x1f'),
725                "",
726            )
727            .into_boxed_str();
728        Self { data }
729    }
730
731    /// Returns the data as a string.
732    pub fn as_str(&self) -> &str {
733        &self.data
734    }
735
736    /// Converts self into a string.
737    pub fn into_string(self) -> String {
738        self.data.into_string()
739    }
740}
741
742impl<T: AsRef<str>> From<T> for XmlString {
743    fn from(s: T) -> Self {
744        XmlString::new(s)
745    }
746}
747
748impl From<XmlString> for String {
749    fn from(s: XmlString) -> Self {
750        s.into_string()
751    }
752}
753
754impl Deref for XmlString {
755    type Target = str;
756
757    fn deref(&self) -> &Self::Target {
758        &self.data
759    }
760}
761
762impl Borrow<str> for XmlString {
763    fn borrow(&self) -> &str {
764        &self.data
765    }
766}
767
768impl PartialOrd for XmlString {
769    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
770        Some(self.data.cmp(&other.data))
771    }
772}
773
774impl Ord for XmlString {
775    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
776        self.data.cmp(&other.data)
777    }
778}
779
780impl Hash for XmlString {
781    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
782        // Need to hash the data as a `str` to obey the `Borrow<str>` invariant.
783        self.data.hash(state);
784    }
785}
786
787impl PartialEq<str> for XmlString {
788    fn eq(&self, other: &str) -> bool {
789        &*self.data == other
790    }
791}
792
793impl PartialEq<XmlString> for str {
794    fn eq(&self, other: &XmlString) -> bool {
795        self == &*other.data
796    }
797}
798
799impl PartialEq<String> for XmlString {
800    fn eq(&self, other: &String) -> bool {
801        &*self.data == other
802    }
803}
804
805#[cfg(test)]
806mod tests {
807    use super::*;
808    use proptest::prop_assume;
809    use std::hash::Hasher;
810    use test_strategy::proptest;
811
812    // Borrow requires Hash and Ord to be consistent -- use properties to ensure that.
813
814    #[proptest]
815    fn xml_string_hash(s: String) {
816        let xml_string = XmlString::new(&s);
817        // If the string has invalid XML characters, it will no longer be the same so reject those
818        // cases.
819        prop_assume!(xml_string == s);
820
821        let mut hasher1 = std::collections::hash_map::DefaultHasher::new();
822        let mut hasher2 = std::collections::hash_map::DefaultHasher::new();
823        s.as_str().hash(&mut hasher1);
824        xml_string.hash(&mut hasher2);
825        assert_eq!(hasher1.finish(), hasher2.finish());
826    }
827
828    #[proptest]
829    fn xml_string_ord(s1: String, s2: String) {
830        let xml_string1 = XmlString::new(&s1);
831        let xml_string2 = XmlString::new(&s2);
832        // If the string has invalid XML characters, it will no longer be the same so reject those
833        // cases.
834        prop_assume!(xml_string1 == s1 && xml_string2 == s2);
835
836        assert_eq!(s1.as_str().cmp(s2.as_str()), xml_string1.cmp(&xml_string2));
837    }
838}