quick-junit-0.3.3/.cargo_vcs_info.json0000644000000001510000000000100132770ustar { "git": { "sha1": "3b57b14302fc8158bdcaad2c44a0c7afd0b9ef3c" }, "path_in_vcs": "quick-junit" }quick-junit-0.3.3/CHANGELOG.md000064400000000000000000000044041046102023000137050ustar 00000000000000# Changelog ## [0.3.3] - 2023-06-07 ### Added - `TestCase` now has an extra `properties` section and an `add_property` method, similar to `TestSuite`. Thanks [@skycoop](https://github.com/skycoop) for your first contribution! ### Changed - Internal dependency update: quick-xml updated to 0.29.0. - MSRV updated to Rust 1.66. ## [0.3.2] - 2022-11-23 ### Changed - Internal dependency update: quick-xml updated to 0.26.0. - MSRV updated to Rust 1.62. ## [0.3.1] - 2022-11-23 (This version was not published due to a code issue.) ## [0.3.0] - 2022-07-27 ### Added - `Report` contains a new `uuid` field with a unique identifier for a particular run. This is an extension to the JUnit spec. ## [0.2.0] - 2022-06-21 ### Changed - quick-xml updated to 0.23.0. - The error type is now defined by quick-junit, so that future breaking changes to quick-xml will not necessitate a breaking change to this crate. - MSRV bumped to Rust 1.59. ## [0.1.5] - 2022-02-14 ### Changed - Lower MSRV to Rust 1.54. ## [0.1.4] - 2022-02-07 ### Fixed - In readme, fix link to cargo-nextest. ### Changed - Update repository location. ## [0.1.3] - 2022-01-29 - In the readme, replace Markdown checkboxes with Unicode ✅ to make them render properly on crates.io. ## [0.1.2] - 2022-01-29 - Expand readme. - Add keywords and categories. ## [0.1.1] - 2022-01-28 - Fix repository field in Cargo.toml. ## [0.1.0] - 2022-01-28 - Initial version. [0.3.3]: https://github.com/nextest-rs/nextest/releases/tag/quick-junit-0.3.3 [0.3.2]: https://github.com/nextest-rs/nextest/releases/tag/quick-junit-0.3.2 [0.3.1]: https://github.com/nextest-rs/nextest/releases/tag/quick-junit-0.3.1 [0.3.0]: https://github.com/nextest-rs/nextest/releases/tag/quick-junit-0.3.0 [0.2.0]: https://github.com/nextest-rs/nextest/releases/tag/quick-junit-0.2.0 [0.1.5]: https://github.com/nextest-rs/nextest/releases/tag/quick-junit-0.1.5 [0.1.4]: https://github.com/nextest-rs/nextest/releases/tag/quick-junit-0.1.4 [0.1.3]: https://github.com/diem/diem-devtools/releases/tag/quick-junit-0.1.3 [0.1.2]: https://github.com/diem/diem-devtools/releases/tag/quick-junit-0.1.2 [0.1.1]: https://github.com/diem/diem-devtools/releases/tag/quick-junit-0.1.1 [0.1.0]: https://github.com/diem/diem-devtools/releases/tag/quick-junit-0.1.0 quick-junit-0.3.3/Cargo.toml0000644000000024010000000000100112750ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.66" name = "quick-junit" version = "0.3.3" description = "Data model and serializer for JUnit/XUnit XML" documentation = "https://docs.rs/quick-junit" readme = "README.md" keywords = [ "junit", "xunit", "xml", "serializer", "flaky-tests", ] categories = [ "encoding", "development-tools", ] license = "Apache-2.0 OR MIT" repository = "https://github.com/nextest-rs/nextest" [dependencies.chrono] version = "0.4.26" [dependencies.indexmap] version = "2.0.0" [dependencies.nextest-workspace-hack] version = "0.1" [dependencies.quick-xml] version = "0.29.0" [dependencies.thiserror] version = "1.0.40" [dependencies.uuid] version = "1.3.4" [dev-dependencies.goldenfile] version = "1.4.5" [dev-dependencies.owo-colors] version = "3.5.0" quick-junit-0.3.3/Cargo.toml.orig000064400000000000000000000012221046102023000147560ustar 00000000000000[package] name = "quick-junit" description = "Data model and serializer for JUnit/XUnit XML" version = "0.3.3" readme = "README.md" license = "Apache-2.0 OR MIT" repository = "https://github.com/nextest-rs/nextest" documentation = "https://docs.rs/quick-junit" keywords = ["junit", "xunit", "xml", "serializer", "flaky-tests"] categories = ["encoding", "development-tools"] edition = "2021" rust-version = "1.66" [dependencies] chrono = "0.4.26" indexmap = "2.0.0" quick-xml = "0.29.0" thiserror = "1.0.40" uuid = "1.3.4" nextest-workspace-hack = { version = "0.1", path = "../workspace-hack" } [dev-dependencies] goldenfile = "1.4.5" owo-colors = "3.5.0" quick-junit-0.3.3/README.md000064400000000000000000000070551046102023000133600ustar 00000000000000# quick-junit [![quick-junit on crates.io](https://img.shields.io/crates/v/quick-junit)](https://crates.io/crates/quick-junit) [![Documentation (latest release)](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://docs.rs/quick-junit/) [![Documentation (main)](https://img.shields.io/badge/docs-main-purple)](https://nexte.st/rustdoc/quick_junit/) [![Changelog](https://img.shields.io/badge/changelog-latest-blue)](CHANGELOG.md) [![License](https://img.shields.io/badge/license-Apache-green.svg)](LICENSE-APACHE) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE-MIT) `quick-junit` is a JUnit/XUnit XML data model and serializer for Rust. This crate allows users to create a JUnit report as an XML file. JUnit XML files are widely supported by test tooling. This crate is built to serve the needs of [cargo-nextest](https://nexte.st). ## Overview The root element of a JUnit report is a `Report`. A `Report` consists of one or more `TestSuite` instances. A `TestSuite` instance consists of one or more `TestCase`s. The status (success, failure, error, or skipped) of a `TestCase` is represented by `TestCaseStatus`. ## Features - ✅ Serializing JUnit/XUnit to the [Jenkins format](https://llg.cubic.org/docs/junit/). - ✅ Including test reruns using `TestRerun` - ✅ Including flaky tests - ✅ Including standard output and error - ✅ Filtering out [invalid XML characters](https://en.wikipedia.org/wiki/Valid_characters_in_XML) (eg ANSI escape codes) from the output - ✅ Automatically keeping track of success, failure and error counts - ✅ Arbitrary properties and extra attributes This crate does not currently support deserializing JUnit XML. (PRs are welcome!) ## Examples ```rust use quick_junit::*; let mut report = Report::new("my-test-run"); let mut test_suite = TestSuite::new("my-test-suite"); let success_case = TestCase::new("success-case", TestCaseStatus::success()); let failure_case = TestCase::new("failure-case", TestCaseStatus::non_success(NonSuccessKind::Failure)); test_suite.add_test_cases([success_case, failure_case]); report.add_test_suite(test_suite); const EXPECTED_XML: &str = r#" "#; assert_eq!(report.to_string().unwrap(), EXPECTED_XML); ``` For a more comprehensive example, including reruns and flaky tests, see [`fixture_tests.rs`](https://github.com/nextest-rs/nextest/blob/main/quick-junit/tests/fixture_tests.rs). ## Minimum supported Rust version (MSRV) The minimum supported Rust version is **Rust 1.66.** While this crate is a pre-release (0.x.x) it may have its MSRV bumped in a patch release. Once a crate has reached 1.x, any MSRV bump will be accompanied with a new minor version. ## Alternatives * [**junit-report**](https://crates.io/crates/junit-report): Older, more mature project. Doesn't appear to support flaky tests or arbitrary properties as of version 0.7.0. ## Contributing See the [CONTRIBUTING](../CONTRIBUTING.md) file for how to help out. ## License This project is available under the terms of either the [Apache 2.0 license](../LICENSE-APACHE) or the [MIT license](../LICENSE-MIT). quick-junit-0.3.3/README.tpl000064400000000000000000000017431046102023000135550ustar 00000000000000# {{crate}} [![quick-junit on crates.io](https://img.shields.io/crates/v/quick-junit)](https://crates.io/crates/quick-junit) [![Documentation (latest release)](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://docs.rs/quick-junit/) [![Documentation (main)](https://img.shields.io/badge/docs-main-purple)](https://nexte.st/rustdoc/quick_junit/) [![Changelog](https://img.shields.io/badge/changelog-latest-blue)](CHANGELOG.md) [![License](https://img.shields.io/badge/license-Apache-green.svg)](LICENSE-APACHE) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE-MIT) {{readme}} ## Contributing See the [CONTRIBUTING](../CONTRIBUTING.md) file for how to help out. ## License This project is available under the terms of either the [Apache 2.0 license](../LICENSE-APACHE) or the [MIT license](../LICENSE-MIT). quick-junit-0.3.3/src/errors.rs000064400000000000000000000006771046102023000145550ustar 00000000000000// Copyright (c) The nextest Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use thiserror::Error; /// An error that occurs while serializing a [`Report`](crate::Report). /// /// Returned by [`Report::serialize`](crate::Report::serialize) and /// [`Report::to_string`](crate::Report::to_string). #[derive(Debug, Error)] #[error("error serializing JUnit report")] pub struct SerializeError { #[from] inner: quick_xml::Error, } quick-junit-0.3.3/src/lib.rs000064400000000000000000000060301046102023000137740ustar 00000000000000// Copyright (c) The nextest Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 #![warn(missing_docs)] //! `quick-junit` is a JUnit/XUnit XML data model and serializer for Rust. This crate allows users //! to create a JUnit report as an XML file. JUnit XML files are widely supported by test tooling. //! //! This crate is built to serve the needs of [cargo-nextest](https://nexte.st). //! //! # Overview //! //! The root element of a JUnit report is a [`Report`]. A [`Report`] consists of one or more //! [`TestSuite`] instances. A [`TestSuite`] instance consists of one or more [`TestCase`]s. //! //! The status (success, failure, error, or skipped) of a [`TestCase`] is represented by [`TestCaseStatus`]. //! //! # Features //! //! - ✅ Serializing JUnit/XUnit to the [Jenkins format](https://llg.cubic.org/docs/junit/). //! - ✅ Including test reruns using [`TestRerun`] //! - ✅ Including flaky tests //! - ✅ Including standard output and error //! - ✅ Filtering out [invalid XML //! characters](https://en.wikipedia.org/wiki/Valid_characters_in_XML) (eg ANSI escape codes) //! from the output //! - ✅ Automatically keeping track of success, failure and error counts //! - ✅ Arbitrary properties and extra attributes //! //! This crate does not currently support deserializing JUnit XML. (PRs are welcome!) //! //! # Examples //! //! ```rust //! use quick_junit::*; //! //! let mut report = Report::new("my-test-run"); //! let mut test_suite = TestSuite::new("my-test-suite"); //! let success_case = TestCase::new("success-case", TestCaseStatus::success()); //! let failure_case = TestCase::new("failure-case", TestCaseStatus::non_success(NonSuccessKind::Failure)); //! test_suite.add_test_cases([success_case, failure_case]); //! report.add_test_suite(test_suite); //! //! const EXPECTED_XML: &str = r#" //! //! //! //! //! //! //! //! //! //! "#; //! //! assert_eq!(report.to_string().unwrap(), EXPECTED_XML); //! ``` //! //! For a more comprehensive example, including reruns and flaky tests, see //! [`fixture_tests.rs`](https://github.com/nextest-rs/nextest/blob/main/quick-junit/tests/fixture_tests.rs). //! //! # Minimum supported Rust version (MSRV) //! //! The minimum supported Rust version is **Rust 1.66.** //! //! While this crate is a pre-release (0.x.x) it may have its MSRV bumped in a patch release. //! Once a crate has reached 1.x, any MSRV bump will be accompanied with a new minor version. //! //! # Alternatives //! //! * [**junit-report**](https://crates.io/crates/junit-report): Older, more mature project. Doesn't //! appear to support flaky tests or arbitrary properties as of version 0.7.0. mod errors; mod report; mod serialize; pub use errors::*; pub use report::*; quick-junit-0.3.3/src/report.rs000064400000000000000000000540641046102023000145530ustar 00000000000000// Copyright (c) The nextest Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{serialize::serialize_report, SerializeError}; use chrono::{DateTime, FixedOffset}; use indexmap::map::IndexMap; use std::{io, iter, time::Duration}; use uuid::Uuid; /// The root element of a JUnit report. #[derive(Clone, Debug)] pub struct Report { /// The name of this report. pub name: String, /// A unique identifier associated with this report. /// /// This is an extension to the spec that's used by nextest. pub uuid: Option, /// The time at which the first test in this report began execution. /// /// This is not part of the JUnit spec, but may be useful for some tools. pub timestamp: Option>, /// The overall time taken by the test suite. /// /// This is serialized as the number of seconds. pub time: Option, /// The total number of tests from all TestSuites. pub tests: usize, /// The total number of failures from all TestSuites. pub failures: usize, /// The total number of errors from all TestSuites. pub errors: usize, /// The test suites contained in this report. pub test_suites: Vec, } impl Report { /// Creates a new `Report` with the given name. pub fn new(name: impl Into) -> Self { Self { name: name.into(), uuid: None, timestamp: None, time: None, tests: 0, failures: 0, errors: 0, test_suites: vec![], } } /// Sets a unique ID for this `Report`. /// /// This is an extension that's used by nextest. pub fn set_uuid(&mut self, uuid: Uuid) -> &mut Self { self.uuid = Some(uuid); self } /// Sets the start timestamp for the report. pub fn set_timestamp(&mut self, timestamp: impl Into>) -> &mut Self { self.timestamp = Some(timestamp.into()); self } /// Sets the time taken for overall execution. pub fn set_time(&mut self, time: Duration) -> &mut Self { self.time = Some(time); self } /// Adds a new TestSuite and updates the `tests`, `failures` and `errors` counts. /// /// When generating a new report, use of this method is recommended over adding to /// `self.TestSuites` directly. pub fn add_test_suite(&mut self, test_suite: TestSuite) -> &mut Self { self.tests += test_suite.tests; self.failures += test_suite.failures; self.errors += test_suite.errors; self.test_suites.push(test_suite); self } /// Adds several [`TestSuite`]s and updates the `tests`, `failures` and `errors` counts. /// /// When generating a new report, use of this method is recommended over adding to /// `self.TestSuites` directly. pub fn add_test_suites( &mut self, test_suites: impl IntoIterator, ) -> &mut Self { for test_suite in test_suites { self.add_test_suite(test_suite); } self } /// Serialize this report to the given writer. pub fn serialize(&self, writer: impl io::Write) -> Result<(), SerializeError> { serialize_report(self, writer) } /// Serialize this report to a string. pub fn to_string(&self) -> Result { let mut buf: Vec = vec![]; self.serialize(&mut buf)?; String::from_utf8(buf).map_err(|utf8_err| quick_xml::Error::from(utf8_err).into()) } } /// Represents a single TestSuite. /// /// A `TestSuite` groups together several `TestCase` instances. #[derive(Clone, Debug)] #[non_exhaustive] pub struct TestSuite { /// The name of this TestSuite. pub name: String, /// The total number of tests in this TestSuite. pub tests: usize, /// The total number of disabled tests in this TestSuite. pub disabled: usize, /// The total number of tests in this suite that errored. /// /// An "error" is usually some sort of *unexpected* issue in a test. pub errors: usize, /// The total number of tests in this suite that failed. /// /// A "failure" is usually some sort of *expected* issue in a test. pub failures: usize, /// The time at which the TestSuite began execution. pub timestamp: Option>, /// The overall time taken by the TestSuite. pub time: Option, /// The test cases that form this TestSuite. pub test_cases: Vec, /// Custom properties set during test execution, e.g. environment variables. pub properties: Vec, /// Data written to standard output while the TestSuite was executed. pub system_out: Option, /// Data written to standard error while the TestSuite was executed. pub system_err: Option, /// Other fields that may be set as attributes, such as "hostname" or "package". pub extra: IndexMap, } impl TestSuite { /// Creates a new `TestSuite`. pub fn new(name: impl Into) -> Self { Self { name: name.into(), time: None, timestamp: None, tests: 0, disabled: 0, errors: 0, failures: 0, test_cases: vec![], properties: vec![], system_out: None, system_err: None, extra: IndexMap::new(), } } /// Sets the start timestamp for the TestSuite. pub fn set_timestamp(&mut self, timestamp: impl Into>) -> &mut Self { self.timestamp = Some(timestamp.into()); self } /// Sets the time taken for the TestSuite. pub fn set_time(&mut self, time: Duration) -> &mut Self { self.time = Some(time); self } /// Adds a property to this TestSuite. pub fn add_property(&mut self, property: impl Into) -> &mut Self { self.properties.push(property.into()); self } /// Adds several properties to this TestSuite. pub fn add_properties( &mut self, properties: impl IntoIterator>, ) -> &mut Self { for property in properties { self.add_property(property); } self } /// Adds a [`TestCase`] to this TestSuite and updates counts. /// /// When generating a new report, use of this method is recommended over adding to /// `self.test_cases` directly. pub fn add_test_case(&mut self, test_case: TestCase) -> &mut Self { self.tests += 1; match &test_case.status { TestCaseStatus::Success { .. } => {} TestCaseStatus::NonSuccess { kind, .. } => match kind { NonSuccessKind::Failure => self.failures += 1, NonSuccessKind::Error => self.errors += 1, }, TestCaseStatus::Skipped { .. } => self.disabled += 1, } self.test_cases.push(test_case); self } /// Adds several [`TestCase`]s to this TestSuite and updates counts. /// /// When generating a new report, use of this method is recommended over adding to /// `self.test_cases` directly. pub fn add_test_cases(&mut self, test_cases: impl IntoIterator) -> &mut Self { for test_case in test_cases { self.add_test_case(test_case); } self } /// Sets standard output. pub fn set_system_out(&mut self, system_out: impl AsRef) -> &mut Self { self.system_out = Some(Output::new(system_out.as_ref())); self } /// Sets standard output from a `Vec`. /// /// The output is converted to a string, lossily. pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self { self.set_system_out(String::from_utf8_lossy(system_out.as_ref())) } /// Sets standard error. pub fn set_system_err(&mut self, system_err: impl AsRef) -> &mut Self { self.system_err = Some(Output::new(system_err.as_ref())); self } /// Sets standard error from a `Vec`. /// /// The output is converted to a string, lossily. pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self { self.set_system_err(String::from_utf8_lossy(system_err.as_ref())) } } /// Represents a single test case. #[derive(Clone, Debug)] #[non_exhaustive] pub struct TestCase { /// The name of the test case. pub name: String, /// The "classname" of the test case. /// /// Typically, this represents the fully qualified path to the test. In other words, /// `classname` + `name` together should uniquely identify and locate a test. pub classname: Option, /// The number of assertions in the test case. pub assertions: Option, /// The time at which this test case began execution. /// /// This is not part of the JUnit spec, but may be useful for some tools. pub timestamp: Option>, /// The time it took to execute this test case. pub time: Option, /// The status of this test. pub status: TestCaseStatus, /// Data written to standard output while the test case was executed. pub system_out: Option, /// Data written to standard error while the test case was executed. pub system_err: Option, /// Other fields that may be set as attributes, such as "classname". pub extra: IndexMap, /// Custom properties set during test execution, e.g. steps. pub properties: Vec, } impl TestCase { /// Creates a new test case. pub fn new(name: impl Into, status: TestCaseStatus) -> Self { Self { name: name.into(), classname: None, assertions: None, timestamp: None, time: None, status, system_out: None, system_err: None, extra: IndexMap::new(), properties: vec![], } } /// Sets the classname of the test. pub fn set_classname(&mut self, classname: impl Into) -> &mut Self { self.classname = Some(classname.into()); self } /// Sets the number of assertions in the test case. pub fn set_assertions(&mut self, assertions: usize) -> &mut Self { self.assertions = Some(assertions); self } /// Sets the start timestamp for the test case. pub fn set_timestamp(&mut self, timestamp: impl Into>) -> &mut Self { self.timestamp = Some(timestamp.into()); self } /// Sets the time taken for the test case. pub fn set_time(&mut self, time: Duration) -> &mut Self { self.time = Some(time); self } /// Sets standard output. pub fn set_system_out(&mut self, system_out: impl AsRef) -> &mut Self { self.system_out = Some(Output::new(system_out.as_ref())); self } /// Sets standard output from a `Vec`. /// /// The output is converted to a string, lossily. pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self { self.set_system_out(String::from_utf8_lossy(system_out.as_ref())) } /// Sets standard error. pub fn set_system_err(&mut self, system_err: impl AsRef) -> &mut Self { self.system_err = Some(Output::new(system_err.as_ref())); self } /// Sets standard error from a `Vec`. /// /// The output is converted to a string, lossily. pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self { self.set_system_err(String::from_utf8_lossy(system_err.as_ref())) } /// Adds a property to this TestCase. pub fn add_property(&mut self, property: impl Into) -> &mut Self { self.properties.push(property.into()); self } /// Adds several properties to this TestCase. pub fn add_properties( &mut self, properties: impl IntoIterator>, ) -> &mut Self { for property in properties { self.add_property(property); } self } } /// Represents the success or failure of a test case. #[derive(Clone, Debug)] pub enum TestCaseStatus { /// This test case passed. Success { /// Prior runs of the test. These are represented as `flakyFailure` or `flakyError` in the /// JUnit XML. flaky_runs: Vec, }, /// This test case did not pass. NonSuccess { /// Whether this test case failed in an expected way (failure) or an unexpected way (error). kind: NonSuccessKind, /// The failure message. message: Option, /// The "type" of failure that occurred. ty: Option, /// The description of the failure. /// /// This is serialized and deserialized from the text node of the element. description: Option, /// Test reruns. These are represented as `rerunFailure` or `rerunError` in the JUnit XML. reruns: Vec, }, /// This test case was not run. Skipped { /// The skip message. message: Option, /// The "type" of skip that occurred. ty: Option, /// The description of the skip. /// /// This is serialized and deserialized from the text node of the element. description: Option, }, } impl TestCaseStatus { /// Creates a new `TestCaseStatus` that represents a successful test. pub fn success() -> Self { TestCaseStatus::Success { flaky_runs: vec![] } } /// Creates a new `TestCaseStatus` that represents an unsuccessful test. pub fn non_success(kind: NonSuccessKind) -> Self { TestCaseStatus::NonSuccess { kind, message: None, ty: None, description: None, reruns: vec![], } } /// Creates a new `TestCaseStatus` that represents a skipped test. pub fn skipped() -> Self { TestCaseStatus::Skipped { message: None, ty: None, description: None, } } /// Sets the message. No-op if this is a success case. pub fn set_message(&mut self, message: impl Into) -> &mut Self { let message_mut = match self { TestCaseStatus::Success { .. } => return self, TestCaseStatus::NonSuccess { message, .. } => message, TestCaseStatus::Skipped { message, .. } => message, }; *message_mut = Some(message.into()); self } /// Sets the type. No-op if this is a success case. pub fn set_type(&mut self, ty: impl Into) -> &mut Self { let ty_mut = match self { TestCaseStatus::Success { .. } => return self, TestCaseStatus::NonSuccess { ty, .. } => ty, TestCaseStatus::Skipped { ty, .. } => ty, }; *ty_mut = Some(ty.into()); self } /// Sets the description (text node). No-op if this is a success case. pub fn set_description(&mut self, description: impl Into) -> &mut Self { let description_mut = match self { TestCaseStatus::Success { .. } => return self, TestCaseStatus::NonSuccess { description, .. } => description, TestCaseStatus::Skipped { description, .. } => description, }; *description_mut = Some(description.into()); self } /// Adds a rerun or flaky run. No-op if this test was skipped. pub fn add_rerun(&mut self, rerun: TestRerun) -> &mut Self { self.add_reruns(iter::once(rerun)) } /// Adds reruns or flaky runs. No-op if this test was skipped. pub fn add_reruns(&mut self, reruns: impl IntoIterator) -> &mut Self { let reruns_mut = match self { TestCaseStatus::Success { flaky_runs } => flaky_runs, TestCaseStatus::NonSuccess { reruns, .. } => reruns, TestCaseStatus::Skipped { .. } => return self, }; reruns_mut.extend(reruns); self } } /// A rerun of a test. /// /// This is serialized as `flakyFailure` or `flakyError` for successes, and as `rerunFailure` or /// `rerunError` for failures/errors. #[derive(Clone, Debug)] pub struct TestRerun { /// The failure kind: error or failure. pub kind: NonSuccessKind, /// The time at which this rerun began execution. /// /// This is not part of the JUnit spec, but may be useful for some tools. pub timestamp: Option>, /// The time it took to execute this rerun. /// /// This is not part of the JUnit spec, but may be useful for some tools. pub time: Option, /// The failure message. pub message: Option, /// The "type" of failure that occurred. pub ty: Option, /// The stack trace, if any. pub stack_trace: Option, /// Data written to standard output while the test rerun was executed. pub system_out: Option, /// Data written to standard error while the test rerun was executed. pub system_err: Option, /// The description of the failure. /// /// This is serialized and deserialized from the text node of the element. pub description: Option, } impl TestRerun { /// Creates a new `TestRerun` of the given kind. pub fn new(kind: NonSuccessKind) -> Self { TestRerun { kind, timestamp: None, time: None, message: None, ty: None, stack_trace: None, system_out: None, system_err: None, description: None, } } /// Sets the start timestamp for this rerun. pub fn set_timestamp(&mut self, timestamp: impl Into>) -> &mut Self { self.timestamp = Some(timestamp.into()); self } /// Sets the time taken for this rerun. pub fn set_time(&mut self, time: Duration) -> &mut Self { self.time = Some(time); self } /// Sets the message. pub fn set_message(&mut self, message: impl Into) -> &mut Self { self.message = Some(message.into()); self } /// Sets the type. pub fn set_type(&mut self, ty: impl Into) -> &mut Self { self.ty = Some(ty.into()); self } /// Sets the stack trace. pub fn set_stack_trace(&mut self, stack_trace: impl Into) -> &mut Self { self.stack_trace = Some(stack_trace.into()); self } /// Sets standard output. pub fn set_system_out(&mut self, system_out: impl AsRef) -> &mut Self { self.system_out = Some(Output::new(system_out.as_ref())); self } /// Sets standard output from a `Vec`. /// /// The output is converted to a string, lossily. pub fn set_system_out_lossy(&mut self, system_out: impl AsRef<[u8]>) -> &mut Self { self.set_system_out(String::from_utf8_lossy(system_out.as_ref())) } /// Sets standard error. pub fn set_system_err(&mut self, system_err: impl AsRef) -> &mut Self { self.system_err = Some(Output::new(system_err.as_ref())); self } /// Sets standard error from a `Vec`. /// /// The output is converted to a string, lossily. pub fn set_system_err_lossy(&mut self, system_err: impl AsRef<[u8]>) -> &mut Self { self.set_system_err(String::from_utf8_lossy(system_err.as_ref())) } /// Sets the description of the failure. pub fn set_description(&mut self, description: impl Into) -> &mut Self { self.description = Some(description.into()); self } } /// Whether a test failure is "expected" or not. /// /// An expected test failure is generally one that is anticipated by the test or the harness, while /// an unexpected failure might be something like an external service being down or a failure to /// execute the binary. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum NonSuccessKind { /// This is an expected failure. Serialized as `failure`, `flakyFailure` or `rerunFailure` /// depending on the context. Failure, /// This is an unexpected error. Serialized as `error`, `flakyError` or `rerunError` depending /// on the context. Error, } /// Custom properties set during test execution, e.g. environment variables. #[derive(Clone, Debug)] pub struct Property { /// The name of the property. pub name: String, /// The value of the property. pub value: String, } impl Property { /// Creates a new `Property` instance. pub fn new(name: impl Into, value: impl Into) -> Self { Self { name: name.into(), value: value.into(), } } } impl From<(T, T)> for Property where T: Into, { fn from((k, v): (T, T)) -> Self { Property::new(k, v) } } /// Represents text that is written out to standard output or standard error during text execution. /// /// # Encoding /// /// On Unix platforms, standard output and standard error are typically bytestrings (`Vec`). /// However, XUnit assumes that the output is valid Unicode, and this type definition reflects /// that. #[derive(Clone, Debug)] pub struct Output { output: Box, } impl Output { /// Creates a new output, removing any non-printable characters from it. pub fn new(output: impl AsRef) -> Self { let output = output.as_ref(); let output = output .replace( |c| matches!(c, '\x00'..='\x08' | '\x0b' | '\x0c' | '\x0e'..='\x1f'), "", ) .into_boxed_str(); Self { output } } /// Returns the output. pub fn as_str(&self) -> &str { &self.output } /// Converts the output into a string. pub fn into_string(self) -> String { self.output.into_string() } } impl AsRef for Output { fn as_ref(&self) -> &str { self.as_str() } } impl From for String { fn from(output: Output) -> Self { output.into_string() } } quick-junit-0.3.3/src/serialize.rs000064400000000000000000000276101046102023000152240ustar 00000000000000// Copyright (c) The nextest Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Serialize a `Report`. use crate::{ NonSuccessKind, Output, Property, Report, SerializeError, TestCase, TestCaseStatus, TestRerun, TestSuite, }; use chrono::{DateTime, FixedOffset}; use quick_xml::{ events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event}, Writer, }; use std::{io, time::Duration}; static TESTSUITES_TAG: &str = "testsuites"; static TESTSUITE_TAG: &str = "testsuite"; static TESTCASE_TAG: &str = "testcase"; static PROPERTIES_TAG: &str = "properties"; static PROPERTY_TAG: &str = "property"; static FAILURE_TAG: &str = "failure"; static ERROR_TAG: &str = "error"; static FLAKY_FAILURE_TAG: &str = "flakyFailure"; static FLAKY_ERROR_TAG: &str = "flakyError"; static RERUN_FAILURE_TAG: &str = "rerunFailure"; static RERUN_ERROR_TAG: &str = "rerunError"; static STACK_TRACE_TAG: &str = "stackTrace"; static SKIPPED_TAG: &str = "skipped"; static SYSTEM_OUT_TAG: &str = "system-out"; static SYSTEM_ERR_TAG: &str = "system-err"; pub(crate) fn serialize_report( report: &Report, writer: impl io::Write, ) -> Result<(), SerializeError> { let mut writer = Writer::new_with_indent(writer, b' ', 4); let decl = BytesDecl::new("1.0", Some("UTF-8"), None); writer.write_event(Event::Decl(decl))?; serialize_report_impl(report, &mut writer)?; // Add a trailing newline. Ok(writer.write_indent()?) } pub(crate) fn serialize_report_impl( report: &Report, writer: &mut Writer, ) -> quick_xml::Result<()> { // Use the destructuring syntax to ensure that all fields are handled. let Report { name, uuid, timestamp, time, tests, failures, errors, test_suites, } = report; let mut testsuites_tag = BytesStart::new(TESTSUITES_TAG); testsuites_tag.extend_attributes([ ("name", name.as_str()), ("tests", tests.to_string().as_str()), ("failures", failures.to_string().as_str()), ("errors", errors.to_string().as_str()), ]); if let Some(uuid) = uuid { testsuites_tag.push_attribute(("uuid", uuid.to_string().as_str())); } if let Some(timestamp) = timestamp { serialize_timestamp(&mut testsuites_tag, timestamp); } if let Some(time) = time { serialize_time(&mut testsuites_tag, time); } writer.write_event(Event::Start(testsuites_tag))?; for test_suite in test_suites { serialize_test_suite(test_suite, writer)?; } serialize_end_tag(TESTSUITES_TAG, writer)?; writer.write_event(Event::Eof)?; Ok(()) } pub(crate) fn serialize_test_suite( test_suite: &TestSuite, writer: &mut Writer, ) -> quick_xml::Result<()> { // Use the destructuring syntax to ensure that all fields are handled. let TestSuite { name, tests, disabled, errors, failures, time, timestamp, test_cases, properties, system_out, system_err, extra, } = test_suite; let mut test_suite_tag = BytesStart::new(TESTSUITE_TAG); test_suite_tag.extend_attributes([ ("name", name.as_str()), ("tests", tests.to_string().as_str()), ("disabled", disabled.to_string().as_str()), ("errors", errors.to_string().as_str()), ("failures", failures.to_string().as_str()), ]); if let Some(timestamp) = timestamp { serialize_timestamp(&mut test_suite_tag, timestamp); } if let Some(time) = time { serialize_time(&mut test_suite_tag, time); } for (k, v) in extra { test_suite_tag.push_attribute((k.as_str(), v.as_str())); } writer.write_event(Event::Start(test_suite_tag))?; if !properties.is_empty() { serialize_empty_start_tag(PROPERTIES_TAG, writer)?; for property in properties { serialize_property(property, writer)?; } serialize_end_tag(PROPERTIES_TAG, writer)?; } for test_case in test_cases { serialize_test_case(test_case, writer)?; } if let Some(system_out) = system_out { serialize_output(system_out, SYSTEM_OUT_TAG, writer)?; } if let Some(system_err) = system_err { serialize_output(system_err, SYSTEM_ERR_TAG, writer)?; } serialize_end_tag(TESTSUITE_TAG, writer)?; Ok(()) } fn serialize_property( property: &Property, writer: &mut Writer, ) -> quick_xml::Result<()> { let mut property_tag = BytesStart::new(PROPERTY_TAG); property_tag.extend_attributes([ ("name", property.name.as_str()), ("value", property.value.as_str()), ]); writer.write_event(Event::Empty(property_tag)) } fn serialize_test_case( test_case: &TestCase, writer: &mut Writer, ) -> quick_xml::Result<()> { let TestCase { name, classname, assertions, timestamp, time, status, system_out, system_err, extra, properties, } = test_case; let mut testcase_tag = BytesStart::new(TESTCASE_TAG); testcase_tag.extend_attributes([("name", name.as_str())]); if let Some(classname) = classname { testcase_tag.push_attribute(("classname", classname.as_str())); } if let Some(assertions) = assertions { testcase_tag.push_attribute(("assertions", format!("{assertions}").as_str())); } if let Some(timestamp) = timestamp { serialize_timestamp(&mut testcase_tag, timestamp); } if let Some(time) = time { serialize_time(&mut testcase_tag, time); } for (k, v) in extra { testcase_tag.push_attribute((k.as_str(), v.as_str())); } writer.write_event(Event::Start(testcase_tag))?; if !properties.is_empty() { serialize_empty_start_tag(PROPERTIES_TAG, writer)?; for property in properties { serialize_property(property, writer)?; } serialize_end_tag(PROPERTIES_TAG, writer)?; } match status { TestCaseStatus::Success { flaky_runs } => { for rerun in flaky_runs { serialize_rerun(rerun, FlakyOrRerun::Flaky, writer)?; } } TestCaseStatus::NonSuccess { kind, message, ty, description, reruns, } => { let tag_name = match kind { NonSuccessKind::Failure => FAILURE_TAG, NonSuccessKind::Error => ERROR_TAG, }; serialize_status( message.as_deref(), ty.as_deref(), description.as_deref(), tag_name, writer, )?; for rerun in reruns { serialize_rerun(rerun, FlakyOrRerun::Rerun, writer)?; } } TestCaseStatus::Skipped { message, ty, description, } => { serialize_status( message.as_deref(), ty.as_deref(), description.as_deref(), SKIPPED_TAG, writer, )?; } } if let Some(system_out) = system_out { serialize_output(system_out, SYSTEM_OUT_TAG, writer)?; } if let Some(system_err) = system_err { serialize_output(system_err, SYSTEM_ERR_TAG, writer)?; } serialize_end_tag(TESTCASE_TAG, writer)?; Ok(()) } fn serialize_status( message: Option<&str>, ty: Option<&str>, description: Option<&str>, tag_name: &'static str, writer: &mut Writer, ) -> quick_xml::Result<()> { let mut tag = BytesStart::new(tag_name); if let Some(message) = message { tag.push_attribute(("message", message)); } if let Some(ty) = ty { tag.push_attribute(("type", ty)); } match description { Some(description) => { writer.write_event(Event::Start(tag))?; writer.write_event(Event::Text(BytesText::new(description)))?; serialize_end_tag(tag_name, writer)?; } None => { writer.write_event(Event::Empty(tag))?; } } Ok(()) } #[derive(Copy, Clone, Debug)] enum FlakyOrRerun { Flaky, Rerun, } fn serialize_rerun( rerun: &TestRerun, flaky_or_rerun: FlakyOrRerun, writer: &mut Writer, ) -> quick_xml::Result<()> { let TestRerun { timestamp, time, kind, message, ty, stack_trace, system_out, system_err, description, } = rerun; let tag_name = match (flaky_or_rerun, *kind) { (FlakyOrRerun::Flaky, NonSuccessKind::Failure) => FLAKY_FAILURE_TAG, (FlakyOrRerun::Flaky, NonSuccessKind::Error) => FLAKY_ERROR_TAG, (FlakyOrRerun::Rerun, NonSuccessKind::Failure) => RERUN_FAILURE_TAG, (FlakyOrRerun::Rerun, NonSuccessKind::Error) => RERUN_ERROR_TAG, }; let mut tag = BytesStart::new(tag_name); if let Some(timestamp) = timestamp { serialize_timestamp(&mut tag, timestamp); } if let Some(time) = time { serialize_time(&mut tag, time); } if let Some(message) = message { tag.push_attribute(("message", message.as_str())); } if let Some(ty) = ty { tag.push_attribute(("type", ty.as_str())); } writer.write_event(Event::Start(tag))?; let mut needs_indent = false; if let Some(description) = description { writer.write_event(Event::Text(BytesText::new(description)))?; needs_indent = true; } // Note that the stack trace, system out and system err should occur in this order according // to the reference schema. if let Some(stack_trace) = stack_trace { if needs_indent { writer.write_indent()?; needs_indent = false; } serialize_empty_start_tag(STACK_TRACE_TAG, writer)?; writer.write_event(Event::Text(BytesText::new(stack_trace)))?; serialize_end_tag(STACK_TRACE_TAG, writer)?; } if let Some(system_out) = system_out { if needs_indent { writer.write_indent()?; needs_indent = false; } serialize_output(system_out, SYSTEM_OUT_TAG, writer)?; } if let Some(system_err) = system_err { if needs_indent { writer.write_indent()?; // needs_indent = false; } serialize_output(system_err, SYSTEM_ERR_TAG, writer)?; } serialize_end_tag(tag_name, writer)?; Ok(()) } fn serialize_output( output: &Output, tag_name: &'static str, writer: &mut Writer, ) -> quick_xml::Result<()> { serialize_empty_start_tag(tag_name, writer)?; let text = BytesText::new(output.as_str()); writer.write_event(Event::Text(text))?; serialize_end_tag(tag_name, writer)?; Ok(()) } fn serialize_empty_start_tag( tag_name: &'static str, writer: &mut Writer, ) -> quick_xml::Result<()> { let tag = BytesStart::new(tag_name); writer.write_event(Event::Start(tag)) } fn serialize_end_tag( tag_name: &'static str, writer: &mut Writer, ) -> quick_xml::Result<()> { let end_tag = BytesEnd::new(tag_name); writer.write_event(Event::End(end_tag)) } fn serialize_timestamp(tag: &mut BytesStart<'_>, timestamp: &DateTime) { // The format string is obtained from https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html#fn8. // The only change is that this only prints timestamps up to 3 decimal places (to match times). static RFC_3339_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.3f%:z"; tag.push_attribute(( "timestamp", format!("{}", timestamp.format(RFC_3339_FORMAT)).as_str(), )); } // Serialize time as seconds with 3 decimal points. fn serialize_time(tag: &mut BytesStart<'_>, time: &Duration) { tag.push_attribute(("time", format!("{:.3}", time.as_secs_f64()).as_str())); } quick-junit-0.3.3/tests/fixture_tests.rs000064400000000000000000000112421046102023000165120ustar 00000000000000// Copyright (c) The nextest Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use chrono::DateTime; use goldenfile::Mint; use owo_colors::OwoColorize; use quick_junit::{ NonSuccessKind, Property, Report, TestCase, TestCaseStatus, TestRerun, TestSuite, }; use std::time::Duration; #[test] fn fixtures() { let mut mint = Mint::new("tests/fixtures"); let f = mint .new_goldenfile("basic_report.xml") .expect("creating new goldenfile succeeds"); let basic_report = basic_report(); basic_report .serialize(f) .expect("serializing basic_report succeeds"); } fn basic_report() -> Report { let mut report = Report::new("my-test-run"); report.set_timestamp( DateTime::parse_from_rfc2822("Thu, 1 Apr 2021 10:52:37 -0800") .expect("valid RFC2822 datetime"), ); report.set_time(Duration::new(42, 234_567_890)); let mut test_suite = TestSuite::new("testsuite0"); test_suite.set_timestamp( DateTime::parse_from_rfc2822("Thu, 1 Apr 2021 10:52:39 -0800") .expect("valid RFC2822 datetime"), ); // --- let test_case_status = TestCaseStatus::success(); let mut test_case = TestCase::new("testcase0", test_case_status); test_case.set_system_out("testcase0-output"); test_suite.add_test_case(test_case); // --- let mut test_case_status = TestCaseStatus::non_success(NonSuccessKind::Failure); test_case_status .set_description("this is the failure description") .set_message("testcase1-message"); let mut test_case = TestCase::new("testcase1", test_case_status); test_case .set_system_err("some sort of failure output") .set_time(Duration::from_millis(4242)); test_suite.add_test_case(test_case); // --- let mut test_case_status = TestCaseStatus::non_success(NonSuccessKind::Error); test_case_status .set_description("testcase2 error description") .set_type("error type"); let mut test_case = TestCase::new("testcase2", test_case_status); test_case.set_time(Duration::from_nanos(421580)); test_suite.add_test_case(test_case); // --- let mut test_case_status = TestCaseStatus::skipped(); test_case_status .set_type("skipped type") .set_message("skipped message"); // no description to test that. let mut test_case = TestCase::new("testcase3", test_case_status); test_case .set_timestamp( DateTime::parse_from_rfc2822("Thu, 1 Apr 2021 11:52:41 -0700") .expect("valid RFC2822 datetime"), ) .set_assertions(20) .set_system_out("testcase3 output") .set_system_err("testcase3 error"); test_suite.add_test_case(test_case); // --- let mut test_case_status = TestCaseStatus::success(); let mut test_rerun = TestRerun::new(NonSuccessKind::Failure); test_rerun .set_type("flaky failure type") .set_description("this is a flaky failure description"); test_case_status.add_rerun(test_rerun); let mut test_rerun = TestRerun::new(NonSuccessKind::Error); test_rerun .set_type("flaky error type") .set_system_out("flaky system output") .set_system_err(format!( "flaky system error with {}", "ANSI escape codes".blue() )) .set_stack_trace("flaky stack trace") .set_description("flaky error description"); test_case_status.add_rerun(test_rerun); let mut test_case = TestCase::new("testcase4", test_case_status); test_case.set_time(Duration::from_millis(661661)); test_suite.add_test_case(test_case); // --- let mut test_case_status = TestCaseStatus::non_success(NonSuccessKind::Failure); test_case_status.set_description("main test failure description"); let mut test_rerun = TestRerun::new(NonSuccessKind::Failure); test_rerun.set_type("retry failure type"); test_case_status.add_rerun(test_rerun); let mut test_rerun = TestRerun::new(NonSuccessKind::Error); test_rerun .set_type("retry error type") .set_system_out("retry error system output") .set_stack_trace("retry error stack trace"); test_case_status.add_rerun(test_rerun); let mut test_case = TestCase::new("testcase5", test_case_status); test_case.set_time(Duration::from_millis(156)); test_suite.add_test_case(test_case); let test_case_status = TestCaseStatus::success(); let mut test_case = TestCase::new("testcase6", test_case_status); test_case.add_property(Property::new("step", "foobar")); test_suite.add_test_case(test_case); test_suite.add_property(Property::new("env", "FOOBAR")); report.add_test_suite(test_suite); report } quick-junit-0.3.3/tests/fixtures/basic_report.xml000064400000000000000000000042761046102023000203140ustar 00000000000000 testcase0-output this is the failure description some sort of failure output testcase2 error description testcase3 output testcase3 error this is a flaky failure description flaky error description flaky stack trace flaky system output flaky system error with [34mANSI escape codes[39m main test failure description retry error stack trace retry error system output