diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bef031588..4d94dd2b7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 # diff --git a/gradle/root/coverage.gradle b/gradle/root/coverage.gradle index 2aee7f3988..b0ba44583e 100644 --- a/gradle/root/coverage.gradle +++ b/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 { @@ -46,4 +59,6 @@ task mockitoCoverage(type: JacocoReport) { } } -task coverageReport(dependsOn: ["mockitoCoverage"]) {} +tasks.register("coverageReport") { + dependsOn(mockitoCoverage) +} diff --git a/subprojects/android/src/test/README.md b/subprojects/android/src/test/README.md new file mode 100644 index 0000000000..c8259e9b47 --- /dev/null +++ b/subprojects/android/src/test/README.md @@ -0,0 +1 @@ +Tests are located in :androidTest subproject. diff --git a/subprojects/androidTest/androidTest.gradle b/subprojects/androidTest/androidTest.gradle index 072c222639..0830ec5bdf 100644 --- a/subprojects/androidTest/androidTest.gradle +++ b/subprojects/androidTest/androidTest.gradle @@ -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' } @@ -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