#![cfg_attr(
feature = "document-features",
cfg_attr(doc, doc = ::document_features::document_features!())
)]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
mod errors;
#[cfg(feature = "chrono")]
use chrono::{DateTime, Utc};
pub use errors::Error;
use quick_xml::escape::unescape;
use quick_xml::events::BytesStart as XMLBytesStart;
use quick_xml::events::Event as XMLEvent;
use quick_xml::name::QName;
use quick_xml::Reader as XMLReader;
use std::borrow::Cow;
#[cfg(feature = "properties_as_hashmap")]
use std::collections::HashMap;
use std::io::prelude::*;
use std::str;
use std::vec::Vec;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)]
pub struct Properties {
#[cfg(feature = "properties_as_hashmap")]
pub hashmap: HashMap<String, String>,
#[cfg(feature = "properties_as_vector")]
pub vec: Vec<(String, String)>,
}
fn parse_property<B: BufRead>(
e: &XMLBytesStart,
r: Option<&mut XMLReader<B>>,
) -> Result<(String, String), Error> {
let mut k: Option<String> = None;
let mut v: Option<String> = None;
for a in e.attributes() {
let a = a?;
match a.key {
QName(b"name") => k = Some(try_from_attribute_value_string(a.value)?),
QName(b"value") => v = Some(try_from_attribute_value_string(a.value)?),
_ => {}
};
}
if let Some(r) = r {
loop {
let mut buf = Vec::new();
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"property") => break,
Ok(XMLEvent::Eof) => {
return Err(Error::UnexpectedEndOfFile("property".to_string()));
}
Ok(XMLEvent::Text(e)) => match v {
None => v = Some(e.decode()?.trim().to_string()),
Some(ref mut val) => {
val.push('\n');
val.push_str(e.decode()?.trim());
}
},
Ok(XMLEvent::CData(e)) => match v {
None => v = Some(str::from_utf8(&e)?.to_string()),
Some(ref mut val) => {
val.push('\n');
val.push_str(str::from_utf8(&e)?);
}
},
Ok(XMLEvent::Start(ref e)) => {
r.read_to_end_into(e.name(), &mut Vec::new())?;
}
Err(err) => return Err(err.into()),
_ => (),
}
buf.clear();
}
}
match (k, v) {
(Some(k), Some(v)) => Ok((k, v)),
(Some(k), None) => Ok((k, "".to_string())),
_ => Err(Error::MissingPropertyName),
}
}
impl Properties {
fn from_reader<B: BufRead>(r: &mut XMLReader<B>) -> Result<Self, Error> {
let mut p = Self::default();
loop {
let mut buf = Vec::new();
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"properties") => break,
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"property") => {
let (k, v) = parse_property::<B>(e, None)?;
p.add_property(k, v);
}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"property") => {
let (k, v) = parse_property(e, Some(r))?;
p.add_property(k, v);
}
Ok(XMLEvent::Start(ref e)) => {
r.read_to_end_into(e.name(), &mut Vec::new())?;
}
Ok(XMLEvent::Eof) => {
return Err(Error::UnexpectedEndOfFile("properties".to_string()));
}
Err(err) => return Err(err.into()),
_ => (),
}
buf.clear();
}
Ok(p)
}
#[cfg_attr(
all(
not(feature = "properties_as_hashmap"),
not(feature = "properties_as_vector")
),
allow(unused_variables)
)]
fn add_property(&mut self, key: String, value: String) {
#[cfg(feature = "properties_as_hashmap")]
self.hashmap.insert(key.clone(), value.clone());
#[cfg(feature = "properties_as_vector")]
self.vec.push((key, value));
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default)]
pub enum RerunOrFlakyKind {
#[default]
FlakyFailure,
FlakyError,
RerunFailure,
RerunError,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)]
pub struct RerunOrFlaky {
#[cfg(feature = "chrono")]
pub timestamp: Option<DateTime<Utc>>,
#[cfg(not(feature = "chrono"))]
pub timestamp: Option<String>,
pub time: f64,
pub rerun_type: String,
pub message: String,
pub text: String,
pub system_out: Option<String>,
pub system_err: Option<String>,
pub stack_trace: Option<String>,
pub kind: RerunOrFlakyKind,
}
impl RerunOrFlaky {
fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
for a in e.attributes() {
let a = a?;
match a.key {
QName(b"type") => self.rerun_type = try_from_attribute_value_string(a.value)?,
QName(b"time") => self.time = try_from_attribute_value_f64(a.value)?,
QName(b"timestamp") => {
#[cfg(feature = "chrono")]
{
self.timestamp = Some(try_from_attribute_value_datetime(a.value)?);
}
#[cfg(not(feature = "chrono"))]
{
self.timestamp = Some(try_from_attribute_value_string(a.value)?);
}
}
QName(b"message") => self.message = try_from_attribute_value_string(a.value)?,
_ => {}
};
}
Ok(())
}
fn new_empty(e: &XMLBytesStart, kind: RerunOrFlakyKind) -> Result<Self, Error> {
let mut rt = Self {
kind,
..Default::default()
};
rt.parse_attributes(e)?;
Ok(rt)
}
fn from_reader<B: BufRead>(
e: &XMLBytesStart,
r: &mut XMLReader<B>,
kind: RerunOrFlakyKind,
) -> Result<Self, Error> {
let mut rt = Self {
kind,
system_out: None,
system_err: None,
stack_trace: None,
..Default::default()
};
rt.parse_attributes(e)?;
let end_tag_name = e.name();
loop {
let mut buf = Vec::new();
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref end_event)) if end_event.name() == end_tag_name => break,
Ok(XMLEvent::Text(e)) => {
if rt.text.is_empty() {
rt.text = e.decode()?.trim().to_string();
} else {
rt.text.push('\n');
rt.text.push_str(e.decode()?.trim());
}
}
Ok(XMLEvent::CData(e)) => {
if rt.text.is_empty() {
rt.text = str::from_utf8(&e)?.to_string();
} else {
rt.text.push('\n');
rt.text.push_str(str::from_utf8(&e)?);
}
}
Ok(XMLEvent::Start(ref start_event)) => match start_event.name() {
QName(b"system-out") => {
if let Some(parsed_content) = parse_system(start_event, r)? {
let current_out = rt.system_out.get_or_insert_with(String::new);
if !current_out.is_empty() {
current_out.push('\n');
}
current_out.push_str(&parsed_content);
}
}
QName(b"system-err") => {
if let Some(parsed_content) = parse_system(start_event, r)? {
let current_err = rt.system_err.get_or_insert_with(String::new);
if !current_err.is_empty() {
current_err.push('\n');
}
current_err.push_str(&parsed_content);
}
}
QName(b"stackTrace") => {
rt.stack_trace = parse_system(start_event, r)?;
}
_ => {
r.read_to_end_into(start_event.name(), &mut Vec::new())?;
}
},
Ok(XMLEvent::Empty(ref empty_event)) => match empty_event.name() {
QName(b"system-out") => {}
QName(b"system-err") => {}
QName(b"stackTrace") => {}
_ => {}
},
Ok(XMLEvent::Eof) => {
let tag_name = String::from_utf8_lossy(end_tag_name.as_ref()).to_string();
return Err(Error::UnexpectedEndOfFile(tag_name));
}
Err(err) => return Err(err.into()),
_ => (),
}
buf.clear();
}
Ok(rt)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)]
pub struct TestFailure {
pub message: String,
pub text: String,
pub failure_type: String,
}
impl TestFailure {
fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
for a in e.attributes() {
let a = a?;
match a.key {
QName(b"type") => self.failure_type = try_from_attribute_value_string(a.value)?,
QName(b"message") => self.message = try_from_attribute_value_string(a.value)?,
_ => {}
};
}
Ok(())
}
fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
let mut tf = Self::default();
tf.parse_attributes(e)?;
Ok(tf)
}
fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
let mut tf = Self::default();
tf.parse_attributes(e)?;
loop {
let mut buf = Vec::new();
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"failure") => break,
Ok(XMLEvent::Text(e)) => {
if tf.text.is_empty() {
tf.text = e.decode()?.trim().to_string();
} else {
tf.text.push('\n');
tf.text.push_str(e.decode()?.trim());
}
}
Ok(XMLEvent::Eof) => {
return Err(Error::UnexpectedEndOfFile("failure".to_string()));
}
Ok(XMLEvent::CData(e)) => {
if tf.text.is_empty() {
tf.text = str::from_utf8(&e)?.to_string();
} else {
tf.text.push('\n');
tf.text.push_str(str::from_utf8(&e)?);
}
}
Ok(XMLEvent::Start(ref e)) => {
r.read_to_end_into(e.name(), &mut Vec::new())?;
}
Err(err) => return Err(err.into()),
_ => (),
}
buf.clear();
}
Ok(tf)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)]
pub struct TestError {
pub message: String,
pub text: String,
pub error_type: String,
}
impl TestError {
fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
for a in e.attributes() {
let a = a?;
match a.key {
QName(b"type") => self.error_type = try_from_attribute_value_string(a.value)?,
QName(b"message") => self.message = try_from_attribute_value_string(a.value)?,
_ => {}
};
}
Ok(())
}
fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
let mut te = Self::default();
te.parse_attributes(e)?;
Ok(te)
}
fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
let mut te = Self::default();
te.parse_attributes(e)?;
loop {
let mut buf = Vec::new();
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"error") => break,
Ok(XMLEvent::Text(e)) => {
if te.text.is_empty() {
te.text = e.decode()?.trim().to_string();
} else {
te.text.push('\n');
te.text.push_str(e.decode()?.trim());
}
}
Ok(XMLEvent::Eof) => {
return Err(Error::UnexpectedEndOfFile("error".to_string()));
}
Ok(XMLEvent::CData(e)) => {
if te.text.is_empty() {
te.text = str::from_utf8(&e)?.to_string();
} else {
te.text.push('\n');
te.text.push_str(str::from_utf8(&e)?);
}
}
Ok(XMLEvent::Start(ref e)) => {
r.read_to_end_into(e.name(), &mut Vec::new())?;
}
Err(err) => return Err(err.into()),
_ => (),
}
buf.clear();
}
Ok(te)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)]
pub struct TestSkipped {
pub message: String,
pub text: String,
pub skipped_type: String,
}
impl TestSkipped {
fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
for a in e.attributes() {
let a = a?;
match a.key {
QName(b"type") => self.skipped_type = try_from_attribute_value_string(a.value)?,
QName(b"message") => self.message = try_from_attribute_value_string(a.value)?,
_ => {}
};
}
Ok(())
}
fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
let mut ts = Self::default();
ts.parse_attributes(e)?;
Ok(ts)
}
fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
let mut ts = Self::default();
ts.parse_attributes(e)?;
loop {
let mut buf = Vec::new();
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"skipped") => break,
Ok(XMLEvent::Text(e)) => {
if ts.text.is_empty() {
ts.text = e.decode()?.trim().to_string();
} else {
ts.text.push('\n');
ts.text.push_str(e.decode()?.trim());
}
}
Ok(XMLEvent::Eof) => {
return Err(Error::UnexpectedEndOfFile("skipped".to_string()));
}
Ok(XMLEvent::CData(e)) => {
if ts.text.is_empty() {
ts.text = str::from_utf8(&e)?.to_string();
} else {
ts.text.push('\n');
ts.text.push_str(str::from_utf8(&e)?);
}
}
Ok(XMLEvent::Start(ref e)) => {
r.read_to_end_into(e.name(), &mut Vec::new())?;
}
Err(err) => return Err(err.into()),
_ => (),
}
buf.clear();
}
Ok(ts)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)]
pub enum TestStatus {
#[default]
Success,
Error(TestError),
Failure(TestFailure),
Skipped(TestSkipped),
}
impl TestStatus {
pub fn is_success(&self) -> bool {
matches!(self, TestStatus::Success)
}
pub fn is_error(&self) -> bool {
matches!(self, TestStatus::Error(_))
}
pub fn error_as_ref(&self) -> &TestError {
if let TestStatus::Error(e) = self {
return e;
}
panic!("called `TestStatus::error()` on a value that is not TestStatus::Error(_)");
}
pub fn is_failure(&self) -> bool {
matches!(self, TestStatus::Failure(_))
}
pub fn failure_as_ref(&self) -> &TestFailure {
if let TestStatus::Failure(e) = self {
return e;
}
panic!("called `TestStatus::failure()` on a value that is not TestStatus::Failure(_)");
}
pub fn is_skipped(&self) -> bool {
matches!(self, TestStatus::Skipped(_))
}
pub fn skipped_as_ref(&self) -> &TestSkipped {
if let TestStatus::Skipped(e) = self {
return e;
}
panic!("called `TestStatus::skipped()` on a value that is not TestStatus::Skipped(_)");
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)]
pub struct TestCase {
pub time: f64,
pub name: String,
pub status: TestStatus,
pub original_name: String,
pub classname: Option<String>,
pub group: Option<String>,
pub file: Option<String>,
pub line: Option<u64>,
pub system_out: Option<String>,
pub system_err: Option<String>,
#[cfg(feature = "chrono")]
pub timestamp: Option<DateTime<Utc>>,
#[cfg(not(feature = "chrono"))]
pub timestamp: Option<String>,
pub properties: Properties,
pub reruns: Vec<RerunOrFlaky>,
}
impl TestCase {
fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
for a in e.attributes() {
let a = a?;
match a.key {
QName(b"time") => self.time = try_from_attribute_value_f64(a.value)?,
QName(b"name") => self.original_name = try_from_attribute_value_string(a.value)?,
QName(b"classname") => {
self.classname = Some(try_from_attribute_value_string(a.value)?)
}
QName(b"group") => self.group = Some(try_from_attribute_value_string(a.value)?),
QName(b"file") => self.file = Some(try_from_attribute_value_string(a.value)?),
QName(b"line") => self.line = Some(try_from_attribute_value_u64(a.value)?),
QName(b"timestamp") => {
#[cfg(feature = "chrono")]
{
self.timestamp = Some(try_from_attribute_value_datetime(a.value)?);
}
#[cfg(not(feature = "chrono"))]
{
self.timestamp = Some(try_from_attribute_value_string(a.value)?);
}
}
_ => {}
};
}
if let Some(cn) = self.classname.as_ref() {
self.name = format!("{}::{}", cn, self.original_name);
} else if let Some(gn) = self.group.as_ref() {
self.name = format!("{}::{}", gn, self.original_name);
} else {
self.name.clone_from(&self.original_name);
}
Ok(())
}
fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
let mut tc = Self::default();
tc.parse_attributes(e)?;
Ok(tc)
}
fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
let mut tc = Self {
system_out: None,
system_err: None,
..Default::default()
};
tc.parse_attributes(e)?;
loop {
let mut buf = Vec::new();
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"testcase") => break,
Ok(XMLEvent::Empty(ref empty_event)) => match empty_event.name() {
QName(b"system-out") => {
if tc.system_out.is_none() {
tc.system_out = Some(String::new());
}
}
QName(b"system-err") => {
if tc.system_err.is_none() {
tc.system_err = Some(String::new());
}
}
QName(b"flakyFailure") => {
tc.reruns.push(RerunOrFlaky::new_empty(
empty_event,
RerunOrFlakyKind::FlakyFailure,
)?);
}
QName(b"flakyError") => {
tc.reruns.push(RerunOrFlaky::new_empty(
empty_event,
RerunOrFlakyKind::FlakyError,
)?);
}
QName(b"rerunFailure") => {
tc.reruns.push(RerunOrFlaky::new_empty(
empty_event,
RerunOrFlakyKind::RerunFailure,
)?);
}
QName(b"rerunError") => {
tc.reruns.push(RerunOrFlaky::new_empty(
empty_event,
RerunOrFlakyKind::RerunError,
)?);
}
QName(b"skipped") => {
tc.status = TestStatus::Skipped(TestSkipped::new_empty(empty_event)?);
}
QName(b"failure") => {
tc.status = TestStatus::Failure(TestFailure::new_empty(empty_event)?);
}
QName(b"error") => {
tc.status = TestStatus::Error(TestError::new_empty(empty_event)?);
}
_ => {}
},
Ok(XMLEvent::Start(ref start_event)) => match start_event.name() {
QName(b"skipped") => {
tc.status = TestStatus::Skipped(TestSkipped::from_reader(start_event, r)?);
}
QName(b"failure") => {
tc.status = TestStatus::Failure(TestFailure::from_reader(start_event, r)?);
}
QName(b"error") => {
tc.status = TestStatus::Error(TestError::from_reader(start_event, r)?);
}
QName(b"flakyFailure") => {
tc.reruns.push(RerunOrFlaky::from_reader(
start_event,
r,
RerunOrFlakyKind::FlakyFailure,
)?);
}
QName(b"flakyError") => {
tc.reruns.push(RerunOrFlaky::from_reader(
start_event,
r,
RerunOrFlakyKind::FlakyError,
)?);
}
QName(b"rerunFailure") => {
tc.reruns.push(RerunOrFlaky::from_reader(
start_event,
r,
RerunOrFlakyKind::RerunFailure,
)?);
}
QName(b"rerunError") => {
tc.reruns.push(RerunOrFlaky::from_reader(
start_event,
r,
RerunOrFlakyKind::RerunError,
)?);
}
QName(b"system-out") => {
if let Some(parsed_content) = parse_system(start_event, r)? {
let current_out = tc.system_out.get_or_insert_with(String::new);
if !current_out.is_empty() {
current_out.push('\n');
}
current_out.push_str(&parsed_content);
}
}
QName(b"system-err") => {
if let Some(parsed_content) = parse_system(start_event, r)? {
let current_err = tc.system_err.get_or_insert_with(String::new);
if !current_err.is_empty() {
current_err.push('\n');
}
current_err.push_str(&parsed_content);
}
}
QName(b"properties") => {
tc.properties = Properties::from_reader(r)?;
}
_ => {
r.read_to_end_into(start_event.name(), &mut Vec::new())?;
}
},
Ok(XMLEvent::Eof) => {
return Err(Error::UnexpectedEndOfFile("testcase".to_string()))
}
Err(err) => return Err(err.into()),
_ => (),
}
buf.clear();
}
Ok(tc)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)]
pub struct TestSuite {
pub cases: Vec<TestCase>,
pub suites: Vec<TestSuite>,
pub time: f64,
pub tests: u64,
pub errors: u64,
pub failures: u64,
pub skipped: u64,
pub assertions: Option<u64>,
pub name: String,
#[cfg(feature = "chrono")]
pub timestamp: Option<DateTime<Utc>>,
#[cfg(not(feature = "chrono"))]
pub timestamp: Option<String>,
pub hostname: Option<String>,
pub id: Option<String>,
pub package: Option<String>,
pub file: Option<String>,
pub log: Option<String>,
pub url: Option<String>,
pub version: Option<String>,
pub system_out: Option<String>,
pub system_err: Option<String>,
pub properties: Properties,
}
impl TestSuite {
fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
for a in e.attributes() {
let a = a?;
match a.key {
QName(b"time") => self.time = try_from_attribute_value_f64(a.value)?,
QName(b"tests") => self.tests = try_from_attribute_value_u64(a.value)?,
QName(b"errors") => self.errors = try_from_attribute_value_u64(a.value)?,
QName(b"failures") => self.failures = try_from_attribute_value_u64(a.value)?,
QName(b"skipped") => self.skipped = try_from_attribute_value_u64(a.value)?,
QName(b"assertions") => {
self.assertions = Some(try_from_attribute_value_u64(a.value)?)
}
QName(b"name") => self.name = try_from_attribute_value_string(a.value)?,
QName(b"timestamp") => {
#[cfg(feature = "chrono")]
{
self.timestamp = Some(try_from_attribute_value_datetime(a.value)?);
}
#[cfg(not(feature = "chrono"))]
{
self.timestamp = Some(try_from_attribute_value_string(a.value)?);
}
}
QName(b"hostname") => {
self.hostname = Some(try_from_attribute_value_string(a.value)?)
}
QName(b"id") => self.id = Some(try_from_attribute_value_string(a.value)?),
QName(b"package") => self.package = Some(try_from_attribute_value_string(a.value)?),
QName(b"file") => self.file = Some(try_from_attribute_value_string(a.value)?),
QName(b"log") => self.log = Some(try_from_attribute_value_string(a.value)?),
QName(b"url") => self.url = Some(try_from_attribute_value_string(a.value)?),
QName(b"version") => self.version = Some(try_from_attribute_value_string(a.value)?),
_ => {}
};
}
Ok(())
}
fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
let mut ts = Self::default();
ts.parse_attributes(e)?;
Ok(ts)
}
fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
let mut ts = Self::default();
ts.parse_attributes(e)?;
loop {
let mut buf = Vec::new();
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"testsuite") => break,
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testsuite") => {
ts.suites.push(TestSuite::from_reader(e, r)?);
}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testcase") => {
ts.cases.push(TestCase::from_reader(e, r)?);
}
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"testcase") => {
ts.cases.push(TestCase::new_empty(e)?);
}
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"system-out") => {}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"system-out") => {
ts.system_out = parse_system(e, r)?;
}
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"system-err") => {}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"system-err") => {
ts.system_err = parse_system(e, r)?;
}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"properties") => {
ts.properties = Properties::from_reader(r)?;
}
Ok(XMLEvent::Start(ref e)) => {
r.read_to_end_into(e.name(), &mut Vec::new())?;
}
Ok(XMLEvent::Eof) => {
return Err(Error::UnexpectedEndOfFile("testsuite".to_string()));
}
Err(err) => return Err(err.into()),
_ => (),
}
buf.clear();
}
Ok(ts)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)]
pub struct TestSuites {
pub suites: Vec<TestSuite>,
pub time: f64,
pub tests: u64,
pub errors: u64,
pub failures: u64,
pub skipped: u64,
pub name: String,
#[cfg(feature = "chrono")]
pub timestamp: Option<DateTime<Utc>>,
#[cfg(not(feature = "chrono"))]
pub timestamp: Option<String>,
}
impl TestSuites {
fn parse_attributes(&mut self, e: &XMLBytesStart) -> Result<(), Error> {
for a in e.attributes() {
let a = a?;
match a.key {
QName(b"time") => self.time = try_from_attribute_value_f64(a.value)?,
QName(b"tests") => self.tests = try_from_attribute_value_u64(a.value)?,
QName(b"errors") => self.errors = try_from_attribute_value_u64(a.value)?,
QName(b"failures") => self.failures = try_from_attribute_value_u64(a.value)?,
QName(b"skipped") => self.skipped = try_from_attribute_value_u64(a.value)?,
QName(b"name") => self.name = try_from_attribute_value_string(a.value)?,
QName(b"timestamp") => {
#[cfg(feature = "chrono")]
{
self.timestamp = Some(try_from_attribute_value_datetime(a.value)?);
}
#[cfg(not(feature = "chrono"))]
{
self.timestamp = Some(try_from_attribute_value_string(a.value)?);
}
}
_ => {}
};
}
Ok(())
}
fn new_empty(e: &XMLBytesStart) -> Result<Self, Error> {
let mut ts = Self::default();
ts.parse_attributes(e)?;
Ok(ts)
}
fn from_reader<B: BufRead>(e: &XMLBytesStart, r: &mut XMLReader<B>) -> Result<Self, Error> {
let mut ts = Self::default();
ts.parse_attributes(e)?;
loop {
let mut buf = Vec::new();
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"testsuites") => break,
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"testrun") => break,
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testsuite") => {
ts.suites.push(TestSuite::from_reader(e, r)?);
}
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"testsuite") => {
ts.suites.push(TestSuite::new_empty(e)?);
}
Ok(XMLEvent::Eof) => {
return Err(Error::UnexpectedEndOfFile("testsuites".to_string()));
}
Ok(XMLEvent::Start(ref e)) => {
r.read_to_end_into(e.name(), &mut Vec::new())?;
}
Err(err) => return Err(err.into()),
_ => (),
}
buf.clear();
}
Ok(ts)
}
}
fn try_from_attribute_value_f64(value: Cow<[u8]>) -> Result<f64, Error> {
match str::from_utf8(&value)? {
"" => Ok(f64::default()),
s => Ok(s.parse::<f64>()?),
}
}
fn try_from_attribute_value_u64(value: Cow<[u8]>) -> Result<u64, Error> {
match str::from_utf8(&value)? {
"" => Ok(u64::default()),
s => Ok(s.parse::<u64>()?),
}
}
fn try_from_attribute_value_string(value: Cow<[u8]>) -> Result<String, Error> {
let s = str::from_utf8(&value)?;
let u = unescape(s)?;
Ok(u.to_string())
}
#[cfg(feature = "chrono")]
fn try_from_attribute_value_datetime(value: Cow<[u8]>) -> Result<DateTime<Utc>, Error> {
let s = str::from_utf8(&value)?;
let u = unescape(s)?;
let dt = DateTime::parse_from_rfc3339(&u)?.with_timezone(&Utc);
Ok(dt)
}
fn parse_system<B: BufRead>(
orig: &XMLBytesStart,
r: &mut XMLReader<B>,
) -> Result<Option<String>, Error> {
let mut res: Option<String> = Some(String::new());
loop {
let mut buf = Vec::new();
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == orig.name() => break,
Ok(XMLEvent::Text(e)) => {
res.get_or_insert(String::new()).push_str(&e.decode()?);
}
Ok(XMLEvent::CData(e)) => {
res.get_or_insert(String::new())
.push_str(str::from_utf8(&e)?);
}
Ok(XMLEvent::Eof) => {
return Err(Error::UnexpectedEndOfFile(format!("{:?}", orig.name())));
}
Ok(XMLEvent::Start(ref e)) => {
r.read_to_end_into(e.name(), &mut Vec::new())?;
}
Err(err) => return Err(err.into()),
_ => (),
}
buf.clear();
}
Ok(res)
}
pub fn from_reader<B: BufRead>(reader: B) -> Result<TestSuites, Error> {
let mut r = XMLReader::from_reader(reader);
loop {
let mut buf = Vec::new();
match r.read_event_into(&mut buf) {
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"testsuites") => {
return TestSuites::new_empty(e);
}
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"testrun") => {
return TestSuites::new_empty(e);
}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testsuites") => {
return TestSuites::from_reader(e, &mut r);
}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testrun") => {
return TestSuites::from_reader(e, &mut r);
}
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"testsuite") => {
let ts = TestSuite::new_empty(e)?;
let mut suites = TestSuites::default();
suites.suites.push(ts);
return Ok(suites);
}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"testsuite") => {
let ts = TestSuite::from_reader(e, &mut r)?;
let mut suites = TestSuites::default();
suites.suites.push(ts);
return Ok(suites);
}
Ok(XMLEvent::Start(ref e)) => {
r.read_to_end_into(e.name(), &mut Vec::new())?;
}
Ok(XMLEvent::Eof) => {
return Err(Error::UnexpectedEndOfFile("testsuites".to_string()));
}
Err(err) => return Err(err.into()),
_ => (),
}
buf.clear();
}
}