From f281c6c36516c35225155cea2b9ac5f5b4e8d72a Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 11 Jan 2024 21:16:31 +0100 Subject: [PATCH] Don't apply Kover to projects with AAR libraries (#2211) * Don't apply Kover to projects with AAR libraries * Move the Kover configuration to the `plugins` project --- app/build.gradle.kts | 199 +-------------- build.gradle.kts | 6 +- gradle/libs.versions.toml | 4 +- plugins/build.gradle.kts | 1 + .../main/kotlin/extension/KoverExtension.kt | 229 ++++++++++++++++++ ...ent.android-compose-application.gradle.kts | 5 + .../kotlin/io.element.android-root.gradle.kts | 7 + 7 files changed, 248 insertions(+), 203 deletions(-) create mode 100644 plugins/src/main/kotlin/extension/KoverExtension.kt create mode 100644 plugins/src/main/kotlin/io.element.android-root.gradle.kts diff --git a/app/build.gradle.kts b/app/build.gradle.kts index da6a852877..543a414091 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,7 +28,8 @@ plugins { alias(libs.plugins.anvil) alias(libs.plugins.ksp) alias(libs.plugins.kapt) - alias(libs.plugins.firebaseAppDistribution) + // When using precompiled plugins, we need to apply the firebase plugin like this + id(libs.plugins.firebaseAppDistribution.get().pluginId) alias(libs.plugins.knit) id("kotlin-parcelize") // To be able to update the firebase.xml files, uncomment and build the project @@ -191,45 +192,6 @@ knit { } } -/** - * Kover configuration - */ - -dependencies { - // Add all sub projects to kover except some of them - project.rootProject.subprojects - .filter { - it.project.projectDir.resolve("build.gradle.kts").exists() - } - .map { it.path } - .sorted() - .filter { - it !in listOf( - ":app", - ":samples", - ":anvilannotations", - ":anvilcodegen", - ":samples:minimal", - ":tests:testutils", - // Exclude `:libraries:matrix:impl` module, it contains only wrappers to access the Rust Matrix - // SDK api, so it is not really relevant to unit test it: there is no logic to test. - ":libraries:matrix:impl", - // Exclude modules which are not Android libraries - // See https://github.com/Kotlin/kotlinx-kover/issues/312 - ":appconfig", - ":libraries:core", - ":libraries:coroutines", - ":libraries:di", - ":libraries:rustsdk", - ":libraries:textcomposer:lib", - ) - } - .forEach { - // println("Add $it to kover") - kover(project(it)) - } -} - val ciBuildProperty = "ci-build" val isCiBuild = if (project.hasProperty(ciBuildProperty)) { val raw = project.property(ciBuildProperty) as? String @@ -250,163 +212,6 @@ kover { } } -// https://kotlin.github.io/kotlinx-kover/ -// Run `./gradlew :app:koverHtmlReport` to get report at ./app/build/reports/kover -// Run `./gradlew :app:koverXmlReport` to get XML report -koverReport { - filters { - excludes { - classes( - // Exclude generated classes. - "*_ModuleKt", - "anvil.hint.binding.io.element.*", - "anvil.hint.merge.*", - "anvil.hint.multibinding.io.element.*", - "anvil.module.*", - "com.airbnb.android.showkase*", - "io.element.android.libraries.designsystem.showkase.*", - "io.element.android.x.di.DaggerAppComponent*", - "*_Factory", - "*_Factory_Impl", - "*_Factory$*", - "*_Module", - "*_Module$*", - "*Module_Provides*", - "Dagger*Component*", - "*ComposableSingletons$*", - "*_AssistedFactory_Impl*", - "*BuildConfig", - // Generated by Showkase - "*Ioelementandroid*PreviewKt$*", - "*Ioelementandroid*PreviewKt", - // Other - // We do not cover Nodes (normally covered by maestro, but code coverage is not computed with maestro) - "*Node", - "*Node$*", - "*Presenter\$present\$*", - // Forked from compose - "io.element.android.libraries.designsystem.theme.components.bottomsheet.*", - ) - annotatedBy( - "androidx.compose.ui.tooling.preview.Preview", - "io.element.android.libraries.architecture.coverage.ExcludeFromCoverage", - "io.element.android.libraries.designsystem.preview.PreviewsDayNight", - "io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight", - ) - } - } - - defaults { - // add reports of both 'debug' and 'release' Android build variants to default reports - mergeWith("debug") - mergeWith("release") - - verify { - onCheck = true - // General rule: minimum code coverage. - rule("Global minimum code coverage.") { - isEnabled = true - entity = kotlinx.kover.gradle.plugin.dsl.GroupingEntityType.APPLICATION - bound { - minValue = 65 - // Setting a max value, so that if coverage is bigger, it means that we have to change minValue. - // For instance if we have minValue = 20 and maxValue = 30, and current code coverage is now 31.32%, update - // minValue to 25 and maxValue to 35. - maxValue = 75 - metric = kotlinx.kover.gradle.plugin.dsl.MetricType.INSTRUCTION - aggregation = kotlinx.kover.gradle.plugin.dsl.AggregationType.COVERED_PERCENTAGE - } - } - // Rule to ensure that coverage of Presenters is sufficient. - rule("Check code coverage of presenters") { - isEnabled = true - entity = kotlinx.kover.gradle.plugin.dsl.GroupingEntityType.CLASS - filters { - includes { - classes( - "*Presenter", - ) - } - excludes { - classes( - "*Fake*Presenter", - "io.element.android.appnav.loggedin.LoggedInPresenter$*", - // Some options can't be tested at the moment - "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*", - "*Presenter\$present\$*", - ) - } - } - bound { - minValue = 85 - metric = kotlinx.kover.gradle.plugin.dsl.MetricType.INSTRUCTION - aggregation = kotlinx.kover.gradle.plugin.dsl.AggregationType.COVERED_PERCENTAGE - } - } - // Rule to ensure that coverage of States is sufficient. - rule("Check code coverage of states") { - isEnabled = true - entity = kotlinx.kover.gradle.plugin.dsl.GroupingEntityType.CLASS - filters { - includes { - classes( - "^*State$", - ) - } - excludes { - classes( - "io.element.android.appnav.root.RootNavState*", - "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*", - "io.element.android.libraries.matrix.api.timeline.item.event.EventSendState$*", - "io.element.android.libraries.matrix.api.room.RoomMembershipState*", - "io.element.android.libraries.matrix.api.room.MatrixRoomMembersState*", - "io.element.android.libraries.push.impl.notifications.NotificationState*", - "io.element.android.features.messages.impl.media.local.pdf.PdfViewerState", - "io.element.android.features.messages.impl.media.local.LocalMediaViewState", - "io.element.android.features.location.impl.map.MapState*", - "io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*", - "io.element.android.libraries.designsystem.swipe.SwipeableActionsState*", - "io.element.android.features.messages.impl.timeline.components.ExpandableState*", - "io.element.android.features.messages.impl.timeline.model.bubble.BubbleState*", - "io.element.android.libraries.maplibre.compose.CameraPositionState*", - "io.element.android.libraries.maplibre.compose.SaveableCameraPositionState", - "io.element.android.libraries.maplibre.compose.SymbolState*", - "io.element.android.features.ftue.api.state.*", - "io.element.android.features.ftue.impl.welcome.state.*", - ) - } - } - bound { - minValue = 90 - metric = kotlinx.kover.gradle.plugin.dsl.MetricType.INSTRUCTION - aggregation = kotlinx.kover.gradle.plugin.dsl.AggregationType.COVERED_PERCENTAGE - } - } - // Rule to ensure that coverage of Views is sufficient (deactivated for now). - rule("Check code coverage of views") { - isEnabled = true - entity = kotlinx.kover.gradle.plugin.dsl.GroupingEntityType.CLASS - filters { - includes { - classes( - "*ViewKt", - ) - } - } - bound { - // TODO Update this value, for now there are too many missing tests. - minValue = 0 - metric = kotlinx.kover.gradle.plugin.dsl.MetricType.INSTRUCTION - aggregation = kotlinx.kover.gradle.plugin.dsl.AggregationType.COVERED_PERCENTAGE - } - } - } - } - - androidReports("release") { - } -} - dependencies { allLibrariesImpl() allServicesImpl() diff --git a/build.gradle.kts b/build.gradle.kts index dfd0c009e9..051a7a4112 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,6 +26,7 @@ buildscript { // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { + id("io.element.android-root") alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.android) apply false @@ -39,7 +40,6 @@ plugins { alias(libs.plugins.ktlint) alias(libs.plugins.dependencygraph) alias(libs.plugins.sonarqube) - alias(libs.plugins.kover) apply false } tasks.register("clean").configure { @@ -161,10 +161,6 @@ allprojects { } } -allprojects { - apply(plugin = "org.jetbrains.kotlinx.kover") -} - // Register quality check tasks. tasks.register("runQualityChecks") { project.subprojects { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 01179e703a..8ce7d5a4ef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,6 +51,7 @@ autoservice = "1.1.1" junit = "4.13.2" androidx-test-ext-junit = "1.1.5" espresso-core = "3.5.1" +kover = "0.7.5" [libraries] # Project @@ -58,6 +59,7 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref # https://developer.android.com/studio/write/java8-support#library-desugaring-versions android_desugar = "com.android.tools:desugar_jdk_libs:2.0.4" kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" } gms_google_services = "com.google.gms:google-services:4.4.0" # https://firebase.google.com/docs/android/setup#available-libraries google_firebase_bom = "com.google.firebase:firebase-bom:32.7.0" @@ -215,7 +217,7 @@ dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12" dependencycheck = "org.owasp.dependencycheck:9.0.8" dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" } paparazzi = "app.cash.paparazzi:1.3.1" -kover = "org.jetbrains.kotlinx.kover:0.7.5" +kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppDistribution" } knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.0" } diff --git a/plugins/build.gradle.kts b/plugins/build.gradle.kts index 78500f5a15..a2934b252d 100644 --- a/plugins/build.gradle.kts +++ b/plugins/build.gradle.kts @@ -26,6 +26,7 @@ repositories { dependencies { implementation(libs.android.gradle.plugin) implementation(libs.kotlin.gradle.plugin) + implementation(libs.kover.gradle.plugin) implementation(platform(libs.google.firebase.bom)) implementation(libs.firebase.appdistribution.gradle) implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) diff --git a/plugins/src/main/kotlin/extension/KoverExtension.kt b/plugins/src/main/kotlin/extension/KoverExtension.kt new file mode 100644 index 0000000000..42b7a5bc1f --- /dev/null +++ b/plugins/src/main/kotlin/extension/KoverExtension.kt @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * 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 + * + * http://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 extension + +import kotlinx.kover.gradle.plugin.dsl.KoverReportExtension +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply + +val localAarProjects = listOf( + ":libraries:rustsdk", + ":libraries:textcomposer:lib" +) + +val excludedKoverSubProjects = listOf( + ":app", + ":samples", + ":anvilannotations", + ":anvilcodegen", + ":samples:minimal", + ":tests:testutils", + // Exclude `:libraries:matrix:impl` module, it contains only wrappers to access the Rust Matrix + // SDK api, so it is not really relevant to unit test it: there is no logic to test. + ":libraries:matrix:impl", + // Exclude modules which are not Android libraries + // See https://github.com/Kotlin/kotlinx-kover/issues/312 + ":appconfig", + ":libraries:core", + ":libraries:coroutines", + ":libraries:di", +) + localAarProjects + +private fun Project.koverReport(action: Action) { + (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("koverReport", action) +} + +fun Project.setupKover() { + // https://kotlin.github.io/kotlinx-kover/ + // Run `./gradlew :app:koverHtmlReport` to get report at ./app/build/reports/kover + // Run `./gradlew :app:koverXmlReport` to get XML report + koverReport { + filters { + excludes { + classes( + // Exclude generated classes. + "*_ModuleKt", + "anvil.hint.binding.io.element.*", + "anvil.hint.merge.*", + "anvil.hint.multibinding.io.element.*", + "anvil.module.*", + "com.airbnb.android.showkase*", + "io.element.android.libraries.designsystem.showkase.*", + "io.element.android.x.di.DaggerAppComponent*", + "*_Factory", + "*_Factory_Impl", + "*_Factory$*", + "*_Module", + "*_Module$*", + "*Module_Provides*", + "Dagger*Component*", + "*ComposableSingletons$*", + "*_AssistedFactory_Impl*", + "*BuildConfig", + // Generated by Showkase + "*Ioelementandroid*PreviewKt$*", + "*Ioelementandroid*PreviewKt", + // Other + // We do not cover Nodes (normally covered by maestro, but code coverage is not computed with maestro) + "*Node", + "*Node$*", + "*Presenter\$present\$*", + // Forked from compose + "io.element.android.libraries.designsystem.theme.components.bottomsheet.*", + ) + annotatedBy( + "androidx.compose.ui.tooling.preview.Preview", + "io.element.android.libraries.architecture.coverage.ExcludeFromCoverage", + "io.element.android.libraries.designsystem.preview.PreviewsDayNight", + "io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight", + ) + } + } + + defaults { + // add reports of both 'debug' and 'release' Android build variants to default reports + mergeWith("debug") + mergeWith("release") + + verify { + onCheck = true + // General rule: minimum code coverage. + rule("Global minimum code coverage.") { + isEnabled = true + entity = kotlinx.kover.gradle.plugin.dsl.GroupingEntityType.APPLICATION + bound { + minValue = 65 + // Setting a max value, so that if coverage is bigger, it means that we have to change minValue. + // For instance if we have minValue = 20 and maxValue = 30, and current code coverage is now 31.32%, update + // minValue to 25 and maxValue to 35. + maxValue = 75 + metric = kotlinx.kover.gradle.plugin.dsl.MetricType.INSTRUCTION + aggregation = kotlinx.kover.gradle.plugin.dsl.AggregationType.COVERED_PERCENTAGE + } + } + // Rule to ensure that coverage of Presenters is sufficient. + rule("Check code coverage of presenters") { + isEnabled = true + entity = kotlinx.kover.gradle.plugin.dsl.GroupingEntityType.CLASS + filters { + includes { + classes( + "*Presenter", + ) + } + excludes { + classes( + "*Fake*Presenter", + "io.element.android.appnav.loggedin.LoggedInPresenter$*", + // Some options can't be tested at the moment + "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*", + "*Presenter\$present\$*", + ) + } + } + bound { + minValue = 85 + metric = kotlinx.kover.gradle.plugin.dsl.MetricType.INSTRUCTION + aggregation = kotlinx.kover.gradle.plugin.dsl.AggregationType.COVERED_PERCENTAGE + } + } + // Rule to ensure that coverage of States is sufficient. + rule("Check code coverage of states") { + isEnabled = true + entity = kotlinx.kover.gradle.plugin.dsl.GroupingEntityType.CLASS + filters { + includes { + classes( + "^*State$", + ) + } + excludes { + classes( + "io.element.android.appnav.root.RootNavState*", + "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*", + "io.element.android.libraries.matrix.api.timeline.item.event.EventSendState$*", + "io.element.android.libraries.matrix.api.room.RoomMembershipState*", + "io.element.android.libraries.matrix.api.room.MatrixRoomMembersState*", + "io.element.android.libraries.push.impl.notifications.NotificationState*", + "io.element.android.features.messages.impl.media.local.pdf.PdfViewerState", + "io.element.android.features.messages.impl.media.local.LocalMediaViewState", + "io.element.android.features.location.impl.map.MapState*", + "io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*", + "io.element.android.libraries.designsystem.swipe.SwipeableActionsState*", + "io.element.android.features.messages.impl.timeline.components.ExpandableState*", + "io.element.android.features.messages.impl.timeline.model.bubble.BubbleState*", + "io.element.android.libraries.maplibre.compose.CameraPositionState*", + "io.element.android.libraries.maplibre.compose.SaveableCameraPositionState", + "io.element.android.libraries.maplibre.compose.SymbolState*", + "io.element.android.features.ftue.api.state.*", + "io.element.android.features.ftue.impl.welcome.state.*", + ) + } + } + bound { + minValue = 90 + metric = kotlinx.kover.gradle.plugin.dsl.MetricType.INSTRUCTION + aggregation = kotlinx.kover.gradle.plugin.dsl.AggregationType.COVERED_PERCENTAGE + } + } + // Rule to ensure that coverage of Views is sufficient (deactivated for now). + rule("Check code coverage of views") { + isEnabled = true + entity = kotlinx.kover.gradle.plugin.dsl.GroupingEntityType.CLASS + filters { + includes { + classes( + "*ViewKt", + ) + } + } + bound { + // TODO Update this value, for now there are too many missing tests. + minValue = 0 + metric = kotlinx.kover.gradle.plugin.dsl.MetricType.INSTRUCTION + aggregation = kotlinx.kover.gradle.plugin.dsl.AggregationType.COVERED_PERCENTAGE + } + } + } + } + + androidReports("release") {} + } +} + +fun Project.applyKoverPluginToAllSubProjects() = rootProject.allprojects { + if (project.path !in localAarProjects) { + apply(plugin = "org.jetbrains.kotlinx.kover") + } +} + +fun Project.koverDependencies() { + project.rootProject.subprojects + .filter { + it.project.projectDir.resolve("build.gradle.kts").exists() + } + .map { it.path } + .sorted() + .filter { + it !in excludedKoverSubProjects + } + .forEach { + // println("Add $it to kover") + dependencies.add("kover", project(it)) + } +} diff --git a/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts b/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts index 2ac8fb8b72..015f0d4db8 100644 --- a/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts +++ b/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts @@ -17,10 +17,12 @@ /** * This will generate the plugin "io.element.android-compose-application" to use by app and samples modules */ +import extension.koverDependencies import extension.androidConfig import extension.commonDependencies import extension.composeConfig import extension.composeDependencies +import extension.setupKover import org.gradle.accessors.dm.LibrariesForLibs val libs = the() @@ -30,6 +32,8 @@ plugins { id("com.autonomousapps.dependency-analysis") } +setupKover() + android { androidConfig(project) composeConfig(libs) @@ -42,4 +46,5 @@ dependencies { commonDependencies(libs) composeDependencies(libs) coreLibraryDesugaring(libs.android.desugar) + koverDependencies() } diff --git a/plugins/src/main/kotlin/io.element.android-root.gradle.kts b/plugins/src/main/kotlin/io.element.android-root.gradle.kts new file mode 100644 index 0000000000..44f8fb070e --- /dev/null +++ b/plugins/src/main/kotlin/io.element.android-root.gradle.kts @@ -0,0 +1,7 @@ +import extension.applyKoverPluginToAllSubProjects + +plugins { + id("org.jetbrains.kotlinx.kover") apply false +} + +applyKoverPluginToAllSubProjects()