Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
a1eaf9b
JACOCO-71 Add Integration tests with aggregate report
dorian-burihabwa-sonarsource Dec 12, 2025
4bb01bf
JACOCO-71 Change IT to use mixed aggregate and module based coverage
dorian-burihabwa-sonarsource Dec 15, 2025
4a12a30
WIP JACOCO-71 Committing before cherry-picking group name collection
dorian-burihabwa-sonarsource Dec 17, 2025
a22adc8
JACOCO-71 Collect group name when parsing report
dorian-burihabwa-sonarsource Dec 14, 2025
3f3c12d
JACOCO-71 Import coverage in a very hacky way
dorian-burihabwa-sonarsource Dec 18, 2025
d90ca99
JACOCO-71 Support resolution for name clashes between siblings
dorian-burihabwa-sonarsource Dec 18, 2025
9608b46
WIP JACOCO-71 Add a level of nesting to test clashes behavior
dorian-burihabwa-sonarsource Dec 18, 2025
ad6c55a
JACOCO-74 WIP Managing clashes with nested modules
dorian-burihabwa-sonarsource Jan 16, 2026
c9c318c
JACOCO-74 Improve coverage
dorian-burihabwa-sonarsource Jan 27, 2026
9dd41aa
JACOCO-74 Prevent crash when sonar.moduleKey is not defined
dorian-burihabwa-sonarsource Jan 27, 2026
370be7f
JACOCO-74 Improve coverage
dorian-burihabwa-sonarsource Jan 27, 2026
7678a81
JACOCO-74 Fix quality flaws
dorian-burihabwa-sonarsource Jan 27, 2026
b28a417
JACOCO-74 Extend search for candidate files to files in module sources
dorian-burihabwa-sonarsource Jan 28, 2026
9d039ae
JACOCO-74 Refactor FileLocator
dorian-burihabwa-sonarsource Jan 28, 2026
64e290c
JACOCO-74 Extend QA coverage test
dorian-burihabwa-sonarsource Jan 28, 2026
986eafb
JACOCO-74 Remove unused import
dorian-burihabwa-sonarsource Jan 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion its/src/test/java/org/sonar/plugins/jacoco/its/JacocoTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -223,6 +223,23 @@ void aggregate_and_module_based_reports_complement_each_over_to_build_total_cove
.containsEntry("uncovered_conditions", 0.0)
.containsEntry("coverage", 100.0);

Map<String, Double> 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<String, Double> 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<String, Double> 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# Aggregate Maven project

A project with 4 modules:
A project with 7 modules with 100% coverage:

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. [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:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.example</groupId>
<artifactId>aggregate-and-module-based-mixed-coverage</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>library-clash</artifactId>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>6.0.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.example;

public final class Library {
String greet(String name) {
return String.format("Hello, %s!", name);
}
}
Original file line number Diff line number Diff line change
@@ -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"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.example</groupId>
<artifactId>nested</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>library-nested</artifactId>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>6.0.1</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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, similarLibrary);
Assertions.assertEquals(library.hashCode(), library.hashCode());
Assertions.assertEquals(library.hashCode(), similarLibrary.hashCode());

Assertions.assertNotEquals(library, new Object());
Assertions.assertNotEquals(library, new Library("Other"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.example</groupId>
<artifactId>aggregate-and-module-based-mixed-coverage</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>nested</artifactId>
<packaging>pom</packaging>

<modules>
<module>library</module>
</modules>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.example;

public final class Library {
String greet(String name) {
return String.format("Hello, %s!", name);
}
}
Original file line number Diff line number Diff line change
@@ -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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

<modules>
<module>library</module>
<module>library-clash</module>
<module>nested</module>
<module>library-test</module>
<module>report</module>
<module>self-covered</module>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>library-clash</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>library-nested</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>library-test</artifactId>
Expand Down
71 changes: 68 additions & 3 deletions src/main/java/org/sonar/plugins/jacoco/FileLocator.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,37 +19,102 @@
*/
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;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.api.batch.fs.InputFile;

public class FileLocator {
private final ReversePathTree tree = new ReversePathTree();
private final KotlinFileLocator kotlinFileLocator;
private ProjectCoverageContext projectCoverageContext;

public FileLocator(Iterable<InputFile> inputFiles, KotlinFileLocator kotlinFileLocator) {
this(StreamSupport.stream(inputFiles.spliterator(), false).collect(Collectors.toList()), kotlinFileLocator);
this(StreamSupport.stream(inputFiles.spliterator(), false).collect(Collectors.toList()), kotlinFileLocator,null);
}

public FileLocator(Iterable<InputFile> inputFiles, KotlinFileLocator kotlinFileLocator, ProjectCoverageContext projectCoverageContext) {
this(StreamSupport.stream(inputFiles.spliterator(), false).collect(Collectors.toList()), kotlinFileLocator, projectCoverageContext);
}

public FileLocator(List<InputFile> inputFiles, KotlinFileLocator kotlinFileLocator) {
this(inputFiles, kotlinFileLocator, null);
}

public FileLocator(List<InputFile> 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
/* 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 = packagePath.isEmpty()
? fileName
: (packagePath + '/' + fileName);
String[] path = filePath.split("/");
InputFile fileWithSuffix = tree.getFileWithSuffix(path);

InputFile fileWithSuffix = groupName == null
? tree.getFileWithSuffix(path)
: getInputFileForProject(groupName, filePath);

if (fileWithSuffix == null && fileName.endsWith(".kt")) {
fileWithSuffix = kotlinFileLocator.getInputFile(packagePath, fileName);
}

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;
}
} 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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();
Expand All @@ -53,7 +60,7 @@ public void execute(SensorContext context) {
}
Iterable<InputFile> inputFiles = context.fileSystem().inputFiles(context.fileSystem().predicates().all());
Stream<InputFile> kotlinInputFileStream = StreamSupport.stream(inputFiles.spliterator(), false).filter(f -> "kotlin".equals(f.language()));
FileLocator locator = new FileLocator(inputFiles, new KotlinFileLocator(kotlinInputFileStream));
FileLocator locator = new FileLocator(inputFiles, new KotlinFileLocator(kotlinInputFileStream), projectCoverageContext);
ReportImporter importer = new ReportImporter(context);

LOG.info("Importing aggregate report {}.", reportPath);
Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/sonar/plugins/jacoco/JacocoPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading