diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index 2ebe942c6..5a31ee198 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -116,7 +116,7 @@ jobs: # Add `createProdDebugUnitTestCoverageReport` if we ever add JVM tests for prod - name: Generate coverage reports for Debug variants - run: ./gradlew createDemoDebugUnitTestCoverageReport + run: ./gradlew createDemoDebugCombinedCoverageReport - name: Display local test coverage id: jacoco diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt index 8d28f99dd..3bbdf8ea2 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt @@ -14,7 +14,9 @@ * limitations under the License. */ +import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.gradle.internal.dsl.BaseAppModuleExtension +import com.google.samples.apps.nowinandroid.configureJacoco import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.getByType @@ -22,6 +24,7 @@ import org.gradle.kotlin.dsl.getByType class AndroidApplicationJacocoConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { + pluginManager.apply("jacoco") val androidExtension = extensions.getByType() androidExtension.buildTypes.configureEach { @@ -29,6 +32,8 @@ class AndroidApplicationJacocoConventionPlugin : Plugin { enableAndroidTestCoverage = true enableUnitTestCoverage = true } + + configureJacoco(extensions.getByType()) } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt index 9733ce6f5..41119f5c5 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt @@ -15,6 +15,9 @@ */ import com.android.build.api.dsl.LibraryExtension +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.android.build.api.variant.LibraryAndroidComponentsExtension +import com.google.samples.apps.nowinandroid.configureJacoco import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.getByType @@ -22,14 +25,16 @@ import org.gradle.kotlin.dsl.getByType class AndroidLibraryJacocoConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { + pluginManager.apply("jacoco") val androidExtension = extensions.getByType() androidExtension.buildTypes.configureEach { // The jacoco plugin is applied automatically when any of these are set - enableAndroidTestCoverage = true enableUnitTestCoverage = true } + + configureJacoco(extensions.getByType()) } } } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt new file mode 100644 index 000000000..7cb757f0d --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid + +import com.android.build.api.artifact.ScopedArtifact +import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.api.variant.ScopedArtifacts +import com.android.build.gradle.internal.coverage.JacocoReportTask +import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.testing.Test +import org.gradle.internal.impldep.bsh.commands.dir +import org.gradle.kotlin.dsl.assign +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.register +import org.gradle.kotlin.dsl.withType +import org.gradle.testing.jacoco.plugins.JacocoPluginExtension +import org.gradle.testing.jacoco.plugins.JacocoTaskExtension +import org.gradle.testing.jacoco.tasks.JacocoCoverageVerification +import org.gradle.testing.jacoco.tasks.JacocoReport +import java.util.Locale + +private val coverageExclusions = listOf( + // Android + "**/R.class", + "**/R\$*.class", + "**/BuildConfig.*", + "**/Manifest*.*", + "**/*_Hilt*.class", + "**/Hilt_*.class", +) + +private fun String.capitalize() = replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() +} + +internal fun Project.configureJacoco( + androidComponentsExtension: AndroidComponentsExtension<*, *, *>, +) { + configure { + toolVersion = libs.findVersion("jacoco").get().toString() + } + + androidComponentsExtension.onVariants { variant -> + val testTaskName = "test${variant.name.capitalize()}UnitTest" + val androidtestTaskName = "connected${variant.name.capitalize()}AndroidTest" + val myObjFactory = project.objects + val buildDir = layout.buildDirectory.get().asFile + val allJars: ListProperty = myObjFactory.listProperty(RegularFile::class.java) + val allDirectories: ListProperty = myObjFactory.listProperty(Directory::class.java) + val reportTask = + tasks.register("create${variant.name.capitalize()}CombinedCoverageReport", JacocoReport::class) { + + if (tasks.findByName(testTaskName) != null) { + dependsOn(testTaskName) + } + if (tasks.findByName(androidtestTaskName) != null) { + dependsOn(androidtestTaskName) + } + classDirectories.setFrom( + allJars, + allDirectories.map { dirs -> + dirs.map { dir -> + myObjFactory.fileTree().setDir(dir).exclude(coverageExclusions) + } + } + ) + reports { + xml.required.set(true) + html.required.set(true) + } + + // TODO: This is missing files in src/debug/, src/prod, src/demo, src/demoDebug... + sourceDirectories.setFrom(files("$projectDir/src/main/java", "$projectDir/src/main/kotlin")) + + executionData.setFrom( + project.fileTree("$buildDir/outputs/unit_test_code_coverage/${variant.name}UnitTest") + .matching { include("**/*.exec") }, + + project.fileTree("$buildDir/outputs/code_coverage/${variant.name}AndroidTest") + .matching { include("**/*.ec") } + ) + } + + + variant.artifacts.forScope(ScopedArtifacts.Scope.PROJECT) + .use(reportTask) + .toGet( + ScopedArtifact.CLASSES, + { _ -> allJars }, + { _ -> allDirectories }, + ) + } + + tasks.withType().configureEach { + configure { + // Required for JaCoCo + Robolectric + // https://github.com/robolectric/robolectric/issues/2230 + isIncludeNoLocationClasses = true + + // Required for JDK 11 with the above + // https://github.com/gradle/gradle/issues/5184#issuecomment-391982009 + excludes = listOf("jdk.internal.*") + } + } +} diff --git a/core/database/src/test/kotlin/EmptyTest.kt b/core/database/src/test/kotlin/EmptyTest.kt deleted file mode 100644 index b6a1d4919..000000000 --- a/core/database/src/test/kotlin/EmptyTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import org.junit.Test - -/** - * Workaround for https://issuetracker.google.com/329869184 - */ -class EmptyTest { - @Test - fun test1() { } -} diff --git a/core/ui/src/test/kotlin/EmptyTest.kt b/core/ui/src/test/kotlin/EmptyTest.kt deleted file mode 100644 index b6a1d4919..000000000 --- a/core/ui/src/test/kotlin/EmptyTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import org.junit.Test - -/** - * Workaround for https://issuetracker.google.com/329869184 - */ -class EmptyTest { - @Test - fun test1() { } -} diff --git a/sync/work/src/test/kotlin/EmptyTest.kt b/sync/work/src/test/kotlin/EmptyTest.kt deleted file mode 100644 index b6a1d4919..000000000 --- a/sync/work/src/test/kotlin/EmptyTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import org.junit.Test - -/** - * Workaround for https://issuetracker.google.com/329869184 - */ -class EmptyTest { - @Test - fun test1() { } -}