From 57a624f37476287facf9ad34d380a82b9b3732db Mon Sep 17 00:00:00 2001 From: jasongilanfarr Date: Wed, 30 Apr 2025 11:46:16 -0700 Subject: [PATCH 1/2] Add parsing support for Scala's Scoverage. Parse scoverage's statement coverage into Line and Branch Coverage. --- src/defs.rs | 2 +- src/lib.rs | 4 +- src/parser.rs | 109 +++++++++++++++++++++++++++++++++++++----- src/path_rewriting.rs | 2 +- src/producer.rs | 47 +++++++++++------- test/scoverage.xml | 34 +++++++++++++ 6 files changed, 164 insertions(+), 34 deletions(-) create mode 100644 test/scoverage.xml diff --git a/src/defs.rs b/src/defs.rs index cf3414317..f1acc1f64 100644 --- a/src/defs.rs +++ b/src/defs.rs @@ -27,7 +27,7 @@ pub enum ItemFormat { Profraw, Profdata, Info, - JacocoXml, + Xml, } #[derive(Debug)] diff --git a/src/lib.rs b/src/lib.rs index d57081f97..68747258c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -309,13 +309,13 @@ pub fn consumer( continue; } } - ItemFormat::Info | ItemFormat::JacocoXml => { + ItemFormat::Info | ItemFormat::Xml => { if let ItemType::Content(content) = work_item.item { if work_item.format == ItemFormat::Info { try_parse!(parse_lcov(content, branch_enabled), work_item.name) } else { let buffer = BufReader::new(Cursor::new(content)); - try_parse!(parse_jacoco_xml_report(buffer), work_item.name) + try_parse!(parse_xml_report(buffer), work_item.name) } } else { error!("Invalid content type"); diff --git a/src/parser.rs b/src/parser.rs index 0707f63c8..17e47e40d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -621,7 +621,68 @@ fn get_xml_attribute( ))) } -fn parse_jacoco_report_sourcefile( +pub fn parse_scoverage_class( + reader: &mut Reader, + buf: &mut Vec, +) -> Result { + let mut lines: BTreeMap = BTreeMap::new(); + let mut branches: BTreeMap> = BTreeMap::new(); + + loop { + match reader.read_event_into(buf) { + Ok(Event::Start(ref event)) if event.local_name().into_inner() == b"statement" => { + let line = get_xml_attribute(reader, event, "line")?.parse::()?; + let branch = get_xml_attribute(reader, event, "branch")? == "true"; + let invoked = get_xml_attribute(reader, event, "invocation-count")?.parse::()?; + + lines.entry(line).and_modify(|v| *v = invoked).or_insert(invoked); + if branch { + branches.entry(line).and_modify(|v| { + v.push(invoked > 0); + }).or_insert(vec![invoked > 0]); + } + } + Ok(Event::End(ref e)) if e.local_name().into_inner() == b"class" => { + return Ok(CovResult { + lines, + branches, + functions: FxHashMap::default(), + }); + } + Err(e) => return Err(ParserError::Parse(e.to_string())), + _ => {} + } + } +} + +pub fn parse_scoverage_report_packages( + parser: &mut Reader, + buf: &mut Vec, +) -> Result, ParserError> { + let mut results_map: FxHashMap = FxHashMap::default(); + loop { + + match parser.read_event_into(buf) { + Ok(Event::Start(ref e)) if e.local_name().into_inner() == b"class" => { + let file = get_xml_attribute(parser, e, "filename")?.replace('.', "/").replace("/scala", ".scala"); + let results = parse_scoverage_class(parser, buf)?; + + results_map.entry(file.to_string()).and_modify(|v| { + v.lines.extend(results.lines.clone()); + v.branches.extend(results.branches.clone()); + }).or_insert(results); + } + Err(e) => return Err(ParserError::Parse(e.to_string())), + Ok(Event::End(ref e)) if e.local_name().into_inner() == b"scoverage" => { + return Ok(results_map.into_iter().collect()); + } + _ => {} + } + } +} + + +fn parse_report_sourcefile( parser: &mut Reader, buf: &mut Vec, ) -> Result { @@ -679,7 +740,7 @@ fn parse_jacoco_report_sourcefile( Ok(JacocoReport { lines, branches }) } -fn parse_jacoco_report_method( +fn parse_report_method( parser: &mut Reader, buf: &mut Vec, start: u32, @@ -703,7 +764,7 @@ fn parse_jacoco_report_method( Ok(Function { start, executed }) } -fn parse_jacoco_report_class( +fn parse_report_class( parser: &mut Reader, buf: &mut Vec, class_name: &str, @@ -717,7 +778,7 @@ fn parse_jacoco_report_class( let full_name = format!("{}#{}", class_name, name); let start_line = get_xml_attribute(parser, e, "line")?.parse::()?; - let function = parse_jacoco_report_method(parser, buf, start_line)?; + let function = parse_report_method(parser, buf, start_line)?; functions.insert(full_name, function); } Ok(Event::End(ref e)) if e.local_name().into_inner() == b"class" => break, @@ -730,7 +791,7 @@ fn parse_jacoco_report_class( Ok(functions) } -fn parse_jacoco_report_package( +fn parse_report_package( parser: &mut Reader, buf: &mut Vec, package: &str, @@ -760,7 +821,7 @@ fn parse_jacoco_report_package( .unwrap_or(format!("{}.java", top_class)); // Process all and for this class - let functions = parse_jacoco_report_class(parser, buf, class)?; + let functions = parse_report_class(parser, buf, class)?; match results_map.entry(file.to_string()) { hash_map::Entry::Occupied(obj) => { @@ -780,7 +841,7 @@ fn parse_jacoco_report_package( let file = get_xml_attribute(parser, e, "name")?; let JacocoReport { lines, branches } = - parse_jacoco_report_sourcefile(parser, buf)?; + parse_report_sourcefile(parser, buf)?; match results_map.entry(file.to_string()) { hash_map::Entry::Occupied(obj) => { @@ -822,7 +883,7 @@ fn parse_jacoco_report_package( .collect()) } -pub fn parse_jacoco_xml_report( +pub fn parse_xml_report( xml_reader: BufReader, ) -> Result, ParserError> { let mut parser = Reader::from_reader(xml_reader); @@ -835,10 +896,14 @@ pub fn parse_jacoco_xml_report( loop { match parser.read_event_into(&mut buf) { + Ok(Event::Start(ref e)) if e.local_name().into_inner() == b"scoverage" => { + let mut package_results = parse_scoverage_report_packages(&mut parser, &mut buf)?; + results.append(&mut package_results); + } Ok(Event::Start(ref e)) if e.local_name().into_inner() == b"package" => { let package = get_xml_attribute(&parser, e, "name")?; let mut package_results = - parse_jacoco_report_package(&mut parser, &mut buf, &package)?; + parse_report_package(&mut parser, &mut buf, &package)?; results.append(&mut package_results); } Ok(Event::Eof) => break, @@ -2032,7 +2097,7 @@ TN:http_3a_2f_2fweb_2dplatform_2etest_3a8000_2freferrer_2dpolicy_2fgen_2fsrcdoc_ let f = File::open("./test/jacoco/basic-report.xml").expect("Failed to open xml file"); let file = BufReader::new(&f); - let results = parse_jacoco_xml_report(file).unwrap(); + let results = parse_xml_report(file).unwrap(); assert_eq!(results, expected); } @@ -2079,7 +2144,7 @@ TN:http_3a_2f_2fweb_2dplatform_2etest_3a8000_2freferrer_2dpolicy_2fgen_2fsrcdoc_ let f = File::open("./test/jacoco/inner-classes.xml").expect("Failed to open xml file"); let file = BufReader::new(&f); - let results = parse_jacoco_xml_report(file).unwrap(); + let results = parse_xml_report(file).unwrap(); assert_eq!(results, expected); } @@ -2154,7 +2219,27 @@ TN:http_3a_2f_2fweb_2dplatform_2etest_3a8000_2freferrer_2dpolicy_2fgen_2fsrcdoc_ let f = File::open("./test/jacoco/kotlin-jacoco-report.xml").expect("Failed to open xml file"); let file = BufReader::new(&f); - let results = parse_jacoco_xml_report(file).unwrap(); + let results = parse_xml_report(file).unwrap(); + assert_eq!(results, expected); + } + + #[test] + fn parse_scoverage() { + let mut lines = BTreeMap::new(); + lines.insert(5, 1); + lines.insert(6, 1); + lines.insert(8, 0); + let mut branches = BTreeMap::new(); + branches.insert(6, vec![true]); + branches.insert(8, vec![false]); + + let expected = vec![ + (String::from("Example2.scala"), CovResult { lines: lines, branches: branches, functions: FxHashMap::default() }) + ]; + + let f = File::open("./test/scoverage.xml").expect("Failed to open scoverage coverage file"); + let file = BufReader::new(&f); + let results = parse_xml_report(file).unwrap(); assert_eq!(results, expected); } } diff --git a/src/path_rewriting.rs b/src/path_rewriting.rs index 0faa081cb..4c7f1c3c5 100644 --- a/src/path_rewriting.rs +++ b/src/path_rewriting.rs @@ -227,7 +227,7 @@ fn to_globset(dirs: &[impl AsRef]) -> GlobSet { glob_builder.build().unwrap() } -const PARTIAL_PATH_EXTENSION: &[&str] = &["java", "kt"]; +const PARTIAL_PATH_EXTENSION: &[&str] = &["java", "kt", "scala"]; pub fn rewrite_paths( result_map: CovResultMap, diff --git a/src/producer.rs b/src/producer.rs index bf7d9f5a5..ecd51a67c 100644 --- a/src/producer.rs +++ b/src/producer.rs @@ -95,7 +95,7 @@ impl Archive { } } "xml" => { - if Archive::check_file(file, &Archive::is_jacoco) { + if Archive::check_file(file, &Archive::is_supported_xml_format) { let filename = clean_path(path); self.insert_vec(filename, xmls); } @@ -119,11 +119,11 @@ impl Archive { && (&bytes[5..] == b"204" || &bytes[5..] == b"804") } - fn is_jacoco(reader: &mut dyn Read) -> bool { + fn is_supported_xml_format(reader: &mut dyn Read) -> bool { let mut bytes: [u8; 256] = [0; 256]; if reader.read_exact(&mut bytes).is_ok() { return match String::from_utf8(bytes.to_vec()) { - Ok(s) => s.contains("-//JACOCO//DTD"), + Ok(s) => s.contains("-//JACOCO//DTD") || s.contains(" false, }; } @@ -574,7 +574,7 @@ pub fn producer( ); file_content_producer(&infos.into_inner(), sender, ItemFormat::Info); - file_content_producer(&xmls.into_inner(), sender, ItemFormat::JacocoXml); + file_content_producer(&xmls.into_inner(), sender, ItemFormat::Xml); llvm_format_producer( tmp_dir, &profdatas.into_inner(), @@ -753,31 +753,31 @@ mod tests { (ItemFormat::Gcno, false, "llvm/file_branch", true), (ItemFormat::Gcno, false, "llvm/reader", true), ( - ItemFormat::JacocoXml, + ItemFormat::Xml, false, "jacoco/basic-jacoco.xml", false, ), ( - ItemFormat::JacocoXml, + ItemFormat::Xml, false, "jacoco/inner-classes.xml", false, ), ( - ItemFormat::JacocoXml, + ItemFormat::Xml, false, "jacoco/multiple-top-level-classes.xml", false, ), ( - ItemFormat::JacocoXml, + ItemFormat::Xml, false, "jacoco/full-junit4-report-multiple-top-level-classes.xml", false, ), ( - ItemFormat::JacocoXml, + ItemFormat::Xml, false, "jacoco/kotlin-jacoco-report.xml", false, @@ -789,6 +789,12 @@ mod tests { "mozillavpn_serverconnection_1.gcno", true, ), + ( + ItemFormat::Xml, + false, + "test/scoverage.xml", + false + ) ]; check_produced(tmp_path, &receiver, expected); @@ -1169,12 +1175,12 @@ mod tests { let expected = vec![ ( - ItemFormat::JacocoXml, + ItemFormat::Xml, false, "jacoco/basic-jacoco.xml", true, ), - (ItemFormat::JacocoXml, false, "inner-classes.xml", true), + (ItemFormat::Xml, false, "inner-classes.xml", true), ]; check_produced(tmp_path, &receiver, expected); @@ -1202,12 +1208,12 @@ mod tests { let expected = vec![ ( - ItemFormat::JacocoXml, + ItemFormat::Xml, false, "jacoco/basic-jacoco.xml", true, ), - (ItemFormat::JacocoXml, false, "inner-classes.xml", true), + (ItemFormat::Xml, false, "inner-classes.xml", true), (ItemFormat::Info, false, "1494603967-2977-2_0.info", true), (ItemFormat::Info, false, "1494603967-2977-3_0.info", true), (ItemFormat::Info, false, "1494603967-2977-4_0.info", true), @@ -1649,30 +1655,35 @@ mod tests { fn test_jacoco_files() { let mut file = File::open("./test/jacoco/basic-report.xml").ok(); assert!( - Archive::check_file(file.as_mut(), &Archive::is_jacoco), + Archive::check_file(file.as_mut(), &Archive::is_supported_xml_format), "A Jacoco XML file expected" ); let mut file = File::open("./test/jacoco/full-junit4-report-multiple-top-level-classes.xml").ok(); assert!( - Archive::check_file(file.as_mut(), &Archive::is_jacoco), + Archive::check_file(file.as_mut(), &Archive::is_supported_xml_format), "A Jacoco XML file expected" ); let mut file = File::open("./test/jacoco/inner-classes.xml").ok(); assert!( - Archive::check_file(file.as_mut(), &Archive::is_jacoco), + Archive::check_file(file.as_mut(), &Archive::is_supported_xml_format), "A Jacoco XML file expected" ); let mut file = File::open("./test/jacoco/multiple-top-level-classes.xml").ok(); assert!( - Archive::check_file(file.as_mut(), &Archive::is_jacoco), + Archive::check_file(file.as_mut(), &Archive::is_supported_xml_format), "A Jacoco XML file expected" ); let mut file = File::open("./test/jacoco/not_jacoco_file.xml").ok(); assert!( - !Archive::check_file(file.as_mut(), &Archive::is_jacoco), + !Archive::check_file(file.as_mut(), &Archive::is_supported_xml_format), "Not a Jacoco XML file expected" ); + let mut file = File::open("./test/scoverage.xml").ok(); + assert!( + Archive::check_file(file.as_mut(), &Archive::is_supported_xml_format), + "An Scoverage XML file expected" + ); } #[test] diff --git a/test/scoverage.xml b/test/scoverage.xml new file mode 100644 index 000000000..66ece8892 --- /dev/null +++ b/test/scoverage.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + From 70d2673946cade90686ae7b94ddf44af30b205cf Mon Sep 17 00:00:00 2001 From: jasongilanfarr Date: Wed, 30 Apr 2025 11:50:40 -0700 Subject: [PATCH 2/2] Make clippy happy --- src/parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser.rs b/src/parser.rs index 17e47e40d..816e8c68e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2234,7 +2234,7 @@ TN:http_3a_2f_2fweb_2dplatform_2etest_3a8000_2freferrer_2dpolicy_2fgen_2fsrcdoc_ branches.insert(8, vec![false]); let expected = vec![ - (String::from("Example2.scala"), CovResult { lines: lines, branches: branches, functions: FxHashMap::default() }) + (String::from("Example2.scala"), CovResult { lines, branches, functions: FxHashMap::default() }) ]; let f = File::open("./test/scoverage.xml").expect("Failed to open scoverage coverage file");