Skip to content

Commit

Permalink
Rewire Jacoco for Gradle 8/9 (#3052)
Browse files Browse the repository at this point in the history
Also generate coverage for the Android subproject.
  • Loading branch information
TWiStErRob committed Jul 8, 2023
1 parent 0bb3388 commit 69e0a6c
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 41 deletions.
27 changes: 14 additions & 13 deletions .github/workflows/ci.yml
Expand Up @@ -70,20 +70,14 @@ jobs:
MOCK_MAKER: ${{ matrix.entry.mock-maker }}
MEMBER_ACCESSOR: ${{ matrix.entry.member-accessor }}

- name: 7. Upload coverage report
run: |
./gradlew coverageReport -s --scan && cp build/reports/jacoco/mockitoCoverage/mockitoCoverage.xml jacoco.xml
curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import # One-time step
- name: 7. Generate coverage report
run: ./gradlew coverageReport --stacktrace --scan

curl -Os https://uploader.codecov.io/latest/linux/codecov
curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM
curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig
gpgv codecov.SHA256SUM.sig codecov.SHA256SUM
shasum -a 256 -c codecov.SHA256SUM
chmod +x codecov
./codecov
- name: 8. Upload coverage report
uses: codecov/codecov-action@v3
with:
files: build/reports/jacoco/mockitoCoverage/mockitoCoverage.xml
fail_ci_if_error: true

#
# Android build job
Expand Down Expand Up @@ -166,6 +160,13 @@ jobs:
${{ github.workspace }}/emulator.log
${{ github.workspace }}/emulator-startup.log
# :androidTest:connectedCheck (which depends on :androidTest:createDebugAndroidTestCoverageReport) already generated coverage report.
- name: 5. Upload coverage report
uses: codecov/codecov-action@v3
with:
files: subprojects/androidTest/build/reports/coverage/androidTest/debug/connected/report.xml
fail_ci_if_error: true

#
# Release job, only for pushes to the main development branch
#
Expand Down
71 changes: 43 additions & 28 deletions gradle/root/coverage.gradle
@@ -1,43 +1,56 @@
task mockitoCoverage(type: JacocoReport) {

allprojects { currentProject ->
// These tests do not have a `test` task.
if (currentProject.name in ['android']) {
return
allprojects { project ->
plugins.withId("java") {
project.apply plugin: "jacoco"
project.jacoco {
toolVersion = '0.8.8'
}
plugins.withId("java") {
mockitoCoverage.sourceSets currentProject.sourceSets.main
}
}

apply plugin: "jacoco"
def mockitoCoverage = tasks.register("mockitoCoverage", JacocoReport) { mockitoCoverage ->
allprojects { Project currentProject ->
plugins.withId("jacoco") {
plugins.withId("java") {
// JacocoReport.sourceSets() appends both sourceDirectories and classDirectories.
mockitoCoverage.sourceSets currentProject.sourceSets.main
Test test = currentProject.tasks.test
mockitoCoverage.executionData(currentProject.files(test.jacoco.destinationFile).builtBy(test))
}
plugins.withId("com.android.base") {
// The :androidTest project only contains test infrastructure, so there's no need to grab sources/classes.

jacoco {
toolVersion = '0.8.8'
def androidTest = currentProject.tasks.connectedDebugAndroidTest
def instrumentationCoverage = currentProject
.layout.buildDirectory.dir("outputs/code_coverage/debugAndroidTest/connected")
.map { it.asFileTree.matching { include "*/coverage.ec" } }
mockitoCoverage.executionData(currentProject.files(instrumentationCoverage))
mockitoCoverage.mustRunAfter(androidTest)

if (currentProject != rootProject) { //already automatically enhanced in mockito main project as "java" plugin was applied before
applyTo test
}
test.jacoco.destinationFile = file("${currentProject.buildDir}/jacoco/test.exec")
def unitTest = currentProject.tasks.testDebugUnitTest
def coverageFiles = currentProject
.layout.buildDirectory.dir("outputs/unit_test_code_coverage/debugUnitTest")
.map { it.asFileTree.matching { include "testDebugUnitTest.exec" } }
mockitoCoverage.executionData(currentProject.files(coverageFiles).builtBy(unitTest))
}
}
}

mockitoCoverage.executionData(files(test.jacoco.destinationFile).builtBy(test))

afterEvaluate {
classDirectories.setFrom(
classDirectories.files.collect {
fileTree(
dir: it,
exclude: ["**/internal/util/concurrent/**"]
)
}
gradle.taskGraph.whenReady { // Theoretically Gradle.projectsEvaluated, but not possible here.
// Run this after all projects' sourceSets have been added (which is delayed to when the jacoco plugin is applied).
classDirectories.setFrom(
classDirectories.files.collect {
fileTree(
dir: it,
exclude: ["**/internal/util/concurrent/**"]
)
}
}
)
}

reports {
xml.required = true
html.required = true
html.outputLocation = file("${buildDir}/jacocoHtml")
html.outputLocation = rootProject.layout.buildDirectory.dir("jacocoHtml")
}

doLast {
Expand All @@ -46,4 +59,6 @@ task mockitoCoverage(type: JacocoReport) {
}
}

task coverageReport(dependsOn: ["mockitoCoverage"]) {}
tasks.register("coverageReport") {
dependsOn(mockitoCoverage)
}
1 change: 1 addition & 0 deletions subprojects/android/src/test/README.md
@@ -0,0 +1 @@
Tests are located in :androidTest subproject.
35 changes: 35 additions & 0 deletions subprojects/androidTest/androidTest.gradle
Expand Up @@ -18,10 +18,39 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
debug {
enableUnitTestCoverage true
enableAndroidTestCoverage true
}
}
afterEvaluate {
// Enable coverage of mockito classes which are not part of this module.
// Without these added classFileCollection dependencies the coverage data/report is empty.
tasks.createDebugAndroidTestCoverageReport { com.android.build.gradle.internal.coverage.JacocoReportTask task ->
// Clear the production code of this module: src/main/java, it has no mockito-user-visible code.
task.classFileCollection.setFrom()
rootProject.allprojects { Project currentProject ->
plugins.withId("java") {
SourceSetContainer sourceSets = currentProject.sourceSets
// Mimic org.gradle.testing.jacoco.tasks.JacocoReportBase.sourceSets().
// Not possible to set task.javaSources because it's initialized with setDisallowChanges.
//task.javaSources.add({ [ sourceSets.main.allJava.srcDirs ] })
task.classFileCollection.from(
sourceSets.main.output.asFileTree.matching {
exclude("**/internal/util/concurrent/**")
}
)
}
}
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}

kotlinOptions {
jvmTarget = '11'
}
Expand All @@ -38,6 +67,12 @@ apply from: "$rootDir/gradle/dependencies.gradle"
dependencies {
implementation libraries.kotlin.stdlib

// Add :android on the classpath so that AGP's jacoco setup thinks it's "production code to be tested".
// Essentially a way to say: tasks.createDebugAndroidTestCoverageReport.classFileCollection.from(project(":android"))
runtimeOnly project(":android")
// Exclude :android from JVM tests, because otherwise it clashes with mockito-core/project(":").
configurations.testImplementation { exclude group: 'org.mockito', module: 'android' }

testImplementation project(":")
testImplementation libraries.junit4
testImplementation libraries.junitJupiterApi
Expand Down

0 comments on commit 69e0a6c

Please sign in to comment.