├── mise.toml ├── .github ├── CODEOWNERS ├── workflows │ ├── pr-cleanup.yml │ ├── releasability.yaml │ ├── release.yml │ ├── mark-prs-stale.yml │ ├── ToggleLockBranch.yml │ ├── PullRequestClosed.yml │ ├── RequestReview.yml │ ├── SubmitReview.yml │ ├── PullRequestCreated.yml │ ├── unified-dogfooding.yml │ └── build.yml └── PULL_REQUEST_TEMPLATE.md ├── its ├── src │ └── test │ │ ├── resources │ │ ├── aggregate-and-module-based-mixed-coverage │ │ │ ├── .gitignore │ │ │ ├── self-covered │ │ │ │ ├── src │ │ │ │ │ ├── main │ │ │ │ │ │ └── java │ │ │ │ │ │ │ └── org │ │ │ │ │ │ │ └── example │ │ │ │ │ │ │ └── Squarer.java │ │ │ │ │ └── test │ │ │ │ │ │ └── java │ │ │ │ │ │ └── org │ │ │ │ │ │ └── example │ │ │ │ │ │ └── SquarerTest.java │ │ │ │ └── pom.xml │ │ │ ├── library │ │ │ │ ├── src │ │ │ │ │ ├── main │ │ │ │ │ │ └── java │ │ │ │ │ │ │ └── org │ │ │ │ │ │ │ └── example │ │ │ │ │ │ │ └── Library.java │ │ │ │ │ └── test │ │ │ │ │ │ └── java │ │ │ │ │ │ └── org │ │ │ │ │ │ └── example │ │ │ │ │ │ └── LibraryTest.java │ │ │ │ └── pom.xml │ │ │ ├── library-test │ │ │ │ ├── src │ │ │ │ │ └── test │ │ │ │ │ │ └── java │ │ │ │ │ │ └── org │ │ │ │ │ │ └── example │ │ │ │ │ │ └── LibraryTest.java │ │ │ │ └── pom.xml │ │ │ ├── README.md │ │ │ ├── pom.xml │ │ │ └── report │ │ │ │ └── pom.xml │ │ ├── simple-project-jacoco │ │ │ ├── jacoco.exec │ │ │ ├── src │ │ │ │ ├── main │ │ │ │ │ └── java │ │ │ │ │ │ └── org │ │ │ │ │ │ └── sonarsource │ │ │ │ │ │ └── test │ │ │ │ │ │ ├── CalcNoCoverage.java │ │ │ │ │ │ └── Calc.java │ │ │ │ └── test │ │ │ │ │ └── java │ │ │ │ │ └── org │ │ │ │ │ └── sonarsource │ │ │ │ │ └── test │ │ │ │ │ └── CalcTest.java │ │ │ ├── jacoco-with-invalid-format.xml │ │ │ ├── pom.xml │ │ │ ├── jacoco.xml │ │ │ ├── jacoco-with-invalid-lines.xml │ │ │ ├── target │ │ │ │ └── site │ │ │ │ │ └── jacoco-it │ │ │ │ │ └── jacoco.xml │ │ │ └── jacoco-with-invalid-sources.xml │ │ └── kotlin-jacoco-project │ │ │ ├── src │ │ │ ├── main │ │ │ │ └── kotlin │ │ │ │ │ └── CoverMe.kt │ │ │ └── test │ │ │ │ └── kotlin │ │ │ │ └── CoverMeTest.kt │ │ │ └── pom.xml │ │ └── java │ │ └── org │ │ └── sonar │ │ └── plugins │ │ └── jacoco │ │ └── its │ │ └── JacocoTest.java └── build.gradle ├── src ├── test │ ├── resources │ │ ├── search │ │ │ ├── f1.xml │ │ │ ├── f2.xml │ │ │ └── subfolder │ │ │ │ ├── g1.xml │ │ │ │ └── g2.xml │ │ ├── name_missing_in_package.xml │ │ ├── sourcefile_not_within_package.xml │ │ ├── line_not_within_sourcefile.xml │ │ ├── line_without_mi_ci_mb_cb.xml │ │ ├── nr_missing_in_line.xml │ │ ├── invalid_nr_in_line.xml │ │ ├── simple.xml │ │ ├── name_missing_in_sourcefile.xml │ │ ├── invalid_ci_in_line.xml │ │ └── invalid_line_number.xml │ └── java │ │ └── org │ │ └── sonar │ │ └── plugins │ │ └── jacoco │ │ ├── SensorUtilsTest.java │ │ ├── JacocoPluginTest.java │ │ ├── ReportImporterTest.java │ │ ├── JacocoAggregateSensorTest.java │ │ ├── FileLocatorTest.java │ │ ├── WildcardPatternFileScannerTest.java │ │ ├── JacocoSensorTest.java │ │ ├── XmlReportParserTest.java │ │ ├── ReportPathsProviderTest.java │ │ └── KotlinFileLocatorTest.java └── main │ └── java │ └── org │ └── sonar │ └── plugins │ └── jacoco │ ├── package-info.java │ ├── SensorUtils.java │ ├── ReportImporter.java │ ├── ReversePathTree.java │ ├── JacocoPlugin.java │ ├── FileLocator.java │ ├── JacocoAggregateSensor.java │ ├── JacocoSensor.java │ ├── ReportPathsProvider.java │ ├── KotlinFileLocator.java │ ├── WildcardPatternFileScanner.java │ └── XmlReportParser.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── settings.gradle ├── .gitignore ├── LICENSE_HEADER ├── SECURITY.md ├── README.md ├── renovate.json ├── gradlew.bat ├── LICENSE └── gradlew /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | java = "17.0" 3 | gradle = "9.2" 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | .github/CODEOWNERS @SonarSource/quality-jvm-squad 2 | -------------------------------------------------------------------------------- /its/src/test/resources/aggregate-and-module-based-mixed-coverage/.gitignore: -------------------------------------------------------------------------------- 1 | **/target/** -------------------------------------------------------------------------------- /src/test/resources/search/f1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/test/resources/search/f2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-jacoco/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/test/resources/search/subfolder/g1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/test/resources/search/subfolder/g2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /its/src/test/resources/simple-project-jacoco/jacoco.exec: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SonarSource/sonar-jacoco/HEAD/its/src/test/resources/simple-project-jacoco/jacoco.exec -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | group=org.sonarsource.jacoco 2 | version=1.5.0-SNAPSHOT 3 | description=SonarQube plugin to import JaCoCo XML coverage reports 4 | projectTitle=SonarQube JaCoCo plugin 5 | -------------------------------------------------------------------------------- /its/src/test/resources/aggregate-and-module-based-mixed-coverage/self-covered/src/main/java/org/example/Squarer.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | public class Squarer { 4 | public int square(int a) { 5 | return a * a; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/test/resources/name_missing_in_package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /its/src/test/resources/kotlin-jacoco-project/src/main/kotlin/CoverMe.kt: -------------------------------------------------------------------------------- 1 | package org.example.cover 2 | 3 | class CoverMe { 4 | 5 | fun f(a: Int) { 6 | if (a < 5) { 7 | println("Hello, World!") 8 | } 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /its/src/test/resources/kotlin-jacoco-project/src/test/kotlin/CoverMeTest.kt: -------------------------------------------------------------------------------- 1 | package org.example.cover 2 | 3 | import org.junit.Test 4 | 5 | class CoverMeTest { 6 | 7 | @Test 8 | fun test() { 9 | val coverMe = CoverMe() 10 | coverMe.f(10) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /its/src/test/resources/aggregate-and-module-based-mixed-coverage/library/src/main/java/org/example/Library.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | public class Library { 4 | public Integer div(int a, int b) { 5 | if (b == 0) { 6 | return null; 7 | } 8 | return a / b; 9 | } 10 | } -------------------------------------------------------------------------------- /.github/workflows/pr-cleanup.yml: -------------------------------------------------------------------------------- 1 | name: Cleanup PR Resources 2 | on: 3 | pull_request: 4 | types: [closed] 5 | 6 | jobs: 7 | cleanup: 8 | runs-on: github-ubuntu-latest-s 9 | permissions: 10 | actions: write 11 | steps: 12 | - uses: SonarSource/ci-github-actions/pr_cleanup@v1 13 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/test/resources/sourcefile_not_within_package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | dependencyResolutionManagement { 9 | repositories { 10 | mavenCentral() 11 | } 12 | } 13 | 14 | rootProject.name = 'sonar-jacoco-plugin' 15 | 16 | include 'its' 17 | 18 | -------------------------------------------------------------------------------- /src/test/resources/line_not_within_sourcefile.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Part of 2 | 8 | -------------------------------------------------------------------------------- /src/test/resources/line_without_mi_ci_mb_cb.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/test/resources/nr_missing_in_line.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/test/resources/invalid_nr_in_line.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/test/resources/simple.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/test/resources/name_missing_in_sourcefile.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /its/src/test/resources/aggregate-and-module-based-mixed-coverage/library-test/src/test/java/org/example/LibraryTest.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | class LibraryTest { 7 | @Test 8 | void incompleteTest() { 9 | Library library = new Library(); 10 | Assertions.assertEquals(2, library.div(2, 1)); 11 | } 12 | } -------------------------------------------------------------------------------- /src/test/resources/invalid_ci_in_line.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/test/resources/invalid_line_number.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /its/src/test/resources/aggregate-and-module-based-mixed-coverage/library/src/test/java/org/example/LibraryTest.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | class LibraryTest { 7 | @Test 8 | void returns_null_when_dividing_by_zero() { 9 | Library library = new Library(); 10 | Assertions.assertNull(library.div(2, 0)); 11 | } 12 | } -------------------------------------------------------------------------------- /its/src/test/resources/aggregate-and-module-based-mixed-coverage/self-covered/src/test/java/org/example/SquarerTest.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | class SquarerTest { 7 | @Test 8 | void returns_squared_value() { 9 | Squarer squarer = new Squarer(); 10 | Assertions.assertEquals(4, squarer.square(2)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /its/src/test/resources/simple-project-jacoco/src/main/java/org/sonarsource/test/CalcNoCoverage.java: -------------------------------------------------------------------------------- 1 | package org.sonarsource.test; 2 | 3 | public class CalcNoCoverage { 4 | private int acc; 5 | 6 | public CalcNoCoverage(int initial) { 7 | this.acc = initial; 8 | } 9 | 10 | public int add(int add) { 11 | acc = acc+add; 12 | if (acc < 0) { 13 | acc = 0; 14 | } 15 | return acc; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /its/src/test/resources/simple-project-jacoco/jacoco-with-invalid-format.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/releasability.yaml: -------------------------------------------------------------------------------- 1 | name: Releasability status 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | update_releasability_status: 8 | runs-on: github-ubuntu-latest-s 9 | name: Releasability status 10 | permissions: 11 | id-token: write 12 | statuses: write 13 | contents: read 14 | steps: 15 | - uses: SonarSource/gh-action_releasability/releasability-status@v3 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---- gradle 2 | build 3 | bin 4 | .gradle 5 | 6 | # ---- IntelliJ IDEA 7 | *.iws 8 | *.iml 9 | *.ipr 10 | .idea/ 11 | out 12 | 13 | # ---- Eclipse 14 | .classpath 15 | .project 16 | .settings 17 | .externalToolBuilders 18 | 19 | # ---- Mac OS X 20 | .DS_Store 21 | Icon? 22 | # Thumbnails 23 | ._* 24 | # Files that might appear on external disk 25 | .Spotlight-V100 26 | .Trashes 27 | 28 | # ---- Windows 29 | # Windows image file caches 30 | Thumbs.db 31 | # Folder config file 32 | Desktop.ini 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: sonar-release 3 | # This workflow is triggered when publishing a new github release 4 | # yamllint disable-line rule:truthy 5 | on: 6 | release: 7 | types: 8 | - published 9 | 10 | jobs: 11 | release: 12 | permissions: 13 | id-token: write 14 | contents: write 15 | uses: SonarSource/gh-action_release/.github/workflows/main.yaml@v6 16 | with: 17 | publishToBinaries: true 18 | mavenCentralSync: true 19 | slackChannel: squad-jvm-notifs 20 | -------------------------------------------------------------------------------- /its/src/test/resources/simple-project-jacoco/src/main/java/org/sonarsource/test/Calc.java: -------------------------------------------------------------------------------- 1 | package org.sonarsource.test; 2 | 3 | public class Calc { 4 | private int acc; 5 | 6 | public Calc(int initial) { 7 | this.acc = initial; 8 | } 9 | 10 | public int add(int add) { 11 | acc = acc+add; 12 | if (acc < 0) { 13 | acc = 0; 14 | } 15 | return acc; 16 | } 17 | 18 | public int subtract(int sub) { 19 | acc = acc - sub; 20 | if (acc > 0) { 21 | acc = 0; 22 | } 23 | return acc; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /its/src/test/resources/aggregate-and-module-based-mixed-coverage/README.md: -------------------------------------------------------------------------------- 1 | # Aggregate Maven project 2 | 3 | A project with 4 modules: 4 | 5 | 1. [library](./library) - containing code but no tests 6 | 2. [library.test](./library.test) - containing test code that uses code from `library` 7 | 3. [report](./report) - generating the aggregate coverage report 8 | 4. [self-covered](./self-covered) - containing code, tests and generating its own module-based coverage report 9 | 10 | 11 | The report can be generated by running the following command: 12 | ```shell 13 | mvn verify --file ./pom.xml 14 | ``` -------------------------------------------------------------------------------- /its/src/test/resources/simple-project-jacoco/src/test/java/org/sonarsource/test/CalcTest.java: -------------------------------------------------------------------------------- 1 | package org.sonarsource.test; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | public class CalcTest { 7 | private Calc calc = new Calc(5); 8 | 9 | @Test 10 | public void should_add() { 11 | Assert.assertEquals(9, calc.add(4)); 12 | } 13 | 14 | @Test 15 | public void should_subtract() { 16 | Assert.assertEquals(-2, calc.subtract(7)); 17 | } 18 | 19 | @Test 20 | public void should_add_and_return_0_if_sum_negative() { 21 | Assert.assertEquals(0, calc.add(-9)); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/mark-prs-stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale PRs' 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '30 2 * * *' 6 | 7 | jobs: 8 | stale: 9 | runs-on: github-ubuntu-latest-s 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | steps: 14 | - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10 15 | with: 16 | stale-pr-message: 'This PR is stale because it has been open 7 days with no activity. If there is no activity in the next 7 days it will be closed automatically' 17 | stale-pr-label: 'stale' 18 | days-before-stale: 7 19 | days-before-close: 7 20 | exempt-pr-labels: 'do-not-close,External Contribution' 21 | -------------------------------------------------------------------------------- /its/src/test/resources/simple-project-jacoco/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.sonarsource.test 8 | test-jacoco-plugin 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | junit 14 | junit 15 | 4.13.1 16 | test 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /LICENSE_HEADER: -------------------------------------------------------------------------------- 1 | SonarQube JaCoCo Plugin 2 | Copyright (C) 2018-${year} SonarSource SA 3 | mailto:info AT sonarsource DOT com 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 3 of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public License 16 | along with this program; if not, write to the Free Software Foundation, 17 | Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | -------------------------------------------------------------------------------- /.github/workflows/ToggleLockBranch.yml: -------------------------------------------------------------------------------- 1 | name: Toggle lock branch 2 | 3 | on: 4 | workflow_dispatch: # Triggered manually from the GitHub UI / Actions 5 | 6 | jobs: 7 | ToggleLockBranch_job: 8 | name: Toggle lock branch 9 | runs-on: github-ubuntu-latest-s 10 | permissions: 11 | id-token: write 12 | steps: 13 | - id: secrets 14 | uses: SonarSource/vault-action-wrapper@v3 15 | with: 16 | secrets: | 17 | development/github/token/{REPO_OWNER_NAME_DASH}-lock token | lock_token; 18 | development/kv/data/slack token | slack_api_token; 19 | - uses: sonarsource/gh-action-lt-backlog/ToggleLockBranch@v2 20 | with: 21 | github-token: ${{ fromJSON(steps.secrets.outputs.vault).lock_token }} 22 | slack-token: ${{ fromJSON(steps.secrets.outputs.vault).slack_api_token }} 23 | slack-channel: squad-jvm-notifs 24 | -------------------------------------------------------------------------------- /its/src/test/resources/aggregate-and-module-based-mixed-coverage/library/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.example 8 | aggregate-and-module-based-mixed-coverage 9 | 1.0-SNAPSHOT 10 | 11 | 12 | library 13 | jar 14 | 15 | 16 | 17 | org.junit.jupiter 18 | junit-jupiter-api 19 | 6.0.1 20 | test 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/PullRequestClosed.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Closed 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | 7 | jobs: 8 | PullRequestMerged_job: 9 | name: Pull Request Merged 10 | runs-on: github-ubuntu-latest-s 11 | permissions: 12 | id-token: write 13 | pull-requests: read 14 | # For external PR, ticket should be moved manually 15 | if: | 16 | github.event.pull_request.head.repo.full_name == github.repository 17 | steps: 18 | - id: secrets 19 | uses: SonarSource/vault-action-wrapper@v3 20 | with: 21 | secrets: | 22 | development/kv/data/jira user | JIRA_USER; 23 | development/kv/data/jira token | JIRA_TOKEN; 24 | - uses: sonarsource/gh-action-lt-backlog/PullRequestClosed@v2 25 | with: 26 | github-token: ${{secrets.GITHUB_TOKEN}} 27 | jira-user: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }} 28 | jira-token: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }} 29 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/jacoco/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | @javax.annotation.ParametersAreNonnullByDefault 21 | package org.sonar.plugins.jacoco; 22 | -------------------------------------------------------------------------------- /.github/workflows/RequestReview.yml: -------------------------------------------------------------------------------- 1 | name: Request review 2 | 3 | on: 4 | pull_request: 5 | types: ["review_requested"] 6 | 7 | jobs: 8 | RequestReview_job: 9 | name: Request review 10 | runs-on: github-ubuntu-latest-s 11 | permissions: 12 | id-token: write 13 | # For external PR, ticket should be moved manually 14 | if: | 15 | github.event.pull_request.head.repo.full_name == github.repository 16 | steps: 17 | - id: secrets 18 | uses: SonarSource/vault-action-wrapper@v3 19 | with: 20 | secrets: | 21 | development/github/token/{REPO_OWNER_NAME_DASH}-jira token | GITHUB_TOKEN; 22 | development/kv/data/jira user | JIRA_USER; 23 | development/kv/data/jira token | JIRA_TOKEN; 24 | - uses: sonarsource/gh-action-lt-backlog/RequestReview@v2 25 | with: 26 | github-token: ${{ fromJSON(steps.secrets.outputs.vault).GITHUB_TOKEN }} 27 | jira-user: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }} 28 | jira-token: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/SubmitReview.yml: -------------------------------------------------------------------------------- 1 | name: Submit Review 2 | 3 | on: 4 | pull_request_review: 5 | types: [submitted] 6 | 7 | jobs: 8 | SubmitReview_job: 9 | name: Submit Review 10 | runs-on: github-ubuntu-latest-s 11 | permissions: 12 | id-token: write 13 | pull-requests: read 14 | # For external PR, ticket should be moved manually 15 | if: | 16 | github.event.pull_request.head.repo.full_name == github.repository 17 | && (github.event.review.state == 'changes_requested' 18 | || github.event.review.state == 'approved') 19 | steps: 20 | - id: secrets 21 | uses: SonarSource/vault-action-wrapper@v3 22 | with: 23 | secrets: | 24 | development/kv/data/jira user | JIRA_USER; 25 | development/kv/data/jira token | JIRA_TOKEN; 26 | - uses: sonarsource/gh-action-lt-backlog/SubmitReview@v2 27 | with: 28 | github-token: ${{secrets.GITHUB_TOKEN}} 29 | jira-user: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }} 30 | jira-token: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/PullRequestCreated.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Created 2 | 3 | on: 4 | pull_request: 5 | types: ["opened"] 6 | 7 | jobs: 8 | PullRequestCreated_job: 9 | name: Pull Request Created 10 | runs-on: github-ubuntu-latest-s 11 | permissions: 12 | id-token: write 13 | # For external PR, ticket should be created manually 14 | if: | 15 | github.event.pull_request.head.repo.full_name == github.repository 16 | steps: 17 | - id: secrets 18 | uses: SonarSource/vault-action-wrapper@v3 19 | with: 20 | secrets: | 21 | development/github/token/{REPO_OWNER_NAME_DASH}-jira token | GITHUB_TOKEN; 22 | development/kv/data/jira user | JIRA_USER; 23 | development/kv/data/jira token | JIRA_TOKEN; 24 | - uses: sonarsource/gh-action-lt-backlog/PullRequestCreated@v2 25 | with: 26 | github-token: ${{ fromJSON(steps.secrets.outputs.vault).GITHUB_TOKEN }} 27 | jira-user: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }} 28 | jira-token: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }} 29 | jira-project: JACOCO 30 | -------------------------------------------------------------------------------- /its/src/test/resources/aggregate-and-module-based-mixed-coverage/library-test/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.example 8 | aggregate-and-module-based-mixed-coverage 9 | 1.0-SNAPSHOT 10 | 11 | 12 | library-test 13 | jar 14 | 15 | 16 | 17 | org.example 18 | library 19 | ${project.version} 20 | test 21 | 22 | 23 | org.junit.jupiter 24 | junit-jupiter-api 25 | 6.0.1 26 | test 27 | 28 | 29 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | A mature software vulnerability treatment process is a cornerstone of a robust information security management system. Contributions from the community play an important role in the evolution and security of our products, and in safeguarding the security and privacy of our users. 4 | 5 | If you believe you have discovered a security vulnerability in Sonar's products, we encourage you to report it immediately. 6 | 7 | To responsibly report a security issue, please email us at [security@sonarsource.com](mailto:security@sonarsource.com). Sonar’s security team will acknowledge your report, guide you through the next steps, or request additional information if necessary. Customers with a support contract can also report the vulnerability directly through the support channel. 8 | 9 | For security vulnerabilities found in third-party libraries, please also contact the library's owner or maintainer directly. 10 | 11 | ## Responsible Disclosure Policy 12 | 13 | For more information about disclosing a security vulnerability to Sonar, please refer to our community post: [Responsible Vulnerability Disclosure](https://community.sonarsource.com/t/responsible-vulnerability-disclosure/9317). -------------------------------------------------------------------------------- /.github/workflows/unified-dogfooding.yml: -------------------------------------------------------------------------------- 1 | name: Unified Dogfooding scans 2 | on: 3 | schedule: 4 | - cron: '45 3 * * *' # Run the workflow every day at 03:45 UTC 5 | workflow_dispatch: 6 | 7 | jobs: 8 | unified-platform-dogfooding: 9 | runs-on: github-ubuntu-latest-s 10 | name: Unified Platform Dogfooding 11 | permissions: 12 | id-token: write 13 | contents: read 14 | steps: 15 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 16 | - uses: SonarSource/ci-github-actions/build-gradle@v1 17 | with: 18 | run-shadow-scans: true 19 | artifactory-reader-role: private-reader 20 | artifactory-deployer-role: qa-deployer 21 | deploy-pull-request: false 22 | - name: Run IRIS Analysis 23 | uses: SonarSource/unified-dogfooding-actions/run-iris@v1 24 | with: 25 | primary_project_key: "org.sonarsource.jacoco:sonar-jacoco" 26 | primary_platform: "Next" 27 | shadow1_project_key: "org.sonarsource.jacoco:sonar-jacoco" 28 | shadow1_platform: "SQC-EU" 29 | shadow2_project_key: "org.sonarsource.jacoco:sonar-jacoco" 30 | shadow2_platform: "SQC-US" 31 | -------------------------------------------------------------------------------- /its/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "java" 3 | } 4 | 5 | description = 'SonarQube JaCoCo :: Integration Tests' 6 | 7 | dependencies { 8 | testImplementation(platform('org.junit:junit-bom:5.6.2')) 9 | testImplementation('org.junit.jupiter:junit-jupiter') 10 | testImplementation('org.junit.jupiter:junit-jupiter-migrationsupport') 11 | testRuntimeOnly('org.junit.platform:junit-platform-launcher') 12 | testImplementation('org.mockito:mockito-core:1.10.19') 13 | testImplementation('org.assertj:assertj-core:3.10.0') 14 | testImplementation('org.sonarsource.orchestrator:sonar-orchestrator-junit4:6.0.1.3892') 15 | testImplementation('org.sonarsource.sonarqube:sonar-ws:6.7') 16 | testImplementation('com.google.code.findbugs:jsr305:3.0.2') 17 | } 18 | 19 | test { 20 | useJUnitPlatform() 21 | } 22 | 23 | sonarqube.skipProject = true 24 | 25 | task integrationTest(type: Test) { 26 | systemProperty 'java.awt.headless', 'true' 27 | 28 | def orchestratorProps = System.getProperties().findAll { it.key.startsWith("orchestrator") || it.key.startsWith("sonar") }.collect { it.key } 29 | systemProperties System.getProperties().subMap(orchestratorProps) 30 | } 31 | 32 | test.onlyIf { project.hasProperty('integrationTests') && project.getProperty('integrationTests') } 33 | 34 | 35 | -------------------------------------------------------------------------------- /its/src/test/resources/aggregate-and-module-based-mixed-coverage/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.example 7 | aggregate-and-module-based-mixed-coverage 8 | 1.0-SNAPSHOT 9 | 10 | pom 11 | 12 | 13 | library 14 | library-test 15 | report 16 | self-covered 17 | 18 | 19 | 20 | 21 | 22 | org.jacoco 23 | jacoco-maven-plugin 24 | 0.8.14 25 | 26 | 27 | prepare-agent 28 | 29 | prepare-agent 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SonarQube JaCoCo Plugin 2 | 3 | [![Build](https://github.com/SonarSource/sonar-jacoco/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/SonarSource/sonar-jacoco/actions/workflows/build.yml) 4 | [![Quality gate](https://next.sonarqube.com/sonarqube/api/project_badges/measure?project=org.sonarsource.jacoco%3Asonar-jacoco&metric=alert_status)](https://next.sonarqube.com/sonarqube/dashboard?id=org.sonarsource.jacoco%3Asonar-jacoco) 5 | [![Coverage](https://next.sonarqube.com/sonarqube/api/project_badges/measure?project=org.sonarsource.jacoco%3Asonar-jacoco&metric=coverage)](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.jacoco%3Asonar-jacoco&metric=coverage) 6 | 7 | 8 | ### How to use? 9 | 10 | Have a look at [importing JaCoCo coverage report in XML format](https://community.sonarsource.com/t/coverage-test-data-importing-jacoco-coverage-report-in-xml-format/12151) guide. 11 | 12 | 13 | ### Have question or feedback? 14 | 15 | 16 | To provide feedback (request a feature, report a bug etc.) use the [SonarQube Community Forum](https://community.sonarsource.com/). Please do not forget to specify the language and tag 'jacoco', plugin version and SonarQube version. 17 | 18 | If you have a question on how to use plugin (and the [guide](https://community.sonarsource.com/t/coverage-test-data-importing-jacoco-coverage-report-in-xml-format/12151) doesn't help you), we also encourage you to use the community forum. 19 | 20 | ### License 21 | 22 | Copyright 2018-2025 SonarSource. 23 | 24 | Licensed under the [GNU Lesser General Public License, Version 3.0](http://www.gnu.org/licenses/lgpl.txt) 25 | -------------------------------------------------------------------------------- /its/src/test/resources/aggregate-and-module-based-mixed-coverage/report/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.example 8 | aggregate-and-module-based-mixed-coverage 9 | 1.0-SNAPSHOT 10 | 11 | 12 | report 13 | jar 14 | 15 | 16 | 17 | org.example 18 | library 19 | ${project.version} 20 | compile 21 | 22 | 23 | org.example 24 | library-test 25 | ${project.version} 26 | test 27 | 28 | 29 | 30 | 31 | 32 | 33 | org.jacoco 34 | jacoco-maven-plugin 35 | 36 | 37 | report-aggregate 38 | verify 39 | 40 | report-aggregate 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>SonarSource/renovate-config:languages-team" 5 | ], 6 | "schedule": [ 7 | "before 4am on Monday" 8 | ], 9 | "rebaseWhen": "conflicted", 10 | "enabledManagers": [ 11 | "gradle", 12 | "gradle-wrapper", 13 | "github-actions" 14 | ], 15 | "packageRules": [ 16 | { 17 | "matchManagers": [ 18 | "github-actions" 19 | ], 20 | "matchPackageNames": [ 21 | "SonarSource/*" 22 | ], 23 | "pinDigests": false, 24 | "groupName": "all Sonar GitHub Actions", 25 | "groupSlug": "all-sonar-github-actions" 26 | }, 27 | { 28 | "matchManagers": [ 29 | "github-actions" 30 | ], 31 | "matchPackageNames": [ 32 | "!SonarSource/*" 33 | ], 34 | "pinDigests": true, 35 | "groupName": "all third-party GitHub Actions", 36 | "groupSlug": "all-3rd-party-github-actions" 37 | }, 38 | { 39 | "matchManagers": [ 40 | "gradle" 41 | ], 42 | "matchPackageNames": [ 43 | "!org.sonarsource.api.plugin:sonar-plugin-api*" 44 | ], 45 | "matchUpdateTypes": [ 46 | "minor", 47 | "patch" 48 | ], 49 | "groupName": "all non-major dependencies", 50 | "groupSlug": "all-minor-patch" 51 | }, 52 | { 53 | "matchManagers": [ 54 | "gradle" 55 | ], 56 | "matchPackageNames": [ 57 | "org.sonarsource.api.plugin:sonar-plugin-api*" 58 | ], 59 | "groupName": "sonar-plugin-api", 60 | "groupSlug": "sonar-plugin-api", 61 | "prHeader": "**Before updating the plugin-api version, make sure to check the [compatibility matrix](https://github.com/SonarSource/sonar-plugin-api?tab=readme-ov-file#compatibility) and stick to the lowest denominator.**" 62 | } 63 | ], 64 | "reviewers": [ 65 | "team:quality-jvm-squad" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/jacoco/SensorUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.util.List; 23 | import org.sonar.api.batch.fs.InputFile; 24 | import org.sonar.api.utils.log.Logger; 25 | 26 | class SensorUtils { 27 | private SensorUtils() { 28 | /* This class should not be instantiated */ 29 | } 30 | 31 | static void importReport(XmlReportParser reportParser, FileLocator locator, ReportImporter importer, Logger logger) { 32 | List sourceFiles = reportParser.parse(); 33 | 34 | for (XmlReportParser.SourceFile sourceFile : sourceFiles) { 35 | InputFile inputFile = locator.getInputFile(sourceFile.packageName(), sourceFile.name()); 36 | if (inputFile == null) { 37 | continue; 38 | } 39 | 40 | try { 41 | importer.importCoverage(sourceFile, inputFile); 42 | } catch (IllegalStateException e) { 43 | logger.error("Cannot import coverage information for file '{}', coverage data is invalid. Error: {}", inputFile, e); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /its/src/test/resources/aggregate-and-module-based-mixed-coverage/self-covered/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.example 8 | aggregate-and-module-based-mixed-coverage 9 | 1.0-SNAPSHOT 10 | 11 | 12 | self-covered 13 | jar 14 | 15 | 16 | 17 | org.junit.jupiter 18 | junit-jupiter-api 19 | 6.0.1 20 | test 21 | 22 | 23 | 24 | 25 | 26 | 27 | org.jacoco 28 | jacoco-maven-plugin 29 | 0.8.14 30 | 31 | 32 | prepare-agent 33 | 34 | prepare-agent 35 | 36 | 37 | 38 | report 39 | 40 | report 41 | 42 | 43 | 44 | XML 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/jacoco/ReportImporter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import org.sonar.api.batch.fs.InputFile; 23 | import org.sonar.api.batch.sensor.SensorContext; 24 | import org.sonar.api.batch.sensor.coverage.NewCoverage; 25 | 26 | public class ReportImporter { 27 | private final SensorContext ctx; 28 | 29 | public ReportImporter(SensorContext ctx) { 30 | this.ctx = ctx; 31 | } 32 | 33 | public void importCoverage(XmlReportParser.SourceFile sourceFile, InputFile inputFile) { 34 | NewCoverage newCoverage = ctx.newCoverage() 35 | .onFile(inputFile); 36 | 37 | for (XmlReportParser.Line line : sourceFile.lines()) { 38 | boolean conditions = false; 39 | if (line.coveredBranches() > 0 || line.missedBranches() > 0) { 40 | int branches = line.coveredBranches() + line.missedBranches(); 41 | newCoverage.conditions(line.number(), branches, line.coveredBranches()); 42 | conditions = true; 43 | } 44 | if (conditions || line.coveredInstrs() > 0 || line.missedInstrs() > 0) { 45 | newCoverage.lineHits(line.number(), line.coveredInstrs() > 0 ? 1 : 0); 46 | } 47 | } 48 | 49 | newCoverage.save(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/jacoco/ReversePathTree.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.util.LinkedHashMap; 23 | import java.util.Map; 24 | import org.sonar.api.batch.fs.InputFile; 25 | 26 | public class ReversePathTree { 27 | private Node root = new Node(); 28 | 29 | public void index(InputFile inputFile, String[] path) { 30 | Node currentNode = root; 31 | for (int i = path.length - 1; i >= 0; i--) { 32 | currentNode = currentNode.children.computeIfAbsent(path[i], e -> new Node()); 33 | } 34 | currentNode.file = inputFile; 35 | } 36 | 37 | public InputFile getFileWithSuffix(String[] path) { 38 | Node currentNode = root; 39 | 40 | for (int i = path.length - 1; i >= 0; i--) { 41 | currentNode = currentNode.children.get(path[i]); 42 | if (currentNode == null) { 43 | return null; 44 | } 45 | } 46 | return getFirstLeaf(currentNode); 47 | } 48 | 49 | private static InputFile getFirstLeaf(Node node) { 50 | while (!node.children.isEmpty()) { 51 | node = node.children.values().iterator().next(); 52 | } 53 | return node.file; 54 | } 55 | 56 | static class Node { 57 | Map children = new LinkedHashMap<>(); 58 | InputFile file = null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/jacoco/JacocoPlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import org.sonar.api.Plugin; 23 | import org.sonar.api.PropertyType; 24 | import org.sonar.api.config.PropertyDefinition; 25 | import org.sonar.api.resources.Qualifiers; 26 | 27 | public class JacocoPlugin implements Plugin { 28 | @Override 29 | public void define(Context context) { 30 | context.addExtension(JacocoSensor.class); 31 | context.addExtension(PropertyDefinition.builder(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY) 32 | .onQualifiers(Qualifiers.PROJECT) 33 | .multiValues(true) 34 | .category("JaCoCo") 35 | .description("Paths to JaCoCo XML coverage report files. Each path can be either absolute or relative" + 36 | " to the project base directory. Wildcard patterns are accepted (*, ** and ?).") 37 | .build()); 38 | 39 | context.addExtension(JacocoAggregateSensor.class); 40 | context.addExtension(PropertyDefinition.builder(ReportPathsProvider.AGGREGATE_REPORT_PATH_PROPERTY_KEY) 41 | .onQualifiers(Qualifiers.PROJECT) 42 | .type(PropertyType.STRING) 43 | .multiValues(false) 44 | .category("JaCoCo") 45 | .description("Single path to aggregate XML coverage report file.") 46 | .build()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/jacoco/FileLocator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.util.List; 23 | import java.util.stream.Collectors; 24 | import java.util.stream.StreamSupport; 25 | import javax.annotation.CheckForNull; 26 | import org.sonar.api.batch.fs.InputFile; 27 | 28 | public class FileLocator { 29 | private final ReversePathTree tree = new ReversePathTree(); 30 | private final KotlinFileLocator kotlinFileLocator; 31 | 32 | public FileLocator(Iterable inputFiles, KotlinFileLocator kotlinFileLocator) { 33 | this(StreamSupport.stream(inputFiles.spliterator(), false).collect(Collectors.toList()), kotlinFileLocator); 34 | } 35 | 36 | public FileLocator(List inputFiles, KotlinFileLocator kotlinFileLocator) { 37 | this.kotlinFileLocator = kotlinFileLocator; 38 | for (InputFile inputFile : inputFiles) { 39 | String[] path = inputFile.relativePath().split("/"); 40 | tree.index(inputFile, path); 41 | } 42 | } 43 | 44 | @CheckForNull 45 | public InputFile getInputFile(String packagePath, String fileName) { 46 | String filePath = packagePath.isEmpty() ? fileName : (packagePath + "/" + fileName); 47 | String[] path = filePath.split("/"); 48 | InputFile fileWithSuffix = tree.getFileWithSuffix(path); 49 | if (fileWithSuffix == null && fileName.endsWith(".kt")) { 50 | fileWithSuffix = kotlinFileLocator.getInputFile(packagePath, fileName); 51 | } 52 | 53 | return fileWithSuffix; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/jacoco/SensorUtilsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.util.Collections; 23 | import org.junit.jupiter.api.Test; 24 | import org.sonar.api.batch.fs.InputFile; 25 | 26 | import static org.mockito.Mockito.mock; 27 | import static org.mockito.Mockito.verify; 28 | import static org.mockito.Mockito.verifyNoInteractions; 29 | import static org.mockito.Mockito.when; 30 | 31 | class SensorUtilsTest { 32 | 33 | @Test 34 | void import_coverage() { 35 | FileLocator locator = mock(FileLocator.class); 36 | ReportImporter importer = mock(ReportImporter.class); 37 | XmlReportParser parser = mock(XmlReportParser.class); 38 | InputFile inputFile = mock(InputFile.class); 39 | 40 | XmlReportParser.SourceFile sourceFile = new XmlReportParser.SourceFile("package", "File.java"); 41 | sourceFile.lines().add(new XmlReportParser.Line(1, 0, 1, 0, 0)); 42 | 43 | when(parser.parse()).thenReturn(Collections.singletonList(sourceFile)); 44 | when(locator.getInputFile("package", "File.java")).thenReturn(inputFile); 45 | 46 | SensorUtils.importReport(parser, locator, importer, null); 47 | 48 | verify(importer).importCoverage(sourceFile, inputFile); 49 | } 50 | 51 | @Test 52 | void do_nothing_if_file_not_found() { 53 | FileLocator locator = mock(FileLocator.class); 54 | ReportImporter importer = mock(ReportImporter.class); 55 | XmlReportParser parser = mock(XmlReportParser.class); 56 | XmlReportParser.SourceFile sourceFile = mock(XmlReportParser.SourceFile.class); 57 | 58 | when(parser.parse()).thenReturn(Collections.singletonList(sourceFile)); 59 | SensorUtils.importReport(parser, locator, importer, null); 60 | 61 | verifyNoInteractions(importer); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/jacoco/JacocoAggregateSensor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.io.FileNotFoundException; 23 | import java.nio.file.Path; 24 | import java.util.stream.Stream; 25 | import java.util.stream.StreamSupport; 26 | import org.sonar.api.batch.fs.InputFile; 27 | import org.sonar.api.batch.sensor.SensorContext; 28 | import org.sonar.api.batch.sensor.SensorDescriptor; 29 | import org.sonar.api.scanner.sensor.ProjectSensor; 30 | import org.sonar.api.utils.log.Logger; 31 | import org.sonar.api.utils.log.Loggers; 32 | 33 | public class JacocoAggregateSensor implements ProjectSensor { 34 | private static final Logger LOG = Loggers.get(JacocoAggregateSensor.class); 35 | 36 | @Override 37 | public void describe(SensorDescriptor descriptor) { 38 | descriptor.name("JaCoCo Aggregate XML Report Importer"); 39 | } 40 | 41 | @Override 42 | public void execute(SensorContext context) { 43 | Path reportPath = null; 44 | try { 45 | reportPath = new ReportPathsProvider(context).getAggregateReportPath(); 46 | } catch (FileNotFoundException e) { 47 | LOG.error(String.format("The aggregate JaCoCo sensor will stop: %s", e.getMessage())); 48 | return; 49 | } 50 | if (reportPath == null) { 51 | LOG.debug("No aggregate XML report found. No coverage coverage information will be added at project level."); 52 | return; 53 | } 54 | Iterable inputFiles = context.fileSystem().inputFiles(context.fileSystem().predicates().all()); 55 | Stream kotlinInputFileStream = StreamSupport.stream(inputFiles.spliterator(), false).filter(f -> "kotlin".equals(f.language())); 56 | FileLocator locator = new FileLocator(inputFiles, new KotlinFileLocator(kotlinInputFileStream)); 57 | ReportImporter importer = new ReportImporter(context); 58 | 59 | LOG.info("Importing aggregate report {}.", reportPath); 60 | SensorUtils.importReport(new XmlReportParser(reportPath), locator, importer, LOG); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/jacoco/JacocoPluginTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import org.junit.jupiter.api.Test; 23 | import org.mockito.ArgumentCaptor; 24 | import org.sonar.api.Plugin; 25 | import org.sonar.api.PropertyType; 26 | import org.sonar.api.config.PropertyDefinition; 27 | import org.sonar.api.resources.Qualifiers; 28 | 29 | import static org.assertj.core.api.Assertions.assertThat; 30 | import static org.mockito.Mockito.mock; 31 | import static org.mockito.Mockito.times; 32 | import static org.mockito.Mockito.verify; 33 | import static org.mockito.Mockito.verifyNoMoreInteractions; 34 | 35 | class JacocoPluginTest { 36 | private JacocoPlugin plugin = new JacocoPlugin(); 37 | private Plugin.Context ctx = mock(Plugin.Context.class); 38 | @Test 39 | void should_add_sensors_and_property_definitions() { 40 | plugin.define(ctx); 41 | 42 | ArgumentCaptor arg = ArgumentCaptor.forClass(Object.class); 43 | verify(ctx, times(4)).addExtension(arg.capture()); 44 | verifyNoMoreInteractions(ctx); 45 | 46 | assertThat(arg.getAllValues().get(0)).isEqualTo(JacocoSensor.class); 47 | assertThat(arg.getAllValues().get(1)).isInstanceOf(PropertyDefinition.class); 48 | PropertyDefinition multiValueReportPaths = (PropertyDefinition) arg.getAllValues().get(1); 49 | assertThat(multiValueReportPaths.key()).isEqualTo("sonar.coverage.jacoco.xmlReportPaths"); 50 | assertThat(multiValueReportPaths.multiValues()).isTrue(); 51 | assertThat(multiValueReportPaths.category()).isEqualTo("JaCoCo"); 52 | assertThat(multiValueReportPaths.qualifiers()).containsOnly(Qualifiers.PROJECT); 53 | 54 | assertThat(arg.getAllValues().get(2)).isEqualTo(JacocoAggregateSensor.class); 55 | PropertyDefinition aggregateReportPath = (PropertyDefinition) arg.getAllValues().get(3); 56 | assertThat(aggregateReportPath.key()).isEqualTo("sonar.coverage.jacoco.aggregateXmlReportPath"); 57 | assertThat(aggregateReportPath.type()).isEqualTo(PropertyType.STRING); 58 | assertThat(aggregateReportPath.multiValues()).isFalse(); 59 | assertThat(aggregateReportPath.category()).isEqualTo("JaCoCo"); 60 | assertThat(aggregateReportPath.qualifiers()).containsOnly(Qualifiers.PROJECT); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/jacoco/JacocoSensor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.nio.file.Path; 23 | import java.util.Collection; 24 | import java.util.stream.Stream; 25 | import java.util.stream.StreamSupport; 26 | import org.sonar.api.batch.fs.InputFile; 27 | import org.sonar.api.batch.sensor.Sensor; 28 | import org.sonar.api.batch.sensor.SensorContext; 29 | import org.sonar.api.batch.sensor.SensorDescriptor; 30 | import org.sonar.api.utils.log.Logger; 31 | import org.sonar.api.utils.log.Loggers; 32 | 33 | public class JacocoSensor implements Sensor { 34 | private static final Logger LOG = Loggers.get(JacocoSensor.class); 35 | 36 | @Override 37 | public void describe(SensorDescriptor descriptor) { 38 | descriptor.name("JaCoCo XML Report Importer"); 39 | } 40 | 41 | @Override 42 | public void execute(SensorContext context) { 43 | Collection reportPaths = new ReportPathsProvider(context).getPaths(); 44 | if (reportPaths.isEmpty()) { 45 | LOG.info("No report imported, no coverage information will be imported by JaCoCo XML Report Importer"); 46 | return; 47 | } 48 | Iterable inputFiles = context.fileSystem().inputFiles(context.fileSystem().predicates().all()); 49 | Stream kotlinInputFileStream = StreamSupport.stream(inputFiles.spliterator(), false).filter(f -> "kotlin".equals(f.language())); 50 | FileLocator locator = new FileLocator(inputFiles, new KotlinFileLocator(kotlinInputFileStream)); 51 | ReportImporter importer = new ReportImporter(context); 52 | 53 | importReports(reportPaths, locator, importer); 54 | } 55 | 56 | void importReports(Collection reportPaths, FileLocator locator, ReportImporter importer) { 57 | LOG.info("Importing {} report(s). Turn your logs in debug mode in order to see the exhaustive list.", reportPaths.size()); 58 | 59 | for (Path reportPath : reportPaths) { 60 | LOG.debug("Reading report '{}'", reportPath); 61 | try { 62 | SensorUtils.importReport(new XmlReportParser(reportPath), locator, importer, LOG); 63 | } catch (Exception e) { 64 | LOG.error("Coverage report '{}' could not be read/imported. Error: {}", reportPath, e); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - branch-* 7 | - dogfood-* 8 | pull_request: 9 | workflow_dispatch: 10 | schedule: 11 | - cron: "30 1 * * *" # Run daily at 01:30 AM UTC 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | build: 19 | runs-on: github-ubuntu-latest-s 20 | name: Build 21 | permissions: 22 | id-token: write # Required for Vault OIDC authentication 23 | contents: write # Required for repository access and tagging 24 | outputs: 25 | build-number: ${{ steps.build-step.outputs.BUILD_NUMBER }} 26 | deployed: ${{ steps.build-step.outputs.deployed }} 27 | steps: 28 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 29 | - uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0 30 | with: 31 | version: 2025.7.12 32 | - uses: SonarSource/ci-github-actions/build-gradle@v1 33 | id: build-step 34 | with: 35 | deploy-pull-request: true 36 | artifactory-reader-role: private-reader 37 | artifactory-deployer-role: qa-deployer 38 | 39 | qa: 40 | needs: [build] 41 | if: ${{ needs.build.outputs.deployed }} 42 | runs-on: github-ubuntu-latest-m 43 | permissions: 44 | id-token: write 45 | contents: read 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | item: 50 | - { sq-version: "LATEST_RELEASE[2025.1]" } 51 | - { sq-version: "LATEST_RELEASE[2025.4]" } 52 | - { sq-version: "DEV" } 53 | name: "QA Tests - SQ : ${{ matrix.item.sq-version }}" 54 | env: 55 | BUILD_NUMBER: ${{ needs.build.outputs.build-number }} 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 59 | - uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3.2.0 60 | with: 61 | version: 2025.7.12 62 | - name: Configure Gradle 63 | uses: SonarSource/ci-github-actions/config-gradle@v1 64 | with: 65 | artifactory-reader-role: private-reader 66 | - name: Run QA Tests 67 | shell: bash 68 | env: 69 | SQ_VERSION: ${{ matrix.item.sq-version }} 70 | run: >- 71 | ./gradlew -DbuildNumber=$BUILD_NUMBER 72 | -PintegrationTests=true 73 | -Dsonar.runtimeVersion=$SQ_VERSION 74 | -Dorchestrator.artifactory.accessToken=$ARTIFACTORY_ACCESS_TOKEN 75 | --console plain --no-daemon --info 76 | build test 77 | 78 | promote: 79 | needs: [build, qa] 80 | if: ${{ needs.build.outputs.deployed }} 81 | runs-on: github-ubuntu-latest-s 82 | name: Promote 83 | permissions: 84 | id-token: write 85 | contents: write 86 | env: 87 | BUILD_NUMBER: ${{ needs.build.outputs.build-number }} 88 | steps: 89 | - uses: SonarSource/ci-github-actions/promote@v1 90 | with: 91 | promote-pull-request: true 92 | -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/jacoco/ReportImporterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.io.IOException; 23 | import java.nio.file.Files; 24 | import java.nio.file.Path; 25 | 26 | import org.assertj.core.api.Assertions; 27 | import org.junit.jupiter.api.BeforeEach; 28 | import org.junit.jupiter.api.Test; 29 | import org.junit.jupiter.api.io.TempDir; 30 | import org.sonar.api.batch.fs.InputFile; 31 | import org.sonar.api.batch.fs.internal.TestInputFileBuilder; 32 | import org.sonar.api.batch.sensor.internal.SensorContextTester; 33 | 34 | class ReportImporterTest { 35 | @TempDir 36 | Path temp; 37 | 38 | private SensorContextTester ctx; 39 | private ReportImporter importer; 40 | 41 | @BeforeEach 42 | void setUp() throws IOException { 43 | Path module = temp.resolve("module"); 44 | Files.createDirectory(module); 45 | ctx = SensorContextTester.create(module); 46 | importer = new ReportImporter(ctx); 47 | } 48 | 49 | @Test 50 | void should_import_coverage() { 51 | InputFile inputFile = TestInputFileBuilder.create("module", "filePath") 52 | .setLines(10) 53 | .build(); 54 | XmlReportParser.SourceFile sourceFile = new XmlReportParser.SourceFile("package", "name"); 55 | sourceFile.lines().add(new XmlReportParser.Line(1, 0, 0, 1, 1)); 56 | sourceFile.lines().add(new XmlReportParser.Line(2, 1, 2, 0, 0)); 57 | sourceFile.lines().add(new XmlReportParser.Line(3, 2, 0, 0, 0)); 58 | 59 | importer.importCoverage(sourceFile, inputFile); 60 | 61 | Assertions.assertThat(ctx.coveredConditions(inputFile.key(), 1)).isEqualTo(1); 62 | Assertions.assertThat(ctx.coveredConditions(inputFile.key(), 2)).isNull(); 63 | Assertions.assertThat(ctx.coveredConditions(inputFile.key(), 3)).isNull(); 64 | 65 | Assertions.assertThat(ctx.conditions(inputFile.key(), 1)).isEqualTo(2); 66 | Assertions.assertThat(ctx.conditions(inputFile.key(), 2)).isNull(); 67 | Assertions.assertThat(ctx.conditions(inputFile.key(), 3)).isNull(); 68 | 69 | Assertions.assertThat(ctx.lineHits(inputFile.key(), 1)).isEqualTo(0); 70 | Assertions.assertThat(ctx.lineHits(inputFile.key(), 2)).isEqualTo(1); 71 | Assertions.assertThat(ctx.lineHits(inputFile.key(), 3)).isEqualTo(0); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /its/src/test/resources/kotlin-jacoco-project/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | org.sonarsource.it.projects 5 | kotlin-jacoco-project 6 | 1.0-SNAPSHOT 7 | 8 | 9 | 1.4.10 10 | 11 | 12 | 13 | 14 | org.jetbrains.kotlin 15 | kotlin-stdlib 16 | ${kotlin.version} 17 | 18 | 19 | junit 20 | junit 21 | 4.11 22 | test 23 | 24 | 25 | org.jacoco 26 | jacoco-maven-plugin 27 | 0.8.8 28 | 29 | 30 | 31 | 32 | ${project.basedir}/src/main/kotlin 33 | ${project.basedir}/src/test/kotlin 34 | 35 | 36 | org.jetbrains.kotlin 37 | kotlin-maven-plugin 38 | ${kotlin.version} 39 | 40 | 41 | 42 | compile 43 | 44 | compile 45 | 46 | 47 | 48 | 49 | test-compile 50 | 51 | test-compile 52 | 53 | 54 | 55 | 56 | 57 | org.apache.maven.plugins 58 | maven-compiler-plugin 59 | 3.8.1 60 | 61 | 11 62 | 63 | 64 | 65 | org.jacoco 66 | jacoco-maven-plugin 67 | 0.8.8 68 | 69 | 70 | 71 | prepare-agent 72 | report 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /its/src/test/resources/simple-project-jacoco/jacoco.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /its/src/test/resources/simple-project-jacoco/jacoco-with-invalid-lines.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /its/src/test/resources/simple-project-jacoco/target/site/jacoco-it/jacoco.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /its/src/test/resources/simple-project-jacoco/jacoco-with-invalid-sources.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/jacoco/JacocoAggregateSensorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.io.IOException; 23 | import java.nio.file.Files; 24 | import java.nio.file.Path; 25 | import org.junit.jupiter.api.BeforeEach; 26 | import org.junit.jupiter.api.Test; 27 | import org.junit.jupiter.api.extension.RegisterExtension; 28 | import org.junit.jupiter.api.io.TempDir; 29 | import org.sonar.api.batch.sensor.SensorDescriptor; 30 | import org.sonar.api.batch.sensor.internal.SensorContextTester; 31 | import org.sonar.api.config.internal.MapSettings; 32 | import org.sonar.api.utils.log.LogTesterJUnit5; 33 | import org.sonar.api.utils.log.LoggerLevel; 34 | 35 | import static org.assertj.core.api.Assertions.assertThat; 36 | import static org.mockito.Mockito.mock; 37 | import static org.mockito.Mockito.verify; 38 | 39 | class JacocoAggregateSensorTest { 40 | private static final String NO_REPORT_TO_IMPORT_LOG_MESSAGE = "No aggregate XML report found. No coverage coverage information will be added at project level."; 41 | @TempDir 42 | Path basedir; 43 | 44 | @RegisterExtension 45 | public LogTesterJUnit5 logTester = new LogTesterJUnit5(); 46 | 47 | private SensorContextTester context; 48 | 49 | @BeforeEach 50 | void setup() { 51 | context = SensorContextTester.create(basedir); 52 | } 53 | 54 | @Test 55 | void description_name_is_as_expected() { 56 | SensorDescriptor descriptor = mock(SensorDescriptor.class); 57 | var sensor = new JacocoAggregateSensor(); 58 | sensor.describe(descriptor); 59 | verify(descriptor).name("JaCoCo Aggregate XML Report Importer"); 60 | } 61 | 62 | @Test 63 | void log_missing_report_and_return_early_when_missing_analysis_parameter() { 64 | var sensor = new JacocoAggregateSensor(); 65 | sensor.execute(context); 66 | 67 | assertThat(logTester.logs(LoggerLevel.DEBUG)).containsOnly(NO_REPORT_TO_IMPORT_LOG_MESSAGE); 68 | } 69 | 70 | @Test 71 | void log_missing_report_and_return_early_when_analysis_parameter_points_to_report_that_does_not_exist() { 72 | MapSettings settings = new MapSettings(); 73 | settings.setProperty(ReportPathsProvider.AGGREGATE_REPORT_PATH_PROPERTY_KEY, "non-existing-report.xml"); 74 | context.setSettings(settings); 75 | 76 | var sensor = new JacocoAggregateSensor(); 77 | sensor.execute(context); 78 | 79 | assertThat(logTester.logs(LoggerLevel.ERROR)). 80 | containsExactly("The aggregate JaCoCo sensor will stop: Aggregate report non-existing-report.xml was not found"); 81 | assertThat(logTester.logs(LoggerLevel.DEBUG)).isEmpty(); 82 | assertThat(logTester.logs(LoggerLevel.INFO)).isEmpty(); 83 | } 84 | 85 | @Test 86 | void executes_as_expected() throws IOException { 87 | Path reportPath = basedir.resolve("my-aggregate-report.xml"); 88 | Files.copy(Path.of("src", "test", "resources", "jacoco.xml"), reportPath); 89 | 90 | MapSettings settings = new MapSettings(); 91 | settings.setProperty(ReportPathsProvider.AGGREGATE_REPORT_PATH_PROPERTY_KEY, reportPath.toAbsolutePath().toString()); 92 | context.setSettings(settings); 93 | 94 | var sensor = new JacocoAggregateSensor(); 95 | sensor.execute(context); 96 | assertThat(logTester.logs(LoggerLevel.DEBUG)).doesNotContain(NO_REPORT_TO_IMPORT_LOG_MESSAGE); 97 | assertThat(logTester.logs(LoggerLevel.INFO)).containsOnly( 98 | String.format("Importing aggregate report %s.", reportPath) 99 | ); 100 | } 101 | 102 | } -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/jacoco/FileLocatorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.util.Arrays; 23 | import java.util.Collections; 24 | import org.junit.jupiter.api.Test; 25 | import org.sonar.api.batch.fs.InputFile; 26 | import org.sonar.api.batch.fs.internal.TestInputFileBuilder; 27 | 28 | import static org.assertj.core.api.Assertions.assertThat; 29 | 30 | import static org.mockito.ArgumentMatchers.any; 31 | import static org.mockito.Mockito.mock; 32 | import static org.mockito.Mockito.never; 33 | import static org.mockito.Mockito.verify; 34 | import static org.mockito.Mockito.when; 35 | 36 | class FileLocatorTest { 37 | 38 | private static KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(null); 39 | @Test 40 | void should_match_suffix() { 41 | InputFile inputFile = new TestInputFileBuilder("module1", "src/main/java/org/sonar/test/File.java").build(); 42 | FileLocator locator = new FileLocator(Collections.singleton(inputFile), kotlinFileLocator); 43 | assertThat(locator.getInputFile("org/sonar/test", "File.java")).isEqualTo(inputFile); 44 | } 45 | 46 | @Test 47 | void should_match_default_package() { 48 | InputFile inputFile = new TestInputFileBuilder("module1", "src/main/java/File.java").build(); 49 | FileLocator locator = new FileLocator(Collections.singleton(inputFile), kotlinFileLocator); 50 | assertThat(locator.getInputFile("", "File.java")).isEqualTo(inputFile); 51 | } 52 | 53 | @Test 54 | void should_not_match() { 55 | InputFile inputFile = new TestInputFileBuilder("module1", "src/main/java/org/sonar/test/File.java").build(); 56 | FileLocator locator = new FileLocator(Collections.singleton(inputFile), kotlinFileLocator); 57 | assertThat(locator.getInputFile("org/sonar/test", "File2.java")).isNull(); 58 | assertThat(locator.getInputFile("org/sonar/test2", "File.java")).isNull(); 59 | } 60 | 61 | @Test 62 | void should_match_first_with_many_options() { 63 | InputFile inputFile1 = new TestInputFileBuilder("module1", "src/main/java/org/sonar/test/File.java").build(); 64 | InputFile inputFile2 = new TestInputFileBuilder("module1", "src/test/java/org/sonar/test/File.java").build(); 65 | 66 | FileLocator locator = new FileLocator(Arrays.asList(inputFile1, inputFile2), kotlinFileLocator); 67 | assertThat(locator.getInputFile("org/sonar/test", "File.java")).isEqualTo(inputFile1); 68 | } 69 | 70 | @Test 71 | void should_fallback_on_Kotlin_file_locator_if_file_was_not_found() { 72 | InputFile inputFile = new TestInputFileBuilder("module1", "src/main/kotlin/File.kt").build(); 73 | 74 | KotlinFileLocator kotlinFileLocatorMock = mock(KotlinFileLocator.class); 75 | 76 | when(kotlinFileLocatorMock.getInputFile("org/sonar/test", "File.kt")).thenReturn(inputFile); 77 | 78 | FileLocator locator = new FileLocator(Arrays.asList(inputFile), kotlinFileLocatorMock); 79 | 80 | assertThat(locator.getInputFile("org/sonar/test", "File.kt")).isEqualTo(inputFile); 81 | } 82 | 83 | @Test 84 | void should_not_fallback_on_Kotlin_file_locator_if_file_is_not_Kotlin() { 85 | InputFile inputFile = new TestInputFileBuilder("module1", "src/main/kotlin/File.java").build(); 86 | 87 | KotlinFileLocator kotlinFileLocatorMock = mock(KotlinFileLocator.class); 88 | when(kotlinFileLocatorMock.getInputFile(any(), any())).thenReturn(inputFile); 89 | 90 | FileLocator locator = new FileLocator(Arrays.asList(inputFile), kotlinFileLocatorMock); 91 | 92 | assertThat(locator.getInputFile("org/sonar/test", "File.java")).isNull(); 93 | verify(kotlinFileLocatorMock, never()).getInputFile(any(), any()); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/jacoco/ReportPathsProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.io.FileNotFoundException; 23 | import java.nio.file.Files; 24 | import java.nio.file.Path; 25 | import java.util.Arrays; 26 | import java.util.Collection; 27 | import java.util.HashSet; 28 | import java.util.List; 29 | import java.util.Optional; 30 | import java.util.Set; 31 | import java.util.stream.Collectors; 32 | import java.util.stream.Stream; 33 | import javax.annotation.CheckForNull; 34 | import org.sonar.api.batch.sensor.SensorContext; 35 | import org.sonar.api.utils.log.Logger; 36 | import org.sonar.api.utils.log.Loggers; 37 | 38 | class ReportPathsProvider { 39 | private static final Logger LOG = Loggers.get(ReportPathsProvider.class); 40 | 41 | private static final String[] DEFAULT_PATHS = {"target/site/jacoco/jacoco.xml", "target/site/jacoco-it/jacoco.xml", "build/reports/jacoco/test/jacocoTestReport.xml"}; 42 | 43 | static final String AGGREGATE_REPORT_PATH_PROPERTY_KEY = "sonar.coverage.jacoco.aggregateXmlReportPath"; 44 | static final String REPORT_PATHS_PROPERTY_KEY = "sonar.coverage.jacoco.xmlReportPaths"; 45 | 46 | private final SensorContext context; 47 | 48 | ReportPathsProvider(SensorContext context) { 49 | this.context = context; 50 | } 51 | 52 | Collection getPaths() { 53 | Path baseDir = context.fileSystem().baseDir().toPath().toAbsolutePath(); 54 | 55 | List patternPathList = Stream.of(context.config().getStringArray(REPORT_PATHS_PROPERTY_KEY)) 56 | .filter(pattern -> !pattern.isEmpty()) 57 | .collect(Collectors.toList()); 58 | 59 | Set reportPaths = new HashSet<>(); 60 | if (!patternPathList.isEmpty()) { 61 | for (String patternPath : patternPathList) { 62 | List paths = WildcardPatternFileScanner.scan(baseDir, patternPath); 63 | if (paths.isEmpty() && patternPathList.size() > 1) { 64 | LOG.info("Coverage report doesn't exist for pattern: '{}'", patternPath); 65 | } 66 | reportPaths.addAll(paths); 67 | } 68 | } 69 | 70 | if (!reportPaths.isEmpty()) { 71 | return reportPaths; 72 | } else { 73 | if (!patternPathList.isEmpty()) { 74 | LOG.warn("No coverage report can be found with sonar.coverage.jacoco.xmlReportPaths='{}'. Using default locations: {}", 75 | String.join(",", patternPathList), String.join(",", DEFAULT_PATHS)); 76 | } else { 77 | LOG.info("'sonar.coverage.jacoco.xmlReportPaths' is not defined. Using default locations: {}", String.join(",", DEFAULT_PATHS)); 78 | } 79 | return Arrays.stream(DEFAULT_PATHS) 80 | .map(baseDir::resolve) 81 | .filter(Files::isRegularFile) 82 | .collect(Collectors.toSet()); 83 | } 84 | } 85 | 86 | /** 87 | * Checks if the aggregate report path property is set, finds the first path matching and returns it. 88 | * 89 | * @return Path to the existing aggregate report if the property is set. Null if none specified. 90 | * @throws FileNotFoundException If a path is set but does not match with an existing file. 91 | */ 92 | @CheckForNull 93 | Path getAggregateReportPath() throws FileNotFoundException { 94 | Optional property = context.config().get(AGGREGATE_REPORT_PATH_PROPERTY_KEY); 95 | if (!property.isPresent()) { 96 | return null; 97 | } 98 | List scanned = WildcardPatternFileScanner.scan(context.fileSystem().baseDir().toPath(), property.get()); 99 | if (scanned.isEmpty()) { 100 | throw new FileNotFoundException(String.format("Aggregate report %s was not found", property.get())); 101 | } 102 | return scanned.get(0); 103 | } 104 | 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/jacoco/KotlinFileLocator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.io.IOException; 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | import java.util.regex.Matcher; 26 | import java.util.regex.Pattern; 27 | import java.util.stream.Stream; 28 | 29 | import org.sonar.api.batch.fs.InputFile; 30 | import org.sonar.api.utils.log.Logger; 31 | import org.sonar.api.utils.log.Loggers; 32 | 33 | public class KotlinFileLocator { 34 | private static final Logger LOGGER = Loggers.get(KotlinFileLocator.class); 35 | private static final String SHEBANG_LINE = "#![^\n]*+"; 36 | private static final String DELIMITED_COMMENT = "/\\*(?:(?!\\*/).)*+\\*/"; 37 | private static final String LINE_COMMENT = "//[^\n]*+"; 38 | private static final String STRING_LITERAL = "\"([^\"]|(?<=\\\\)\")*+\""; 39 | private static final String MULTILINE_STRING_LITERAL = "\"\"\"(?:(?!\"\"\").)*+\"\"\""; 40 | private static final String PRE_PACKAGE = "(?s)^(" + SHEBANG_LINE + ")?(" + DELIMITED_COMMENT + "|" + LINE_COMMENT + "|" 41 | + STRING_LITERAL + "|" + MULTILINE_STRING_LITERAL + "|" + "(?!package).)*+"; 42 | private static final String HIDDEN = "(" + DELIMITED_COMMENT + "|" + LINE_COMMENT + "|[\\u0020\\u0009\\u000c]|\r?\n)*+"; 43 | private static final String IDENTIFIER = "([\\p{Lu}\\p{Lo}\\p{Ll}\\p{Lt}\\p{Lm}_\\p{Nd}]++|`[^\r\n`]++`)"; 44 | private static final String PACKAGE_DECLARATION_REGEX = "package" + HIDDEN + "(?" + IDENTIFIER + "(" 45 | + HIDDEN + "\\." + HIDDEN + IDENTIFIER + ")*+)"; 46 | private static final Pattern PACKAGE_REGEX = Pattern.compile(PRE_PACKAGE + PACKAGE_DECLARATION_REGEX); 47 | private static final Pattern FIRST_IDENTIFIER_REGEX = Pattern.compile(HIDDEN + "(?" + IDENTIFIER + ")"); 48 | private static final Pattern NEXT_IDENTIFIER_REGEX = Pattern.compile(HIDDEN + "\\." + HIDDEN + "(?" + IDENTIFIER + ")"); 49 | 50 | private final Map fqnToInputFile = new HashMap<>(); 51 | private boolean populated = false; 52 | private final Stream inputFileStream; 53 | 54 | public KotlinFileLocator(Stream kotlinInputFileStream) { 55 | inputFileStream = kotlinInputFileStream; 56 | } 57 | 58 | public InputFile getInputFile(String packagePath, String fileName) { 59 | String fqn = packagePath.replace("/", ".") + "." + fileName; 60 | if (!populated) { 61 | populate(); 62 | } 63 | return fqnToInputFile.get(fqn); 64 | } 65 | 66 | private void populate() { 67 | inputFileStream.forEach(f -> { 68 | try { 69 | String packageName = getPackage(f.contents()); 70 | if (packageName != null) { 71 | String key = packageName + "." + f.filename(); 72 | fqnToInputFile.put(key, f); 73 | } 74 | } catch (IOException e) { 75 | LOGGER.error(e.getMessage()); 76 | } 77 | }); 78 | populated = true; 79 | } 80 | 81 | /* 82 | The idea is to skip everything before the package declaration. 'package' is a keyword and can't be used as an identifier. 83 | However, 'package' can be inside comments, shebang line or string literals, so we need to match them explicitly. 84 | */ 85 | private static String getPackage(String content) { 86 | Matcher matcher = PACKAGE_REGEX.matcher(content); 87 | if (matcher.find()) { 88 | StringBuilder resolvedPackage = new StringBuilder(); 89 | String packageName = matcher.group("packageName"); 90 | Matcher firstIdentifierMatcher = FIRST_IDENTIFIER_REGEX.matcher(packageName); 91 | // The find() invocation will always return true as we've already matched the big regular expression 92 | firstIdentifierMatcher.find(); 93 | resolvedPackage.append(removeBackticks(firstIdentifierMatcher.group("firstIdentifier"))); 94 | Matcher nextIdentifierMatcher = NEXT_IDENTIFIER_REGEX.matcher(packageName); 95 | while (nextIdentifierMatcher.find()) { 96 | resolvedPackage.append("."); 97 | resolvedPackage.append(removeBackticks(nextIdentifierMatcher.group("nextIdentifier"))); 98 | } 99 | 100 | return resolvedPackage.toString(); 101 | } 102 | return null; 103 | } 104 | 105 | private static String removeBackticks(String s) { 106 | if (s.startsWith("`") && s.endsWith("`")) { 107 | return s.substring(1, s.length() - 1); 108 | } 109 | return s; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/jacoco/WildcardPatternFileScanner.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.io.File; 23 | import java.io.IOException; 24 | import java.nio.file.Files; 25 | import java.nio.file.Path; 26 | import java.nio.file.Paths; 27 | import java.util.ArrayList; 28 | import java.util.Collections; 29 | import java.util.List; 30 | import java.util.stream.Stream; 31 | import org.sonar.api.utils.WildcardPattern; 32 | import org.sonar.api.utils.log.Logger; 33 | import org.sonar.api.utils.log.Loggers; 34 | 35 | public final class WildcardPatternFileScanner { 36 | 37 | private static final Logger LOG = Loggers.get(WildcardPatternFileScanner.class); 38 | 39 | private static final int SEARCH_MAX_DEPTH = 64; 40 | 41 | private static final String PATH_MATCHER_SPECIAL_CHAR = "*?"; 42 | 43 | private WildcardPatternFileScanner() { 44 | // utility class 45 | } 46 | 47 | public static List scan(Path baseDirectory, String patternPath) { 48 | String unixLikePatternPath = toUnixLikePath(patternPath); 49 | int specialCharIndex = indexOfMatcherSpecialChar(unixLikePatternPath); 50 | if (specialCharIndex == -1) { 51 | return scanNonWildcardPattern(baseDirectory, unixLikePatternPath); 52 | } else { 53 | // For performance reason, we don't want to scan recursively all files in baseDirectory 54 | // when patternPath start with "none wildcard" subfolder names. For example, 55 | // scanWildcardPattern("/base", "sub1/sub2/**/file*.xml") is converted into 56 | // scanWildcardPattern("/base/sub1/sub2", "**/file*.xml") 57 | int additionalBaseDirectoryPart = unixLikePatternPath.lastIndexOf('/', specialCharIndex); 58 | if (additionalBaseDirectoryPart != -1) { 59 | Path additionalBaseDirectory = toFileSystemPath(unixLikePatternPath.substring(0, additionalBaseDirectoryPart + 1)); 60 | String remainingWildcardPart = unixLikePatternPath.substring(additionalBaseDirectoryPart + 1); 61 | Path moreSpecificBaseDirectory = baseDirectory.resolve(additionalBaseDirectory); 62 | return scanWildcardPattern(moreSpecificBaseDirectory, remainingWildcardPart); 63 | } else { 64 | return scanWildcardPattern(baseDirectory, unixLikePatternPath); 65 | } 66 | } 67 | } 68 | private static List scanNonWildcardPattern(Path baseDirectory, String unixLikePath) { 69 | Path path = baseDirectory.resolve(toFileSystemPath(unixLikePath)); 70 | if (Files.isRegularFile(path)) { 71 | return Collections.singletonList(path); 72 | } 73 | return Collections.emptyList(); 74 | } 75 | 76 | private static List scanWildcardPattern(Path baseDirectory, String unixLikePatternPath) { 77 | if (!Files.exists(baseDirectory)) { 78 | return Collections.emptyList(); 79 | } 80 | try { 81 | Path absoluteBaseDirectory = baseDirectory.toRealPath(); 82 | if (absoluteBaseDirectory.equals(absoluteBaseDirectory.getRoot())) { 83 | throw new IOException("For performance reason, wildcard pattern search is not possible from filesystem root"); 84 | } 85 | List paths = new ArrayList<>(); 86 | WildcardPattern matcher = WildcardPattern.create(toUnixLikePath(absoluteBaseDirectory.toString()) + "/" + unixLikePatternPath); 87 | try (Stream stream = Files.walk(absoluteBaseDirectory, SEARCH_MAX_DEPTH)) { 88 | stream 89 | .filter(Files::isRegularFile) 90 | .filter(path -> matcher.match(toUnixLikePath(path.toString()))) 91 | .forEach(paths::add); 92 | } 93 | return paths; 94 | } catch (IOException | RuntimeException e) { 95 | LOG.error("Failed to get Jacoco report paths: Scanning '" + baseDirectory + "' with pattern '" + unixLikePatternPath + "'" + 96 | " threw a " + e.getClass().getSimpleName() + ": " + e.getMessage()); 97 | return Collections.emptyList(); 98 | } 99 | } 100 | 101 | /* Is visible for testing reasons */ 102 | static String toUnixLikePath(String path) { 103 | return path.indexOf('\\') != -1 ? path.replace('\\', '/') : path; 104 | } 105 | 106 | /* Is visible for testing reasons */ 107 | static Path toFileSystemPath(String unixLikePath) { 108 | return Paths.get(unixLikePath.replace('/', File.separatorChar)); 109 | } 110 | 111 | /* Is visible for testing reasons */ 112 | static int indexOfMatcherSpecialChar(String path) { 113 | for (int i = 0; i < path.length(); i++) { 114 | if (PATH_MATCHER_SPECIAL_CHAR.indexOf(path.charAt(i)) != -1) { 115 | return i; 116 | } 117 | } 118 | return -1; 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/jacoco/WildcardPatternFileScannerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import org.junit.jupiter.api.Test; 23 | import org.junit.jupiter.api.extension.RegisterExtension; 24 | import org.sonar.api.utils.log.LogTesterJUnit5; 25 | import org.sonar.api.utils.log.LoggerLevel; 26 | 27 | import java.io.IOException; 28 | import java.nio.file.Path; 29 | import java.nio.file.Paths; 30 | 31 | import static org.assertj.core.api.Assertions.assertThat; 32 | import static org.sonar.plugins.jacoco.WildcardPatternFileScanner.indexOfMatcherSpecialChar; 33 | import static org.sonar.plugins.jacoco.WildcardPatternFileScanner.scan; 34 | import static org.sonar.plugins.jacoco.WildcardPatternFileScanner.toUnixLikePath; 35 | 36 | class WildcardPatternFileScannerTest { 37 | 38 | static final Path RELATIVE_BASE_FOLDER = Paths.get("src", "test", "resources", "search"); 39 | 40 | static final Path RELATIVE_F1 = RELATIVE_BASE_FOLDER.resolve("f1.xml"); 41 | static final Path RELATIVE_F2 = RELATIVE_BASE_FOLDER.resolve("f2.xml"); 42 | static final Path RELATIVE_SUBFOLDER = RELATIVE_BASE_FOLDER.resolve(Paths.get("subfolder")); 43 | static final Path RELATIVE_G1 = RELATIVE_BASE_FOLDER.resolve(Paths.get("subfolder", "g1.xml")); 44 | static final Path RELATIVE_G2 = RELATIVE_BASE_FOLDER.resolve(Paths.get("subfolder", "g2.xml")); 45 | 46 | static final Path ABSOLUTE_BASE_FOLDER; 47 | static final Path ABSOLUTE_F1; 48 | static final Path ABSOLUTE_F2; 49 | static final Path ABSOLUTE_SUBFOLDER; 50 | static final Path ABSOLUTE_G1; 51 | static final Path ABSOLUTE_G2; 52 | static { 53 | try { 54 | ABSOLUTE_BASE_FOLDER = RELATIVE_BASE_FOLDER.toRealPath(); 55 | ABSOLUTE_F1 = RELATIVE_F1.toRealPath(); 56 | ABSOLUTE_F2 = RELATIVE_F2.toRealPath(); 57 | ABSOLUTE_SUBFOLDER = RELATIVE_SUBFOLDER.toRealPath(); 58 | ABSOLUTE_G1 = RELATIVE_G1.toRealPath(); 59 | ABSOLUTE_G2 = RELATIVE_G2.toRealPath(); 60 | } catch (IOException e) { 61 | throw new IllegalStateException(e); 62 | } 63 | } 64 | 65 | @RegisterExtension 66 | public LogTesterJUnit5 logTester = new LogTesterJUnit5(); 67 | 68 | @Test 69 | void search_pattern_paths_in_folder() throws IOException { 70 | assertThat(scan(RELATIVE_BASE_FOLDER, "*")).containsExactlyInAnyOrder(ABSOLUTE_F1, ABSOLUTE_F2); 71 | assertThat(scan(RELATIVE_BASE_FOLDER, "**")).containsExactlyInAnyOrder(ABSOLUTE_F1, ABSOLUTE_F2, ABSOLUTE_G1, ABSOLUTE_G2); 72 | assertThat(scan(RELATIVE_BASE_FOLDER, "**.xml")).containsExactlyInAnyOrder(ABSOLUTE_F1, ABSOLUTE_F2, ABSOLUTE_G1, ABSOLUTE_G2); 73 | assertThat(scan(RELATIVE_BASE_FOLDER, "**/*")).containsExactlyInAnyOrder(ABSOLUTE_F1, ABSOLUTE_F2, ABSOLUTE_G1, ABSOLUTE_G2); 74 | assertThat(scan(RELATIVE_BASE_FOLDER, "*/**")).containsExactlyInAnyOrder(ABSOLUTE_G1, ABSOLUTE_G2); 75 | assertThat(scan(RELATIVE_BASE_FOLDER, "subfolder/*")).containsExactlyInAnyOrder(ABSOLUTE_G1, ABSOLUTE_G2); 76 | assertThat(scan(RELATIVE_BASE_FOLDER, "**/?2.xml")).containsExactlyInAnyOrder(ABSOLUTE_F2, ABSOLUTE_G2); 77 | assertThat(scan(RELATIVE_BASE_FOLDER, "**/g*.xml")).containsExactlyInAnyOrder(ABSOLUTE_G1, ABSOLUTE_G2); 78 | assertThat(scan(RELATIVE_BASE_FOLDER, ABSOLUTE_BASE_FOLDER + "/f*.xml")).containsExactlyInAnyOrder(ABSOLUTE_F1, ABSOLUTE_F2); 79 | assertThat(scan(RELATIVE_BASE_FOLDER, "**.txt")).isEmpty(); 80 | assertThat(logTester.logs()).isEmpty(); 81 | } 82 | 83 | @Test 84 | void search_non_pattern_paths_in_folder() throws IOException { 85 | assertThat(scan(RELATIVE_BASE_FOLDER, "f1.xml")).containsExactlyInAnyOrder(RELATIVE_F1); 86 | assertThat(scan(RELATIVE_BASE_FOLDER, "subfolder/g1.xml")).containsExactlyInAnyOrder(RELATIVE_G1); 87 | assertThat(scan(RELATIVE_BASE_FOLDER, "subfolder\\g1.xml")).containsExactlyInAnyOrder(RELATIVE_G1); 88 | assertThat(scan(RELATIVE_BASE_FOLDER, ABSOLUTE_F1.toString())).containsExactlyInAnyOrder(ABSOLUTE_F1); 89 | assertThat(scan(RELATIVE_BASE_FOLDER, "unknown-file.xml")).isEmpty(); 90 | assertThat(logTester.logs()).isEmpty(); 91 | } 92 | 93 | @Test 94 | void search_paths_in_not_existing_folder() throws IOException { 95 | assertThat(scan(Paths.get("not-existing-folder"), "*")).isEmpty(); 96 | assertThat(logTester.logs()).isEmpty(); 97 | } 98 | 99 | @Test 100 | void not_allowed_to_search_from_filesystem_root() throws IOException { 101 | Path root = ABSOLUTE_BASE_FOLDER.getRoot(); 102 | assertThat(scan(root, "*.xml")).isEmpty(); 103 | assertThat(logTester.logs()).hasSize(1); 104 | assertThat(logTester.logs(LoggerLevel.ERROR)).containsExactly( 105 | "Failed to get Jacoco report paths: Scanning '" + root + "'" + 106 | " with pattern '*.xml' threw a IOException: For performance reason, wildcard pattern search is not possible from filesystem root"); 107 | } 108 | 109 | @Test 110 | void to_unix_path() { 111 | assertThat(toUnixLikePath("c:\\a\\b")).isEqualTo("c:/a/b"); 112 | assertThat(toUnixLikePath("/a/b")).isEqualTo("/a/b"); 113 | assertThat(toUnixLikePath("a/b")).isEqualTo("a/b"); 114 | assertThat(toUnixLikePath("a")).isEqualTo("a"); 115 | } 116 | 117 | @Test 118 | void index_of_special_char() { 119 | assertThat(indexOfMatcherSpecialChar("")).isEqualTo(-1); 120 | assertThat(indexOfMatcherSpecialChar("/a/b")).isEqualTo(-1); 121 | assertThat(indexOfMatcherSpecialChar("c:/b")).isEqualTo(-1); 122 | assertThat(indexOfMatcherSpecialChar("*/abc")).isEqualTo(0); 123 | assertThat(indexOfMatcherSpecialChar("**/*/abc")).isEqualTo(0); 124 | assertThat(indexOfMatcherSpecialChar("a/?/b/*")).isEqualTo(2); 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/jacoco/JacocoSensorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.io.File; 23 | import org.junit.Rule; 24 | import org.junit.jupiter.api.Test; 25 | import org.junit.jupiter.api.extension.RegisterExtension; 26 | import org.junit.rules.TemporaryFolder; 27 | import org.sonar.api.batch.fs.InputFile; 28 | import org.sonar.api.batch.fs.internal.DefaultFileSystem; 29 | import org.sonar.api.batch.fs.internal.TestInputFileBuilder; 30 | import org.sonar.api.batch.sensor.SensorDescriptor; 31 | import org.sonar.api.batch.sensor.internal.SensorContextTester; 32 | import org.sonar.api.config.internal.MapSettings; 33 | import org.sonar.api.utils.log.LogTesterJUnit5; 34 | import org.sonar.api.utils.log.LoggerLevel; 35 | 36 | import java.io.IOException; 37 | import java.net.URISyntaxException; 38 | import java.nio.file.Files; 39 | import java.nio.file.Path; 40 | import java.nio.file.Paths; 41 | import java.util.Arrays; 42 | import java.util.Collections; 43 | 44 | import static org.assertj.core.api.Assertions.assertThat; 45 | import static org.mockito.ArgumentMatchers.any; 46 | import static org.mockito.ArgumentMatchers.eq; 47 | import static org.mockito.Mockito.mock; 48 | import static org.mockito.Mockito.never; 49 | import static org.mockito.Mockito.spy; 50 | import static org.mockito.Mockito.times; 51 | import static org.mockito.Mockito.verify; 52 | import static org.mockito.Mockito.verifyNoInteractions; 53 | import static org.mockito.Mockito.when; 54 | 55 | @org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport 56 | class JacocoSensorTest { 57 | @Rule 58 | public TemporaryFolder temp = new TemporaryFolder(); 59 | 60 | @RegisterExtension 61 | public LogTesterJUnit5 logTester = new LogTesterJUnit5(); 62 | 63 | private JacocoSensor sensor = new JacocoSensor(); 64 | 65 | @Test 66 | void describe_sensor() { 67 | SensorDescriptor descriptor = mock(SensorDescriptor.class); 68 | sensor.describe(descriptor); 69 | verify(descriptor).name("JaCoCo XML Report Importer"); 70 | } 71 | 72 | @Test 73 | void do_not_index_files_when_no_report_was_found() throws IOException { 74 | File emptyFolderWithoutReport = temp.newFolder(); 75 | SensorContextTester spiedContext = spy(SensorContextTester.create(emptyFolderWithoutReport)); 76 | DefaultFileSystem spiedFileSystem = spy(spiedContext.fileSystem()); 77 | when(spiedContext.fileSystem()).thenReturn(spiedFileSystem); 78 | sensor.execute(spiedContext); 79 | // indexing all files in the filesystem is time consuming and should not be done if there no jacoco reports to import 80 | // one way to assert this is to ensure there's no calls on fileSystem.inputFiles(...) 81 | verify(spiedFileSystem, never()).inputFiles(any()); 82 | assertThat(logTester.logs()).containsExactlyInAnyOrder( 83 | "'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", 84 | "No report imported, no coverage information will be imported by JaCoCo XML Report Importer"); 85 | } 86 | 87 | @Test 88 | void do_nothing_if_report_parse_failure() { 89 | FileLocator locator = mock(FileLocator.class); 90 | ReportImporter importer = mock(ReportImporter.class); 91 | 92 | sensor.importReports(Collections.singletonList(Paths.get("invalid.xml")), locator, importer); 93 | 94 | assertThat(logTester.logs(LoggerLevel.INFO)).contains("Importing 1 report(s). Turn your logs in debug mode in order to see the exhaustive list."); 95 | 96 | assertThat(logTester.logs(LoggerLevel.ERROR)).hasSize(1); 97 | assertThat(logTester.logs(LoggerLevel.ERROR)).allMatch(s -> s.startsWith("Coverage report 'invalid.xml' could not be read/imported")); 98 | 99 | verifyNoInteractions(locator, importer); 100 | } 101 | 102 | @Test 103 | void parse_failure_do_not_fail_analysis() { 104 | FileLocator locator = mock(FileLocator.class); 105 | ReportImporter importer = mock(ReportImporter.class); 106 | InputFile inputFile = mock(InputFile.class); 107 | Path baseDir = Paths.get("src", "test", "resources"); 108 | Path invalidFile = baseDir.resolve("invalid_ci_in_line.xml"); 109 | Path validFile = baseDir.resolve("jacoco.xml"); 110 | 111 | when(locator.getInputFile("org/sonarlint/cli", "Stats.java")).thenReturn(inputFile); 112 | 113 | sensor.importReports(Arrays.asList(invalidFile, validFile), locator, importer); 114 | 115 | String expectedErrorMessage = String.format( 116 | "Coverage report '%s' could not be read/imported. Error: java.lang.IllegalStateException: Invalid report: failed to parse integer from the attribute 'ci' for the sourcefile 'File.java' at line 6 column 61", 117 | invalidFile.toString()); 118 | 119 | assertThat(logTester.logs(LoggerLevel.INFO)).contains("Importing 2 report(s). Turn your logs in debug mode in order to see the exhaustive list."); 120 | 121 | assertThat(logTester.logs(LoggerLevel.ERROR)).contains(expectedErrorMessage); 122 | 123 | verify(importer, times(1)).importCoverage(any(), eq(inputFile)); 124 | } 125 | 126 | @Test 127 | void test_load_real_report() throws URISyntaxException, IOException { 128 | MapSettings settings = new MapSettings(); 129 | settings.setProperty(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY, "jacoco.xml"); 130 | SensorContextTester tester = SensorContextTester.create(temp.getRoot()); 131 | tester.setSettings(settings); 132 | InputFile inputFile = TestInputFileBuilder 133 | .create("module", "org/sonarlint/cli/Main.java") 134 | .setLines(1000) 135 | .build(); 136 | tester.fileSystem().add(inputFile); 137 | Path sample = load("jacoco.xml"); 138 | Files.copy(sample, temp.getRoot().toPath().resolve("jacoco.xml")); 139 | 140 | sensor.execute(tester); 141 | assertThat(tester.lineHits(inputFile.key(), 110)).isEqualTo(1); 142 | assertThat(tester.conditions(inputFile.key(), 110)).isEqualTo(2); 143 | assertThat(tester.coveredConditions(inputFile.key(), 110)).isEqualTo(1); 144 | 145 | assertThat(logTester.logs(LoggerLevel.INFO)).contains("Importing 1 report(s). Turn your logs in debug mode in order to see the exhaustive list."); 146 | } 147 | 148 | @Test 149 | void import_failure_do_not_fail_analysis() throws URISyntaxException, IOException { 150 | MapSettings settings = new MapSettings(); 151 | settings.setProperty(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY, "invalid_line_number.xml"); 152 | SensorContextTester tester = SensorContextTester.create(temp.getRoot()); 153 | tester.setSettings(settings); 154 | InputFile inputFile = TestInputFileBuilder 155 | .create("module", "org/sonarlint/cli/File.java") 156 | .setLines(1000) 157 | .build(); 158 | tester.fileSystem().add(inputFile); 159 | Path sample = load("invalid_line_number.xml"); 160 | Files.copy(sample, temp.getRoot().toPath().resolve("invalid_line_number.xml")); 161 | 162 | sensor.execute(tester); 163 | 164 | assertThat(logTester.logs(LoggerLevel.INFO)).contains("Importing 1 report(s). Turn your logs in debug mode in order to see the exhaustive list."); 165 | 166 | String expectedLogError = String.format( 167 | "Cannot import coverage information for file '%s', coverage data is invalid. Error: java.lang.IllegalStateException: Line 1001 is out of range in the file %s (lines: 1000)", 168 | inputFile, 169 | inputFile); 170 | assertThat(logTester.logs(LoggerLevel.ERROR)).contains(expectedLogError); 171 | } 172 | 173 | private Path load(String name) throws URISyntaxException { 174 | return Paths.get(this.getClass().getClassLoader().getResource(name).toURI()); 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /src/main/java/org/sonar/plugins/jacoco/XmlReportParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.io.IOException; 23 | import java.io.Reader; 24 | import java.nio.charset.StandardCharsets; 25 | import java.nio.file.Files; 26 | import java.nio.file.Path; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | import java.util.Objects; 30 | import java.util.function.Supplier; 31 | import javax.xml.stream.XMLInputFactory; 32 | import javax.xml.stream.XMLStreamConstants; 33 | import javax.xml.stream.XMLStreamException; 34 | import javax.xml.stream.XMLStreamReader; 35 | 36 | public class XmlReportParser { 37 | private final Path xmlReportPath; 38 | 39 | private static final String COLUMN = " column "; 40 | 41 | public XmlReportParser(Path xmlReportPath) { 42 | this.xmlReportPath = xmlReportPath; 43 | } 44 | 45 | public List parse() { 46 | XMLInputFactory factory = XMLInputFactory.newInstance(); 47 | factory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, false); 48 | factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); 49 | 50 | XMLStreamReader xmlStreamReaderParser = null; 51 | try (Reader reader = Files.newBufferedReader(xmlReportPath, StandardCharsets.UTF_8)) { 52 | xmlStreamReaderParser = factory.createXMLStreamReader(reader); 53 | // Need to be effectively final to be used in Supplier lambdas 54 | final XMLStreamReader parser = xmlStreamReaderParser; 55 | 56 | List sourceFiles = new ArrayList<>(); 57 | 58 | String packageName = null; 59 | String sourceFileName = null; 60 | 61 | while (true) { 62 | int event = parser.next(); 63 | 64 | if (event == XMLStreamConstants.END_DOCUMENT) { 65 | parser.close(); 66 | break; 67 | } else if (event == XMLStreamConstants.END_ELEMENT) { 68 | String element = parser.getLocalName(); 69 | if (element.equals("package")) { 70 | packageName = null; 71 | } else if (element.equals("sourcefile")) { 72 | sourceFileName = null; 73 | } 74 | } else if (event == XMLStreamConstants.START_ELEMENT) { 75 | String element = parser.getLocalName(); 76 | 77 | if (element.equals("package")) { 78 | packageName = getStringAttr(parser, "name", () -> "for a 'package' at line " + parser.getLocation().getLineNumber() + COLUMN + parser.getLocation().getColumnNumber()); 79 | } else if (element.equals("sourcefile")) { 80 | if (packageName == null) { 81 | throw new IllegalStateException("Invalid report: expected to find 'sourcefile' within a 'package' at line " 82 | + parser.getLocation().getLineNumber() + COLUMN + parser.getLocation().getColumnNumber()); 83 | } 84 | sourceFileName = getStringAttr(parser, "name", () -> "for a sourcefile at line " 85 | + parser.getLocation().getLineNumber() + COLUMN + parser.getLocation().getColumnNumber()); 86 | sourceFiles.add(new SourceFile(packageName, sourceFileName)); 87 | } else if (element.equals("line")) { 88 | if (sourceFileName == null) { 89 | throw new IllegalStateException("Invalid report: expected to find 'line' within a 'sourcefile' at line " 90 | + parser.getLocation().getLineNumber() + COLUMN + parser.getLocation().getColumnNumber()); 91 | } 92 | SourceFile file = sourceFiles.get(sourceFiles.size() - 1); 93 | Supplier errorCtx = () -> "for the sourcefile '" + file.name() + "' at line " 94 | + parser.getLocation().getLineNumber() + COLUMN + parser.getLocation().getColumnNumber(); 95 | 96 | Line line = new Line( 97 | getIntAttr(parser, "nr", errorCtx), 98 | getOptionalIntAttr(parser, "mi", errorCtx), 99 | getOptionalIntAttr(parser, "ci", errorCtx), 100 | getOptionalIntAttr(parser, "mb", errorCtx), 101 | getOptionalIntAttr(parser, "cb", errorCtx)); 102 | file.lines().add(line); 103 | } 104 | } 105 | } 106 | 107 | return sourceFiles; 108 | } catch (XMLStreamException | IOException e) { 109 | throw new IllegalStateException("Failed to parse JaCoCo XML report: " + xmlReportPath.toAbsolutePath(), e); 110 | } finally { 111 | if (xmlStreamReaderParser != null) { 112 | try { 113 | xmlStreamReaderParser.close(); 114 | } catch (XMLStreamException e) { 115 | // do nothing - the stream used to read from will be closed by the try-with-resource 116 | } 117 | } 118 | } 119 | } 120 | 121 | private static String getStringAttr(XMLStreamReader parser, String name, Supplier errorContext) { 122 | String value = parser.getAttributeValue(null, name); 123 | if (value == null) { 124 | throw new IllegalStateException("Invalid report: couldn't find the attribute '" + name + "' " + errorContext.get()); 125 | } 126 | return value; 127 | } 128 | 129 | private static int getOptionalIntAttr(XMLStreamReader parser, String name, Supplier errorContext) { 130 | String value = parser.getAttributeValue(null, name); 131 | if (value == null) { 132 | return 0; 133 | } 134 | try { 135 | return Integer.parseInt(value); 136 | } catch (Exception e) { 137 | throw new IllegalStateException("Invalid report: failed to parse integer from the attribute '" + name + "' " + errorContext.get()); 138 | } 139 | } 140 | 141 | private static int getIntAttr(XMLStreamReader parser, String name, Supplier errorContext) { 142 | String value = getStringAttr(parser, name, errorContext); 143 | try { 144 | return Integer.parseInt(value); 145 | } catch (Exception e) { 146 | throw new IllegalStateException("Invalid report: failed to parse integer from the attribute '" + name + "' " + errorContext.get()); 147 | } 148 | } 149 | 150 | static class SourceFile { 151 | private String name; 152 | private String packageName; 153 | private List lines = new ArrayList<>(); 154 | 155 | SourceFile(String packageName, String name) { 156 | this.name = name; 157 | this.packageName = packageName; 158 | } 159 | 160 | public String name() { 161 | return name; 162 | } 163 | 164 | public String packageName() { 165 | return packageName; 166 | } 167 | 168 | public List lines() { 169 | return lines; 170 | } 171 | } 172 | 173 | static class Line { 174 | private int number; 175 | private int missedInstrs; 176 | private int coveredInstrs; 177 | private int missedBranches; 178 | private int coveredBranches; 179 | 180 | Line(int number, int mi, int ci, int mb, int cb) { 181 | this.number = number; 182 | this.missedInstrs = mi; 183 | this.coveredInstrs = ci; 184 | this.missedBranches = mb; 185 | this.coveredBranches = cb; 186 | } 187 | 188 | public int number() { 189 | return number; 190 | } 191 | 192 | public int missedInstrs() { 193 | return missedInstrs; 194 | } 195 | 196 | public int coveredInstrs() { 197 | return coveredInstrs; 198 | } 199 | 200 | public int missedBranches() { 201 | return missedBranches; 202 | } 203 | 204 | public int coveredBranches() { 205 | return coveredBranches; 206 | } 207 | 208 | @Override 209 | public boolean equals(Object o) { 210 | if (!(o instanceof Line)) return false; 211 | Line line = (Line) o; 212 | return number == line.number && 213 | missedInstrs == line.missedInstrs && coveredInstrs == line.coveredInstrs && 214 | missedBranches == line.missedBranches && coveredBranches == line.coveredBranches; 215 | } 216 | 217 | @Override 218 | public int hashCode() { 219 | return Objects.hash(number, missedInstrs, coveredInstrs, missedBranches, coveredBranches); 220 | } 221 | } 222 | 223 | } 224 | -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/jacoco/XmlReportParserTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.io.IOException; 23 | import java.net.URISyntaxException; 24 | import java.nio.file.Path; 25 | import java.nio.file.Paths; 26 | import java.util.List; 27 | import org.junit.jupiter.api.Test; 28 | import org.junit.jupiter.api.io.TempDir; 29 | 30 | import static org.assertj.core.api.Assertions.assertThat; 31 | import static org.junit.jupiter.api.Assertions.assertThrows; 32 | 33 | class XmlReportParserTest { 34 | 35 | @TempDir 36 | Path temp; 37 | 38 | private Path load(String name) throws URISyntaxException { 39 | return Paths.get(this.getClass().getClassLoader().getResource(name).toURI()); 40 | } 41 | 42 | @Test 43 | void should_parse_all_items_in_report() throws URISyntaxException { 44 | Path sample = load("jacoco.xml"); 45 | XmlReportParser report = new XmlReportParser(sample); 46 | List sourceFiles = report.parse(); 47 | 48 | assertThat(sourceFiles).hasSize(36); 49 | assertThat(sourceFiles.stream().mapToInt(sf -> sf.lines().size()).sum()).isEqualTo(1321); 50 | } 51 | 52 | @Test 53 | void should_parse_all_attributes() throws URISyntaxException { 54 | Path sample = load("simple.xml"); 55 | XmlReportParser report = new XmlReportParser(sample); 56 | List sourceFiles = report.parse(); 57 | 58 | assertThat(sourceFiles).hasSize(1); 59 | assertThat(sourceFiles.stream().mapToInt(sf -> sf.lines().size()).sum()).isEqualTo(1); 60 | assertThat(sourceFiles.get(0).name()).isEqualTo("File.java"); 61 | assertThat(sourceFiles.get(0).packageName()).isEqualTo("org/sonarlint/cli"); 62 | assertThat(sourceFiles.get(0).lines().get(0).number()).isEqualTo(24); 63 | assertThat(sourceFiles.get(0).lines().get(0).missedInstrs()).isEqualTo(1); 64 | assertThat(sourceFiles.get(0).lines().get(0).coveredInstrs()).isEqualTo(2); 65 | assertThat(sourceFiles.get(0).lines().get(0).missedBranches()).isEqualTo(3); 66 | assertThat(sourceFiles.get(0).lines().get(0).coveredBranches()).isEqualTo(4); 67 | } 68 | 69 | @Test 70 | void should_treat_missing_mi_ci_mb_cb_in_line_as_zeros() throws Exception { 71 | Path sample = load("line_without_mi_ci_mb_cb.xml"); 72 | XmlReportParser report = new XmlReportParser(sample); 73 | List sourceFiles = report.parse(); 74 | 75 | assertThat(sourceFiles).hasSize(1); 76 | assertThat(sourceFiles.stream().mapToInt(sf -> sf.lines().size()).sum()).isEqualTo(1); 77 | assertThat(sourceFiles.get(0).name()).isEqualTo("Example.java"); 78 | assertThat(sourceFiles.get(0).packageName()).isEqualTo("org/example"); 79 | assertThat(sourceFiles.get(0).lines().get(0).number()).isEqualTo(42); 80 | assertThat(sourceFiles.get(0).lines().get(0).missedInstrs()).isEqualTo(0); 81 | assertThat(sourceFiles.get(0).lines().get(0).coveredInstrs()).isEqualTo(0); 82 | assertThat(sourceFiles.get(0).lines().get(0).missedBranches()).isEqualTo(0); 83 | assertThat(sourceFiles.get(0).lines().get(0).coveredBranches()).isEqualTo(0); 84 | } 85 | 86 | @Test 87 | void should_fail_if_report_is_not_xml() throws IOException { 88 | Path filePath = temp.resolve("report.xml"); 89 | XmlReportParser report = new XmlReportParser(filePath); 90 | 91 | IllegalStateException exception = assertThrows(IllegalStateException.class, report::parse); 92 | assertThat(exception).hasMessage("Failed to parse JaCoCo XML report: " + filePath.toAbsolutePath()); 93 | } 94 | 95 | @Test 96 | void should_fail_if_name_missing_in_package() throws URISyntaxException { 97 | Path sample = load("name_missing_in_package.xml"); 98 | XmlReportParser report = new XmlReportParser(sample); 99 | 100 | IllegalStateException exception = assertThrows(IllegalStateException.class, report::parse); 101 | assertThat(exception).hasMessage("Invalid report: couldn't find the attribute 'name' for a 'package' at line 4 column 14"); 102 | } 103 | 104 | @Test 105 | void should_fail_if_name_missing_in_sourcefile() throws URISyntaxException { 106 | Path sample = load("name_missing_in_sourcefile.xml"); 107 | XmlReportParser report = new XmlReportParser(sample); 108 | 109 | IllegalStateException exception = assertThrows(IllegalStateException.class, report::parse); 110 | assertThat(exception).hasMessage("Invalid report: couldn't find the attribute 'name' for a sourcefile at line 5 column 21"); 111 | } 112 | 113 | @Test 114 | void should_fail_if_line_not_within_sourcefile() throws URISyntaxException { 115 | Path sample = load("line_not_within_sourcefile.xml"); 116 | XmlReportParser report = new XmlReportParser(sample); 117 | 118 | IllegalStateException exception = assertThrows(IllegalStateException.class, report::parse); 119 | assertThat(exception).hasMessage( "Invalid report: expected to find 'line' within a 'sourcefile' at line 5 column 52"); 120 | } 121 | 122 | @Test 123 | void should_fail_if_sourcefile_not_within_package() throws URISyntaxException { 124 | Path sample = load("sourcefile_not_within_package.xml"); 125 | XmlReportParser report = new XmlReportParser(sample); 126 | 127 | IllegalStateException exception = assertThrows(IllegalStateException.class, report::parse); 128 | assertThat(exception).hasMessage( "Invalid report: expected to find 'sourcefile' within a 'package' at line 4 column 17"); 129 | } 130 | 131 | @Test 132 | void should_fail_if_ci_is_invalid_in_line() throws URISyntaxException { 133 | Path sample = load("invalid_ci_in_line.xml"); 134 | XmlReportParser report = new XmlReportParser(sample); 135 | 136 | IllegalStateException exception = assertThrows(IllegalStateException.class, report::parse); 137 | assertThat(exception).hasMessage("Invalid report: failed to parse integer from the attribute 'ci' for the sourcefile 'File.java' at line 6 column 61"); 138 | } 139 | 140 | @Test 141 | void should_fail_if_nr_is_invalid_in_line() throws URISyntaxException { 142 | Path sample = load("invalid_nr_in_line.xml"); 143 | XmlReportParser report = new XmlReportParser(sample); 144 | 145 | IllegalStateException exception = assertThrows(IllegalStateException.class, report::parse); 146 | assertThat(exception).hasMessage("Invalid report: failed to parse integer from the attribute 'nr' for the sourcefile 'File.java' at line 6 column 31"); 147 | } 148 | 149 | @Test 150 | void should_fail_if_nr_missing_in_line() throws URISyntaxException { 151 | Path sample = load("nr_missing_in_line.xml"); 152 | XmlReportParser report = new XmlReportParser(sample); 153 | 154 | IllegalStateException exception = assertThrows(IllegalStateException.class, report::parse); 155 | assertThat(exception).hasMessage("Invalid report: couldn't find the attribute 'nr' for the sourcefile 'File.java' at line 6 column 21"); 156 | } 157 | 158 | @Test 159 | void should_import_kotlin_report() throws URISyntaxException { 160 | Path sample = load("kotlin.xml"); 161 | XmlReportParser report = new XmlReportParser(sample); 162 | List sourceFiles = report.parse(); 163 | 164 | assertThat(sourceFiles).hasSize(5); 165 | assertThat(sourceFiles.stream().mapToInt(sf -> sf.lines().size()).sum()).isEqualTo(79); 166 | } 167 | 168 | @Test 169 | void line_equality_checks_work_as_expected() { 170 | var line = new XmlReportParser.Line(1, 2, 3, 4, 5); 171 | var identicalLine = new XmlReportParser.Line(1, 2, 3, 4, 5); 172 | 173 | assertThat(line) 174 | //Equality 175 | .isEqualTo(line) 176 | .hasSameHashCodeAs(line) 177 | .isEqualTo(identicalLine) 178 | .hasSameHashCodeAs(identicalLine) 179 | // Inequality 180 | .isNotEqualTo(null) 181 | .isNotEqualTo(new Object()) 182 | .isNotEqualTo(new XmlReportParser.Line(42, 2, 3, 4, 5)) 183 | .isNotEqualTo(new XmlReportParser.Line(1, 42, 3, 4, 5)) 184 | .isNotEqualTo(new XmlReportParser.Line(1, 2, 42, 4, 5)) 185 | .isNotEqualTo(new XmlReportParser.Line(1, 2, 3, 42, 5)) 186 | .isNotEqualTo(new XmlReportParser.Line(1, 2, 3, 4, 42)); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/jacoco/ReportPathsProviderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.io.FileNotFoundException; 23 | import java.io.IOException; 24 | import java.nio.file.Files; 25 | import java.nio.file.Path; 26 | import java.nio.file.Paths; 27 | import java.util.Collection; 28 | import org.junit.jupiter.api.BeforeEach; 29 | import org.junit.jupiter.api.Test; 30 | import org.junit.jupiter.api.extension.RegisterExtension; 31 | import org.junit.jupiter.api.io.TempDir; 32 | import org.sonar.api.batch.sensor.internal.SensorContextTester; 33 | import org.sonar.api.config.internal.MapSettings; 34 | import org.sonar.api.utils.log.LogTesterJUnit5; 35 | import org.sonar.api.utils.log.LoggerLevel; 36 | 37 | import static org.assertj.core.api.Assertions.assertThat; 38 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 39 | 40 | class ReportPathsProviderTest { 41 | @TempDir 42 | Path temp; 43 | 44 | @RegisterExtension 45 | public LogTesterJUnit5 logTester = new LogTesterJUnit5(); 46 | 47 | private MapSettings settings; 48 | private SensorContextTester tester; 49 | private ReportPathsProvider provider; 50 | private Path mavenPath1 = Paths.get("target", "site", "jacoco", "jacoco.xml"); 51 | private Path mavenPath2 = Paths.get("target", "site", "jacoco-it", "jacoco.xml"); 52 | private Path baseDir; 53 | 54 | @BeforeEach 55 | void setUp() throws IOException { 56 | baseDir = temp.resolve("baseDir"); 57 | Files.createDirectory(baseDir); 58 | settings = new MapSettings(); 59 | tester = SensorContextTester.create(baseDir); 60 | tester.setSettings(settings); 61 | provider = new ReportPathsProvider(tester); 62 | } 63 | 64 | private void createMavenReport(Path relativePath) throws IOException { 65 | Path reportPath = baseDir.resolve(relativePath); 66 | Files.createDirectories(reportPath.getParent()); 67 | Files.createFile(reportPath); 68 | } 69 | 70 | @Test 71 | void should_return_null_if_the_aggregate_report_property_is_not_defined() throws FileNotFoundException { 72 | assertThat(provider.getAggregateReportPath()).isNull(); 73 | } 74 | 75 | @Test 76 | void should_throw_an_exception_if_the_aggregate_report_property_points_to_a_file_that_should_not_exist() { 77 | Path reportThatDoesNotExist = temp.resolve("report-that-does-not-exist.xml"); 78 | settings.setProperty(ReportPathsProvider.AGGREGATE_REPORT_PATH_PROPERTY_KEY, reportThatDoesNotExist.toString()); 79 | assertThatThrownBy(() -> assertThat(provider.getAggregateReportPath()).isNull()) 80 | .isInstanceOf(FileNotFoundException.class) 81 | .hasMessage(String.format("Aggregate report %s was not found", reportThatDoesNotExist)); 82 | } 83 | 84 | @Test 85 | void should_return_the_expected_report_path_when_the_aggregate_report_property_is_set_correctly() throws IOException { 86 | Path report = temp.resolve("aggregate-report.xml"); 87 | Files.createDirectories(report.getParent()); 88 | Files.createFile(report); 89 | settings.setProperty(ReportPathsProvider.AGGREGATE_REPORT_PATH_PROPERTY_KEY, report.toString()); 90 | assertThat(provider.getAggregateReportPath()).isEqualTo(report); 91 | } 92 | 93 | @Test 94 | void should_use_provided_paths() throws IOException { 95 | // even though a report will exist in a default location, it shouldn't get loaded since a path is passed as a parameter. 96 | createMavenReport(mavenPath1); 97 | createMavenReport(Paths.get("mypath1")); 98 | createMavenReport(Paths.get("mypath2")); 99 | 100 | settings.setProperty(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY, "mypath1,mypath2"); 101 | 102 | Collection paths = provider.getPaths(); 103 | assertThat(paths).containsOnly(baseDir.resolve("mypath1"), baseDir.resolve("mypath2")); 104 | assertThat(logTester.logs()).isEmpty(); 105 | } 106 | 107 | @Test 108 | void should_use_provided_absolute_path() throws IOException { 109 | Path absolutePath = baseDir.resolve("path"); 110 | 111 | createMavenReport(absolutePath); 112 | 113 | settings.setProperty(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY, absolutePath.toString()); 114 | 115 | Collection paths = provider.getPaths(); 116 | assertThat(paths).containsOnly(absolutePath); 117 | assertThat(logTester.logs()).isEmpty(); 118 | } 119 | 120 | @Test 121 | void should_resolve_relative_path_pattern() throws IOException { 122 | Path reportPath = baseDir.resolve(Paths.get("target", "custom", "jacoco.xml")); 123 | Files.createDirectories(reportPath.getParent()); 124 | Files.createFile(reportPath); 125 | 126 | Path realPath = reportPath.toRealPath(); 127 | 128 | settings.setProperty(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY, "target/custom/*.xml"); 129 | assertThat(provider.getPaths()).containsOnly(realPath); 130 | 131 | settings.setProperty(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY, "target/**/ja*.xml"); 132 | assertThat(provider.getPaths()).containsOnly(realPath); 133 | 134 | settings.setProperty(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY, "target\\**\\ja*.xml"); 135 | assertThat(provider.getPaths()).containsOnly(realPath); 136 | 137 | settings.setProperty(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY, "**/*.xml"); 138 | assertThat(provider.getPaths()).containsOnly(realPath); 139 | assertThat(logTester.logs()).isEmpty(); 140 | 141 | settings.setProperty(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY, "target/**/unknown.xml"); 142 | assertThat(provider.getPaths()).isEmpty(); 143 | assertThat(logTester.logs()).hasSize(1); 144 | assertThat(logTester.logs(LoggerLevel.WARN)).contains("No coverage report can be found with sonar.coverage.jacoco.xmlReportPaths='target/**/unknown.xml'." + 145 | " Using default locations: target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml,build/reports/jacoco/test/jacocoTestReport.xml"); 146 | } 147 | 148 | @Test 149 | void should_resolve_absolute_path_pattern() throws IOException { 150 | Path reportPath = baseDir.resolve(Paths.get("target", "custom", "jacoco.xml")); 151 | Files.createDirectories(reportPath.getParent()); 152 | Files.createFile(reportPath); 153 | 154 | String unixLikeAbsoluteBaseDir = baseDir.toRealPath().toString().replace('\\', '/'); 155 | String unixLikeAbsoluteXmlPattern = unixLikeAbsoluteBaseDir + "/**/*.xml"; 156 | String windowsLikeAbsoluteXmlPattern = unixLikeAbsoluteXmlPattern.replace('/', '\\'); 157 | 158 | Path realPath = reportPath.toRealPath(); 159 | 160 | settings.setProperty(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY, unixLikeAbsoluteXmlPattern); 161 | assertThat(provider.getPaths()).containsOnly(realPath); 162 | 163 | settings.setProperty(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY, windowsLikeAbsoluteXmlPattern); 164 | assertThat(provider.getPaths()).containsOnly(realPath); 165 | assertThat(logTester.logs()).isEmpty(); 166 | } 167 | 168 | @Test 169 | void should_return_empty_if_provided_and_default_does_not_exist() throws IOException { 170 | settings.setProperty(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY, "mypath1"); 171 | 172 | Collection paths = provider.getPaths(); 173 | 174 | assertThat(paths).isEmpty(); 175 | assertThat(logTester.logs()).hasSize(1); 176 | assertThat(logTester.logs(LoggerLevel.WARN)).containsExactly("No coverage report can be found with sonar.coverage.jacoco.xmlReportPaths='mypath1'." + 177 | " Using default locations: target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml,build/reports/jacoco/test/jacocoTestReport.xml"); 178 | } 179 | 180 | @Test 181 | void should_return_empty_with_log_details_if_several_provided_paths_does_not_exist() throws IOException { 182 | settings.setProperty(ReportPathsProvider.REPORT_PATHS_PROPERTY_KEY, "mypath1,mypath2,"); 183 | 184 | Collection paths = provider.getPaths(); 185 | 186 | assertThat(paths).isEmpty(); 187 | assertThat(logTester.logs()).hasSize(3); 188 | assertThat(logTester.logs(LoggerLevel.WARN)).containsExactly("No coverage report can be found with sonar.coverage.jacoco.xmlReportPaths='mypath1,mypath2'. Using default locations: target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml,build/reports/jacoco/test/jacocoTestReport.xml"); 189 | assertThat(logTester.logs(LoggerLevel.INFO)).containsExactly( 190 | "Coverage report doesn't exist for pattern: 'mypath1'", 191 | "Coverage report doesn't exist for pattern: 'mypath2'"); 192 | } 193 | 194 | @Test 195 | void should_fallback_to_defaults_if_exist() throws IOException { 196 | createMavenReport(mavenPath1); 197 | createMavenReport(mavenPath2); 198 | 199 | Collection paths = provider.getPaths(); 200 | assertThat(paths).containsOnly(baseDir.resolve(mavenPath1), baseDir.resolve(mavenPath2)); 201 | assertThat(logTester.logs()).hasSize(1); 202 | assertThat(logTester.logs(LoggerLevel.INFO)).contains("'sonar.coverage.jacoco.xmlReportPaths' is not defined." + 203 | " Using default locations: target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml,build/reports/jacoco/test/jacocoTestReport.xml"); 204 | } 205 | 206 | @Test 207 | void should_return_empty_if_nothing_specified_and_default_doesnt_exist() throws IOException { 208 | Collection paths = provider.getPaths(); 209 | assertThat(paths).isEmpty(); 210 | assertThat(logTester.logs()).hasSize(1); 211 | assertThat(logTester.logs(LoggerLevel.INFO)) 212 | .containsExactly("'sonar.coverage.jacoco.xmlReportPaths' is not defined." + 213 | " Using default locations: target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml,build/reports/jacoco/test/jacocoTestReport.xml"); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/test/java/org/sonar/plugins/jacoco/KotlinFileLocatorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * SonarQube JaCoCo Plugin 3 | * Copyright (C) 2018-2025 SonarSource SA 4 | * mailto:info AT sonarsource DOT com 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with this program; if not, write to the Free Software Foundation, 18 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | */ 20 | package org.sonar.plugins.jacoco; 21 | 22 | import java.nio.charset.Charset; 23 | import org.junit.jupiter.api.Test; 24 | import org.sonar.api.batch.fs.InputFile; 25 | import org.sonar.api.batch.fs.internal.TestInputFileBuilder; 26 | 27 | import java.util.stream.Stream; 28 | 29 | import static org.assertj.core.api.Assertions.assertThat; 30 | 31 | class KotlinFileLocatorTest { 32 | @Test 33 | void should_return_an_input_file_by_package_and_filename() { 34 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 35 | .setContents("package abc.def.ghijkl") 36 | .setCharset(Charset.defaultCharset()) 37 | .build(); 38 | 39 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 40 | 41 | assertThat(kotlinFileLocator.getInputFile("abc/def/ghijkl", "File.kt")).isEqualTo(inputFile); 42 | } 43 | 44 | @Test 45 | void should_not_return_an_input_file_if_package_is_incorrect() { 46 | 47 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 48 | .setContents("package a.b.c") 49 | .setCharset(Charset.defaultCharset()) 50 | .build(); 51 | 52 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 53 | 54 | assertThat(kotlinFileLocator.getInputFile("a/b/c/d", "File.kt")).isNull(); 55 | } 56 | @Test 57 | void should_not_return_an_input_file_if_package_is_missing() { 58 | 59 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 60 | .setContents("val a = b + c") 61 | .setCharset(Charset.defaultCharset()) 62 | .build(); 63 | 64 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 65 | 66 | assertThat(kotlinFileLocator.getInputFile("a/b/c", "File.kt")).isNull(); 67 | } 68 | 69 | @Test 70 | void should_skip_endline_comments() { 71 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 72 | .setContents("// This is a top-level comment" + System.lineSeparator() + 73 | "package a.b.c") 74 | .setCharset(Charset.defaultCharset()) 75 | .build(); 76 | 77 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 78 | 79 | assertThat(kotlinFileLocator.getInputFile("a/b/c", "File.kt")).isEqualTo(inputFile); 80 | } 81 | 82 | @Test 83 | void should_skip_package_in_shebang_line() { 84 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 85 | .setContents("#! /env/usr/bin kotlin package x.y.z" + System.lineSeparator() + 86 | "package a.b.c") 87 | .setCharset(Charset.defaultCharset()) 88 | .build(); 89 | 90 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 91 | 92 | assertThat(kotlinFileLocator.getInputFile("a/b/c", "File.kt")).isEqualTo(inputFile); 93 | assertThat(kotlinFileLocator.getInputFile("x/y/z", "File.kt")).isNull(); 94 | } 95 | 96 | @Test 97 | void should_skip_empty_lines() { 98 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 99 | .setContents(" " + System.lineSeparator() + 100 | "\t" + System.lineSeparator() + 101 | "" + System.lineSeparator() + 102 | "package a.b.c") 103 | .setCharset(Charset.defaultCharset()) 104 | .build(); 105 | 106 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 107 | 108 | assertThat(kotlinFileLocator.getInputFile("a/b/c", "File.kt")).isEqualTo(inputFile); 109 | } 110 | 111 | @Test 112 | void should_skip_inline_delimited_comments_in_package_header() { 113 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 114 | .setContents("package /* comment */ a.b.c") 115 | .setCharset(Charset.defaultCharset()) 116 | .build(); 117 | 118 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 119 | 120 | assertThat(kotlinFileLocator.getInputFile("a/b/c", "File.kt")).isEqualTo(inputFile); 121 | } 122 | 123 | @Test 124 | void should_skip_multiline_delimited_comments_in_package_header() { 125 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 126 | .setContents("package /*comment" + System.lineSeparator() + 127 | "continued */ a.b.c") 128 | .setCharset(Charset.defaultCharset()) 129 | .build(); 130 | 131 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 132 | 133 | assertThat(kotlinFileLocator.getInputFile("a/b/c", "File.kt")).isEqualTo(inputFile); 134 | } 135 | 136 | 137 | @Test 138 | void should_skip_delimited_comments_before_endline_comment() { 139 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 140 | .setContents("/**/// package a.b.c") 141 | .setCharset(Charset.defaultCharset()) 142 | .build(); 143 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 144 | 145 | assertThat(kotlinFileLocator.getInputFile("a/b/c", "File.kt")).isNull(); 146 | } 147 | 148 | @Test 149 | void should_skip_file_annotation_before_package() { 150 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 151 | .setContents("/**/@file:SuppressWarnings(\"UNUSED\")@file:JvmName(\"hi\")package a.b.c") 152 | .setCharset(Charset.defaultCharset()) 153 | .build(); 154 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 155 | 156 | assertThat(kotlinFileLocator.getInputFile("a/b/c", "File.kt")).isEqualTo(inputFile); 157 | } 158 | 159 | @Test 160 | void should_not_read_missing_package_header_from_string() { 161 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 162 | .setContents( 163 | "@file:SuppressWarnings(\"UNUSED\")" + System.lineSeparator() + 164 | "val a = \"package a.b.c\"" 165 | ) 166 | .setCharset(Charset.defaultCharset()) 167 | .build(); 168 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 169 | 170 | assertThat(kotlinFileLocator.getInputFile("a/b/c", "File.kt")).isNull(); 171 | } 172 | 173 | @Test 174 | void should_not_read_package_header_from_comments() { 175 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 176 | .setContents("/* package x.y.z */ package a.b.c //package g.h.i") 177 | .setCharset(Charset.defaultCharset()) 178 | .build(); 179 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 180 | 181 | assertThat(kotlinFileLocator.getInputFile("x/y/z", "File.kt")).isNull(); 182 | assertThat(kotlinFileLocator.getInputFile("g/h/i", "File.kt")).isNull(); 183 | assertThat(kotlinFileLocator.getInputFile("a/b/c", "File.kt")).isEqualTo(inputFile); 184 | } 185 | 186 | @Test 187 | void should_not_read_missing_package_header_from_annotation_value() { 188 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 189 | .setContents("@file:SuppressWarnings(\" package x.y.z \")@file:JvmName(\" package g.h.i \") //package a.b.c") 190 | .setCharset(Charset.defaultCharset()) 191 | .build(); 192 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 193 | 194 | assertThat(kotlinFileLocator.getInputFile("x/y/z", "File.kt")).isNull(); 195 | assertThat(kotlinFileLocator.getInputFile("g/h/i", "File.kt")).isNull(); 196 | assertThat(kotlinFileLocator.getInputFile("a/b/c", "File.kt")).isNull(); 197 | } 198 | 199 | @Test 200 | void should_not_read_package_header_from_strings_and_annotation_values() { 201 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 202 | .setContents("@file:SuppressWarnings(\" package x.y.z \")@file:JvmName(\" package g.h.i \") package a.b.c; val c = (\" package d.e.f \")") 203 | .setCharset(Charset.defaultCharset()) 204 | .build(); 205 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 206 | 207 | assertThat(kotlinFileLocator.getInputFile("x/y/z", "File.kt")).isNull(); 208 | assertThat(kotlinFileLocator.getInputFile("g/h/i", "File.kt")).isNull(); 209 | assertThat(kotlinFileLocator.getInputFile("d/e/f", "File.kt")).isNull(); 210 | assertThat(kotlinFileLocator.getInputFile("a/b/c", "File.kt")).isEqualTo(inputFile); 211 | } 212 | 213 | @Test 214 | void should_not_read_missing_package_header_from_strings_and_annotation_values() { 215 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 216 | .setContents("@file:SuppressWarnings(\" package x.y.z \")@file:JvmName(\" package g.h.i \") val c = (\" package d.e.f \")") 217 | .setCharset(Charset.defaultCharset()) 218 | .build(); 219 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 220 | 221 | assertThat(kotlinFileLocator.getInputFile("x/y/z", "File.kt")).isNull(); 222 | assertThat(kotlinFileLocator.getInputFile("g/h/i", "File.kt")).isNull(); 223 | assertThat(kotlinFileLocator.getInputFile("d/e/f", "File.kt")).isNull(); 224 | assertThat(kotlinFileLocator.getInputFile("a/b/c", "File.kt")).isNull(); 225 | } 226 | 227 | @Test 228 | void should_accept_package_with_backticks_and_newlines() { 229 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 230 | .setContents( 231 | "@file:SuppressWarnings(\" package x.y.z \")@file:JvmName(\" package g.h.i \") val c = (\" package d.e.f \")" + System.lineSeparator() + 232 | "package a " + System.lineSeparator() + 233 | " .`b b` " + System.lineSeparator() + 234 | ".c " 235 | ) 236 | .setCharset(Charset.defaultCharset()) 237 | .build(); 238 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 239 | 240 | assertThat(kotlinFileLocator.getInputFile("x/y/z", "File.kt")).isNull(); 241 | assertThat(kotlinFileLocator.getInputFile("g/h/i", "File.kt")).isNull(); 242 | assertThat(kotlinFileLocator.getInputFile("d/e/f", "File.kt")).isNull(); 243 | assertThat(kotlinFileLocator.getInputFile("a/b b/c", "File.kt")).isEqualTo(inputFile); 244 | } 245 | 246 | @Test 247 | void should_skip_comments_inside_package_declaration() { 248 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 249 | .setContents( 250 | "package a/*abc*/./*def*/b/*ghi*/./*j*/c" + System.lineSeparator() 251 | + System.lineSeparator() 252 | + " // some comment here" + System.lineSeparator() 253 | + ".d" 254 | ) 255 | .setCharset(Charset.defaultCharset()) 256 | .build(); 257 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 258 | 259 | assertThat(kotlinFileLocator.getInputFile("a/b/c/d", "File.kt")).isEqualTo(inputFile); 260 | } 261 | 262 | @Test 263 | void should_accept_package_with_package_keyword_in_backticks() { 264 | InputFile inputFile = new TestInputFileBuilder("module", "src/main/java/org/sonar/test/File.kt") 265 | .setContents( 266 | "@file:SuppressWarnings(\" package x.y.z \")@file:JvmName(\" package g.h.i \") val c = (\" package d.e.f \")" + System.lineSeparator() + 267 | "package a " + System.lineSeparator() + 268 | " .`package` " + System.lineSeparator() + 269 | ".c " 270 | ) 271 | .setCharset(Charset.defaultCharset()) 272 | .build(); 273 | KotlinFileLocator kotlinFileLocator = new KotlinFileLocator(Stream.of(inputFile)); 274 | 275 | assertThat(kotlinFileLocator.getInputFile("x/y/z", "File.kt")).isNull(); 276 | assertThat(kotlinFileLocator.getInputFile("g/h/i", "File.kt")).isNull(); 277 | assertThat(kotlinFileLocator.getInputFile("d/e/f", "File.kt")).isNull(); 278 | assertThat(kotlinFileLocator.getInputFile("a/package/c", "File.kt")).isEqualTo(inputFile); 279 | } 280 | } 281 | 282 | 283 | -------------------------------------------------------------------------------- /its/src/test/java/org/sonar/plugins/jacoco/its/JacocoTest.java: -------------------------------------------------------------------------------- 1 | package org.sonar.plugins.jacoco.its; 2 | 3 | import com.sonar.orchestrator.junit4.OrchestratorRule; 4 | import com.sonar.orchestrator.junit4.OrchestratorRuleBuilder; 5 | import com.sonar.orchestrator.build.BuildResult; 6 | import com.sonar.orchestrator.build.MavenBuild; 7 | import com.sonar.orchestrator.build.SonarScanner; 8 | import com.sonar.orchestrator.locator.FileLocation; 9 | import com.sonar.orchestrator.locator.Location; 10 | import com.sonar.orchestrator.locator.MavenLocation; 11 | import com.sonar.orchestrator.locator.URLLocation; 12 | import org.apache.commons.io.FileUtils; 13 | import org.apache.commons.lang3.StringUtils; 14 | import org.junit.jupiter.api.AfterAll; 15 | import org.junit.jupiter.api.BeforeAll; 16 | import org.junit.jupiter.api.Test; 17 | import org.junit.jupiter.api.io.TempDir; 18 | import org.sonarqube.ws.WsMeasures; 19 | import org.sonarqube.ws.client.HttpConnector; 20 | import org.sonarqube.ws.client.WsClient; 21 | import org.sonarqube.ws.client.WsClientFactories; 22 | import org.sonarqube.ws.client.measure.ComponentWsRequest; 23 | 24 | import java.io.File; 25 | import java.io.IOException; 26 | import java.net.MalformedURLException; 27 | import java.net.URL; 28 | import java.nio.file.Files; 29 | import java.nio.file.Path; 30 | import java.nio.file.Paths; 31 | import java.util.Arrays; 32 | import java.util.List; 33 | import java.util.Map; 34 | import java.util.stream.Collectors; 35 | 36 | import static org.assertj.core.api.Assertions.assertThat; 37 | 38 | public class JacocoTest { 39 | private final static String PROJECT_KEY = "jacoco-test-project"; 40 | private static final String FILE_KEY = "jacoco-test-project:src/main/java/org/sonarsource/test/Calc.java"; 41 | private static final String KOTLIN_FILE_KEY = "org.sonarsource.it.projects:kotlin-jacoco-project:src/main/kotlin/CoverMe.kt"; 42 | private static final String FILE_WITHOUT_COVERAGE_KEY = "jacoco-test-project:src/main/java/org/sonarsource/test/CalcNoCoverage.java"; 43 | 44 | private static OrchestratorRule orchestrator; 45 | 46 | @TempDir 47 | Path temp; 48 | 49 | @BeforeAll 50 | static void beforeAll() { 51 | String defaultRuntimeVersion = "true".equals(System.getenv("SONARSOURCE_QA")) ? null : "LATEST_RELEASE"; 52 | OrchestratorRuleBuilder builder = OrchestratorRule.builderEnv() 53 | .useDefaultAdminCredentialsForBuilds(true) 54 | .setOrchestratorProperty("orchestrator.workspaceDir", "build") 55 | .setSonarVersion(System.getProperty("sonar.runtimeVersion", defaultRuntimeVersion)); 56 | 57 | String pluginVersion = System.getProperty("jacocoVersion"); 58 | Location pluginLocation; 59 | if (StringUtils.isEmpty(pluginVersion) || pluginVersion.endsWith("-SNAPSHOT")) { 60 | pluginLocation = FileLocation.byWildcardMavenFilename(new File("../build/libs"), "sonar-jacoco-*.jar"); 61 | } else { 62 | pluginLocation = MavenLocation.of("org.sonarsource.jacoco", "sonar-jacoco-plugin", pluginVersion); 63 | } 64 | builder.addPlugin(pluginLocation); 65 | try { 66 | // The versions of these 2 plugins were chosen because they have shipped with SQS 2025.1 and greater 67 | builder.addPlugin(URLLocation.create(new URL("https://binaries.sonarsource.com/Distribution/sonar-java-plugin/sonar-java-plugin-8.9.3.40136.jar"))); 68 | builder.addPlugin(URLLocation.create(new URL("https://binaries.sonarsource.com/Distribution/sonar-kotlin-plugin/sonar-kotlin-plugin-2.22.1.6674.jar"))); 69 | } catch (MalformedURLException e) { 70 | throw new IllegalStateException("Failed to download plugin", e); 71 | } 72 | orchestrator = builder.build(); 73 | orchestrator.start(); 74 | } 75 | 76 | @AfterAll 77 | static void afterAll() { 78 | orchestrator.stop(); 79 | } 80 | 81 | @Test 82 | void should_import_coverage() throws IOException { 83 | SonarScanner build = SonarScanner.create() 84 | .setProjectKey(PROJECT_KEY) 85 | .setDebugLogs(true) 86 | .setSourceDirs("src/main") 87 | .setTestDirs("src/test") 88 | .setProperty("sonar.coverage.jacoco.xmlReportPaths", "jacoco.xml") 89 | .setProperty("sonar.java.binaries", ".") 90 | .setProjectDir(prepareProject("simple-project-jacoco")); 91 | orchestrator.executeBuild(build); 92 | 93 | checkCoveredFile(); 94 | checkUncoveredFile(); 95 | } 96 | 97 | @Test 98 | void should_import_coverage_from_one_of_default_locations() throws IOException { 99 | SonarScanner build = SonarScanner.create() 100 | .setProjectKey(PROJECT_KEY) 101 | .setDebugLogs(true) 102 | .setSourceDirs("src/main") 103 | .setTestDirs("src/test") 104 | .setProperty("sonar.java.binaries", ".") 105 | .setProjectDir(prepareProject("simple-project-jacoco")); 106 | orchestrator.executeBuild(build); 107 | 108 | checkCoveredFile(); 109 | checkUncoveredFile(); 110 | } 111 | 112 | @Test 113 | void should_import_coverage_even_when_java_also_imports() throws IOException { 114 | SonarScanner build = SonarScanner.create() 115 | .setProjectKey(PROJECT_KEY) 116 | .setDebugLogs(true) 117 | .setSourceDirs("src/main") 118 | .setTestDirs("src/test") 119 | .setProperty("sonar.coverage.jacoco.xmlReportPaths", "jacoco.xml") 120 | .setProperty("sonar.jacoco.reportPath", "jacoco.exec") 121 | .setProperty("sonar.java.binaries", ".") 122 | .setProjectDir(prepareProject("simple-project-jacoco")); 123 | orchestrator.executeBuild(build); 124 | 125 | checkCoveredFile(); 126 | checkUncoveredFile(); 127 | } 128 | 129 | @Test 130 | void should_give_warning_if_report_doesnt_exist() throws IOException { 131 | SonarScanner build = SonarScanner.create() 132 | .setProjectKey(PROJECT_KEY) 133 | .setDebugLogs(true) 134 | .setSourceDirs("src/main") 135 | .setTestDirs("src/test") 136 | .setProperty("sonar.coverage.jacoco.xmlReportPaths", "invalid_file.xml") 137 | .setProperty("sonar.java.binaries", ".") 138 | .setProjectDir(prepareProject("simple-project-jacoco")); 139 | BuildResult result = orchestrator.executeBuild(build); 140 | result.getLogs().contains("Report doesn't exist: "); 141 | } 142 | 143 | @Test 144 | void should_not_import_coverage_if_no_property_given() throws IOException { 145 | File baseDir = prepareProject("simple-project-jacoco"); 146 | Files.delete(baseDir.toPath().resolve("target/site/jacoco-it/jacoco.xml")); 147 | SonarScanner build = SonarScanner.create() 148 | .setProjectKey(PROJECT_KEY) 149 | .setDebugLogs(true) 150 | .setSourceDirs("src/main") 151 | .setTestDirs("src/test") 152 | .setProperty("sonar.java.binaries", ".") 153 | .setProjectDir(baseDir); 154 | orchestrator.executeBuild(build); 155 | checkNoJacocoCoverage(); 156 | } 157 | 158 | @Test 159 | void should_not_import_coverage_if_report_contains_files_that_cant_be_found() throws IOException { 160 | SonarScanner build = SonarScanner.create() 161 | .setProjectKey(PROJECT_KEY) 162 | .setDebugLogs(true) 163 | .setSourceDirs("src/main") 164 | .setTestDirs("src/test") 165 | .setProperty("sonar.java.binaries", ".") 166 | .setProperty("sonar.coverage.jacoco.xmlReportPaths", "jacoco-with-invalid-sources.xml") 167 | .setProjectDir(prepareProject("simple-project-jacoco")); 168 | orchestrator.executeBuild(build); 169 | checkNoJacocoCoverage(); 170 | } 171 | 172 | @Test 173 | void no_failure_with_invalid_reports() throws IOException { 174 | SonarScanner build = SonarScanner.create() 175 | .setProjectKey(PROJECT_KEY) 176 | .setDebugLogs(true) 177 | .setSourceDirs("src/main") 178 | .setTestDirs("src/test") 179 | .setProperty("sonar.java.binaries", ".") 180 | .setProperty("sonar.coverage.jacoco.xmlReportPaths", "jacoco-with-invalid-lines.xml,jacoco-with-invalid-format.xml") 181 | .setProjectDir(prepareProject("simple-project-jacoco")); 182 | orchestrator.executeBuild(build); 183 | checkCoveredFile(); 184 | 185 | // No coverage info from JaCoCo for second file 186 | Map measures = getCoverageMeasures(FILE_WITHOUT_COVERAGE_KEY); 187 | assertThat(measures.get("line_coverage")).isEqualTo(0.0); 188 | assertThat(measures.get("lines_to_cover")).isEqualTo(6); 189 | assertThat(measures.get("uncovered_lines")).isEqualTo(6.0); 190 | assertThat(measures.get("branch_coverage")).isNull(); 191 | assertThat(measures.get("conditions_to_cover")).isNull(); 192 | assertThat(measures.get("uncovered_conditions")).isNull(); 193 | assertThat(measures.get("coverage")).isEqualTo(0.0); 194 | } 195 | 196 | @Test 197 | void aggregate_and_module_based_reports_complement_each_over_to_build_total_coverage() { 198 | Path project = Path.of("src", "test", "resources", "aggregate-and-module-based-mixed-coverage"); 199 | Path rootPom = project.resolve("pom.xml"); 200 | Path reportLocation = project.resolve("report") 201 | .resolve("target") 202 | .resolve("site") 203 | .resolve("jacoco-aggregate") 204 | .resolve("jacoco.xml"); 205 | MavenBuild build = MavenBuild.create() 206 | .setPom(rootPom.toFile()) 207 | .addGoal("clean verify") 208 | .addSonarGoal() 209 | .setProperty("sonar.coverage.jacoco.aggregateXmlReportPath", reportLocation.toAbsolutePath().toString()); 210 | 211 | orchestrator.executeBuild(build, true); 212 | 213 | Map measuresForLibrary = getCoverageMeasures("org.example:aggregate-and-module-based-mixed-coverage:library/src/main/java/org/example/Library.java"); 214 | assertThat(measuresForLibrary) 215 | .containsEntry("line_coverage", 100.0) 216 | .containsEntry("lines_to_cover", 4.0) 217 | .containsEntry("uncovered_lines", 0.0) 218 | .containsEntry("branch_coverage", 100.0) 219 | .containsEntry("conditions_to_cover", 2.0) 220 | .containsEntry("uncovered_conditions", 0.0) 221 | .containsEntry("coverage", 100.0); 222 | 223 | Map measuresForSquarer = getCoverageMeasures("org.example:aggregate-and-module-based-mixed-coverage:self-covered/src/main/java/org/example/Squarer.java"); 224 | assertThat(measuresForSquarer) 225 | .containsEntry("line_coverage", 100.0) 226 | .containsEntry("lines_to_cover", 2.0) 227 | .containsEntry("uncovered_lines", 0.0) 228 | .containsEntry("coverage", 100.0); 229 | } 230 | 231 | @Test 232 | void kotlin_files_should_be_located_and_covered() { 233 | Path BASE_DIRECTORY = Paths.get("src/test/resources"); 234 | MavenBuild build = MavenBuild.create() 235 | .setPom(new File(BASE_DIRECTORY.toFile(), "kotlin-jacoco-project/pom.xml")) 236 | .setGoals("clean install", "sonar:sonar"); 237 | orchestrator.executeBuild(build); 238 | 239 | checkCoveredKotlinFile(); 240 | } 241 | 242 | private void checkNoJacocoCoverage() { 243 | Map measures = getCoverageMeasures(FILE_KEY); 244 | assertThat(measures.get("line_coverage")).isEqualTo(0.0); 245 | // java doesn't consider the declaration of the constructor as executable line, so less one than with jacoco 246 | assertThat(measures.get("lines_to_cover")).isEqualTo(10.0); 247 | assertThat(measures.get("uncovered_lines")).isEqualTo(10.0); 248 | assertThat(measures.get("branch_coverage")).isNull(); 249 | assertThat(measures.get("conditions_to_cover")).isNull(); 250 | assertThat(measures.get("uncovered_conditions")).isNull(); 251 | assertThat(measures.get("coverage")).isEqualTo(0.0); 252 | } 253 | 254 | private void checkUncoveredFile() { 255 | Map measures = getCoverageMeasures(FILE_WITHOUT_COVERAGE_KEY); 256 | assertThat(measures.get("line_coverage")).isEqualTo(0.0); 257 | assertThat(measures.get("lines_to_cover")).isEqualTo(7.0); 258 | assertThat(measures.get("uncovered_lines")).isEqualTo(7.0); 259 | assertThat(measures.get("branch_coverage")).isEqualTo(0.0); 260 | assertThat(measures.get("conditions_to_cover")).isEqualTo(2.0); 261 | assertThat(measures.get("uncovered_conditions")).isEqualTo(2.0); 262 | assertThat(measures.get("coverage")).isEqualTo(0.0); 263 | } 264 | 265 | private void checkCoveredFile() { 266 | Map measures = getCoverageMeasures(FILE_KEY); 267 | assertThat(measures.get("line_coverage")).isEqualTo(90.9); 268 | assertThat(measures.get("lines_to_cover")).isEqualTo(11.0); 269 | assertThat(measures.get("uncovered_lines")).isEqualTo(1.0); 270 | assertThat(measures.get("branch_coverage")).isEqualTo(75.0); 271 | assertThat(measures.get("conditions_to_cover")).isEqualTo(4.0); 272 | assertThat(measures.get("uncovered_conditions")).isEqualTo(1.0); 273 | assertThat(measures.get("coverage")).isEqualTo(86.7); 274 | } 275 | 276 | private void checkCoveredKotlinFile() { 277 | Map measures = getCoverageMeasures(KOTLIN_FILE_KEY); 278 | assertThat(measures.get("line_coverage")).isEqualTo(75); 279 | assertThat(measures.get("lines_to_cover")).isEqualTo(4.0); 280 | assertThat(measures.get("uncovered_lines")).isEqualTo(1.0); 281 | assertThat(measures.get("branch_coverage")).isEqualTo(50.0); 282 | assertThat(measures.get("conditions_to_cover")).isEqualTo(2.0); 283 | assertThat(measures.get("uncovered_conditions")).isEqualTo(1.0); 284 | assertThat(measures.get("coverage")).isEqualTo(66.7); 285 | } 286 | 287 | private Map getCoverageMeasures(String fileKey) { 288 | List metricKeys = Arrays.asList("line_coverage", "lines_to_cover", 289 | "uncovered_lines", "branch_coverage", 290 | "conditions_to_cover", "uncovered_conditions", "coverage"); 291 | 292 | return getWsClient().measures().component(new ComponentWsRequest() 293 | .setComponent(fileKey) 294 | .setMetricKeys(metricKeys)) 295 | .getComponent().getMeasuresList() 296 | .stream() 297 | .collect(Collectors.toMap(WsMeasures.Measure::getMetric, m -> Double.parseDouble(m.getValue()))); 298 | } 299 | 300 | private WsClient getWsClient() { 301 | return WsClientFactories.getDefault().newClient(HttpConnector.newBuilder() 302 | .url(orchestrator.getServer().getUrl()) 303 | .build()); 304 | } 305 | 306 | private File prepareProject(String name) throws IOException { 307 | Path projectRoot = Paths.get("src/test/resources").resolve(name); 308 | File targetDir = temp.resolve(name).toFile(); 309 | FileUtils.copyDirectory(projectRoot.toFile(), targetDir); 310 | return targetDir; 311 | } 312 | } 313 | --------------------------------------------------------------------------------