use crate::{
DeserializeError, DeserializeErrorKind, FlakyOrRerun, NonSuccessKind, NonSuccessReruns,
PathElement, Property, Report, ReportUuid, TestCase, TestCaseStatus, TestRerun, TestSuite,
XmlString,
};
use chrono::{DateTime, FixedOffset};
use indexmap::IndexMap;
use newtype_uuid::GenericUuid;
use quick_xml::{
escape::{resolve_xml_entity, unescape_with},
events::{BytesStart, Event},
Reader,
};
use std::{io::BufRead, time::Duration};
impl Report {
pub fn deserialize<R: BufRead>(reader: R) -> Result<Self, DeserializeError> {
let mut xml_reader = Reader::from_reader(reader);
xml_reader.config_mut().trim_text(false);
deserialize_report(&mut xml_reader)
}
pub fn deserialize_from_str(xml: &str) -> Result<Self, DeserializeError> {
Self::deserialize(xml.as_bytes())
}
}
fn deserialize_report<R: BufRead>(reader: &mut Reader<R>) -> Result<Report, DeserializeError> {
let mut buf = Vec::new();
let mut report: Option<Report> = None;
let mut properly_closed = false;
let root_path = vec![PathElement::TestSuites];
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) if e.name().as_ref() == b"testsuites" => {
report = Some(parse_testsuites_element(&e, &root_path)?);
}
Ok(Event::Empty(e)) if e.name().as_ref() == b"testsuites" => {
report = Some(parse_testsuites_element(&e, &root_path)?);
properly_closed = true; }
Ok(Event::Start(e)) if e.name().as_ref() == b"testsuite" => {
if let Some(ref mut report) = report {
let suite_index = report.test_suites.len();
let test_suite =
deserialize_test_suite(reader, &e, false, &root_path, suite_index)?;
report.test_suites.push(test_suite);
}
}
Ok(Event::Empty(e)) if e.name().as_ref() == b"testsuite" => {
if let Some(ref mut report) = report {
let suite_index = report.test_suites.len();
let test_suite =
deserialize_test_suite(reader, &e, true, &root_path, suite_index)?;
report.test_suites.push(test_suite);
}
}
Ok(Event::End(e)) if e.name().as_ref() == b"testsuites" => {
properly_closed = true;
break;
}
Ok(Event::Eof) => break,
Ok(_) => {}
Err(e) => {
return Err(DeserializeError::new(
DeserializeErrorKind::XmlError(e),
root_path.clone(),
))
}
}
buf.clear();
}
if !properly_closed && report.is_some() {
return Err(DeserializeError::new(
DeserializeErrorKind::InvalidStructure(
"unexpected EOF, <testsuites> not properly closed".to_string(),
),
root_path,
));
}
report.ok_or_else(|| {
DeserializeError::new(
DeserializeErrorKind::InvalidStructure("missing <testsuites> element".to_string()),
Vec::new(),
)
})
}
fn parse_testsuites_element(
element: &BytesStart<'_>,
path: &[PathElement],
) -> Result<Report, DeserializeError> {
let mut name = None;
let mut uuid = None;
let mut timestamp = None;
let mut time = None;
let mut tests = 0;
let mut failures = 0;
let mut errors = 0;
for attr in element.attributes() {
let attr = attr.map_err(|e| {
DeserializeError::new(DeserializeErrorKind::AttrError(e), path.to_vec())
})?;
let mut attr_path = path.to_vec();
match attr.key.as_ref() {
b"name" => {
attr_path.push(PathElement::Attribute("name".to_string()));
name = Some(parse_xml_string(&attr.value, &attr_path)?);
}
b"uuid" => {
attr_path.push(PathElement::Attribute("uuid".to_string()));
uuid = Some(parse_uuid(&attr.value, &attr_path)?);
}
b"timestamp" => {
attr_path.push(PathElement::Attribute("timestamp".to_string()));
timestamp = Some(parse_timestamp(&attr.value, &attr_path)?);
}
b"time" => {
attr_path.push(PathElement::Attribute("time".to_string()));
time = Some(parse_duration(&attr.value, &attr_path)?);
}
b"tests" => {
attr_path.push(PathElement::Attribute("tests".to_string()));
tests = parse_usize(&attr.value, &attr_path)?;
}
b"failures" => {
attr_path.push(PathElement::Attribute("failures".to_string()));
failures = parse_usize(&attr.value, &attr_path)?;
}
b"errors" => {
attr_path.push(PathElement::Attribute("errors".to_string()));
errors = parse_usize(&attr.value, &attr_path)?;
}
_ => {} }
}
let name = require_attribute(name, "name", path)?;
Ok(Report {
name,
uuid,
timestamp,
time,
tests,
failures,
errors,
test_suites: Vec::new(),
})
}
fn deserialize_test_suite<R: BufRead>(
reader: &mut Reader<R>,
element: &BytesStart<'_>,
is_empty: bool,
path: &[PathElement],
suite_index: usize,
) -> Result<TestSuite, DeserializeError> {
let mut name = None;
let mut tests = 0;
let mut disabled = 0;
let mut errors = 0;
let mut failures = 0;
let mut timestamp = None;
let mut time = None;
let mut extra = IndexMap::new();
let mut element_path = path.to_vec();
element_path.push(PathElement::TestSuite(suite_index, None));
for attr in element.attributes() {
let attr = attr.map_err(|e| {
DeserializeError::new(DeserializeErrorKind::AttrError(e), element_path.clone())
})?;
let mut attr_path = element_path.clone();
match attr.key.as_ref() {
b"name" => {
attr_path.push(PathElement::Attribute("name".to_string()));
name = Some(parse_xml_string(&attr.value, &attr_path)?);
}
b"tests" => {
attr_path.push(PathElement::Attribute("tests".to_string()));
tests = parse_usize(&attr.value, &attr_path)?;
}
b"disabled" => {
attr_path.push(PathElement::Attribute("disabled".to_string()));
disabled = parse_usize(&attr.value, &attr_path)?;
}
b"errors" => {
attr_path.push(PathElement::Attribute("errors".to_string()));
errors = parse_usize(&attr.value, &attr_path)?;
}
b"failures" => {
attr_path.push(PathElement::Attribute("failures".to_string()));
failures = parse_usize(&attr.value, &attr_path)?;
}
b"timestamp" => {
attr_path.push(PathElement::Attribute("timestamp".to_string()));
timestamp = Some(parse_timestamp(&attr.value, &attr_path)?);
}
b"time" => {
attr_path.push(PathElement::Attribute("time".to_string()));
time = Some(parse_duration(&attr.value, &attr_path)?);
}
_ => {
let key = parse_xml_string(attr.key.as_ref(), &attr_path)?;
let value = parse_xml_string(&attr.value, &attr_path)?;
extra.insert(key, value);
}
}
}
let name = require_attribute(name, "name", &element_path)?;
if is_empty {
return Ok(TestSuite {
name,
tests,
disabled,
errors,
failures,
timestamp,
time,
test_cases: Vec::new(),
properties: Vec::new(),
system_out: None,
system_err: None,
extra,
});
}
let mut suite_path = path.to_vec();
suite_path.push(PathElement::TestSuite(
suite_index,
Some(name.as_str().to_string()),
));
let mut test_cases = Vec::new();
let mut properties = Vec::new();
let mut system_out = None;
let mut system_err = None;
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let element_name = e.name().as_ref().to_vec();
if &element_name == b"testcase" {
let test_case =
deserialize_test_case(reader, e, false, &suite_path, test_cases.len())?;
test_cases.push(test_case);
} else if &element_name == b"properties" {
properties = deserialize_properties(reader, &suite_path)?;
} else if &element_name == b"system-out" {
let mut child_path = suite_path.clone();
child_path.push(PathElement::SystemOut);
system_out = Some(read_text_content(reader, b"system-out", &child_path)?);
} else if &element_name == b"system-err" {
let mut child_path = suite_path.clone();
child_path.push(PathElement::SystemErr);
system_err = Some(read_text_content(reader, b"system-err", &child_path)?);
} else {
let tag_name = e.name().to_owned();
reader
.read_to_end_into(tag_name, &mut Vec::new())
.map_err(|e| {
DeserializeError::new(
DeserializeErrorKind::XmlError(e),
suite_path.clone(),
)
})?;
}
}
Ok(Event::Empty(ref e)) => {
if e.name().as_ref() == b"testcase" {
let test_case =
deserialize_test_case(reader, e, true, &suite_path, test_cases.len())?;
test_cases.push(test_case);
}
}
Ok(Event::End(ref e)) if e.name().as_ref() == b"testsuite" => break,
Ok(Event::Eof) => {
return Err(DeserializeError::new(
DeserializeErrorKind::InvalidStructure(
"unexpected EOF in <testsuite>".to_string(),
),
suite_path,
))
}
Ok(_) => {}
Err(e) => {
return Err(DeserializeError::new(
DeserializeErrorKind::XmlError(e),
suite_path,
))
}
}
buf.clear();
}
Ok(TestSuite {
name,
tests,
disabled,
errors,
failures,
timestamp,
time,
test_cases,
properties,
system_out,
system_err,
extra,
})
}
fn deserialize_test_case<R: BufRead>(
reader: &mut Reader<R>,
element: &BytesStart<'_>,
is_empty: bool,
path: &[PathElement],
case_index: usize,
) -> Result<TestCase, DeserializeError> {
let mut name = None;
let mut classname = None;
let mut assertions = None;
let mut timestamp = None;
let mut time = None;
let mut extra = IndexMap::new();
let mut element_path = path.to_vec();
element_path.push(PathElement::TestCase(case_index, None));
for attr in element.attributes() {
let attr = attr.map_err(|e| {
DeserializeError::new(DeserializeErrorKind::AttrError(e), element_path.clone())
})?;
let mut attr_path = element_path.clone();
match attr.key.as_ref() {
b"name" => {
attr_path.push(PathElement::Attribute("name".to_string()));
name = Some(parse_xml_string(&attr.value, &attr_path)?);
}
b"classname" => {
attr_path.push(PathElement::Attribute("classname".to_string()));
classname = Some(parse_xml_string(&attr.value, &attr_path)?);
}
b"assertions" => {
attr_path.push(PathElement::Attribute("assertions".to_string()));
assertions = Some(parse_usize(&attr.value, &attr_path)?);
}
b"timestamp" => {
attr_path.push(PathElement::Attribute("timestamp".to_string()));
timestamp = Some(parse_timestamp(&attr.value, &attr_path)?);
}
b"time" => {
attr_path.push(PathElement::Attribute("time".to_string()));
time = Some(parse_duration(&attr.value, &attr_path)?);
}
_ => {
let key = parse_xml_string(attr.key.as_ref(), &attr_path)?;
let value = parse_xml_string(&attr.value, &attr_path)?;
extra.insert(key, value);
}
}
}
let name = require_attribute(name, "name", &element_path)?;
if is_empty {
return Ok(TestCase {
name,
classname,
assertions,
timestamp,
time,
status: TestCaseStatus::success(),
system_out: None,
system_err: None,
extra,
properties: Vec::new(),
});
}
let mut case_path = path.to_vec();
case_path.push(PathElement::TestCase(
case_index,
Some(name.as_str().to_string()),
));
let mut properties = Vec::new();
let mut system_out = None;
let mut system_err = None;
let mut status_elements = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let element_name = e.name().as_ref().to_vec();
if is_status_element(&element_name) {
let status_element = deserialize_status_element(reader, e, false, &case_path)?;
status_elements.push(status_element);
} else if &element_name == b"properties" {
properties = deserialize_properties(reader, &case_path)?;
} else if &element_name == b"system-out" {
let mut child_path = case_path.clone();
child_path.push(PathElement::SystemOut);
system_out = Some(read_text_content(reader, b"system-out", &child_path)?);
} else if &element_name == b"system-err" {
let mut child_path = case_path.clone();
child_path.push(PathElement::SystemErr);
system_err = Some(read_text_content(reader, b"system-err", &child_path)?);
} else {
let tag_name = e.name().to_owned();
reader
.read_to_end_into(tag_name, &mut Vec::new())
.map_err(|e| {
DeserializeError::new(
DeserializeErrorKind::XmlError(e),
case_path.clone(),
)
})?;
}
}
Ok(Event::Empty(ref e)) => {
let element_name = e.name().as_ref().to_vec();
if is_status_element(&element_name) {
let status_element = deserialize_status_element(reader, e, true, &case_path)?;
status_elements.push(status_element);
}
}
Ok(Event::End(ref e)) if e.name().as_ref() == b"testcase" => break,
Ok(Event::Eof) => {
return Err(DeserializeError::new(
DeserializeErrorKind::InvalidStructure(
"unexpected EOF in <testcase>".to_string(),
),
case_path,
))
}
Ok(_) => {}
Err(e) => {
return Err(DeserializeError::new(
DeserializeErrorKind::XmlError(e),
case_path,
))
}
}
buf.clear();
}
let status = build_test_case_status(status_elements, &case_path)?;
Ok(TestCase {
name,
classname,
assertions,
timestamp,
time,
status,
system_out,
system_err,
extra,
properties,
})
}
#[derive(Debug)]
struct StatusElementData {
message: Option<XmlString>,
ty: Option<XmlString>,
description: Option<XmlString>,
stack_trace: Option<XmlString>,
system_out: Option<XmlString>,
system_err: Option<XmlString>,
timestamp: Option<DateTime<FixedOffset>>,
time: Option<Duration>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum MainStatusKind {
Failure,
Error,
Skipped,
}
impl MainStatusKind {
fn to_non_success_kind(self) -> NonSuccessKind {
match self {
MainStatusKind::Failure => NonSuccessKind::Failure,
MainStatusKind::Error => NonSuccessKind::Error,
MainStatusKind::Skipped => {
panic!("to_non_success_kind called on Skipped")
}
}
}
}
struct MainStatusElement {
kind: MainStatusKind,
data: StatusElementData,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum RerunStatusKind {
Failure,
Error,
}
struct RerunStatusElement {
kind: RerunStatusKind,
data: StatusElementData,
}
enum StatusElement {
Main(MainStatusElement),
Flaky(RerunStatusElement),
Rerun(RerunStatusElement),
}
enum StatusCategory {
Main(MainStatusKind),
Flaky(RerunStatusKind),
Rerun(RerunStatusKind),
}
fn deserialize_status_element<R: BufRead>(
reader: &mut Reader<R>,
element: &BytesStart<'_>,
is_empty: bool,
path: &[PathElement],
) -> Result<StatusElement, DeserializeError> {
let (category, status_path_elem) = match element.name().as_ref() {
b"failure" => (
StatusCategory::Main(MainStatusKind::Failure),
PathElement::Failure,
),
b"error" => (
StatusCategory::Main(MainStatusKind::Error),
PathElement::Error,
),
b"skipped" => (
StatusCategory::Main(MainStatusKind::Skipped),
PathElement::Skipped,
),
b"flakyFailure" => (
StatusCategory::Flaky(RerunStatusKind::Failure),
PathElement::FlakyFailure,
),
b"flakyError" => (
StatusCategory::Flaky(RerunStatusKind::Error),
PathElement::FlakyError,
),
b"rerunFailure" => (
StatusCategory::Rerun(RerunStatusKind::Failure),
PathElement::RerunFailure,
),
b"rerunError" => (
StatusCategory::Rerun(RerunStatusKind::Error),
PathElement::RerunError,
),
_ => {
return Err(DeserializeError::new(
DeserializeErrorKind::UnexpectedElement(
String::from_utf8_lossy(element.name().as_ref()).to_string(),
),
path.to_vec(),
))
}
};
let mut status_path = path.to_vec();
status_path.push(status_path_elem);
let mut message = None;
let mut ty = None;
let mut timestamp = None;
let mut time = None;
for attr in element.attributes() {
let attr = attr.map_err(|e| {
DeserializeError::new(DeserializeErrorKind::AttrError(e), status_path.clone())
})?;
let mut attr_path = status_path.clone();
match attr.key.as_ref() {
b"message" => {
attr_path.push(PathElement::Attribute("message".to_string()));
message = Some(parse_xml_string(&attr.value, &attr_path)?);
}
b"type" => {
attr_path.push(PathElement::Attribute("type".to_string()));
ty = Some(parse_xml_string(&attr.value, &attr_path)?);
}
b"timestamp" => {
attr_path.push(PathElement::Attribute("timestamp".to_string()));
timestamp = Some(parse_timestamp(&attr.value, &attr_path)?);
}
b"time" => {
attr_path.push(PathElement::Attribute("time".to_string()));
time = Some(parse_duration(&attr.value, &attr_path)?);
}
_ => {} }
}
let mut description_text = String::new();
let mut stack_trace = None;
let mut system_out = None;
let mut system_err = None;
if !is_empty {
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
let element_name = e.name().as_ref().to_vec();
if &element_name == b"stackTrace" {
let mut child_path = status_path.clone();
child_path.push(PathElement::Attribute("stackTrace".to_string()));
stack_trace = Some(read_text_content(reader, b"stackTrace", &child_path)?);
} else if &element_name == b"system-out" {
let mut child_path = status_path.clone();
child_path.push(PathElement::SystemOut);
system_out = Some(read_text_content(reader, b"system-out", &child_path)?);
} else if &element_name == b"system-err" {
let mut child_path = status_path.clone();
child_path.push(PathElement::SystemErr);
system_err = Some(read_text_content(reader, b"system-err", &child_path)?);
} else {
let tag_name = e.name().to_owned();
reader
.read_to_end_into(tag_name, &mut Vec::new())
.map_err(|e| {
DeserializeError::new(
DeserializeErrorKind::XmlError(e),
status_path.clone(),
)
})?;
}
}
Ok(Event::Text(ref e)) => {
let text = std::str::from_utf8(e.as_ref()).map_err(|e| {
DeserializeError::new(
DeserializeErrorKind::Utf8Error(e),
status_path.clone(),
)
})?;
let unescaped = unescape_with(text, resolve_xml_entity).map_err(|e| {
DeserializeError::new(
DeserializeErrorKind::EscapeError(e),
status_path.clone(),
)
})?;
description_text.push_str(&unescaped);
}
Ok(Event::CData(ref e)) => {
let text = std::str::from_utf8(e.as_ref()).map_err(|e| {
DeserializeError::new(
DeserializeErrorKind::Utf8Error(e),
status_path.clone(),
)
})?;
description_text.push_str(text);
}
Ok(Event::GeneralRef(ref e)) => {
let entity_name = std::str::from_utf8(e.as_ref()).map_err(|e| {
DeserializeError::new(
DeserializeErrorKind::Utf8Error(e),
status_path.clone(),
)
})?;
let unescaped = resolve_xml_entity(entity_name).ok_or_else(|| {
DeserializeError::new(
DeserializeErrorKind::InvalidStructure(format!(
"unrecognized entity: {entity_name}",
)),
status_path.clone(),
)
})?;
description_text.push_str(unescaped);
}
Ok(Event::End(ref e)) if is_status_element(e.name().as_ref()) => {
break;
}
Ok(Event::Eof) => {
return Err(DeserializeError::new(
DeserializeErrorKind::InvalidStructure(
"unexpected EOF in status element".to_string(),
),
status_path,
))
}
Ok(_) => {}
Err(e) => {
return Err(DeserializeError::new(
DeserializeErrorKind::XmlError(e),
status_path,
))
}
}
buf.clear();
}
}
let description = if !description_text.trim().is_empty() {
Some(XmlString::new(description_text.trim()))
} else {
None
};
let data = StatusElementData {
message,
ty,
description,
stack_trace,
system_out,
system_err,
timestamp,
time,
};
Ok(match category {
StatusCategory::Main(kind) => StatusElement::Main(MainStatusElement { kind, data }),
StatusCategory::Flaky(kind) => StatusElement::Flaky(RerunStatusElement { kind, data }),
StatusCategory::Rerun(kind) => StatusElement::Rerun(RerunStatusElement { kind, data }),
})
}
fn build_test_case_status(
status_elements: Vec<StatusElement>,
path: &[PathElement],
) -> Result<TestCaseStatus, DeserializeError> {
let mut main_status: Option<&MainStatusElement> = None;
let mut flaky_runs = Vec::new();
let mut reruns = Vec::new();
for element in &status_elements {
match element {
StatusElement::Main(main) => {
if main_status.is_some() {
return Err(DeserializeError::new(
DeserializeErrorKind::InvalidStructure(
"multiple main status elements (failure/error/skipped) are not allowed"
.to_string(),
),
path.to_vec(),
));
}
main_status = Some(main);
}
StatusElement::Flaky(flaky) => {
flaky_runs.push(flaky);
}
StatusElement::Rerun(rerun) => {
reruns.push(rerun);
}
}
}
let main_with_kind = main_status.map(|m| (m, m.kind));
let has_flaky = !flaky_runs.is_empty();
let has_reruns = !reruns.is_empty();
match (main_with_kind, has_flaky, has_reruns) {
(None, false, false) => Ok(TestCaseStatus::success()),
(None, true, false) => {
let flaky_runs = flaky_runs.into_iter().map(build_test_rerun).collect();
Ok(TestCaseStatus::Success { flaky_runs })
}
(None, false, true) => Err(DeserializeError::new(
DeserializeErrorKind::InvalidStructure(
"found rerunFailure/rerunError elements without a corresponding \
failure or error element"
.to_string(),
),
path.to_vec(),
)),
(Some((_, MainStatusKind::Skipped)), true, _)
| (Some((_, MainStatusKind::Skipped)), _, true) => Err(DeserializeError::new(
DeserializeErrorKind::InvalidStructure(
"skipped test case cannot have flakyFailure, flakyError, \
rerunFailure, or rerunError elements"
.to_string(),
),
path.to_vec(),
)),
(Some((main, MainStatusKind::Skipped)), false, false) => Ok(TestCaseStatus::Skipped {
message: main.data.message.clone(),
ty: main.data.ty.clone(),
description: main.data.description.clone(),
}),
(Some((main, MainStatusKind::Failure | MainStatusKind::Error)), _, false)
| (Some((main, MainStatusKind::Failure | MainStatusKind::Error)), false, _) => {
let kind = main.kind.to_non_success_kind();
let (rerun_kind, runs) = if has_flaky {
(FlakyOrRerun::Flaky, flaky_runs)
} else {
(FlakyOrRerun::Rerun, reruns)
};
Ok(TestCaseStatus::NonSuccess {
kind,
message: main.data.message.clone(),
ty: main.data.ty.clone(),
description: main.data.description.clone(),
reruns: NonSuccessReruns {
kind: rerun_kind,
runs: runs.into_iter().map(build_test_rerun).collect(),
},
})
}
(_, true, true) => Err(DeserializeError::new(
DeserializeErrorKind::InvalidStructure(
"test case has both flakyFailure/flakyError and \
rerunFailure/rerunError elements, which is not supported"
.to_string(),
),
path.to_vec(),
)),
}
}
fn build_test_rerun(element: &RerunStatusElement) -> TestRerun {
let kind = match element.kind {
RerunStatusKind::Failure => NonSuccessKind::Failure,
RerunStatusKind::Error => NonSuccessKind::Error,
};
TestRerun {
kind,
timestamp: element.data.timestamp,
time: element.data.time,
message: element.data.message.clone(),
ty: element.data.ty.clone(),
stack_trace: element.data.stack_trace.clone(),
system_out: element.data.system_out.clone(),
system_err: element.data.system_err.clone(),
description: element.data.description.clone(),
}
}
fn is_status_element(name: &[u8]) -> bool {
matches!(
name,
b"failure"
| b"error"
| b"skipped"
| b"flakyFailure"
| b"flakyError"
| b"rerunFailure"
| b"rerunError"
)
}
fn deserialize_properties<R: BufRead>(
reader: &mut Reader<R>,
path: &[PathElement],
) -> Result<Vec<Property>, DeserializeError> {
let mut properties = Vec::new();
let mut buf = Vec::new();
let mut prop_path = path.to_vec();
prop_path.push(PathElement::Properties);
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(e)) if e.name().as_ref() == b"property" => {
let mut elem_path = prop_path.clone();
elem_path.push(PathElement::Property(properties.len()));
let property = deserialize_property(&e, &elem_path)?;
properties.push(property);
}
Ok(Event::End(e)) if e.name().as_ref() == b"properties" => break,
Ok(Event::Eof) => {
return Err(DeserializeError::new(
DeserializeErrorKind::InvalidStructure(
"unexpected EOF in <properties>".to_string(),
),
prop_path,
))
}
Ok(_) => {}
Err(e) => {
return Err(DeserializeError::new(
DeserializeErrorKind::XmlError(e),
prop_path,
))
}
}
buf.clear();
}
Ok(properties)
}
fn deserialize_property(
element: &BytesStart<'_>,
path: &[PathElement],
) -> Result<Property, DeserializeError> {
let mut name = None;
let mut value = None;
for attr in element.attributes() {
let attr = attr.map_err(|e| {
DeserializeError::new(DeserializeErrorKind::AttrError(e), path.to_vec())
})?;
let mut attr_path = path.to_vec();
match attr.key.as_ref() {
b"name" => {
attr_path.push(PathElement::Attribute("name".to_string()));
name = Some(parse_xml_string(&attr.value, &attr_path)?);
}
b"value" => {
attr_path.push(PathElement::Attribute("value".to_string()));
value = Some(parse_xml_string(&attr.value, &attr_path)?);
}
_ => {} }
}
let name = name.ok_or_else(|| {
let mut attr_path = path.to_vec();
attr_path.push(PathElement::Attribute("name".to_string()));
DeserializeError::new(
DeserializeErrorKind::MissingAttribute("name".to_string()),
attr_path,
)
})?;
let value = value.ok_or_else(|| {
let mut attr_path = path.to_vec();
attr_path.push(PathElement::Attribute("value".to_string()));
DeserializeError::new(
DeserializeErrorKind::MissingAttribute("value".to_string()),
attr_path,
)
})?;
Ok(Property { name, value })
}
fn read_text_content<R: BufRead>(
reader: &mut Reader<R>,
element_name: &[u8],
path: &[PathElement],
) -> Result<XmlString, DeserializeError> {
let mut text = String::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Text(e)) => {
let s = std::str::from_utf8(e.as_ref()).map_err(|e| {
DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec())
})?;
let unescaped = unescape_with(s, resolve_xml_entity).map_err(|e| {
DeserializeError::new(DeserializeErrorKind::EscapeError(e), path.to_vec())
})?;
text.push_str(&unescaped);
}
Ok(Event::CData(e)) => {
let s = std::str::from_utf8(e.as_ref()).map_err(|e| {
DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec())
})?;
text.push_str(s);
}
Ok(Event::GeneralRef(e)) => {
let entity_name = std::str::from_utf8(e.as_ref()).map_err(|e| {
DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec())
})?;
let unescaped = resolve_xml_entity(entity_name).ok_or_else(|| {
DeserializeError::new(
DeserializeErrorKind::InvalidStructure(format!(
"unrecognized entity: {entity_name}",
)),
path.to_vec(),
)
})?;
text.push_str(unescaped);
}
Ok(Event::End(e)) if e.name().as_ref() == element_name => break,
Ok(Event::Eof) => {
return Err(DeserializeError::new(
DeserializeErrorKind::InvalidStructure(format!(
"unexpected EOF in <{}>",
String::from_utf8_lossy(element_name)
)),
path.to_vec(),
))
}
Ok(_) => {}
Err(e) => {
return Err(DeserializeError::new(
DeserializeErrorKind::XmlError(e),
path.to_vec(),
))
}
}
buf.clear();
}
Ok(XmlString::new(text.trim()))
}
fn require_attribute<T>(
value: Option<T>,
attr_name: &str,
path: &[PathElement],
) -> Result<T, DeserializeError> {
value.ok_or_else(|| {
let mut attr_path = path.to_vec();
attr_path.push(PathElement::Attribute(attr_name.to_string()));
DeserializeError::new(
DeserializeErrorKind::MissingAttribute(attr_name.to_string()),
attr_path,
)
})
}
fn parse_xml_string(bytes: &[u8], path: &[PathElement]) -> Result<XmlString, DeserializeError> {
let s = std::str::from_utf8(bytes)
.map_err(|e| DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec()))?;
let unescaped = unescape_with(s, resolve_xml_entity)
.map_err(|e| DeserializeError::new(DeserializeErrorKind::EscapeError(e), path.to_vec()))?;
Ok(XmlString::new(unescaped.as_ref()))
}
fn parse_usize(bytes: &[u8], path: &[PathElement]) -> Result<usize, DeserializeError> {
let s = std::str::from_utf8(bytes)
.map_err(|e| DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec()))?;
s.parse()
.map_err(|e| DeserializeError::new(DeserializeErrorKind::ParseIntError(e), path.to_vec()))
}
fn parse_duration(bytes: &[u8], path: &[PathElement]) -> Result<Duration, DeserializeError> {
let s = std::str::from_utf8(bytes)
.map_err(|e| DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec()))?;
let seconds: f64 = s.parse().map_err(|_| {
DeserializeError::new(
DeserializeErrorKind::ParseDurationError(s.to_string()),
path.to_vec(),
)
})?;
Duration::try_from_secs_f64(seconds).map_err(|_| {
DeserializeError::new(
DeserializeErrorKind::ParseDurationError(s.to_string()),
path.to_vec(),
)
})
}
fn parse_timestamp(
bytes: &[u8],
path: &[PathElement],
) -> Result<DateTime<FixedOffset>, DeserializeError> {
let s = std::str::from_utf8(bytes)
.map_err(|e| DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec()))?;
DateTime::parse_from_rfc3339(s).map_err(|_| {
DeserializeError::new(
DeserializeErrorKind::ParseTimestampError(s.to_string()),
path.to_vec(),
)
})
}
fn parse_uuid(bytes: &[u8], path: &[PathElement]) -> Result<ReportUuid, DeserializeError> {
let s = std::str::from_utf8(bytes)
.map_err(|e| DeserializeError::new(DeserializeErrorKind::Utf8Error(e), path.to_vec()))?;
let uuid = s.parse().map_err(|e| {
DeserializeError::new(DeserializeErrorKind::ParseUuidError(e), path.to_vec())
})?;
Ok(ReportUuid::from_untyped_uuid(uuid))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_report() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="my-test-run" tests="1" failures="0" errors="0">
<testsuite name="my-test-suite" tests="1" disabled="0" errors="0" failures="0">
<testcase name="success-case"/>
</testsuite>
</testsuites>
"#;
let report = Report::deserialize_from_str(xml).unwrap();
assert_eq!(report.name.as_str(), "my-test-run");
assert_eq!(report.tests, 1);
assert_eq!(report.failures, 0);
assert_eq!(report.errors, 0);
assert_eq!(report.test_suites.len(), 1);
let suite = &report.test_suites[0];
assert_eq!(suite.name.as_str(), "my-test-suite");
assert_eq!(suite.test_cases.len(), 1);
let case = &suite.test_cases[0];
assert_eq!(case.name.as_str(), "success-case");
assert!(matches!(case.status, TestCaseStatus::Success { .. }));
}
#[test]
fn test_parse_report_with_failure() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="test-run" tests="1" failures="1" errors="0">
<testsuite name="suite" tests="1" disabled="0" errors="0" failures="1">
<testcase name="failing-test">
<failure message="assertion failed">Expected true but got false</failure>
</testcase>
</testsuite>
</testsuites>
"#;
let report = Report::deserialize_from_str(xml).unwrap();
let case = &report.test_suites[0].test_cases[0];
match &case.status {
TestCaseStatus::NonSuccess {
kind,
message,
description,
..
} => {
assert_eq!(*kind, NonSuccessKind::Failure);
assert_eq!(message.as_ref().unwrap().as_str(), "assertion failed");
assert_eq!(
description.as_ref().unwrap().as_str(),
"Expected true but got false"
);
}
_ => panic!("Expected NonSuccess status"),
}
}
#[test]
fn test_parse_report_with_properties() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="test-run" tests="1" failures="0" errors="0">
<testsuite name="suite" tests="1" disabled="0" errors="0" failures="0">
<properties>
<property name="env" value="test"/>
<property name="platform" value="linux"/>
</properties>
<testcase name="test"/>
</testsuite>
</testsuites>
"#;
let report = Report::deserialize_from_str(xml).unwrap();
let suite = &report.test_suites[0];
assert_eq!(suite.properties.len(), 2);
assert_eq!(suite.properties[0].name.as_str(), "env");
assert_eq!(suite.properties[0].value.as_str(), "test");
assert_eq!(suite.properties[1].name.as_str(), "platform");
assert_eq!(suite.properties[1].value.as_str(), "linux");
}
}