use anyhow::{Context, Result};
use quick_xml::events::{BytesStart, Event};
use quick_xml::reader::Reader;
use super::model::{CoverageReport, FileCoverage};
pub fn parse(content: &str) -> Result<CoverageReport> {
let mut reader = Reader::from_str(content);
reader.config_mut().check_end_names = false;
let mut report = CoverageReport::new();
let mut current: Option<FileCoverage> = None;
loop {
match reader.read_event().context("malformed cobertura XML")? {
Event::Eof => break,
Event::Start(e) | Event::Empty(e) => {
handle_start(&e, &mut current, &mut report)?;
}
Event::End(e) if e.name().as_ref() == b"class" => {
if let Some(file) = current.take() {
report.insert(file);
}
}
_ => {}
}
}
if let Some(file) = current.take() {
report.insert(file);
}
Ok(report)
}
fn handle_start(
e: &BytesStart,
current: &mut Option<FileCoverage>,
report: &mut CoverageReport,
) -> Result<()> {
match e.name().as_ref() {
b"class" => {
if let Some(file) = current.take() {
report.insert(file);
}
if let Some(filename) = attr(e, b"filename")? {
*current = Some(FileCoverage::new(filename));
}
}
b"line" => {
if let Some(file) = current.as_mut() {
let number = attr(e, b"number")?
.and_then(|s| s.parse::<u32>().ok())
.context("cobertura <line> missing/invalid number attribute")?;
let hits = attr(e, b"hits")?
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);
file.record(number, hits);
}
}
_ => {}
}
Ok(())
}
fn attr(e: &BytesStart, key: &[u8]) -> Result<Option<String>> {
for attr in e.attributes() {
let attr = attr.context("malformed cobertura attribute")?;
if attr.key.as_ref() == key {
let value = attr
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.context("invalid cobertura attribute value")?;
return Ok(Some(value.into_owned()));
}
}
Ok(None)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn parses_classes_and_lines() {
let xml = r#"<?xml version="1.0" ?>
<coverage>
<packages>
<package name="omni-dev">
<classes>
<class name="a" filename="src/a.rs">
<lines>
<line number="1" hits="5"/>
<line number="2" hits="0"/>
</lines>
</class>
<class name="b" filename="src/b.rs">
<lines>
<line number="1" hits="1"/>
</lines>
</class>
</classes>
</package>
</packages>
</coverage>"#;
let report = parse(xml).unwrap();
assert_eq!(report.files.len(), 2);
assert_eq!(report.hits("src/a.rs", 1), Some(5));
assert_eq!(report.hits("src/a.rs", 2), Some(0));
assert_eq!(report.hits("src/b.rs", 1), Some(1));
}
#[test]
fn merges_split_file_classes() {
let xml = r#"<coverage><packages><package><classes>
<class filename="src/a.rs"><lines><line number="1" hits="0"/></lines></class>
<class filename="src/a.rs"><lines><line number="2" hits="3"/></lines></class>
</classes></package></packages></coverage>"#;
let report = parse(xml).unwrap();
assert_eq!(report.files.len(), 1);
assert_eq!(report.hits("src/a.rs", 1), Some(0));
assert_eq!(report.hits("src/a.rs", 2), Some(3));
}
#[test]
fn ignores_branch_conditions() {
let xml = r#"<coverage><packages><package><classes>
<class filename="src/a.rs"><lines>
<line number="1" hits="2" branch="true" condition-coverage="50% (1/2)">
<conditions><condition number="0" type="jump" coverage="50%"/></conditions>
</line>
</lines></class>
</classes></package></packages></coverage>"#;
let report = parse(xml).unwrap();
let f = &report.files["src/a.rs"];
assert_eq!(f.lines.len(), 1);
assert_eq!(f.lines.get(&1), Some(&2));
}
#[test]
fn tolerates_unclosed_class() {
let xml = r#"<coverage><class filename="a.rs"><lines><line number="1" hits="1"/></lines>"#;
let report = parse(xml).unwrap();
assert_eq!(report.hits("a.rs", 1), Some(1));
}
#[test]
fn new_class_closes_previous_unclosed_class() {
let xml = r#"<coverage>
<class filename="a.rs"><lines><line number="1" hits="1"/></lines>
<class filename="b.rs"><lines><line number="2" hits="0"/></lines></class>
</coverage>"#;
let report = parse(xml).unwrap();
assert_eq!(report.hits("a.rs", 1), Some(1));
assert_eq!(report.hits("b.rs", 2), Some(0));
}
#[test]
fn lines_outside_a_class_are_ignored() {
let xml = r#"<coverage><lines><line number="1" hits="1"/></lines></coverage>"#;
let report = parse(xml).unwrap();
assert!(report.files.is_empty());
}
#[test]
fn malformed_xml_errors() {
assert!(parse("<coverage><class").is_err());
}
}