From a1eaf9b6091ea8f60a63a13ad6bcf455ceefa758 Mon Sep 17 00:00:00 2001 From: Dorian Burihabwa Date: Fri, 12 Dec 2025 15:51:48 +0100 Subject: [PATCH 01/16] JACOCO-71 Add Integration tests with aggregate report This simple project that produces an aggregate report when running `mvn verify sonar:sonar` should correctly produce coverage information on 1 file covered by a test in another sub project. --- .../aggregate-maven-project/.gitignore | 1 + .../aggregate-maven-project/README.md | 13 +++++ .../library-test/pom.xml | 29 ++++++++++++ .../test/java/org/example/LibraryTest.java | 11 +++++ .../aggregate-maven-project/library/pom.xml | 14 ++++++ .../src/main/java/org/example/Library.java | 10 ++++ .../resources/aggregate-maven-project/pom.xml | 35 ++++++++++++++ .../aggregate-maven-project/report/pom.xml | 47 +++++++++++++++++++ 8 files changed, 160 insertions(+) create mode 100644 its/src/test/resources/aggregate-maven-project/.gitignore create mode 100644 its/src/test/resources/aggregate-maven-project/README.md create mode 100644 its/src/test/resources/aggregate-maven-project/library-test/pom.xml create mode 100644 its/src/test/resources/aggregate-maven-project/library-test/src/test/java/org/example/LibraryTest.java create mode 100644 its/src/test/resources/aggregate-maven-project/library/pom.xml create mode 100644 its/src/test/resources/aggregate-maven-project/library/src/main/java/org/example/Library.java create mode 100644 its/src/test/resources/aggregate-maven-project/pom.xml create mode 100644 its/src/test/resources/aggregate-maven-project/report/pom.xml diff --git a/its/src/test/resources/aggregate-maven-project/.gitignore b/its/src/test/resources/aggregate-maven-project/.gitignore new file mode 100644 index 00000000..94d9f410 --- /dev/null +++ b/its/src/test/resources/aggregate-maven-project/.gitignore @@ -0,0 +1 @@ +**/target/** \ No newline at end of file diff --git a/its/src/test/resources/aggregate-maven-project/README.md b/its/src/test/resources/aggregate-maven-project/README.md new file mode 100644 index 00000000..cca25a6a --- /dev/null +++ b/its/src/test/resources/aggregate-maven-project/README.md @@ -0,0 +1,13 @@ +# Aggregate Maven project + +A project with 3 modules: + +1. [library](./library) - containing code but no tests +2. [library.test](./library.test) - containing test code that uses code from `library` +3. [report](./report) - generating the aggregate coverage report + + +The report can be generated by running the following command: +```shell +mvn verify --file ./pom.xml +``` \ No newline at end of file diff --git a/its/src/test/resources/aggregate-maven-project/library-test/pom.xml b/its/src/test/resources/aggregate-maven-project/library-test/pom.xml new file mode 100644 index 00000000..2bc46136 --- /dev/null +++ b/its/src/test/resources/aggregate-maven-project/library-test/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + + org.example + aggregate-maven-project + 1.0-SNAPSHOT + + + library-test + jar + + + + org.example + library + ${project.version} + test + + + org.junit.jupiter + junit-jupiter-api + 6.0.1 + test + + + \ No newline at end of file diff --git a/its/src/test/resources/aggregate-maven-project/library-test/src/test/java/org/example/LibraryTest.java b/its/src/test/resources/aggregate-maven-project/library-test/src/test/java/org/example/LibraryTest.java new file mode 100644 index 00000000..ab16dee2 --- /dev/null +++ b/its/src/test/resources/aggregate-maven-project/library-test/src/test/java/org/example/LibraryTest.java @@ -0,0 +1,11 @@ +package org.example; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class LibraryTest { + @Test + void incompleteTest() { + Assertions.assertEquals(2, Library.div(2, 1)); + } +} \ No newline at end of file diff --git a/its/src/test/resources/aggregate-maven-project/library/pom.xml b/its/src/test/resources/aggregate-maven-project/library/pom.xml new file mode 100644 index 00000000..d8a76b41 --- /dev/null +++ b/its/src/test/resources/aggregate-maven-project/library/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + + + org.example + aggregate-maven-project + 1.0-SNAPSHOT + + + library + jar + \ No newline at end of file diff --git a/its/src/test/resources/aggregate-maven-project/library/src/main/java/org/example/Library.java b/its/src/test/resources/aggregate-maven-project/library/src/main/java/org/example/Library.java new file mode 100644 index 00000000..1ce8f0a5 --- /dev/null +++ b/its/src/test/resources/aggregate-maven-project/library/src/main/java/org/example/Library.java @@ -0,0 +1,10 @@ +package org.example; + +public class Library { + public static Integer div(int a, int b) { + if (b == 0) { + return null; + } + return a / b; + } +} \ No newline at end of file diff --git a/its/src/test/resources/aggregate-maven-project/pom.xml b/its/src/test/resources/aggregate-maven-project/pom.xml new file mode 100644 index 00000000..78c2b460 --- /dev/null +++ b/its/src/test/resources/aggregate-maven-project/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + org.example + aggregate-maven-project + 1.0-SNAPSHOT + + pom + + + library + library-test + report + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.14 + + + prepare-agent + + prepare-agent + + + + + + + \ No newline at end of file diff --git a/its/src/test/resources/aggregate-maven-project/report/pom.xml b/its/src/test/resources/aggregate-maven-project/report/pom.xml new file mode 100644 index 00000000..80ed935d --- /dev/null +++ b/its/src/test/resources/aggregate-maven-project/report/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + + org.example + aggregate-maven-project + 1.0-SNAPSHOT + + + report + jar + + + + org.example + library + ${project.version} + compile + + + org.example + library-test + ${project.version} + test + + + + + + + org.jacoco + jacoco-maven-plugin + + + report-aggregate + verify + + report-aggregate + + + + + + + \ No newline at end of file From 4bb01bfb144b68a7b065cc990615337857ff1599 Mon Sep 17 00:00:00 2001 From: Dorian Burihabwa Date: Mon, 15 Dec 2025 17:29:26 +0100 Subject: [PATCH 02/16] JACOCO-71 Change IT to use mixed aggregate and module based coverage Add a sub project isolated from the aggregate report coverage to see how both sensor can work on the same project. --- .../aggregate-maven-project/.gitignore | 1 - .../aggregate-maven-project/README.md | 13 ----- .../library-test/pom.xml | 29 ------------ .../test/java/org/example/LibraryTest.java | 11 ----- .../aggregate-maven-project/library/pom.xml | 14 ------ .../src/main/java/org/example/Library.java | 10 ---- .../resources/aggregate-maven-project/pom.xml | 35 -------------- .../aggregate-maven-project/report/pom.xml | 47 ------------------- 8 files changed, 160 deletions(-) delete mode 100644 its/src/test/resources/aggregate-maven-project/.gitignore delete mode 100644 its/src/test/resources/aggregate-maven-project/README.md delete mode 100644 its/src/test/resources/aggregate-maven-project/library-test/pom.xml delete mode 100644 its/src/test/resources/aggregate-maven-project/library-test/src/test/java/org/example/LibraryTest.java delete mode 100644 its/src/test/resources/aggregate-maven-project/library/pom.xml delete mode 100644 its/src/test/resources/aggregate-maven-project/library/src/main/java/org/example/Library.java delete mode 100644 its/src/test/resources/aggregate-maven-project/pom.xml delete mode 100644 its/src/test/resources/aggregate-maven-project/report/pom.xml diff --git a/its/src/test/resources/aggregate-maven-project/.gitignore b/its/src/test/resources/aggregate-maven-project/.gitignore deleted file mode 100644 index 94d9f410..00000000 --- a/its/src/test/resources/aggregate-maven-project/.gitignore +++ /dev/null @@ -1 +0,0 @@ -**/target/** \ No newline at end of file diff --git a/its/src/test/resources/aggregate-maven-project/README.md b/its/src/test/resources/aggregate-maven-project/README.md deleted file mode 100644 index cca25a6a..00000000 --- a/its/src/test/resources/aggregate-maven-project/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Aggregate Maven project - -A project with 3 modules: - -1. [library](./library) - containing code but no tests -2. [library.test](./library.test) - containing test code that uses code from `library` -3. [report](./report) - generating the aggregate coverage report - - -The report can be generated by running the following command: -```shell -mvn verify --file ./pom.xml -``` \ No newline at end of file diff --git a/its/src/test/resources/aggregate-maven-project/library-test/pom.xml b/its/src/test/resources/aggregate-maven-project/library-test/pom.xml deleted file mode 100644 index 2bc46136..00000000 --- a/its/src/test/resources/aggregate-maven-project/library-test/pom.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - 4.0.0 - - - org.example - aggregate-maven-project - 1.0-SNAPSHOT - - - library-test - jar - - - - org.example - library - ${project.version} - test - - - org.junit.jupiter - junit-jupiter-api - 6.0.1 - test - - - \ No newline at end of file diff --git a/its/src/test/resources/aggregate-maven-project/library-test/src/test/java/org/example/LibraryTest.java b/its/src/test/resources/aggregate-maven-project/library-test/src/test/java/org/example/LibraryTest.java deleted file mode 100644 index ab16dee2..00000000 --- a/its/src/test/resources/aggregate-maven-project/library-test/src/test/java/org/example/LibraryTest.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.example; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -class LibraryTest { - @Test - void incompleteTest() { - Assertions.assertEquals(2, Library.div(2, 1)); - } -} \ No newline at end of file diff --git a/its/src/test/resources/aggregate-maven-project/library/pom.xml b/its/src/test/resources/aggregate-maven-project/library/pom.xml deleted file mode 100644 index d8a76b41..00000000 --- a/its/src/test/resources/aggregate-maven-project/library/pom.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - 4.0.0 - - - org.example - aggregate-maven-project - 1.0-SNAPSHOT - - - library - jar - \ No newline at end of file diff --git a/its/src/test/resources/aggregate-maven-project/library/src/main/java/org/example/Library.java b/its/src/test/resources/aggregate-maven-project/library/src/main/java/org/example/Library.java deleted file mode 100644 index 1ce8f0a5..00000000 --- a/its/src/test/resources/aggregate-maven-project/library/src/main/java/org/example/Library.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.example; - -public class Library { - public static Integer div(int a, int b) { - if (b == 0) { - return null; - } - return a / b; - } -} \ No newline at end of file diff --git a/its/src/test/resources/aggregate-maven-project/pom.xml b/its/src/test/resources/aggregate-maven-project/pom.xml deleted file mode 100644 index 78c2b460..00000000 --- a/its/src/test/resources/aggregate-maven-project/pom.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - 4.0.0 - - org.example - aggregate-maven-project - 1.0-SNAPSHOT - - pom - - - library - library-test - report - - - - - - org.jacoco - jacoco-maven-plugin - 0.8.14 - - - prepare-agent - - prepare-agent - - - - - - - \ No newline at end of file diff --git a/its/src/test/resources/aggregate-maven-project/report/pom.xml b/its/src/test/resources/aggregate-maven-project/report/pom.xml deleted file mode 100644 index 80ed935d..00000000 --- a/its/src/test/resources/aggregate-maven-project/report/pom.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - 4.0.0 - - - org.example - aggregate-maven-project - 1.0-SNAPSHOT - - - report - jar - - - - org.example - library - ${project.version} - compile - - - org.example - library-test - ${project.version} - test - - - - - - - org.jacoco - jacoco-maven-plugin - - - report-aggregate - verify - - report-aggregate - - - - - - - \ No newline at end of file From 4a12a30b515baa9e863b89b86b4b045b0fdf8e82 Mon Sep 17 00:00:00 2001 From: Dorian Burihabwa Date: Wed, 17 Dec 2025 23:13:22 +0100 Subject: [PATCH 03/16] WIP JACOCO-71 Committing before cherry-picking group name collection --- .../sonar/plugins/jacoco/its/JacocoTest.java | 7 ++++++ .../README.md | 9 ++++---- .../library-clash/pom.xml | 23 +++++++++++++++++++ .../src/main/java/org/example/Library.java | 7 ++++++ .../test/java/org/example/LibraryTest.java | 12 ++++++++++ .../pom.xml | 1 + .../report/pom.xml | 6 +++++ .../org/sonar/plugins/jacoco/SensorUtils.java | 1 + 8 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 its/src/test/resources/aggregate-and-module-based-mixed-coverage/library-clash/pom.xml create mode 100644 its/src/test/resources/aggregate-and-module-based-mixed-coverage/library-clash/src/main/java/org/example/Library.java create mode 100644 its/src/test/resources/aggregate-and-module-based-mixed-coverage/library-clash/src/test/java/org/example/LibraryTest.java diff --git a/its/src/test/java/org/sonar/plugins/jacoco/its/JacocoTest.java b/its/src/test/java/org/sonar/plugins/jacoco/its/JacocoTest.java index a385c086..e68b9bdb 100644 --- a/its/src/test/java/org/sonar/plugins/jacoco/its/JacocoTest.java +++ b/its/src/test/java/org/sonar/plugins/jacoco/its/JacocoTest.java @@ -223,6 +223,13 @@ void aggregate_and_module_based_reports_complement_each_over_to_build_total_cove .containsEntry("uncovered_conditions", 0.0) .containsEntry("coverage", 100.0); + Map measuresForLibraryClash = getCoverageMeasures("org.example:aggregate-and-module-based-mixed-coverage:library-clash/src/main/java/org/example/Library.java"); + assertThat(measuresForLibraryClash) + .containsEntry("line_coverage", 100.0) + .containsEntry("lines_to_cover", 2.0) + .containsEntry("uncovered_lines", 0.0) + .containsEntry("coverage", 100.0); + Map measuresForSquarer = getCoverageMeasures("org.example:aggregate-and-module-based-mixed-coverage:self-covered/src/main/java/org/example/Squarer.java"); assertThat(measuresForSquarer) .containsEntry("line_coverage", 100.0) diff --git a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/README.md b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/README.md index fc0f365d..19157e09 100644 --- a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/README.md +++ b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/README.md @@ -1,11 +1,12 @@ # Aggregate Maven project -A project with 4 modules: +A project with 5 modules: 1. [library](./library) - containing code but no tests -2. [library.test](./library.test) - containing test code that uses code from `library` -3. [report](./report) - generating the aggregate coverage report -4. [self-covered](./self-covered) - containing code, tests and generating its own module-based coverage report +2. [library-clash](./library-clash) - containing a class whose name clashes with the one in `library` +3. [library.test](./library.test) - containing test code that uses code from `library` +4. [report](./report) - generating the aggregate coverage report +5. [self-covered](./self-covered) - containing code, tests and generating its own module-based coverage report The report can be generated by running the following command: diff --git a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/library-clash/pom.xml b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/library-clash/pom.xml new file mode 100644 index 00000000..afedfbf7 --- /dev/null +++ b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/library-clash/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + + org.example + aggregate-and-module-based-mixed-coverage + 1.0-SNAPSHOT + + + library-clash + jar + + + + org.junit.jupiter + junit-jupiter-api + 6.0.1 + test + + + \ No newline at end of file diff --git a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/library-clash/src/main/java/org/example/Library.java b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/library-clash/src/main/java/org/example/Library.java new file mode 100644 index 00000000..86d954fb --- /dev/null +++ b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/library-clash/src/main/java/org/example/Library.java @@ -0,0 +1,7 @@ +package org.example; + +public final class Library { + String greet(String name) { + return String.format("Hello, %s!", name); + } +} \ No newline at end of file diff --git a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/library-clash/src/test/java/org/example/LibraryTest.java b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/library-clash/src/test/java/org/example/LibraryTest.java new file mode 100644 index 00000000..dd0f00e2 --- /dev/null +++ b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/library-clash/src/test/java/org/example/LibraryTest.java @@ -0,0 +1,12 @@ +package org.example; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class LibraryTest { + @Test + void incompleteTest() { + Library library = new Library(); + Assertions.assertEquals("Hello, World!", library.greet("World")); + } +} \ No newline at end of file diff --git a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/pom.xml b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/pom.xml index fb78b2b9..1fed10f1 100644 --- a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/pom.xml +++ b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/pom.xml @@ -11,6 +11,7 @@ library + library-clash library-test report self-covered diff --git a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/report/pom.xml b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/report/pom.xml index 77833d03..653fa6ae 100644 --- a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/report/pom.xml +++ b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/report/pom.xml @@ -19,6 +19,12 @@ ${project.version} compile + + org.example + library-clash + ${project.version} + compile + org.example library-test diff --git a/src/main/java/org/sonar/plugins/jacoco/SensorUtils.java b/src/main/java/org/sonar/plugins/jacoco/SensorUtils.java index 32aceafe..c2c82308 100644 --- a/src/main/java/org/sonar/plugins/jacoco/SensorUtils.java +++ b/src/main/java/org/sonar/plugins/jacoco/SensorUtils.java @@ -32,6 +32,7 @@ static void importReport(XmlReportParser reportParser, FileLocator locator, Repo List sourceFiles = reportParser.parse(); for (XmlReportParser.SourceFile sourceFile : sourceFiles) { + // FIXME for the case of project sensor, we need the group InputFile inputFile = locator.getInputFile(sourceFile.packageName(), sourceFile.name()); if (inputFile == null) { continue; From a22adc8454ad7fe4a206c9b6e234af8958890533 Mon Sep 17 00:00:00 2001 From: Dorian Burihabwa Date: Sun, 14 Dec 2025 12:03:48 +0100 Subject: [PATCH 04/16] JACOCO-71 Collect group name when parsing report --- .../sonar/plugins/jacoco/XmlReportParser.java | 25 +++++++- .../plugins/jacoco/XmlReportParserTest.java | 21 +++++++ src/test/resources/jacoco-aggregate.xml | 61 +++++++++++++++++++ 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 src/test/resources/jacoco-aggregate.xml diff --git a/src/main/java/org/sonar/plugins/jacoco/XmlReportParser.java b/src/main/java/org/sonar/plugins/jacoco/XmlReportParser.java index 2518e59e..cbae7fff 100644 --- a/src/main/java/org/sonar/plugins/jacoco/XmlReportParser.java +++ b/src/main/java/org/sonar/plugins/jacoco/XmlReportParser.java @@ -28,6 +28,8 @@ import java.util.List; import java.util.Objects; import java.util.function.Supplier; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; @@ -55,6 +57,7 @@ public List parse() { List sourceFiles = new ArrayList<>(); + String groupName = null; String packageName = null; String sourceFileName = null; @@ -70,11 +73,15 @@ public List parse() { packageName = null; } else if (element.equals("sourcefile")) { sourceFileName = null; + } else if (element.equals("group")) { + groupName = null; } } else if (event == XMLStreamConstants.START_ELEMENT) { String element = parser.getLocalName(); - if (element.equals("package")) { + if (element.equals("group")) { + groupName = getStringAttr(parser, "name", () -> "for a 'group' at line" + parser.getLocation().getLineNumber() + COLUMN + parser.getLocation().getColumnNumber()); + } else if (element.equals("package")) { packageName = getStringAttr(parser, "name", () -> "for a 'package' at line " + parser.getLocation().getLineNumber() + COLUMN + parser.getLocation().getColumnNumber()); } else if (element.equals("sourcefile")) { if (packageName == null) { @@ -83,7 +90,7 @@ public List parse() { } sourceFileName = getStringAttr(parser, "name", () -> "for a sourcefile at line " + parser.getLocation().getLineNumber() + COLUMN + parser.getLocation().getColumnNumber()); - sourceFiles.add(new SourceFile(packageName, sourceFileName)); + sourceFiles.add(new SourceFile(packageName, sourceFileName, groupName)); } else if (element.equals("line")) { if (sourceFileName == null) { throw new IllegalStateException("Invalid report: expected to find 'line' within a 'sourcefile' at line " @@ -150,11 +157,17 @@ private static int getIntAttr(XMLStreamReader parser, String name, Supplier lines = new ArrayList<>(); SourceFile(String packageName, String name) { + this(packageName, name, null); + } + + SourceFile(String packageName, String name, @Nullable String groupName) { this.name = name; this.packageName = packageName; + this.groupName = groupName; } public String name() { @@ -165,6 +178,14 @@ public String packageName() { return packageName; } + @CheckForNull + /** + * Name of the group, aka (sub-)project, which is optional information grouping source files in aggregate reports. + */ + public String groupName() { + return groupName; + } + public List lines() { return lines; } diff --git a/src/test/java/org/sonar/plugins/jacoco/XmlReportParserTest.java b/src/test/java/org/sonar/plugins/jacoco/XmlReportParserTest.java index 7a5e59da..e2bd995f 100644 --- a/src/test/java/org/sonar/plugins/jacoco/XmlReportParserTest.java +++ b/src/test/java/org/sonar/plugins/jacoco/XmlReportParserTest.java @@ -185,4 +185,25 @@ void line_equality_checks_work_as_expected() { .isNotEqualTo(new XmlReportParser.Line(1, 2, 3, 42, 5)) .isNotEqualTo(new XmlReportParser.Line(1, 2, 3, 4, 42)); } + + @Test + void should_import_aggregate_report() throws URISyntaxException { + Path sample = load("jacoco-aggregate.xml"); + XmlReportParser parser = new XmlReportParser(sample); + + List sourceFiles = parser.parse(); + + assertThat(sourceFiles).hasSize(1); + var singleFile = sourceFiles.get(0); + assertThat(singleFile.packageName()).isEqualTo("org/example"); + assertThat(singleFile.name()).isEqualTo("Library.java"); + assertThat(singleFile.lines()).containsExactly( + new XmlReportParser.Line(3, 3, 0, 0, 0), + new XmlReportParser.Line(5, 0, 2, 1, 1), + new XmlReportParser.Line(6, 2, 0, 0, 0), + new XmlReportParser.Line(8, 0, 5, 0, 0) + ); + + assertThat(singleFile.groupName()).isEqualTo("library"); + } } diff --git a/src/test/resources/jacoco-aggregate.xml b/src/test/resources/jacoco-aggregate.xml new file mode 100644 index 00000000..d31ae50a --- /dev/null +++ b/src/test/resources/jacoco-aggregate.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 3f3c12dc0a19dd385bdba9d895f925de11122592 Mon Sep 17 00:00:00 2001 From: Dorian Burihabwa Date: Thu, 18 Dec 2025 01:04:49 +0100 Subject: [PATCH 05/16] JACOCO-71 Import coverage in a very hacky way --- .../org/sonar/plugins/jacoco/FileLocator.java | 18 +++++++++++++++++- .../org/sonar/plugins/jacoco/SensorUtils.java | 3 +-- .../sonar/plugins/jacoco/JacocoSensorTest.java | 2 +- .../sonar/plugins/jacoco/SensorUtilsTest.java | 2 +- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/sonar/plugins/jacoco/FileLocator.java b/src/main/java/org/sonar/plugins/jacoco/FileLocator.java index b9419908..02488179 100644 --- a/src/main/java/org/sonar/plugins/jacoco/FileLocator.java +++ b/src/main/java/org/sonar/plugins/jacoco/FileLocator.java @@ -23,6 +23,7 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; import javax.annotation.CheckForNull; +import javax.annotation.Nullable; import org.sonar.api.batch.fs.InputFile; public class FileLocator { @@ -42,8 +43,23 @@ public FileLocator(List inputFiles, KotlinFileLocator kotlinFileLocat } @CheckForNull + /* Visible for testing */ public InputFile getInputFile(String packagePath, String fileName) { - String filePath = packagePath.isEmpty() ? fileName : (packagePath + "/" + fileName); + return getInputFile(null, packagePath, fileName); + } + + @CheckForNull + public InputFile getInputFile(@Nullable String groupName, String packagePath, String fileName) { + String filePath = ""; + if (groupName != null) { + // FIXME This hacky source directory computation should be replaced with a call to the .sonar.sources + filePath = groupName + '/' + String.format("src/main/%s", fileName.substring(fileName.lastIndexOf('.') + 1)) + '/'; + } + if (!packagePath.isEmpty()) { + filePath += packagePath + '/'; + } + filePath += fileName; + String[] path = filePath.split("/"); InputFile fileWithSuffix = tree.getFileWithSuffix(path); if (fileWithSuffix == null && fileName.endsWith(".kt")) { diff --git a/src/main/java/org/sonar/plugins/jacoco/SensorUtils.java b/src/main/java/org/sonar/plugins/jacoco/SensorUtils.java index c2c82308..d3723b0a 100644 --- a/src/main/java/org/sonar/plugins/jacoco/SensorUtils.java +++ b/src/main/java/org/sonar/plugins/jacoco/SensorUtils.java @@ -32,8 +32,7 @@ static void importReport(XmlReportParser reportParser, FileLocator locator, Repo List sourceFiles = reportParser.parse(); for (XmlReportParser.SourceFile sourceFile : sourceFiles) { - // FIXME for the case of project sensor, we need the group - InputFile inputFile = locator.getInputFile(sourceFile.packageName(), sourceFile.name()); + InputFile inputFile = locator.getInputFile(sourceFile.groupName(), sourceFile.packageName(), sourceFile.name()); if (inputFile == null) { continue; } diff --git a/src/test/java/org/sonar/plugins/jacoco/JacocoSensorTest.java b/src/test/java/org/sonar/plugins/jacoco/JacocoSensorTest.java index 06e6485b..694461d8 100644 --- a/src/test/java/org/sonar/plugins/jacoco/JacocoSensorTest.java +++ b/src/test/java/org/sonar/plugins/jacoco/JacocoSensorTest.java @@ -108,7 +108,7 @@ void parse_failure_do_not_fail_analysis() { Path invalidFile = baseDir.resolve("invalid_ci_in_line.xml"); Path validFile = baseDir.resolve("jacoco.xml"); - when(locator.getInputFile("org/sonarlint/cli", "Stats.java")).thenReturn(inputFile); + when(locator.getInputFile(null, "org/sonarlint/cli", "Stats.java")).thenReturn(inputFile); sensor.importReports(Arrays.asList(invalidFile, validFile), locator, importer); diff --git a/src/test/java/org/sonar/plugins/jacoco/SensorUtilsTest.java b/src/test/java/org/sonar/plugins/jacoco/SensorUtilsTest.java index d08ea166..20a05481 100644 --- a/src/test/java/org/sonar/plugins/jacoco/SensorUtilsTest.java +++ b/src/test/java/org/sonar/plugins/jacoco/SensorUtilsTest.java @@ -41,7 +41,7 @@ void import_coverage() { sourceFile.lines().add(new XmlReportParser.Line(1, 0, 1, 0, 0)); when(parser.parse()).thenReturn(Collections.singletonList(sourceFile)); - when(locator.getInputFile("package", "File.java")).thenReturn(inputFile); + when(locator.getInputFile(null,"package", "File.java")).thenReturn(inputFile); SensorUtils.importReport(parser, locator, importer, null); From d90ca990930e659ce004434f825ae352651c7bb0 Mon Sep 17 00:00:00 2001 From: Dorian Burihabwa Date: Thu, 18 Dec 2025 10:16:20 +0100 Subject: [PATCH 06/16] JACOCO-71 Support resolution for name clashes between siblings --- .../org/sonar/plugins/jacoco/FileLocator.java | 19 ++--- .../sonar/plugins/jacoco/ReversePathTree.java | 24 ++++++ .../plugins/jacoco/ReversePathTreeTest.java | 79 +++++++++++++++++++ 3 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 src/test/java/org/sonar/plugins/jacoco/ReversePathTreeTest.java diff --git a/src/main/java/org/sonar/plugins/jacoco/FileLocator.java b/src/main/java/org/sonar/plugins/jacoco/FileLocator.java index 02488179..a0c3393d 100644 --- a/src/main/java/org/sonar/plugins/jacoco/FileLocator.java +++ b/src/main/java/org/sonar/plugins/jacoco/FileLocator.java @@ -50,18 +50,15 @@ public InputFile getInputFile(String packagePath, String fileName) { @CheckForNull public InputFile getInputFile(@Nullable String groupName, String packagePath, String fileName) { - String filePath = ""; - if (groupName != null) { - // FIXME This hacky source directory computation should be replaced with a call to the .sonar.sources - filePath = groupName + '/' + String.format("src/main/%s", fileName.substring(fileName.lastIndexOf('.') + 1)) + '/'; - } - if (!packagePath.isEmpty()) { - filePath += packagePath + '/'; - } - filePath += fileName; - + String filePath = packagePath.isEmpty() ? + fileName : + packagePath + '/' + fileName; String[] path = filePath.split("/"); - InputFile fileWithSuffix = tree.getFileWithSuffix(path); + + InputFile fileWithSuffix = groupName == null ? + tree.getFileWithSuffix(path) : + tree.getFileWithSuffix(groupName, path); + if (fileWithSuffix == null && fileName.endsWith(".kt")) { fileWithSuffix = kotlinFileLocator.getInputFile(packagePath, fileName); } diff --git a/src/main/java/org/sonar/plugins/jacoco/ReversePathTree.java b/src/main/java/org/sonar/plugins/jacoco/ReversePathTree.java index 7ee5720d..7f6b0b3f 100644 --- a/src/main/java/org/sonar/plugins/jacoco/ReversePathTree.java +++ b/src/main/java/org/sonar/plugins/jacoco/ReversePathTree.java @@ -46,6 +46,30 @@ public InputFile getFileWithSuffix(String[] path) { return getFirstLeaf(currentNode); } + public InputFile getFileWithSuffix(String module, String[] path) { + Node currentNode = root; + for (int i = path.length - 1; i >= 0; i--) { + currentNode = currentNode.children.get(path[i]); + if (currentNode == null) { + return null; + } + } + + while (!currentNode.children.isEmpty()) { + if (currentNode.children.size() == 1) { + currentNode = currentNode.children.values().iterator().next(); + } else { + for (String candidate : currentNode.children.keySet()) { + if (module.equals(candidate)) { + return currentNode.children.get(candidate).file; + } + } + return null; + } + } + return null; + } + private static InputFile getFirstLeaf(Node node) { while (!node.children.isEmpty()) { node = node.children.values().iterator().next(); diff --git a/src/test/java/org/sonar/plugins/jacoco/ReversePathTreeTest.java b/src/test/java/org/sonar/plugins/jacoco/ReversePathTreeTest.java new file mode 100644 index 00000000..b5e1f266 --- /dev/null +++ b/src/test/java/org/sonar/plugins/jacoco/ReversePathTreeTest.java @@ -0,0 +1,79 @@ +/* + * SonarQube JaCoCo Plugin + * Copyright (C) 2018-2026 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.jacoco; + +import org.junit.jupiter.api.Test; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.internal.TestInputFileBuilder; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class ReversePathTreeTest { + @Test + void module_aware_resolution_of_input_file_with_name_clashes_across_modules_works_as_expected() { + String module = "module"; + InputFile target = TestInputFileBuilder.create("", "module/src/main/java/org/example/App.java").build(); + String[] path = target.relativePath().split("/"); + + InputFile clashingFile = TestInputFileBuilder.create("", "module-clash/src/main/java/org/example/App.java").build(); + String[] clashingPath = clashingFile.relativePath().split("/"); + + var reverseParseTree = new ReversePathTree(); + reverseParseTree.index(clashingFile, clashingPath); + reverseParseTree.index(target, path); + + String[] pathWithoutSourceDirectory = new String[]{"org", "example", "App.java"}; + assertThat(reverseParseTree.getFileWithSuffix(module, pathWithoutSourceDirectory)).isEqualTo(target); + } + + @Test + void module_aware_resolution_of_input_file_with_an_empty_index_returns_null() { + var reverseParseTree = new ReversePathTree(); + String[] pathWithoutSourceDirectory = new String[]{"src","main", "java","org", "example", "App.java"}; + assertThat(reverseParseTree.getFileWithSuffix("my-module", pathWithoutSourceDirectory)).isNull(); + } + + @Test + void module_aware_resolution_of_input_file_with_missing_file_returns_null() { + var reverseParseTree = new ReversePathTree(); + InputFile differentFile = TestInputFileBuilder.create("", "module-clash/src/main/java/org/example/App.java").build(); + String[] differentFilePath = differentFile.relativePath().split("/"); + reverseParseTree.index(differentFile, differentFilePath); + + String[] pathWithoutSourceDirectory = new String[]{"src","main", "java","org", "example", "App.java"}; + assertThat(reverseParseTree.getFileWithSuffix("my-module", pathWithoutSourceDirectory)).isNull(); + } + + @Test + void module_aware_resolution_of_input_file_with_missing_file_despite_may_similar_files_returns_null() { + var reverseParseTree = new ReversePathTree(); + + InputFile differentFile = TestInputFileBuilder.create("", "module-clash/src/main/java/org/example/App.java").build(); + String[] differentFilePath = differentFile.relativePath().split("/"); + reverseParseTree.index(differentFile, differentFilePath); + + InputFile yetAnotherDifferentFile = TestInputFileBuilder.create("", "module-clash-again/src/main/java/org/example/App.java").build(); + String[] yeAnotherDifferentFilePath = yetAnotherDifferentFile.relativePath().split("/"); + reverseParseTree.index(yetAnotherDifferentFile, yeAnotherDifferentFilePath); + + String[] pathWithoutSourceDirectory = new String[]{"src","main", "java","org", "example", "App.java"}; + assertThat(reverseParseTree.getFileWithSuffix("my-module", pathWithoutSourceDirectory)).isNull(); + } +} \ No newline at end of file From 9608b4613312dd3ddfd4ff12af7ce013c323fc75 Mon Sep 17 00:00:00 2001 From: Dorian Burihabwa Date: Thu, 18 Dec 2025 15:58:45 +0100 Subject: [PATCH 07/16] WIP JACOCO-71 Add a level of nesting to test clashes behavior --- .../README.md | 10 ++++--- .../nested/library/pom.xml | 24 +++++++++++++++++ .../src/main/java/org/example/Library.java | 27 +++++++++++++++++++ .../test/java/org/example/LibraryTest.java | 22 +++++++++++++++ .../nested/pom.xml | 18 +++++++++++++ .../src/main/java/org/example/Library.java | 7 +++++ .../test/java/org/example/LibraryTest.java | 12 +++++++++ .../pom.xml | 1 + .../report/pom.xml | 6 +++++ 9 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/library/pom.xml create mode 100644 its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/library/src/main/java/org/example/Library.java create mode 100644 its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/library/src/test/java/org/example/LibraryTest.java create mode 100644 its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/pom.xml create mode 100644 its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/src/main/java/org/example/Library.java create mode 100644 its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/src/test/java/org/example/LibraryTest.java diff --git a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/README.md b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/README.md index 19157e09..1727351c 100644 --- a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/README.md +++ b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/README.md @@ -1,12 +1,14 @@ # Aggregate Maven project -A project with 5 modules: +A project with 7 modules with 100% coverage: 1. [library](./library) - containing code but no tests 2. [library-clash](./library-clash) - containing a class whose name clashes with the one in `library` -3. [library.test](./library.test) - containing test code that uses code from `library` -4. [report](./report) - generating the aggregate coverage report -5. [self-covered](./self-covered) - containing code, tests and generating its own module-based coverage report +3. [nested](./nested) - containing one subproject +4. [library-nested](./nested/library) - a nested subproject containing a class whose name clashes with the one in `library` +5. [library-test](./library.test) - containing test code that uses code from `library` +6. [report](./report) - generating the aggregate coverage report +7[self-covered](./self-covered) - containing code, tests and generating its own module-based coverage report The report can be generated by running the following command: diff --git a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/library/pom.xml b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/library/pom.xml new file mode 100644 index 00000000..fa0bd054 --- /dev/null +++ b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/library/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + + org.example + nested + 1.0-SNAPSHOT + + + library-nested + jar + + + + org.junit.jupiter + junit-jupiter-api + 6.0.1 + test + + + + \ No newline at end of file diff --git a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/library/src/main/java/org/example/Library.java b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/library/src/main/java/org/example/Library.java new file mode 100644 index 00000000..c2a237b0 --- /dev/null +++ b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/library/src/main/java/org/example/Library.java @@ -0,0 +1,27 @@ +package org.example; + +import java.util.Objects; + +/** + * This class contains code that has very little to do with the libraries + * It is also shaped in a way that applying coverage metrics from the other files should not work. + */ +public final class Library { + public final String name; + + public Library(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Library)) return false; + Library library = (Library) o; + return Objects.equals(name, library.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } +} \ No newline at end of file diff --git a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/library/src/test/java/org/example/LibraryTest.java b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/library/src/test/java/org/example/LibraryTest.java new file mode 100644 index 00000000..76280d4d --- /dev/null +++ b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/library/src/test/java/org/example/LibraryTest.java @@ -0,0 +1,22 @@ +package org.example; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class LibraryTest { + @Test + void test() { + Library library = new Library("City"); + Assertions.assertEquals("City", library.name); + + Library similarLibrary = new Library("City"); + Assertions.assertEquals(library, library); + Assertions.assertEquals(library, similarLibrary); + Assertions.assertEquals(library.hashCode(), library.hashCode()); + Assertions.assertEquals(library.hashCode(), similarLibrary.hashCode()); + + Assertions.assertFalse(library.equals(null)); + Assertions.assertFalse(library.equals(new Object())); + Assertions.assertNotEquals(new Library("Other"), library); + } +} diff --git a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/pom.xml b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/pom.xml new file mode 100644 index 00000000..742423a9 --- /dev/null +++ b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + + org.example + aggregate-and-module-based-mixed-coverage + 1.0-SNAPSHOT + + + nested + pom + + + library + + \ No newline at end of file diff --git a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/src/main/java/org/example/Library.java b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/src/main/java/org/example/Library.java new file mode 100644 index 00000000..86d954fb --- /dev/null +++ b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/src/main/java/org/example/Library.java @@ -0,0 +1,7 @@ +package org.example; + +public final class Library { + String greet(String name) { + return String.format("Hello, %s!", name); + } +} \ No newline at end of file diff --git a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/src/test/java/org/example/LibraryTest.java b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/src/test/java/org/example/LibraryTest.java new file mode 100644 index 00000000..dd0f00e2 --- /dev/null +++ b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/src/test/java/org/example/LibraryTest.java @@ -0,0 +1,12 @@ +package org.example; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class LibraryTest { + @Test + void incompleteTest() { + Library library = new Library(); + Assertions.assertEquals("Hello, World!", library.greet("World")); + } +} \ No newline at end of file diff --git a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/pom.xml b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/pom.xml index 1fed10f1..13ba7c9b 100644 --- a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/pom.xml +++ b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/pom.xml @@ -12,6 +12,7 @@ library library-clash + nested library-test report self-covered diff --git a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/report/pom.xml b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/report/pom.xml index 653fa6ae..0998fb69 100644 --- a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/report/pom.xml +++ b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/report/pom.xml @@ -25,6 +25,12 @@ ${project.version} compile + + org.example + library-nested + ${project.version} + compile + org.example library-test From ad6c55ac01dfe5a11500430efc95031dc0a43684 Mon Sep 17 00:00:00 2001 From: Dorian Burihabwa Date: Fri, 16 Jan 2026 12:34:33 +0100 Subject: [PATCH 08/16] JACOCO-74 WIP Managing clashes with nested modules --- .../org/sonar/plugins/jacoco/FileLocator.java | 35 +++++++- .../plugins/jacoco/JacocoAggregateSensor.java | 8 ++ .../sonar/plugins/jacoco/JacocoPlugin.java | 1 + .../sonar/plugins/jacoco/JacocoSensor.java | 13 +++ .../plugins/jacoco/ModuleCoverageContext.java | 88 +++++++++++++++++++ .../jacoco/ProjectCoverageContext.java | 49 +++++++++++ .../jacoco/JacocoAggregateSensorTest.java | 20 ++--- .../plugins/jacoco/JacocoPluginTest.java | 14 +-- .../plugins/jacoco/JacocoSensorTest.java | 31 ++++--- .../jacoco/ModuleCoverageContextTest.java | 82 +++++++++++++++++ .../jacoco/ProjectCoverageContextTest.java | 49 +++++++++++ 11 files changed, 361 insertions(+), 29 deletions(-) create mode 100644 src/main/java/org/sonar/plugins/jacoco/ModuleCoverageContext.java create mode 100644 src/main/java/org/sonar/plugins/jacoco/ProjectCoverageContext.java create mode 100644 src/test/java/org/sonar/plugins/jacoco/ModuleCoverageContextTest.java create mode 100644 src/test/java/org/sonar/plugins/jacoco/ProjectCoverageContextTest.java diff --git a/src/main/java/org/sonar/plugins/jacoco/FileLocator.java b/src/main/java/org/sonar/plugins/jacoco/FileLocator.java index a0c3393d..93c4428b 100644 --- a/src/main/java/org/sonar/plugins/jacoco/FileLocator.java +++ b/src/main/java/org/sonar/plugins/jacoco/FileLocator.java @@ -19,6 +19,9 @@ */ package org.sonar.plugins.jacoco; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -29,11 +32,16 @@ public class FileLocator { private final ReversePathTree tree = new ReversePathTree(); private final KotlinFileLocator kotlinFileLocator; + private ProjectCoverageContext projectCoverageContext; public FileLocator(Iterable inputFiles, KotlinFileLocator kotlinFileLocator) { this(StreamSupport.stream(inputFiles.spliterator(), false).collect(Collectors.toList()), kotlinFileLocator); } + void setProjectCoverageContext(ProjectCoverageContext projectCoverageContext) { + this.projectCoverageContext = projectCoverageContext; + } + public FileLocator(List inputFiles, KotlinFileLocator kotlinFileLocator) { this.kotlinFileLocator = kotlinFileLocator; for (InputFile inputFile : inputFiles) { @@ -55,9 +63,30 @@ public InputFile getInputFile(@Nullable String groupName, String packagePath, St packagePath + '/' + fileName; String[] path = filePath.split("/"); - InputFile fileWithSuffix = groupName == null ? - tree.getFileWithSuffix(path) : - tree.getFileWithSuffix(groupName, path); + InputFile fileWithSuffix = tree.getFileWithSuffix(path); + + if (groupName != null) { + fileWithSuffix = tree.getFileWithSuffix(groupName, path); + if (fileWithSuffix == null) { + var candidateGroups = projectCoverageContext.getModuleContexts() + .stream() + .filter(mcc -> groupName.equals(mcc.name)) + .collect(Collectors.toList()); + for (ModuleCoverageContext candidateGroup : candidateGroups) { + for (Path source : candidateGroup.sources) { + if (Files.isDirectory(source)) { + Path relativePath = projectCoverageContext.getProjectBaseDir().relativize(source.resolve(Paths.get(filePath))); + path = relativePath.toString().split("/"); + source.relativize(candidateGroup.baseDir); + fileWithSuffix = tree.getFileWithSuffix(path); + if (fileWithSuffix != null) { + return fileWithSuffix; + } + } + } + } + } + } if (fileWithSuffix == null && fileName.endsWith(".kt")) { fileWithSuffix = kotlinFileLocator.getInputFile(packagePath, fileName); diff --git a/src/main/java/org/sonar/plugins/jacoco/JacocoAggregateSensor.java b/src/main/java/org/sonar/plugins/jacoco/JacocoAggregateSensor.java index 1a767cc0..e9e2c275 100644 --- a/src/main/java/org/sonar/plugins/jacoco/JacocoAggregateSensor.java +++ b/src/main/java/org/sonar/plugins/jacoco/JacocoAggregateSensor.java @@ -21,6 +21,7 @@ import java.io.FileNotFoundException; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.sonar.api.batch.fs.InputFile; @@ -32,6 +33,11 @@ public class JacocoAggregateSensor implements ProjectSensor { private static final Logger LOG = Loggers.get(JacocoAggregateSensor.class); + private final ProjectCoverageContext projectCoverageContext; + + public JacocoAggregateSensor(ProjectCoverageContext projectCoverageContext) { + this.projectCoverageContext = projectCoverageContext; + } @Override public void describe(SensorDescriptor descriptor) { @@ -40,6 +46,7 @@ public void describe(SensorDescriptor descriptor) { @Override public void execute(SensorContext context) { + this.projectCoverageContext.setProjectBaseDir(Paths.get(context.config().get("sonar.projectBaseDir").get())); Path reportPath = null; try { reportPath = new ReportPathsProvider(context).getAggregateReportPath(); @@ -54,6 +61,7 @@ public void execute(SensorContext context) { Iterable inputFiles = context.fileSystem().inputFiles(context.fileSystem().predicates().all()); Stream kotlinInputFileStream = StreamSupport.stream(inputFiles.spliterator(), false).filter(f -> "kotlin".equals(f.language())); FileLocator locator = new FileLocator(inputFiles, new KotlinFileLocator(kotlinInputFileStream)); + locator.setProjectCoverageContext(projectCoverageContext); ReportImporter importer = new ReportImporter(context); LOG.info("Importing aggregate report {}.", reportPath); diff --git a/src/main/java/org/sonar/plugins/jacoco/JacocoPlugin.java b/src/main/java/org/sonar/plugins/jacoco/JacocoPlugin.java index 5ae1fec4..838d68a1 100644 --- a/src/main/java/org/sonar/plugins/jacoco/JacocoPlugin.java +++ b/src/main/java/org/sonar/plugins/jacoco/JacocoPlugin.java @@ -27,6 +27,7 @@ public class JacocoPlugin implements Plugin { @Override public void define(Context context) { + context.addExtension(ProjectCoverageContext.class); context.addExtension(JacocoSensor.class); context.addExtension(PropertyDefinition.builder(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY) .onQualifiers(Qualifiers.PROJECT) diff --git a/src/main/java/org/sonar/plugins/jacoco/JacocoSensor.java b/src/main/java/org/sonar/plugins/jacoco/JacocoSensor.java index cdd0b15b..741a7b41 100644 --- a/src/main/java/org/sonar/plugins/jacoco/JacocoSensor.java +++ b/src/main/java/org/sonar/plugins/jacoco/JacocoSensor.java @@ -33,6 +33,12 @@ public class JacocoSensor implements Sensor { private static final Logger LOG = Loggers.get(JacocoSensor.class); + private final ProjectCoverageContext projectCoverageContext; + + public JacocoSensor(ProjectCoverageContext projectCoverageContext) { + this.projectCoverageContext = projectCoverageContext; + } + @Override public void describe(SensorDescriptor descriptor) { descriptor.name("JaCoCo XML Report Importer"); @@ -40,6 +46,7 @@ public void describe(SensorDescriptor descriptor) { @Override public void execute(SensorContext context) { + recordModuleCoverageContext(context); Collection reportPaths = new ReportPathsProvider(context).getPaths(); if (reportPaths.isEmpty()) { LOG.info("No report imported, no coverage information will be imported by JaCoCo XML Report Importer"); @@ -65,4 +72,10 @@ void importReports(Collection reportPaths, FileLocator locator, ReportImpo } } } + + private void recordModuleCoverageContext(SensorContext sensorContext) { + var moduleCoverageContext = ModuleCoverageContext.from(sensorContext); + this.projectCoverageContext.add(moduleCoverageContext); + LOG.debug(String.format("Recorded module coverage context for aggregation: %s", moduleCoverageContext)); + } } diff --git a/src/main/java/org/sonar/plugins/jacoco/ModuleCoverageContext.java b/src/main/java/org/sonar/plugins/jacoco/ModuleCoverageContext.java new file mode 100644 index 00000000..789c61a3 --- /dev/null +++ b/src/main/java/org/sonar/plugins/jacoco/ModuleCoverageContext.java @@ -0,0 +1,88 @@ +/* + * SonarQube JaCoCo Plugin + * Copyright (C) 2018-2026 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.jacoco; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.config.Configuration; + +/** + * The context necessary to enable the aggregate sensor connect a SourceFile to a module. + */ +public class ModuleCoverageContext { + public final String name; + public final Path baseDir; + public final List sources; + + ModuleCoverageContext(String name, Path baseDir, List sourceDirectories) { + this.name = name; + this.baseDir = baseDir; + this.sources = sourceDirectories; + } + + static ModuleCoverageContext from(SensorContext sensorContext) { + Configuration config = sensorContext.config(); + String moduleKey = extractModuleKey(config); + Path baseDir = Path.of(config.get("sonar.projectBaseDir").get()); + List sourceDirectories = Arrays.asList(config.getStringArray("sonar.sources")) + .stream() + .map(Path::of) + .map(dir -> dir.isAbsolute() ? dir : baseDir.resolve(dir)) + .collect(Collectors.toList()); + return new ModuleCoverageContext(moduleKey, baseDir, sourceDirectories); + } + + static String extractModuleKey(Configuration configuration) { + String moduleKey = configuration.get("sonar.moduleKey").get(); + // If the module key contains a colon, we are looking at a subproject and should use the text that follows the last colon in the module key + int indexOfLastColon = moduleKey.lastIndexOf(':'); + if (indexOfLastColon == -1 || moduleKey.length() <= (indexOfLastColon + 1)) { + return moduleKey; + } + return moduleKey.substring(indexOfLastColon + 1); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ModuleCoverageContext)) return false; + ModuleCoverageContext that = (ModuleCoverageContext) o; + return Objects.equals(name, that.name) && + Objects.equals(baseDir, that.baseDir) && + Objects.equals(sources, that.sources); + } + + @Override + public int hashCode() { + return Objects.hash(name, baseDir, sources); + } + + @Override + public String toString() { + return "ModuleCoverageContext{" + + "name='" + name + '\'' + + ", baseDir=" + baseDir + + ", sourceDirectories=" + sources + + '}'; + } +} diff --git a/src/main/java/org/sonar/plugins/jacoco/ProjectCoverageContext.java b/src/main/java/org/sonar/plugins/jacoco/ProjectCoverageContext.java new file mode 100644 index 00000000..357630aa --- /dev/null +++ b/src/main/java/org/sonar/plugins/jacoco/ProjectCoverageContext.java @@ -0,0 +1,49 @@ +/* + * SonarQube JaCoCo Plugin + * Copyright (C) 2018-2026 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.jacoco; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import org.sonar.api.scanner.ScannerSide; + +@ScannerSide +public class ProjectCoverageContext { + private Path projectBaseDir; + + private List moduleContexts = new ArrayList<>(); + + public List getModuleContexts() { + return moduleContexts; + } + + public void add(ModuleCoverageContext moduleContext) { + this.moduleContexts.add(moduleContext); + } + + + public Path getProjectBaseDir() { + return projectBaseDir; + } + + public void setProjectBaseDir(Path projectBaseDir) { + this.projectBaseDir = projectBaseDir; + } +} diff --git a/src/test/java/org/sonar/plugins/jacoco/JacocoAggregateSensorTest.java b/src/test/java/org/sonar/plugins/jacoco/JacocoAggregateSensorTest.java index 894b9a91..d1ea6b9a 100644 --- a/src/test/java/org/sonar/plugins/jacoco/JacocoAggregateSensorTest.java +++ b/src/test/java/org/sonar/plugins/jacoco/JacocoAggregateSensorTest.java @@ -49,19 +49,21 @@ class JacocoAggregateSensorTest { @BeforeEach void setup() { context = SensorContextTester.create(basedir); + context.settings().clear(); + context.settings().setProperty("sonar.projectBaseDir", basedir.toString()); } @Test void description_name_is_as_expected() { SensorDescriptor descriptor = mock(SensorDescriptor.class); - var sensor = new JacocoAggregateSensor(); + var sensor = new JacocoAggregateSensor(new ProjectCoverageContext()); sensor.describe(descriptor); verify(descriptor).name("JaCoCo Aggregate XML Report Importer"); } @Test void log_missing_report_and_return_early_when_missing_analysis_parameter() { - var sensor = new JacocoAggregateSensor(); + var sensor = new JacocoAggregateSensor(new ProjectCoverageContext()); sensor.execute(context); assertThat(logTester.logs(LoggerLevel.DEBUG)).containsOnly(NO_REPORT_TO_IMPORT_LOG_MESSAGE); @@ -69,11 +71,10 @@ void log_missing_report_and_return_early_when_missing_analysis_parameter() { @Test void log_missing_report_and_return_early_when_analysis_parameter_points_to_report_that_does_not_exist() { - MapSettings settings = new MapSettings(); - settings.setProperty(ReportPathsProvider.AGGREGATE_REPORT_PATH_PROPERTY_KEY, "non-existing-report.xml"); - context.setSettings(settings); + context.settings() + .setProperty(ReportPathsProvider.AGGREGATE_REPORT_PATH_PROPERTY_KEY, "non-existing-report.xml"); - var sensor = new JacocoAggregateSensor(); + var sensor = new JacocoAggregateSensor(new ProjectCoverageContext()); sensor.execute(context); assertThat(logTester.logs(LoggerLevel.ERROR)). @@ -87,11 +88,10 @@ void executes_as_expected() throws IOException { Path reportPath = basedir.resolve("my-aggregate-report.xml"); Files.copy(Path.of("src", "test", "resources", "jacoco.xml"), reportPath); - MapSettings settings = new MapSettings(); - settings.setProperty(ReportPathsProvider.AGGREGATE_REPORT_PATH_PROPERTY_KEY, reportPath.toAbsolutePath().toString()); - context.setSettings(settings); + context.settings() + .setProperty(ReportPathsProvider.AGGREGATE_REPORT_PATH_PROPERTY_KEY, reportPath.toAbsolutePath().toString()); - var sensor = new JacocoAggregateSensor(); + var sensor = new JacocoAggregateSensor(new ProjectCoverageContext()); sensor.execute(context); assertThat(logTester.logs(LoggerLevel.DEBUG)).doesNotContain(NO_REPORT_TO_IMPORT_LOG_MESSAGE); assertThat(logTester.logs(LoggerLevel.INFO)).containsOnly( diff --git a/src/test/java/org/sonar/plugins/jacoco/JacocoPluginTest.java b/src/test/java/org/sonar/plugins/jacoco/JacocoPluginTest.java index 6c967220..d8a99e9e 100644 --- a/src/test/java/org/sonar/plugins/jacoco/JacocoPluginTest.java +++ b/src/test/java/org/sonar/plugins/jacoco/JacocoPluginTest.java @@ -40,19 +40,21 @@ void should_add_sensors_and_property_definitions() { plugin.define(ctx); ArgumentCaptor arg = ArgumentCaptor.forClass(Object.class); - verify(ctx, times(4)).addExtension(arg.capture()); + verify(ctx, times(5)).addExtension(arg.capture()); verifyNoMoreInteractions(ctx); - assertThat(arg.getAllValues().get(0)).isEqualTo(JacocoSensor.class); - assertThat(arg.getAllValues().get(1)).isInstanceOf(PropertyDefinition.class); - PropertyDefinition multiValueReportPaths = (PropertyDefinition) arg.getAllValues().get(1); + assertThat(arg.getAllValues().get(0)).isEqualTo(ProjectCoverageContext.class); + + assertThat(arg.getAllValues().get(1)).isEqualTo(JacocoSensor.class); + assertThat(arg.getAllValues().get(2)).isInstanceOf(PropertyDefinition.class); + PropertyDefinition multiValueReportPaths = (PropertyDefinition) arg.getAllValues().get(2); assertThat(multiValueReportPaths.key()).isEqualTo("sonar.coverage.jacoco.xmlReportPaths"); assertThat(multiValueReportPaths.multiValues()).isTrue(); assertThat(multiValueReportPaths.category()).isEqualTo("JaCoCo"); assertThat(multiValueReportPaths.qualifiers()).containsOnly(Qualifiers.PROJECT); - assertThat(arg.getAllValues().get(2)).isEqualTo(JacocoAggregateSensor.class); - PropertyDefinition aggregateReportPath = (PropertyDefinition) arg.getAllValues().get(3); + assertThat(arg.getAllValues().get(3)).isEqualTo(JacocoAggregateSensor.class); + PropertyDefinition aggregateReportPath = (PropertyDefinition) arg.getAllValues().get(4); assertThat(aggregateReportPath.key()).isEqualTo("sonar.coverage.jacoco.aggregateXmlReportPath"); assertThat(aggregateReportPath.type()).isEqualTo(PropertyType.STRING); assertThat(aggregateReportPath.multiValues()).isFalse(); diff --git a/src/test/java/org/sonar/plugins/jacoco/JacocoSensorTest.java b/src/test/java/org/sonar/plugins/jacoco/JacocoSensorTest.java index 694461d8..88671121 100644 --- a/src/test/java/org/sonar/plugins/jacoco/JacocoSensorTest.java +++ b/src/test/java/org/sonar/plugins/jacoco/JacocoSensorTest.java @@ -20,6 +20,13 @@ package org.sonar.plugins.jacoco; import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; import org.junit.Rule; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -33,14 +40,6 @@ import org.sonar.api.utils.log.LogTesterJUnit5; import org.sonar.api.utils.log.LoggerLevel; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collections; - import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -60,7 +59,7 @@ class JacocoSensorTest { @RegisterExtension public LogTesterJUnit5 logTester = new LogTesterJUnit5(); - private JacocoSensor sensor = new JacocoSensor(); + private JacocoSensor sensor = new JacocoSensor(new ProjectCoverageContext()); @Test void describe_sensor() { @@ -73,13 +72,21 @@ void describe_sensor() { void do_not_index_files_when_no_report_was_found() throws IOException { File emptyFolderWithoutReport = temp.newFolder(); SensorContextTester spiedContext = spy(SensorContextTester.create(emptyFolderWithoutReport)); + MapSettings settings = new MapSettings() + .setProperty("sonar.moduleKey", "module") + .setProperty("sonar.projectBaseDir", temp.getRoot().getAbsolutePath()); + spiedContext.setSettings(settings); DefaultFileSystem spiedFileSystem = spy(spiedContext.fileSystem()); when(spiedContext.fileSystem()).thenReturn(spiedFileSystem); sensor.execute(spiedContext); // indexing all files in the filesystem is time consuming and should not be done if there no jacoco reports to import // one way to assert this is to ensure there's no calls on fileSystem.inputFiles(...) verify(spiedFileSystem, never()).inputFiles(any()); - assertThat(logTester.logs()).containsExactlyInAnyOrder( + var infoLevelLogs = logTester.getLogs(LoggerLevel.INFO) + .stream() + .map(laa -> laa.getFormattedMsg()) + .toList(); + assertThat(infoLevelLogs).containsExactlyInAnyOrder( "'sonar.coverage.jacoco.xmlReportPaths' is not defined. Using default locations: target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml,build/reports/jacoco/test/jacocoTestReport.xml", "No report imported, no coverage information will be imported by JaCoCo XML Report Importer"); } @@ -127,6 +134,8 @@ void parse_failure_do_not_fail_analysis() { void test_load_real_report() throws URISyntaxException, IOException { MapSettings settings = new MapSettings(); settings.setProperty(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY, "jacoco.xml"); + settings.setProperty("sonar.moduleKey", "module"); + settings.setProperty("sonar.projectBaseDir", temp.getRoot().getAbsolutePath()); SensorContextTester tester = SensorContextTester.create(temp.getRoot()); tester.setSettings(settings); InputFile inputFile = TestInputFileBuilder @@ -149,6 +158,8 @@ void test_load_real_report() throws URISyntaxException, IOException { void import_failure_do_not_fail_analysis() throws URISyntaxException, IOException { MapSettings settings = new MapSettings(); settings.setProperty(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY, "invalid_line_number.xml"); + settings.setProperty("sonar.moduleKey", "module"); + settings.setProperty("sonar.projectBaseDir", temp.getRoot().getAbsolutePath()); SensorContextTester tester = SensorContextTester.create(temp.getRoot()); tester.setSettings(settings); InputFile inputFile = TestInputFileBuilder diff --git a/src/test/java/org/sonar/plugins/jacoco/ModuleCoverageContextTest.java b/src/test/java/org/sonar/plugins/jacoco/ModuleCoverageContextTest.java new file mode 100644 index 00000000..3a2d5ddd --- /dev/null +++ b/src/test/java/org/sonar/plugins/jacoco/ModuleCoverageContextTest.java @@ -0,0 +1,82 @@ +/* + * SonarQube JaCoCo Plugin + * Copyright (C) 2018-2026 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.jacoco; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.sonar.api.batch.sensor.internal.SensorContextTester; +import org.sonar.api.config.Configuration; +import org.sonar.api.config.internal.MapSettings; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ModuleCoverageContextTest { + @TempDir + Path temp; + + @Test + void extracts_information_for_a_single_module_project() { + SensorContextTester context = SensorContextTester.create(temp); + MapSettings settings = new MapSettings(); + settings.setProperty("sonar.moduleKey", "single-module"); + Path projectBaseDir = temp.toAbsolutePath(); + settings.setProperty("sonar.projectBaseDir", projectBaseDir.toString()); + settings.setProperty("sonar.sources", "src/main/kotlin"); + context.setSettings(settings); + + ModuleCoverageContext expected = new ModuleCoverageContext( + "single-module", + projectBaseDir, + List.of(projectBaseDir.resolve("src").resolve("main").resolve("kotlin")) + ); + + assertThat(ModuleCoverageContext.from(context)).isEqualTo(expected); + } + + @Test + void extracts_information_for_multi_module_project() throws IOException { + Path projectBaseDir = temp.toAbsolutePath(); + SensorContextTester contextTester = SensorContextTester.create(temp); + Path moduleBaseDir = temp.resolve("submodule"); + Files.createDirectories(moduleBaseDir); + + + // Because the module prefix is systematically popped when analyzing a module, we can use the same keys for each submodule. + MapSettings settings = new MapSettings() + .setProperty("sonar.moduleKey", "org.example:submodule") + .setProperty("sonar.projectBaseDir", projectBaseDir.toString()) + .setProperty("sonar.sources", "src/main/kotlin"); + + SensorContextTester context = SensorContextTester.create(projectBaseDir); + context.setSettings(settings); + + ModuleCoverageContext expected = new ModuleCoverageContext( + "submodule", + projectBaseDir, + List.of(projectBaseDir.resolve("src").resolve("main").resolve("kotlin")) + ); + + assertThat(ModuleCoverageContext.from(context)).isEqualTo(expected); + } +} diff --git a/src/test/java/org/sonar/plugins/jacoco/ProjectCoverageContextTest.java b/src/test/java/org/sonar/plugins/jacoco/ProjectCoverageContextTest.java new file mode 100644 index 00000000..eeed520b --- /dev/null +++ b/src/test/java/org/sonar/plugins/jacoco/ProjectCoverageContextTest.java @@ -0,0 +1,49 @@ +/* + * SonarQube JaCoCo Plugin + * Copyright (C) 2018-2026 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.jacoco; + +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; + +class ProjectCoverageContextTest { + + @Test + void is_empty_when_initialized() { + assertThat(new ProjectCoverageContext().getModuleContexts()).isEmpty(); + } + + @Test + void added_module_coverage_contexts_can_be_retrieved(@TempDir Path temp) { + ModuleCoverageContext moduleCoverageContext = new ModuleCoverageContext( + "top-level", + temp, + List.of(temp.resolve("src").resolve("main").resolve("java")) + ); + + ProjectCoverageContext projectCoverageContext = new ProjectCoverageContext(); + projectCoverageContext.add(moduleCoverageContext); + + assertThat(projectCoverageContext.getModuleContexts()).containsOnly(moduleCoverageContext); + } +} \ No newline at end of file From c9c318c5fc62ea9e52db6a2c7eb4407b09408bbe Mon Sep 17 00:00:00 2001 From: Dorian Burihabwa Date: Tue, 27 Jan 2026 18:14:31 +0100 Subject: [PATCH 09/16] JACOCO-74 Improve coverage --- .../org/sonar/plugins/jacoco/FileLocator.java | 65 ++++++----- .../sonar/plugins/jacoco/FileLocatorTest.java | 103 +++++++++++++++++- 2 files changed, 140 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/sonar/plugins/jacoco/FileLocator.java b/src/main/java/org/sonar/plugins/jacoco/FileLocator.java index 93c4428b..6dc354ff 100644 --- a/src/main/java/org/sonar/plugins/jacoco/FileLocator.java +++ b/src/main/java/org/sonar/plugins/jacoco/FileLocator.java @@ -58,35 +58,14 @@ public InputFile getInputFile(String packagePath, String fileName) { @CheckForNull public InputFile getInputFile(@Nullable String groupName, String packagePath, String fileName) { - String filePath = packagePath.isEmpty() ? - fileName : - packagePath + '/' + fileName; + String filePath = packagePath.isEmpty() + ? fileName + : (packagePath + '/' + fileName); String[] path = filePath.split("/"); - InputFile fileWithSuffix = tree.getFileWithSuffix(path); - - if (groupName != null) { - fileWithSuffix = tree.getFileWithSuffix(groupName, path); - if (fileWithSuffix == null) { - var candidateGroups = projectCoverageContext.getModuleContexts() - .stream() - .filter(mcc -> groupName.equals(mcc.name)) - .collect(Collectors.toList()); - for (ModuleCoverageContext candidateGroup : candidateGroups) { - for (Path source : candidateGroup.sources) { - if (Files.isDirectory(source)) { - Path relativePath = projectCoverageContext.getProjectBaseDir().relativize(source.resolve(Paths.get(filePath))); - path = relativePath.toString().split("/"); - source.relativize(candidateGroup.baseDir); - fileWithSuffix = tree.getFileWithSuffix(path); - if (fileWithSuffix != null) { - return fileWithSuffix; - } - } - } - } - } - } + InputFile fileWithSuffix = groupName == null + ? tree.getFileWithSuffix(path) + : getInputFileForProject(groupName, filePath); if (fileWithSuffix == null && fileName.endsWith(".kt")) { fileWithSuffix = kotlinFileLocator.getInputFile(packagePath, fileName); @@ -94,4 +73,36 @@ public InputFile getInputFile(@Nullable String groupName, String packagePath, St return fileWithSuffix; } + + @CheckForNull + private InputFile getInputFileForProject(String groupName, String filePath) { + // First, try to look up the file in the tree using the computed path + String[] pathSegments = filePath.split("/"); + InputFile file = tree.getFileWithSuffix(groupName, pathSegments); + if (file != null) { + return file; + } + // If the file cannot be found by looking up the tree, due for instance to ambiguities between sub-projects with similar structures, + // then we must rebuild the path by identifying the correct sub-project, and building the path from its known sources + return projectCoverageContext.getModuleContexts() + .stream() + .filter(mcc -> groupName.equals(mcc.name)) + .findFirst() + .map(mcc -> getInputFileForModule(mcc, filePath)).orElse(null); + } + + @CheckForNull + private InputFile getInputFileForModule(ModuleCoverageContext moduleCoverageContext, String filePath) { + for (Path source : moduleCoverageContext.sources) { + if (Files.isDirectory(source)) { + Path relativePath = projectCoverageContext.getProjectBaseDir().relativize(source.resolve(Paths.get(filePath))); + String[] segments = relativePath.toString().split("/"); + InputFile file = tree.getFileWithSuffix(segments); + if (file != null) { + return file; + } + } + } + return null; + } } diff --git a/src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java b/src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java index 99f4d59f..29906c76 100644 --- a/src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java +++ b/src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java @@ -19,14 +19,18 @@ */ package org.sonar.plugins.jacoco; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.internal.TestInputFileBuilder; import static org.assertj.core.api.Assertions.assertThat; - import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -93,4 +97,101 @@ void should_not_fallback_on_Kotlin_file_locator_if_file_is_not_Kotlin() { verify(kotlinFileLocatorMock, never()).getInputFile(any(), any()); } + @Test + void should_be_able_to_look_up_ambiguous_names(@TempDir Path temp) throws IOException { + /* + /tmp/junit153873785933058202/my-project + ├── app + │ ├── pom.xml + │ ├── src + │ │ └── main + │ │ └── java + │ │ └── File.java + │ └── utils + │ ├── pom.xml + │ └── src + │ └── main + │ └── java + │ └── File.java + ├── pom.xml + └── utils + ├── pom.xml + └── src + └── main + └── java + └── File.java + */ + + // Top level project + Path myProjectBaseDir = Files.createDirectories(temp.resolve("my-project")); + Path myProjectPomXml = Files.createFile(myProjectBaseDir.resolve("pom.xml")); + // App module + Path appModuleBaseDir = Files.createDirectory(myProjectBaseDir.resolve("app")); + Path appModuleJavaSources = Files.createDirectories(appModuleBaseDir.resolve("src").resolve("main").resolve("java")); + Path appModuleFileJava = Files.createFile(appModuleJavaSources.resolve("File.java")); + Path appModulePomXml = Files.createFile(appModuleBaseDir.resolve("pom.xml")); + // Utils module + Path utilsModuleBaseDir = Files.createDirectory(myProjectBaseDir.resolve("utils")); + Path utilsModuleJavaSources = Files.createDirectories(utilsModuleBaseDir.resolve("src").resolve("main").resolve("java")); + Path utilsModuleFileJava = Files.createFile(utilsModuleJavaSources.resolve("File.java")); + Path utilsModulePomXml = Files.createFile(utilsModuleBaseDir.resolve("pom.xml")); + // Utils module nested into App module + Path nestedUtilsModuleBaseDir = Files.createDirectory(appModuleBaseDir.resolve("utils")); + Path nestedUtilsModuleJavaSources = Files.createDirectories(nestedUtilsModuleBaseDir.resolve("src").resolve("main").resolve("java")); + Path nestedUtilsModuleFileJava = Files.createFile(nestedUtilsModuleJavaSources.resolve("File.java")); + Path nestedUtilsModulePomXml = Files.createFile(nestedUtilsModuleBaseDir.resolve("pom.xml")); + + // Prepare all the input files to index + InputFile appFile = new TestInputFileBuilder("my-project", myProjectBaseDir.toFile(), appModuleFileJava.toFile()).build(); + InputFile utilsFile = new TestInputFileBuilder("my-project", myProjectBaseDir.toFile(), utilsModuleFileJava.toFile()).build(); + InputFile nestedUtilsFile = new TestInputFileBuilder("my-project", myProjectBaseDir.toFile(), nestedUtilsModuleFileJava.toFile()).build(); + List filesToIndex = List.of( + new TestInputFileBuilder("my-project", myProjectBaseDir.toFile(), myProjectPomXml.toFile()).build(), + appFile, + new TestInputFileBuilder("my-project", myProjectBaseDir.toFile(), appModulePomXml.toFile()).build(), + utilsFile, + new TestInputFileBuilder("my-project", myProjectBaseDir.toFile(), utilsModulePomXml.toFile()).build(), + nestedUtilsFile, + new TestInputFileBuilder("my-project", myProjectBaseDir.toFile(), nestedUtilsModulePomXml.toFile()).build() + ); + + FileLocator locator = new FileLocator(filesToIndex, null); + + ProjectCoverageContext pcc = new ProjectCoverageContext(); + pcc.setProjectBaseDir(myProjectBaseDir); + pcc.add( + new ModuleCoverageContext( + "app", + appModuleBaseDir, + List.of(appModulePomXml, appModuleJavaSources) + ) + ); + + pcc.add( + new ModuleCoverageContext( + "utils", + utilsModuleBaseDir, + List.of(utilsModulePomXml, utilsModuleJavaSources) + ) + ); + + pcc.add( + new ModuleCoverageContext( + "app-utils", + utilsModuleBaseDir, + List.of(nestedUtilsModulePomXml, nestedUtilsModuleJavaSources) + ) + ); + + locator.setProjectCoverageContext(pcc); + + // Test existing files + assertThat(locator.getInputFile("app", "", "File.java")).isEqualTo(appFile); + assertThat(locator.getInputFile("utils", "", "File.java")).isEqualTo(utilsFile); + assertThat(locator.getInputFile("app-utils", "", "File.java")).isEqualTo(nestedUtilsFile); + + // Test non-existing files + assertThat(locator.getInputFile("app", "org/example", "Main.java")).isEqualTo(null); + } + } From 9dd41aadfa6781998db68d6bbf4d13e2cbf00c8e Mon Sep 17 00:00:00 2001 From: Dorian Burihabwa Date: Tue, 27 Jan 2026 21:19:02 +0100 Subject: [PATCH 10/16] JACOCO-74 Prevent crash when sonar.moduleKey is not defined --- .../sonar/plugins/jacoco/ModuleCoverageContext.java | 4 +++- .../plugins/jacoco/ModuleCoverageContextTest.java | 10 ++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/sonar/plugins/jacoco/ModuleCoverageContext.java b/src/main/java/org/sonar/plugins/jacoco/ModuleCoverageContext.java index 789c61a3..3e1b1598 100644 --- a/src/main/java/org/sonar/plugins/jacoco/ModuleCoverageContext.java +++ b/src/main/java/org/sonar/plugins/jacoco/ModuleCoverageContext.java @@ -54,7 +54,9 @@ static ModuleCoverageContext from(SensorContext sensorContext) { } static String extractModuleKey(Configuration configuration) { - String moduleKey = configuration.get("sonar.moduleKey").get(); + String moduleKey = configuration.get("sonar.moduleKey").isPresent() + ? configuration.get("sonar.moduleKey").get() + : configuration.get("sonar.projectKey").get(); // If the module key contains a colon, we are looking at a subproject and should use the text that follows the last colon in the module key int indexOfLastColon = moduleKey.lastIndexOf(':'); if (indexOfLastColon == -1 || moduleKey.length() <= (indexOfLastColon + 1)) { diff --git a/src/test/java/org/sonar/plugins/jacoco/ModuleCoverageContextTest.java b/src/test/java/org/sonar/plugins/jacoco/ModuleCoverageContextTest.java index 3a2d5ddd..fcf3d08d 100644 --- a/src/test/java/org/sonar/plugins/jacoco/ModuleCoverageContextTest.java +++ b/src/test/java/org/sonar/plugins/jacoco/ModuleCoverageContextTest.java @@ -26,27 +26,26 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.sonar.api.batch.sensor.internal.SensorContextTester; -import org.sonar.api.config.Configuration; import org.sonar.api.config.internal.MapSettings; import static org.assertj.core.api.Assertions.assertThat; -public class ModuleCoverageContextTest { +class ModuleCoverageContextTest { @TempDir Path temp; @Test - void extracts_information_for_a_single_module_project() { + void uses_the_project_key_as_the_name_when_module_key_is_not_defined() { SensorContextTester context = SensorContextTester.create(temp); MapSettings settings = new MapSettings(); - settings.setProperty("sonar.moduleKey", "single-module"); + settings.setProperty("sonar.projectKey", "single-module-project"); Path projectBaseDir = temp.toAbsolutePath(); settings.setProperty("sonar.projectBaseDir", projectBaseDir.toString()); settings.setProperty("sonar.sources", "src/main/kotlin"); context.setSettings(settings); ModuleCoverageContext expected = new ModuleCoverageContext( - "single-module", + "single-module-project", projectBaseDir, List.of(projectBaseDir.resolve("src").resolve("main").resolve("kotlin")) ); @@ -57,7 +56,6 @@ void extracts_information_for_a_single_module_project() { @Test void extracts_information_for_multi_module_project() throws IOException { Path projectBaseDir = temp.toAbsolutePath(); - SensorContextTester contextTester = SensorContextTester.create(temp); Path moduleBaseDir = temp.resolve("submodule"); Files.createDirectories(moduleBaseDir); From 370be7fd3c41344e800253d79c65c94346a0828e Mon Sep 17 00:00:00 2001 From: Dorian Burihabwa Date: Tue, 27 Jan 2026 21:43:47 +0100 Subject: [PATCH 11/16] JACOCO-74 Improve coverage --- .../jacoco/ModuleCoverageContextTest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/test/java/org/sonar/plugins/jacoco/ModuleCoverageContextTest.java b/src/test/java/org/sonar/plugins/jacoco/ModuleCoverageContextTest.java index fcf3d08d..c3730c00 100644 --- a/src/test/java/org/sonar/plugins/jacoco/ModuleCoverageContextTest.java +++ b/src/test/java/org/sonar/plugins/jacoco/ModuleCoverageContextTest.java @@ -77,4 +77,21 @@ void extracts_information_for_multi_module_project() throws IOException { assertThat(ModuleCoverageContext.from(context)).isEqualTo(expected); } + + @Test + void equality() { + var mcc = new ModuleCoverageContext("name", Path.of("name"), List.of(Path.of("src", "main", "java"))); + assertThat(mcc) + .isEqualTo(mcc) + .hasSameHashCodeAs(mcc); + + var otherName = new ModuleCoverageContext("other", Path.of("name"), List.of(Path.of("src", "main", "java"))); + var otherBaseDir = new ModuleCoverageContext("name", Path.of("other"), List.of(Path.of("src", "main", "java"))); + var otherSources = new ModuleCoverageContext("name", Path.of("name"), List.of(Path.of("src", "main", "kotlin"))); + assertThat(mcc) + .isNotEqualTo(otherName) + .isNotEqualTo(otherBaseDir) + .isNotEqualTo(otherSources) + .isNotEqualTo(new Object()); + } } From 7678a8106831e7fe4416c34c67cf940b430a68e8 Mon Sep 17 00:00:00 2001 From: Dorian Burihabwa Date: Tue, 27 Jan 2026 21:52:44 +0100 Subject: [PATCH 12/16] JACOCO-74 Fix quality flaws --- .../org/sonar/plugins/jacoco/ModuleCoverageContext.java | 6 ++++-- src/main/java/org/sonar/plugins/jacoco/ReversePathTree.java | 2 +- src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/sonar/plugins/jacoco/ModuleCoverageContext.java b/src/main/java/org/sonar/plugins/jacoco/ModuleCoverageContext.java index 3e1b1598..6637da36 100644 --- a/src/main/java/org/sonar/plugins/jacoco/ModuleCoverageContext.java +++ b/src/main/java/org/sonar/plugins/jacoco/ModuleCoverageContext.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.config.Configuration; @@ -54,8 +55,9 @@ static ModuleCoverageContext from(SensorContext sensorContext) { } static String extractModuleKey(Configuration configuration) { - String moduleKey = configuration.get("sonar.moduleKey").isPresent() - ? configuration.get("sonar.moduleKey").get() + Optional module = configuration.get("sonar.moduleKey"); + String moduleKey = module.isPresent() + ? module.get() : configuration.get("sonar.projectKey").get(); // If the module key contains a colon, we are looking at a subproject and should use the text that follows the last colon in the module key int indexOfLastColon = moduleKey.lastIndexOf(':'); diff --git a/src/main/java/org/sonar/plugins/jacoco/ReversePathTree.java b/src/main/java/org/sonar/plugins/jacoco/ReversePathTree.java index 7f6b0b3f..d752d354 100644 --- a/src/main/java/org/sonar/plugins/jacoco/ReversePathTree.java +++ b/src/main/java/org/sonar/plugins/jacoco/ReversePathTree.java @@ -61,7 +61,7 @@ public InputFile getFileWithSuffix(String module, String[] path) { } else { for (String candidate : currentNode.children.keySet()) { if (module.equals(candidate)) { - return currentNode.children.get(candidate).file; + return currentNode.children.get(candidate).file; } } return null; diff --git a/src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java b/src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java index 29906c76..cbdfb335 100644 --- a/src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java +++ b/src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java @@ -191,7 +191,7 @@ void should_be_able_to_look_up_ambiguous_names(@TempDir Path temp) throws IOExce assertThat(locator.getInputFile("app-utils", "", "File.java")).isEqualTo(nestedUtilsFile); // Test non-existing files - assertThat(locator.getInputFile("app", "org/example", "Main.java")).isEqualTo(null); + assertThat(locator.getInputFile("app", "org/example", "Main.java")).isNull(); } } From b28a4171d66f1316c05c9ef7c6faaa11acf4a3af Mon Sep 17 00:00:00 2001 From: Dorian Burihabwa Date: Wed, 28 Jan 2026 10:00:17 +0100 Subject: [PATCH 13/16] JACOCO-74 Extend search for candidate files to files in module sources --- src/main/java/org/sonar/plugins/jacoco/FileLocator.java | 7 +++++++ .../java/org/sonar/plugins/jacoco/FileLocatorTest.java | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/sonar/plugins/jacoco/FileLocator.java b/src/main/java/org/sonar/plugins/jacoco/FileLocator.java index 6dc354ff..eca35101 100644 --- a/src/main/java/org/sonar/plugins/jacoco/FileLocator.java +++ b/src/main/java/org/sonar/plugins/jacoco/FileLocator.java @@ -101,6 +101,13 @@ private InputFile getInputFileForModule(ModuleCoverageContext moduleCoverageCont if (file != null) { return file; } + } else if (source.endsWith(filePath)) { + Path relativePah = projectCoverageContext.getProjectBaseDir().relativize(source); + String[] segments = relativePah.toString().split("/"); + InputFile file = tree.getFileWithSuffix(segments); + if (file != null) { + return file; + } } } return null; diff --git a/src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java b/src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java index cbdfb335..b62396f6 100644 --- a/src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java +++ b/src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java @@ -145,6 +145,7 @@ void should_be_able_to_look_up_ambiguous_names(@TempDir Path temp) throws IOExce InputFile appFile = new TestInputFileBuilder("my-project", myProjectBaseDir.toFile(), appModuleFileJava.toFile()).build(); InputFile utilsFile = new TestInputFileBuilder("my-project", myProjectBaseDir.toFile(), utilsModuleFileJava.toFile()).build(); InputFile nestedUtilsFile = new TestInputFileBuilder("my-project", myProjectBaseDir.toFile(), nestedUtilsModuleFileJava.toFile()).build(); + InputFile nestUtilsPomXmlFile = new TestInputFileBuilder("my-project", myProjectBaseDir.toFile(), nestedUtilsModulePomXml.toFile()).build(); List filesToIndex = List.of( new TestInputFileBuilder("my-project", myProjectBaseDir.toFile(), myProjectPomXml.toFile()).build(), appFile, @@ -152,7 +153,7 @@ void should_be_able_to_look_up_ambiguous_names(@TempDir Path temp) throws IOExce utilsFile, new TestInputFileBuilder("my-project", myProjectBaseDir.toFile(), utilsModulePomXml.toFile()).build(), nestedUtilsFile, - new TestInputFileBuilder("my-project", myProjectBaseDir.toFile(), nestedUtilsModulePomXml.toFile()).build() + nestUtilsPomXmlFile ); FileLocator locator = new FileLocator(filesToIndex, null); @@ -189,6 +190,7 @@ void should_be_able_to_look_up_ambiguous_names(@TempDir Path temp) throws IOExce assertThat(locator.getInputFile("app", "", "File.java")).isEqualTo(appFile); assertThat(locator.getInputFile("utils", "", "File.java")).isEqualTo(utilsFile); assertThat(locator.getInputFile("app-utils", "", "File.java")).isEqualTo(nestedUtilsFile); + assertThat(locator.getInputFile("app-utils", "", "pom.xml")).isEqualTo(nestUtilsPomXmlFile); // Test non-existing files assertThat(locator.getInputFile("app", "org/example", "Main.java")).isNull(); From 9d039aef4d11b48dc066c2d15e0a4159627fd084 Mon Sep 17 00:00:00 2001 From: Dorian Burihabwa Date: Wed, 28 Jan 2026 10:10:53 +0100 Subject: [PATCH 14/16] JACOCO-74 Refactor FileLocator --- .../java/org/sonar/plugins/jacoco/FileLocator.java | 11 ++++++++--- .../sonar/plugins/jacoco/JacocoAggregateSensor.java | 3 +-- .../org/sonar/plugins/jacoco/FileLocatorTest.java | 4 +--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/sonar/plugins/jacoco/FileLocator.java b/src/main/java/org/sonar/plugins/jacoco/FileLocator.java index eca35101..6b1ca964 100644 --- a/src/main/java/org/sonar/plugins/jacoco/FileLocator.java +++ b/src/main/java/org/sonar/plugins/jacoco/FileLocator.java @@ -35,19 +35,24 @@ public class FileLocator { private ProjectCoverageContext projectCoverageContext; public FileLocator(Iterable inputFiles, KotlinFileLocator kotlinFileLocator) { - this(StreamSupport.stream(inputFiles.spliterator(), false).collect(Collectors.toList()), kotlinFileLocator); + this(StreamSupport.stream(inputFiles.spliterator(), false).collect(Collectors.toList()), kotlinFileLocator,null); } - void setProjectCoverageContext(ProjectCoverageContext projectCoverageContext) { - this.projectCoverageContext = projectCoverageContext; + public FileLocator(Iterable inputFiles, KotlinFileLocator kotlinFileLocator, ProjectCoverageContext projectCoverageContext) { + this(StreamSupport.stream(inputFiles.spliterator(), false).collect(Collectors.toList()), kotlinFileLocator, projectCoverageContext); } public FileLocator(List inputFiles, KotlinFileLocator kotlinFileLocator) { + this(inputFiles, kotlinFileLocator, null); + } + + public FileLocator(List inputFiles, KotlinFileLocator kotlinFileLocator, @Nullable ProjectCoverageContext projectCoverageContext) { this.kotlinFileLocator = kotlinFileLocator; for (InputFile inputFile : inputFiles) { String[] path = inputFile.relativePath().split("/"); tree.index(inputFile, path); } + this.projectCoverageContext = projectCoverageContext; } @CheckForNull diff --git a/src/main/java/org/sonar/plugins/jacoco/JacocoAggregateSensor.java b/src/main/java/org/sonar/plugins/jacoco/JacocoAggregateSensor.java index e9e2c275..0921ac82 100644 --- a/src/main/java/org/sonar/plugins/jacoco/JacocoAggregateSensor.java +++ b/src/main/java/org/sonar/plugins/jacoco/JacocoAggregateSensor.java @@ -60,8 +60,7 @@ public void execute(SensorContext context) { } Iterable inputFiles = context.fileSystem().inputFiles(context.fileSystem().predicates().all()); Stream kotlinInputFileStream = StreamSupport.stream(inputFiles.spliterator(), false).filter(f -> "kotlin".equals(f.language())); - FileLocator locator = new FileLocator(inputFiles, new KotlinFileLocator(kotlinInputFileStream)); - locator.setProjectCoverageContext(projectCoverageContext); + FileLocator locator = new FileLocator(inputFiles, new KotlinFileLocator(kotlinInputFileStream), projectCoverageContext); ReportImporter importer = new ReportImporter(context); LOG.info("Importing aggregate report {}.", reportPath); diff --git a/src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java b/src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java index b62396f6..531afa73 100644 --- a/src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java +++ b/src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java @@ -156,8 +156,6 @@ void should_be_able_to_look_up_ambiguous_names(@TempDir Path temp) throws IOExce nestUtilsPomXmlFile ); - FileLocator locator = new FileLocator(filesToIndex, null); - ProjectCoverageContext pcc = new ProjectCoverageContext(); pcc.setProjectBaseDir(myProjectBaseDir); pcc.add( @@ -184,7 +182,7 @@ void should_be_able_to_look_up_ambiguous_names(@TempDir Path temp) throws IOExce ) ); - locator.setProjectCoverageContext(pcc); + FileLocator locator = new FileLocator(filesToIndex, null, pcc); // Test existing files assertThat(locator.getInputFile("app", "", "File.java")).isEqualTo(appFile); From 64e290ce61070b389c74ecb5babe2932009305e2 Mon Sep 17 00:00:00 2001 From: Dorian Burihabwa Date: Wed, 28 Jan 2026 12:09:10 +0100 Subject: [PATCH 15/16] JACOCO-74 Extend QA coverage test --- .../org/sonar/plugins/jacoco/its/JacocoTest.java | 12 +++++++++++- .../src/test/java/org/example/LibraryTest.java | 6 ++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/its/src/test/java/org/sonar/plugins/jacoco/its/JacocoTest.java b/its/src/test/java/org/sonar/plugins/jacoco/its/JacocoTest.java index e68b9bdb..e94eab3d 100644 --- a/its/src/test/java/org/sonar/plugins/jacoco/its/JacocoTest.java +++ b/its/src/test/java/org/sonar/plugins/jacoco/its/JacocoTest.java @@ -36,7 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat; -public class JacocoTest { +class JacocoTest { private final static String PROJECT_KEY = "jacoco-test-project"; private static final String FILE_KEY = "jacoco-test-project:src/main/java/org/sonarsource/test/Calc.java"; private static final String KOTLIN_FILE_KEY = "org.sonarsource.it.projects:kotlin-jacoco-project:src/main/kotlin/CoverMe.kt"; @@ -230,6 +230,16 @@ void aggregate_and_module_based_reports_complement_each_over_to_build_total_cove .containsEntry("uncovered_lines", 0.0) .containsEntry("coverage", 100.0); + Map measuresForLibraryNested = getCoverageMeasures("org.example:aggregate-and-module-based-mixed-coverage:nested/library/src/main/java/org/example/Library.java"); + assertThat(measuresForLibraryNested) + .containsEntry("branch_coverage", 100.0) + .containsEntry("conditions_to_cover", 2.0) + .containsEntry("coverage", 100.0) + .containsEntry("line_coverage", 100.0) + .containsEntry("lines_to_cover", 7.0) + .containsEntry("uncovered_conditions", 0.0) + .containsEntry("uncovered_lines", 0.0); + Map measuresForSquarer = getCoverageMeasures("org.example:aggregate-and-module-based-mixed-coverage:self-covered/src/main/java/org/example/Squarer.java"); assertThat(measuresForSquarer) .containsEntry("line_coverage", 100.0) diff --git a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/library/src/test/java/org/example/LibraryTest.java b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/library/src/test/java/org/example/LibraryTest.java index 76280d4d..9b5146b4 100644 --- a/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/library/src/test/java/org/example/LibraryTest.java +++ b/its/src/test/resources/aggregate-and-module-based-mixed-coverage/nested/library/src/test/java/org/example/LibraryTest.java @@ -10,13 +10,11 @@ void test() { Assertions.assertEquals("City", library.name); Library similarLibrary = new Library("City"); - Assertions.assertEquals(library, library); Assertions.assertEquals(library, similarLibrary); Assertions.assertEquals(library.hashCode(), library.hashCode()); Assertions.assertEquals(library.hashCode(), similarLibrary.hashCode()); - Assertions.assertFalse(library.equals(null)); - Assertions.assertFalse(library.equals(new Object())); - Assertions.assertNotEquals(new Library("Other"), library); + Assertions.assertNotEquals(library, new Object()); + Assertions.assertNotEquals(library, new Library("Other")); } } From 986eafbf992a2243b898785691c0bf08cb60eb64 Mon Sep 17 00:00:00 2001 From: Dorian Burihabwa Date: Thu, 29 Jan 2026 11:06:04 +0100 Subject: [PATCH 16/16] JACOCO-74 Remove unused import --- .../java/org/sonar/plugins/jacoco/JacocoAggregateSensorTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/org/sonar/plugins/jacoco/JacocoAggregateSensorTest.java b/src/test/java/org/sonar/plugins/jacoco/JacocoAggregateSensorTest.java index d1ea6b9a..23a353cf 100644 --- a/src/test/java/org/sonar/plugins/jacoco/JacocoAggregateSensorTest.java +++ b/src/test/java/org/sonar/plugins/jacoco/JacocoAggregateSensorTest.java @@ -28,7 +28,6 @@ import org.junit.jupiter.api.io.TempDir; import org.sonar.api.batch.sensor.SensorDescriptor; import org.sonar.api.batch.sensor.internal.SensorContextTester; -import org.sonar.api.config.internal.MapSettings; import org.sonar.api.utils.log.LogTesterJUnit5; import org.sonar.api.utils.log.LoggerLevel;