#[cfg(feature = "proptest")]
use crate::proptest_impls::{
datetime_strategy, duration_strategy, test_name_strategy, text_node_strategy,
xml_attr_index_map_strategy,
};
use crate::{serialize::serialize_report, SerializeError};
use chrono::{DateTime, FixedOffset};
use indexmap::map::IndexMap;
use newtype_uuid::{GenericUuid, TypedUuid, TypedUuidKind, TypedUuidTag};
#[cfg(feature = "proptest")]
use proptest::{collection, option, prelude::*};
use std::{borrow::Borrow, hash::Hash, io, iter, ops::Deref, time::Duration};
use uuid::Uuid;
pub enum ReportKind {}
impl TypedUuidKind for ReportKind {
fn tag() -> TypedUuidTag {
const TAG: TypedUuidTag = TypedUuidTag::new("quick-junit-report");
TAG
}
}
pub type ReportUuid = TypedUuid<ReportKind>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Report {
pub name: XmlString,
pub uuid: Option<ReportUuid>,
pub timestamp: Option<DateTime<FixedOffset>>,
pub time: Option<Duration>,
pub tests: usize,
pub failures: usize,
pub errors: usize,
pub test_suites: Vec<TestSuite>,
}
impl Report {
pub fn new(name: impl Into<XmlString>) -> Self {
Self {
name: name.into(),
uuid: None,
timestamp: None,
time: None,
tests: 0,
failures: 0,
errors: 0,
test_suites: vec![],
}
}
pub fn set_report_uuid(&mut self, uuid: ReportUuid) -> &mut Self {
self.uuid = Some(uuid);
self
}
pub fn set_uuid(&mut self, uuid: Uuid) -> &mut Self {
self.uuid = Some(ReportUuid::from_untyped_uuid(uuid));
self
}
pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
self.timestamp = Some(timestamp.into());
self
}
pub fn set_time(&mut self, time: Duration) -> &mut Self {
self.time = Some(time);
self
}
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
}
pub fn add_test_suites(
&mut self,
test_suites: impl IntoIterator<Item = TestSuite>,
) -> &mut Self {
for test_suite in test_suites {
self.add_test_suite(test_suite);
}
self
}
pub fn serialize(&self, writer: impl io::Write) -> Result<(), SerializeError> {
serialize_report(self, writer)
}
pub fn to_string(&self) -> Result<String, SerializeError> {
let mut buf: Vec<u8> = vec![];
self.serialize(&mut buf)?;
String::from_utf8(buf).map_err(|utf8_err| {
quick_xml::encoding::EncodingError::from(utf8_err.utf8_error()).into()
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct TestSuite {
pub name: XmlString,
pub tests: usize,
pub disabled: usize,
pub errors: usize,
pub failures: usize,
pub timestamp: Option<DateTime<FixedOffset>>,
pub time: Option<Duration>,
pub test_cases: Vec<TestCase>,
pub properties: Vec<Property>,
pub system_out: Option<XmlString>,
pub system_err: Option<XmlString>,
pub extra: IndexMap<XmlString, XmlString>,
}
impl TestSuite {
pub fn new(name: impl Into<XmlString>) -> 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(),
}
}
pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
self.timestamp = Some(timestamp.into());
self
}
pub fn set_time(&mut self, time: Duration) -> &mut Self {
self.time = Some(time);
self
}
pub fn add_property(&mut self, property: impl Into<Property>) -> &mut Self {
self.properties.push(property.into());
self
}
pub fn add_properties(
&mut self,
properties: impl IntoIterator<Item = impl Into<Property>>,
) -> &mut Self {
for property in properties {
self.add_property(property);
}
self
}
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
}
pub fn add_test_cases(&mut self, test_cases: impl IntoIterator<Item = TestCase>) -> &mut Self {
for test_case in test_cases {
self.add_test_case(test_case);
}
self
}
pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
self.system_out = Some(system_out.into());
self
}
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()))
}
pub fn set_system_err(&mut self, system_err: impl Into<XmlString>) -> &mut Self {
self.system_err = Some(system_err.into());
self
}
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()))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
#[non_exhaustive]
pub struct TestCase {
#[cfg_attr(feature = "proptest", strategy(test_name_strategy()))]
pub name: XmlString,
pub classname: Option<XmlString>,
pub assertions: Option<usize>,
#[cfg_attr(feature = "proptest", strategy(option::of(datetime_strategy())))]
pub timestamp: Option<DateTime<FixedOffset>>,
#[cfg_attr(feature = "proptest", strategy(option::of(duration_strategy())))]
pub time: Option<Duration>,
pub status: TestCaseStatus,
pub system_out: Option<XmlString>,
pub system_err: Option<XmlString>,
#[cfg_attr(feature = "proptest", strategy(xml_attr_index_map_strategy()))]
pub extra: IndexMap<XmlString, XmlString>,
#[cfg_attr(feature = "proptest", strategy(collection::vec(any::<Property>(), 0..3)))]
pub properties: Vec<Property>,
}
impl TestCase {
pub fn new(name: impl Into<XmlString>, 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![],
}
}
pub fn set_classname(&mut self, classname: impl Into<XmlString>) -> &mut Self {
self.classname = Some(classname.into());
self
}
pub fn set_assertions(&mut self, assertions: usize) -> &mut Self {
self.assertions = Some(assertions);
self
}
pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
self.timestamp = Some(timestamp.into());
self
}
pub fn set_time(&mut self, time: Duration) -> &mut Self {
self.time = Some(time);
self
}
pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
self.system_out = Some(system_out.into());
self
}
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()))
}
pub fn set_system_err(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
self.system_err = Some(system_out.into());
self
}
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()))
}
pub fn add_property(&mut self, property: impl Into<Property>) -> &mut Self {
self.properties.push(property.into());
self
}
pub fn add_properties(
&mut self,
properties: impl IntoIterator<Item = impl Into<Property>>,
) -> &mut Self {
for property in properties {
self.add_property(property);
}
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
pub enum TestCaseStatus {
Success {
flaky_runs: Vec<TestRerun>,
},
NonSuccess {
kind: NonSuccessKind,
message: Option<XmlString>,
ty: Option<XmlString>,
#[cfg_attr(feature = "proptest", strategy(option::of(text_node_strategy())))]
description: Option<XmlString>,
reruns: NonSuccessReruns,
},
Skipped {
message: Option<XmlString>,
ty: Option<XmlString>,
#[cfg_attr(feature = "proptest", strategy(option::of(text_node_strategy())))]
description: Option<XmlString>,
},
}
impl TestCaseStatus {
pub fn success() -> Self {
TestCaseStatus::Success { flaky_runs: vec![] }
}
pub fn non_success(kind: NonSuccessKind) -> Self {
TestCaseStatus::NonSuccess {
kind,
message: None,
ty: None,
description: None,
reruns: NonSuccessReruns::default(),
}
}
pub fn skipped() -> Self {
TestCaseStatus::Skipped {
message: None,
ty: None,
description: None,
}
}
pub fn set_message(&mut self, message: impl Into<XmlString>) -> &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
}
pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &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
}
pub fn set_description(&mut self, description: impl Into<XmlString>) -> &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
}
pub fn add_rerun(&mut self, rerun: TestRerun) -> &mut Self {
self.add_reruns(iter::once(rerun))
}
pub fn add_reruns(&mut self, new_reruns: impl IntoIterator<Item = TestRerun>) -> &mut Self {
match self {
TestCaseStatus::Success { flaky_runs } => {
flaky_runs.extend(new_reruns);
}
TestCaseStatus::NonSuccess { reruns, .. } => {
reruns.runs.extend(new_reruns);
}
TestCaseStatus::Skipped { .. } => {}
}
self
}
pub fn set_rerun_kind(&mut self, kind: FlakyOrRerun) -> &mut Self {
if let TestCaseStatus::NonSuccess { reruns, .. } = self {
reruns.kind = kind;
}
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
pub struct TestRerun {
pub kind: NonSuccessKind,
#[cfg_attr(feature = "proptest", strategy(option::of(datetime_strategy())))]
pub timestamp: Option<DateTime<FixedOffset>>,
#[cfg_attr(feature = "proptest", strategy(option::of(duration_strategy())))]
pub time: Option<Duration>,
pub message: Option<XmlString>,
pub ty: Option<XmlString>,
pub stack_trace: Option<XmlString>,
pub system_out: Option<XmlString>,
pub system_err: Option<XmlString>,
#[cfg_attr(feature = "proptest", strategy(option::of(text_node_strategy())))]
pub description: Option<XmlString>,
}
impl TestRerun {
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,
}
}
pub fn set_timestamp(&mut self, timestamp: impl Into<DateTime<FixedOffset>>) -> &mut Self {
self.timestamp = Some(timestamp.into());
self
}
pub fn set_time(&mut self, time: Duration) -> &mut Self {
self.time = Some(time);
self
}
pub fn set_message(&mut self, message: impl Into<XmlString>) -> &mut Self {
self.message = Some(message.into());
self
}
pub fn set_type(&mut self, ty: impl Into<XmlString>) -> &mut Self {
self.ty = Some(ty.into());
self
}
pub fn set_stack_trace(&mut self, stack_trace: impl Into<XmlString>) -> &mut Self {
self.stack_trace = Some(stack_trace.into());
self
}
pub fn set_system_out(&mut self, system_out: impl Into<XmlString>) -> &mut Self {
self.system_out = Some(system_out.into());
self
}
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()))
}
pub fn set_system_err(&mut self, system_err: impl Into<XmlString>) -> &mut Self {
self.system_err = Some(system_err.into());
self
}
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()))
}
pub fn set_description(&mut self, description: impl Into<XmlString>) -> &mut Self {
self.description = Some(description.into());
self
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
pub enum NonSuccessKind {
Failure,
Error,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NonSuccessReruns {
pub kind: FlakyOrRerun,
pub runs: Vec<TestRerun>,
}
impl Default for NonSuccessReruns {
fn default() -> Self {
Self {
kind: FlakyOrRerun::Rerun,
runs: vec![],
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
pub enum FlakyOrRerun {
Flaky,
Rerun,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
pub struct Property {
pub name: XmlString,
pub value: XmlString,
}
impl Property {
pub fn new(name: impl Into<XmlString>, value: impl Into<XmlString>) -> Self {
Self {
name: name.into(),
value: value.into(),
}
}
}
impl<T> From<(T, T)> for Property
where
T: Into<XmlString>,
{
fn from((k, v): (T, T)) -> Self {
Property::new(k, v)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct XmlString {
data: Box<str>,
}
impl XmlString {
pub fn new(data: impl AsRef<str>) -> Self {
let data = data.as_ref();
let data = strip_ansi_escapes::strip_str(data);
let data = data
.replace(
|c| matches!(c, '\x00'..='\x08' | '\x0b' | '\x0c' | '\x0e'..='\x1f'),
"",
)
.into_boxed_str();
Self { data }
}
pub fn as_str(&self) -> &str {
&self.data
}
pub fn into_string(self) -> String {
self.data.into_string()
}
}
impl<T: AsRef<str>> From<T> for XmlString {
fn from(s: T) -> Self {
XmlString::new(s)
}
}
impl From<XmlString> for String {
fn from(s: XmlString) -> Self {
s.into_string()
}
}
impl Deref for XmlString {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl Borrow<str> for XmlString {
fn borrow(&self) -> &str {
&self.data
}
}
impl PartialOrd for XmlString {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for XmlString {
#[inline]
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.data.cmp(&other.data)
}
}
impl Hash for XmlString {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.data.hash(state);
}
}
impl PartialEq<str> for XmlString {
fn eq(&self, other: &str) -> bool {
&*self.data == other
}
}
impl PartialEq<XmlString> for str {
fn eq(&self, other: &XmlString) -> bool {
self == &*other.data
}
}
impl PartialEq<String> for XmlString {
fn eq(&self, other: &String) -> bool {
&*self.data == other
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prop_assume;
use std::hash::Hasher;
use test_strategy::proptest;
#[proptest]
fn xml_string_hash(s: String) {
let xml_string = XmlString::new(&s);
prop_assume!(xml_string == s);
let mut hasher1 = std::collections::hash_map::DefaultHasher::new();
let mut hasher2 = std::collections::hash_map::DefaultHasher::new();
s.as_str().hash(&mut hasher1);
xml_string.hash(&mut hasher2);
assert_eq!(hasher1.finish(), hasher2.finish());
}
#[proptest]
fn xml_string_ord(s1: String, s2: String) {
let xml_string1 = XmlString::new(&s1);
let xml_string2 = XmlString::new(&s2);
prop_assume!(xml_string1 == s1 && xml_string2 == s2);
assert_eq!(s1.as_str().cmp(s2.as_str()), xml_string1.cmp(&xml_string2));
}
}