pub mod cobertura;
pub mod jacoco;
pub mod lcov;
use std::path::Path;
use anyhow::Result;
use quick_xml::events::BytesStart;
use quick_xml::reader::Reader;
use crate::model::CoverageData;
pub trait CoverageParser {
fn format(&self) -> Format;
fn can_parse(&self, path: &Path, content: &[u8]) -> bool;
fn parse(&self, input: &[u8]) -> Result<CoverageData>;
}
pub(crate) fn sniff_head(content: &[u8]) -> std::borrow::Cow<'_, str> {
let n = content.len().min(4096);
String::from_utf8_lossy(&content[..n])
}
pub(crate) fn looks_like_xml(head: &str) -> bool {
head.contains("<?xml") || head.trim_start().starts_with('<')
}
pub(crate) fn get_attr(e: &BytesStart<'_>, name: &[u8]) -> Option<String> {
let attr = e.try_get_attribute(name).ok()??;
attr.unescape_value().ok().map(|v| v.into_owned())
}
pub(crate) fn xml_reader(input: &[u8]) -> Reader<&[u8]> {
let mut reader = Reader::from_reader(input);
reader.trim_text(true);
reader
}
pub(crate) fn xml_err(e: quick_xml::Error, reader: &Reader<&[u8]>) -> anyhow::Error {
let pos = reader.buffer_position();
anyhow::anyhow!("XML parse error at position {pos}: {e}")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
Cobertura,
Jacoco,
Lcov,
}
impl std::fmt::Display for Format {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Format::Cobertura => f.write_str("cobertura"),
Format::Jacoco => f.write_str("jacoco"),
Format::Lcov => f.write_str("lcov"),
}
}
}
impl std::str::FromStr for Format {
type Err = anyhow::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"cobertura" => Ok(Format::Cobertura),
"jacoco" => Ok(Format::Jacoco),
"lcov" => Ok(Format::Lcov),
_ => Err(anyhow::anyhow!(
"Unknown format: '{s}'. Supported: cobertura, jacoco, lcov"
)),
}
}
}
pub fn all() -> Vec<Box<dyn CoverageParser>> {
vec![
Box::new(lcov::LcovParser),
Box::new(jacoco::JacocoParser),
Box::new(cobertura::CoberturaParser),
]
}
pub fn detect(path: &Path, content: &[u8]) -> Option<Box<dyn CoverageParser>> {
all().into_iter().find(|p| p.can_parse(path, content))
}
pub fn for_format(format: Format) -> Box<dyn CoverageParser> {
match format {
Format::Cobertura => Box::new(cobertura::CoberturaParser),
Format::Jacoco => Box::new(jacoco::JacocoParser),
Format::Lcov => Box::new(lcov::LcovParser),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_lcov_by_extension() {
let parser = detect(Path::new("coverage.info"), b"").unwrap();
assert_eq!(parser.format(), Format::Lcov);
let parser = detect(Path::new("coverage.lcov"), b"").unwrap();
assert_eq!(parser.format(), Format::Lcov);
}
#[test]
fn test_detect_lcov_by_content() {
let content = b"TN:test\nSF:/src/lib.rs\nDA:1,5\nend_of_record\n";
let parser = detect(Path::new("coverage.txt"), content).unwrap();
assert_eq!(parser.format(), Format::Lcov);
}
#[test]
fn test_detect_jacoco_by_content() {
let content =
b"<?xml version=\"1.0\"?>\n<report name=\"test\"><package name=\"com/example\">";
let parser = detect(Path::new("jacoco.xml"), content).unwrap();
assert_eq!(parser.format(), Format::Jacoco);
}
#[test]
fn test_detect_jacoco_by_doctype() {
let content =
b"<?xml version=\"1.0\"?><!DOCTYPE report PUBLIC \"-//JACOCO//DTD Report 1.1//EN\" \"report.dtd\"><report name=\"test\">";
let parser = detect(Path::new("report.xml"), content).unwrap();
assert_eq!(parser.format(), Format::Jacoco);
}
#[test]
fn test_detect_cobertura_by_content() {
let content = b"<?xml version=\"1.0\"?>\n<coverage version=\"1.0\">";
let parser = detect(Path::new("coverage.xml"), content).unwrap();
assert_eq!(parser.format(), Format::Cobertura);
}
#[test]
fn test_detect_unknown() {
assert!(detect(Path::new("random.dat"), b"hello world").is_none());
}
}