#![cfg_attr(
feature = "document-features",
cfg_attr(doc, doc = ::document_features::document_features!())
)]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![cfg_attr(docs_rs, feature(doc_auto_cfg))]
mod errors;
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::Error as XMLError;
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 {
let mut buf = Vec::new();
loop {
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"property") => break,
Ok(XMLEvent::Eof) => {
return Err(XMLError::UnexpectedEof("property".to_string()).into())
}
Ok(XMLEvent::Text(e)) => {
v = Some(e.unescape()?.trim().to_string());
}
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();
let mut buf = Vec::new();
loop {
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::Eof) => {
return Err(XMLError::UnexpectedEof("properties".to_string()).into())
}
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, 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)?;
let mut buf = Vec::new();
loop {
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"failure") => break,
Ok(XMLEvent::Text(e)) => {
tf.text = e.unescape()?.trim().to_string();
}
Ok(XMLEvent::Eof) => {
return Err(XMLError::UnexpectedEof("failure".to_string()).into())
}
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)?;
let mut buf = Vec::new();
loop {
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"error") => break,
Ok(XMLEvent::Text(e)) => {
te.text = e.unescape()?.trim().to_string();
}
Ok(XMLEvent::Eof) => {
return Err(XMLError::UnexpectedEof("error".to_string()).into())
}
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)?;
let mut buf = Vec::new();
loop {
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"skipped") => break,
Ok(XMLEvent::Text(e)) => {
ts.text = e.unescape()?.trim().to_string();
}
Ok(XMLEvent::Eof) => {
return Err(XMLError::UnexpectedEof("skipped".to_string()).into())
}
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(ref 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(ref 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(ref 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>,
pub properties: Properties,
}
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)?),
_ => {}
};
}
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 = self.original_name.clone();
}
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::default();
tc.parse_attributes(e)?;
let mut buf = Vec::new();
loop {
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"testcase") => break,
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"skipped") => {
let ts = TestSkipped::from_reader(e, r)?;
tc.status = TestStatus::Skipped(ts);
}
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"skipped") => {
let ts = TestSkipped::new_empty(e)?;
tc.status = TestStatus::Skipped(ts);
}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"failure") => {
let tf = TestFailure::from_reader(e, r)?;
tc.status = TestStatus::Failure(tf);
}
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"failure") => {
let tf = TestFailure::new_empty(e)?;
tc.status = TestStatus::Failure(tf);
}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"error") => {
let te = TestError::from_reader(e, r)?;
tc.status = TestStatus::Error(te);
}
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"error") => {
let te = TestError::new_empty(e)?;
tc.status = TestStatus::Error(te);
}
Ok(XMLEvent::Empty(ref e)) if e.name() == QName(b"system-out") => {}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"system-out") => {
tc.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") => {
tc.system_err = parse_system(e, r)?;
}
Ok(XMLEvent::Start(ref e)) if e.name() == QName(b"properties") => {
tc.properties = Properties::from_reader(r)?;
}
Ok(XMLEvent::Eof) => {
return Err(XMLError::UnexpectedEof("testcase".to_string()).into())
}
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 time: f64,
pub tests: u64,
pub errors: u64,
pub failures: u64,
pub skipped: u64,
pub assertions: Option<u64>,
pub name: String,
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") => {
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)?;
let mut buf = Vec::new();
loop {
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"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::Eof) => {
return Err(XMLError::UnexpectedEof("testsuite".to_string()).into())
}
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,
}
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)?,
_ => {}
};
}
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)?;
let mut buf = Vec::new();
loop {
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == QName(b"testsuites") => 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(XMLError::UnexpectedEof("testsuites".to_string()).into())
}
Err(err) => return Err(err.into()),
_ => (),
}
}
buf.clear();
Ok(ts)
}
}
fn try_from_attribute_value_f64(value: Cow<[u8]>) -> Result<f64, Error> {
match value {
Cow::Borrowed(b) => {
let s = str::from_utf8(b)?;
if s.is_empty() {
return Ok(0f64);
}
Ok(s.parse::<f64>()?)
}
Cow::Owned(ref b) => {
let s = str::from_utf8(b)?;
if s.is_empty() {
return Ok(0f64);
}
Ok(s.parse::<f64>()?)
}
}
}
fn try_from_attribute_value_u64(value: Cow<[u8]>) -> Result<u64, Error> {
match value {
Cow::Borrowed(b) => {
let s = str::from_utf8(b)?;
if s.is_empty() {
return Ok(0u64);
}
Ok(s.parse::<u64>()?)
}
Cow::Owned(ref b) => {
let s = str::from_utf8(b)?;
if s.is_empty() {
return Ok(0u64);
}
Ok(s.parse::<u64>()?)
}
}
}
fn try_from_attribute_value_string(value: Cow<[u8]>) -> Result<String, Error> {
let s = match value {
Cow::Borrowed(b) => str::from_utf8(b)?,
Cow::Owned(ref b) => str::from_utf8(b)?,
};
match unescape(s)? {
Cow::Borrowed(u) => Ok(u.to_owned()),
Cow::Owned(ref u) => Ok(u.to_owned()),
}
}
fn parse_system<B: BufRead>(
orig: &XMLBytesStart,
r: &mut XMLReader<B>,
) -> Result<Option<String>, Error> {
let mut buf = Vec::new();
let mut res = None;
loop {
match r.read_event_into(&mut buf) {
Ok(XMLEvent::End(ref e)) if e.name() == orig.name() => break,
Ok(XMLEvent::Text(e)) => {
res = Some(e.unescape()?.to_string());
}
Ok(XMLEvent::Eof) => {
return Err(XMLError::UnexpectedEof(format!("{:?}", orig.name())).into());
}
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);
let mut buf = Vec::new();
loop {
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::Start(ref e)) if e.name() == QName(b"testsuites") => {
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::Eof) => {
return Err(XMLError::UnexpectedEof("testsuites".to_string()).into())
}
Err(err) => return Err(err.into()),
_ => (),
}
}
}