use std::collections::HashMap;
use std::path::Path;
use anyhow::Result;
use quick_xml::events::Event;
use super::{get_attr, CoverageParser, Format};
use crate::model::*;
pub struct JacocoParser;
impl CoverageParser for JacocoParser {
fn format(&self) -> Format {
Format::Jacoco
}
fn can_parse(&self, _path: &Path, content: &[u8]) -> bool {
let head = super::sniff_head(content);
super::looks_like_xml(&head)
&& head.contains("<report")
&& (head.contains("jacoco") || head.contains("JACOCO") || head.contains("<package"))
}
fn parse(&self, input: &[u8]) -> Result<CoverageData> {
parse(input)
}
}
pub fn parse(input: &[u8]) -> Result<CoverageData> {
let mut reader = super::xml_reader(input);
let mut data = CoverageData::new();
let mut buf = Vec::new();
let mut current_package: Option<String> = None;
let mut current_sourcefile: Option<FileCoverage> = None;
let mut branch_indices: HashMap<u32, u32> = HashMap::new();
let mut class_methods: HashMap<(String, String), Vec<FunctionCoverage>> = HashMap::new();
let mut current_class_source: Option<String> = None;
let mut in_method = false;
let mut current_method_name: Option<String> = None;
let mut current_method_line: Option<u32> = None;
let mut method_hit: bool = false;
loop {
let event = reader.read_event_into(&mut buf);
let is_start_event = matches!(&event, Ok(Event::Start(_)));
match event {
Err(e) => return Err(super::xml_err(e, &reader)),
Ok(Event::Eof) => break,
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
match e.name().as_ref() {
b"package" => {
current_package = get_attr(e, b"name");
}
b"class" if is_start_event => {
current_class_source = get_attr(e, b"sourcefilename");
}
b"method" => {
in_method = true;
current_method_name = get_attr(e, b"name");
current_method_line =
get_attr(e, b"line").and_then(|v| v.parse::<u32>().ok());
method_hit = false;
}
b"counter" if in_method => {
if let Some(counter_type) = get_attr(e, b"type") {
if counter_type == "METHOD" {
let covered: u64 = get_attr(e, b"covered")
.and_then(|v| v.parse().ok())
.unwrap_or(0);
if covered > 0 {
method_hit = true;
}
}
}
}
b"sourcefile" => {
if let Some(name) = get_attr(e, b"name") {
let path = match ¤t_package {
Some(pkg) => format!("{}/{}", pkg, name),
None => name.clone(),
};
current_sourcefile = Some(FileCoverage::new(path));
branch_indices.clear();
if let Some(pkg) = ¤t_package {
let key = (pkg.clone(), name);
if let Some(methods) = class_methods.remove(&key) {
current_sourcefile.as_mut().unwrap().functions = methods;
}
}
}
}
b"line" => {
if let Some(file) = current_sourcefile.as_mut() {
let mut nr: Option<u32> = None;
let mut ci: u64 = 0;
let mut mi: u64 = 0;
let mut cb: u32 = 0;
let mut mb: u32 = 0;
for attr in e.attributes().flatten() {
match attr.key.as_ref() {
b"nr" => {
nr =
attr.unescape_value().ok().and_then(|v| v.parse().ok());
}
b"ci" => {
ci = attr
.unescape_value()
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
}
b"mi" => {
mi = attr
.unescape_value()
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
}
b"cb" => {
cb = attr
.unescape_value()
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
}
b"mb" => {
mb = attr
.unescape_value()
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
}
_ => {}
}
}
if let Some(line_number) = nr {
if ci > 0 || mi > 0 {
file.lines.push(LineCoverage {
line_number,
hit_count: ci,
});
}
let total_branches = cb + mb;
if total_branches > 0 {
let idx = branch_indices.entry(line_number).or_insert(0);
for i in 0..total_branches {
let branch_hit: u64 = if i < cb { 1 } else { 0 };
file.branches.push(BranchCoverage {
line_number,
branch_index: *idx,
hit_count: branch_hit,
});
*idx += 1;
}
}
}
}
}
_ => {}
}
}
Ok(Event::End(ref e)) => match e.name().as_ref() {
b"package" => {
current_package = None;
}
b"class" => {
current_class_source = None;
}
b"method" => {
if in_method {
if let (Some(pkg), Some(src), Some(name)) = (
¤t_package,
¤t_class_source,
current_method_name.take(),
) {
let key = (pkg.clone(), src.clone());
class_methods
.entry(key)
.or_default()
.push(FunctionCoverage {
name,
start_line: current_method_line,
end_line: None,
hit_count: if method_hit { 1 } else { 0 },
});
}
in_method = false;
current_method_name = None;
current_method_line = None;
}
}
b"sourcefile" => {
if let Some(file) = current_sourcefile.take() {
data.files.push(file);
}
}
_ => {}
},
_ => {}
}
buf.clear();
}
if let Some(file) = current_sourcefile.take() {
data.files.push(file);
}
for file in &mut data.files {
file.lines.sort_by_key(|l| l.line_number);
}
Ok(data)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_jacoco() {
let input = include_bytes!("../../tests/fixtures/sample_jacoco.xml");
let data = parse(input).unwrap();
assert_eq!(data.files.len(), 2);
let foo = &data.files[0];
assert_eq!(foo.path, "com/example/Foo.java");
assert_eq!(foo.lines.len(), 5);
assert_eq!(foo.lines[0].line_number, 3);
assert_eq!(foo.lines[0].hit_count, 3); assert_eq!(foo.lines[1].line_number, 10);
assert_eq!(foo.lines[1].hit_count, 5); assert_eq!(foo.lines[2].line_number, 11);
assert_eq!(foo.lines[2].hit_count, 5); assert_eq!(foo.lines[3].line_number, 12);
assert_eq!(foo.lines[3].hit_count, 0); assert_eq!(foo.lines[4].line_number, 15);
assert_eq!(foo.lines[4].hit_count, 3);
assert_eq!(foo.branches.len(), 2);
assert_eq!(foo.branches[0].line_number, 11);
assert_eq!(foo.branches[0].hit_count, 1); assert_eq!(foo.branches[1].line_number, 11);
assert_eq!(foo.branches[1].hit_count, 0);
assert_eq!(foo.functions.len(), 2);
assert_eq!(foo.functions[0].name, "<init>");
assert_eq!(foo.functions[0].start_line, Some(3));
assert_eq!(foo.functions[0].hit_count, 1);
assert_eq!(foo.functions[1].name, "doStuff");
assert_eq!(foo.functions[1].start_line, Some(10));
assert_eq!(foo.functions[1].hit_count, 1);
let bar = &data.files[1];
assert_eq!(bar.path, "com/example/Bar.java");
assert_eq!(bar.lines.len(), 2);
assert_eq!(bar.branches.len(), 0);
}
#[test]
fn test_parse_jacoco_no_package() {
let input = include_bytes!("../../tests/fixtures/jacoco_no_package.xml");
let data = parse(input).unwrap();
assert_eq!(data.files.len(), 1);
assert_eq!(data.files[0].path, "App.java");
assert_eq!(data.files[0].lines.len(), 2);
}
#[test]
fn test_parse_jacoco_empty() {
let input = include_bytes!("../../tests/fixtures/empty_jacoco.xml");
let data = parse(input).unwrap();
assert_eq!(data.files.len(), 0);
}
#[test]
fn test_parse_jacoco_malformed() {
let input = include_bytes!("../../tests/fixtures/malformed_jacoco.xml");
let result = parse(input);
assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("position"),
"Error should contain position info: {err_msg}",
);
}
#[test]
fn test_can_parse_jacoco() {
let parser = JacocoParser;
let content = br#"<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE report PUBLIC "-//JACOCO//DTD Report 1.1//EN" "report.dtd"><report name="test">"#;
assert!(parser.can_parse(Path::new("jacoco.xml"), content));
let content = br#"<?xml version="1.0"?><report name="test"><package name="com/example">"#;
assert!(parser.can_parse(Path::new("report.xml"), content));
let content = br#"<?xml version="1.0"?><coverage version="1.0">"#;
assert!(!parser.can_parse(Path::new("coverage.xml"), content));
}
}