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