Skip to content

Commit 101d758

Browse files
Improve JaCoCo parsing and improve Kotlin support (#1326)
The jacoco parser made some relatively safe assumptions about the jacoco report, except when jacoco is used to instrument kotlin. Kotlin doesn't have the same restrictions as java when it comes to top level classes, so the report parsing will just immediately error out. This improves the parser by making it sourcefilename centric instead of class centric (falling back on the pattern of top level class + .java == sourcefilename if the sourcefilename is missing). In addition, when aggregated across multiple projects, the report commonly doesn't have the full relative paths, so we need to be able to find both java and kotlin files. There is also an assumption that a fully qualified class can only exist in one location and it would panic; however, this isn't entirely uncommon, so don't panic, but instead, inform the user that its not fully supported. Fixes #1279 Co-authored-by: Jason Gilanfarr <[email protected]>
1 parent 0c6939b commit 101d758

File tree

5 files changed

+295
-45
lines changed

5 files changed

+295
-45
lines changed

src/parser.rs

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -742,7 +742,6 @@ fn parse_jacoco_report_package<T: BufRead>(
742742
Ok(Event::Start(ref e)) => {
743743
match e.local_name().into_inner() {
744744
b"class" => {
745-
// Fully qualified class name: "org/example/Person$Age"
746745
let fq_class = get_xml_attribute(parser, e, "name")?;
747746
// Class name: "Person$Age"
748747
let class = fq_class
@@ -754,11 +753,16 @@ fn parse_jacoco_report_package<T: BufRead>(
754753
.split('$')
755754
.next()
756755
.expect("Failed to parse top class name");
756+
// Fully qualified class name: "org/example/Person$Age"
757+
// Generally, we will use the filename if its present,
758+
// but if it isn't, fallback to the top level class name
759+
let file = get_xml_attribute(parser, e, "sourcefilename")
760+
.unwrap_or(format!("{}.java", top_class));
757761

758762
// Process all <method /> and <counter /> for this class
759763
let functions = parse_jacoco_report_class(parser, buf, class)?;
760764

761-
match results_map.entry(top_class.to_string()) {
765+
match results_map.entry(file.to_string()) {
762766
hash_map::Entry::Occupied(obj) => {
763767
obj.into_mut().functions.extend(functions);
764768
}
@@ -772,12 +776,13 @@ fn parse_jacoco_report_package<T: BufRead>(
772776
};
773777
}
774778
b"sourcefile" => {
775-
let sourcefile = get_xml_attribute(parser, e, "name")?;
776-
let class = sourcefile.trim_end_matches(".java");
779+
// Fully qualified class name: "org/example/Person$Age"
780+
let file = get_xml_attribute(parser, e, "name")?;
781+
777782
let JacocoReport { lines, branches } =
778783
parse_jacoco_report_sourcefile(parser, buf)?;
779784

780-
match results_map.entry(class.to_string()) {
785+
match results_map.entry(file.to_string()) {
781786
hash_map::Entry::Occupied(obj) => {
782787
let obj = obj.into_mut();
783788
obj.lines = lines;
@@ -801,23 +806,14 @@ fn parse_jacoco_report_package<T: BufRead>(
801806
}
802807
}
803808

804-
for (class, result) in &results_map {
805-
if result.lines.is_empty() && result.branches.is_empty() {
806-
return Err(ParserError::InvalidData(format!(
807-
"Class {}/{} is not the top class in its file.",
808-
package, class
809-
)));
810-
}
811-
}
812-
813809
// Change all keys from the class name to the file name and turn the result into a Vec.
814810
// If package is the empty string, we have to trim the leading '/' in order to obtain a
815811
// relative path.
816812
Ok(results_map
817813
.into_iter()
818814
.map(|(class, result)| {
819815
(
820-
format!("{}/{}.java", package, class)
816+
format!("{}/{}", package, class)
821817
.trim_start_matches('/')
822818
.to_string(),
823819
result,
@@ -2089,20 +2085,76 @@ TN:http_3a_2f_2fweb_2dplatform_2etest_3a8000_2freferrer_2dpolicy_2fgen_2fsrcdoc_
20892085
}
20902086

20912087
#[test]
2092-
#[should_panic]
2093-
fn test_parser_jacoco_xml_non_top_level_classes_panics() {
2094-
let f = File::open("./test/jacoco/multiple-top-level-classes.xml")
2095-
.expect("Failed to open xml file");
2096-
let file = BufReader::new(&f);
2097-
let _results = parse_jacoco_xml_report(file).unwrap();
2098-
}
2088+
fn test_parser_jacoco_kotlin() {
2089+
let mut lines: BTreeMap<u32, u64> = BTreeMap::new();
2090+
for i in &[
2091+
(5, 0),
2092+
(9, 0),
2093+
(14, 0),
2094+
(27, 0),
2095+
(30, 0),
2096+
(32, 0),
2097+
(41, 0),
2098+
(49, 0),
2099+
(57, 0),
2100+
(65, 0),
2101+
(73, 0),
2102+
(81, 0),
2103+
(89, 0),
2104+
(97, 0),
2105+
(104, 0),
2106+
(105, 0),
2107+
(106, 0),
2108+
(107, 0),
2109+
(108, 0),
2110+
(109, 0),
2111+
(110, 0),
2112+
(111, 0),
2113+
(112, 0),
2114+
(118, 0),
2115+
(119, 0),
2116+
(120, 0),
2117+
] {
2118+
lines.insert(i.0, i.1);
2119+
}
20992120

2100-
#[test]
2101-
#[should_panic]
2102-
fn test_parser_jacoco_xml_full_report_with_non_top_level_classes_panics() {
2103-
let f = File::open("./test/jacoco/full-junit4-report-multiple-top-level-classes.xml")
2104-
.expect("Failed to open xml file");
2121+
let mut functions: FunctionMap = FxHashMap::default();
2122+
for (name, start, executed) in vec![
2123+
("Breakpoint#getEntries", 112, false),
2124+
("BreakpointValue#xxsmall", 49, false),
2125+
("Breakpoint#<clinit>", 104, false),
2126+
("BreakpointValue#xxlarge", 97, false),
2127+
("BreakpointValue#none", 41, false),
2128+
("BreakpointValue#xlarge", 89, false),
2129+
("BreakpointValue#setValue", 26, false),
2130+
("BreakpointValue#<init>", 5, false),
2131+
("BreakpointValue#getBreakpointValueMap", 14, false),
2132+
("BreakpointDirection#getEntries", 120, false),
2133+
("BreakpointValue#small", 65, false),
2134+
("BreakpointDirection#<clinit>", 118, false),
2135+
("BreakpointValue#xsmall", 57, false),
2136+
("BreakpointValue#large", 81, false),
2137+
("BreakpointValue#medium", 73, false),
2138+
] {
2139+
functions.insert(String::from(name), Function { start, executed });
2140+
}
2141+
2142+
let mut branches: BTreeMap<u32, Vec<bool>> = BTreeMap::new();
2143+
branches.insert(26, vec![false, false, false, false]);
2144+
2145+
let expected = vec![(
2146+
String::from("BreakpointValue.kt"),
2147+
CovResult {
2148+
lines,
2149+
branches,
2150+
functions,
2151+
},
2152+
)];
2153+
2154+
let f =
2155+
File::open("./test/jacoco/kotlin-jacoco-report.xml").expect("Failed to open xml file");
21052156
let file = BufReader::new(&f);
2106-
let _results = parse_jacoco_xml_report(file).unwrap();
2157+
let results = parse_jacoco_xml_report(file).unwrap();
2158+
assert_eq!(results, expected);
21072159
}
21082160
}

src/path_rewriting.rs

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use globset::{Glob, GlobSet, GlobSetBuilder};
2+
use log::error;
23
use rayon::prelude::*;
34
use rustc_hash::FxHashMap;
45
use serde_json::Value;
@@ -186,14 +187,14 @@ fn map_partial_path(file_to_paths: &FxHashMap<String, Vec<PathBuf>>, path: PathB
186187
let mut result: Option<&PathBuf> = None;
187188
for option in options {
188189
if option.ends_with(&path) {
189-
assert!(
190-
result.is_none(),
191-
"Only one file in the repository should end with {} ({} and {} both end with that)",
190+
if result.is_some() {
191+
error!("Only one file in the repository should end with {} ({} and {} both end with that).",
192192
path.display(),
193193
result.unwrap().display(),
194-
option.display()
195-
);
196-
result = Some(option)
194+
option.display());
195+
} else {
196+
result = Some(option)
197+
}
197198
}
198199
}
199200

@@ -226,7 +227,7 @@ fn to_globset(dirs: &[impl AsRef<str>]) -> GlobSet {
226227
glob_builder.build().unwrap()
227228
}
228229

229-
const PARTIAL_PATH_EXTENSION: &str = "java";
230+
const PARTIAL_PATH_EXTENSION: &[&str] = &["java", "kt"];
230231

231232
pub fn rewrite_paths(
232233
result_map: CovResultMap,
@@ -258,9 +259,11 @@ pub fn rewrite_paths(
258259
// The function's purpose is to figure out whether a covered path is inside a subdirectory.
259260

260261
// This only happens for Java, so if there are no Java files, we don't need to do it.
261-
let has_java = result_map
262-
.keys()
263-
.any(|path| check_extension(Path::new(&path), PARTIAL_PATH_EXTENSION));
262+
let has_java = result_map.keys().any(|path| {
263+
PARTIAL_PATH_EXTENSION
264+
.iter()
265+
.any(|&ext| check_extension(Path::new(&path), ext))
266+
});
264267

265268
// If all covered paths are direct childs of the source directory, then that function will
266269
// do nothing, and we don't have to do its pre-requisite data gathering.
@@ -296,7 +299,10 @@ pub fn rewrite_paths(
296299
panic!("Failed to open directory '{}'.", source_dir.display())
297300
});
298301

299-
if !check_extension(entry.path(), PARTIAL_PATH_EXTENSION) {
302+
if !PARTIAL_PATH_EXTENSION
303+
.iter()
304+
.any(|&ext| check_extension(entry.path(), ext))
305+
{
300306
continue;
301307
}
302308

@@ -336,12 +342,15 @@ pub fn rewrite_paths(
336342
let rel_path = remove_prefix(prefix_dir, rel_path);
337343

338344
// Try mapping a partial path to a full path.
339-
let rel_path =
340-
if map_partial_path_needed && check_extension(&rel_path, PARTIAL_PATH_EXTENSION) {
341-
map_partial_path(&file_to_paths, rel_path)
342-
} else {
343-
rel_path
344-
};
345+
let rel_path = if map_partial_path_needed
346+
&& PARTIAL_PATH_EXTENSION
347+
.iter()
348+
.any(|&ext| check_extension(&rel_path, ext))
349+
{
350+
map_partial_path(&file_to_paths, rel_path)
351+
} else {
352+
rel_path
353+
};
345354

346355
// Get absolute path to the source file.
347356
let (abs_path, rel_path) = get_abs_path(source_dir, rel_path)?;
@@ -1121,6 +1130,33 @@ mod tests {
11211130
assert_eq!(result, empty_result!());
11221131
}
11231132

1133+
#[cfg(unix)]
1134+
#[test]
1135+
fn test_rewrite_paths_rewrite_path_for_java_kotlin_and_rust() {
1136+
let mut result_map: CovResultMap = FxHashMap::default();
1137+
result_map.insert("kt/main.kt".to_string(), empty_result!());
1138+
result_map.insert("main.rs".to_string(), empty_result!());
1139+
1140+
let mut results = rewrite_paths(
1141+
result_map,
1142+
None,
1143+
Some(&canonicalize_path(".").unwrap()),
1144+
None,
1145+
true,
1146+
&[""; 0],
1147+
&[""; 0],
1148+
None,
1149+
Default::default(),
1150+
);
1151+
assert!(results.len() == 1);
1152+
1153+
let (abs_path, rel_path, result) = results.remove(0);
1154+
assert!(abs_path.is_absolute());
1155+
assert!(abs_path.ends_with("test/kotlin/main.kt"));
1156+
assert_eq!(rel_path, PathBuf::from("test/kotlin/main.kt"));
1157+
assert_eq!(result, empty_result!());
1158+
}
1159+
11241160
#[cfg(windows)]
11251161
#[test]
11261162
fn test_rewrite_paths_rewrite_path_for_java_and_rust() {

src/producer.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,12 @@ mod tests {
776776
"jacoco/full-junit4-report-multiple-top-level-classes.xml",
777777
false,
778778
),
779+
(
780+
ItemFormat::JacocoXml,
781+
false,
782+
"jacoco/kotlin-jacoco-report.xml",
783+
false,
784+
),
779785
(ItemFormat::Profraw, true, "default_1.profraw", false),
780786
(
781787
ItemFormat::Gcno,

0 commit comments

Comments
 (0)