From 97384ce56ada7affe5fd925034f09860a0ba684d Mon Sep 17 00:00:00 2001 From: Mateusz Armatys Date: Thu, 27 Apr 2023 13:15:24 +0200 Subject: [PATCH] build(coverage): Add plugins for global coverage and common coverage configuration. --- build.gradle.kts | 12 +++ coverage/build.gradle.kts | 5 + plugins/README.md | 18 +++- plugins/coverage/build.gradle.kts | 16 +++- .../gradle/plugins/coverage/ProjectExt.kt | 19 ++++ .../ProtonCoverageCommonConfigPlugin.kt | 40 ++++++++ .../coverage/ProtonCoverageExtension.kt | 22 +++++ .../plugins/coverage/ProtonCoveragePlugin.kt | 91 ++++--------------- .../coverage/ProtonGlobalCoveragePlugin.kt | 51 +++++++++++ .../plugins/coverage/rules/androidRules.kt | 20 ++++ .../plugins/coverage/rules/daggerRules.kt | 24 +++++ .../coverage/rules/kotlinParcelizeRules.kt | 9 ++ .../rules/kotlinSerializationRules.kt | 13 +++ .../plugins/coverage/rules/roomDbRules.kt | 18 ++++ 14 files changed, 283 insertions(+), 75 deletions(-) create mode 100644 coverage/build.gradle.kts create mode 100644 plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProjectExt.kt create mode 100644 plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProtonCoverageCommonConfigPlugin.kt create mode 100644 plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProtonGlobalCoveragePlugin.kt create mode 100644 plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/androidRules.kt create mode 100644 plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/daggerRules.kt create mode 100644 plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/kotlinParcelizeRules.kt create mode 100644 plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/kotlinSerializationRules.kt create mode 100644 plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/roomDbRules.kt diff --git a/build.gradle.kts b/build.gradle.kts index 7043ffa47..72f350bcf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,6 +34,7 @@ plugins { id("publish-core-libraries") id("me.proton.core.gradle-plugins.tests") id("me.proton.core.gradle-plugins.jacoco") + id("me.proton.core.gradle-plugins.coverage-config") alias(libs.plugins.benManes.versions.gradle) alias(libs.plugins.kotlin.binaryCompatibilityValidator) alias(libs.plugins.kotlin.gradle) @@ -56,6 +57,17 @@ repositories { mavenCentral() } +protonCoverage { + excludes.add { + classes( + "me.proton.core.accountmanager.data.db.AccountManagerDatabaseMigrations*", + "*Database_Impl*", + "*Database\$Companion*", // DB migrations + "*LogTag" + ) + } +} + fun isNonStable(version: String): Boolean { val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.toUpperCase().contains(it) } val regex = "^[0-9,.v-]+(-r)?$".toRegex() diff --git a/coverage/build.gradle.kts b/coverage/build.gradle.kts new file mode 100644 index 000000000..56ea544b9 --- /dev/null +++ b/coverage/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("me.proton.core.gradle-plugins.global-coverage") +} + +publishOption.shouldBePublishedAsLib = false diff --git a/plugins/README.md b/plugins/README.md index 9ea3ca622..0fddca401 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -35,10 +35,26 @@ Use internally in core project to orchestrate dependencies and apply android/kot - Plugin id: `me.proton.core.gradle-plugins.coverage` - Published on MavenCentral. -Apply the plugin to each module for which the code coverage is needed. +Apply the plugin to each (non-root) project for which the code coverage is needed. Additional settings can be configured via `protonCoverage` extension, e.g. setting custom minimum coverage levels, excluding additional files. +## Global coverage plugin +- Plugin id: `me.proton.core.gradle-plugins.global-coverage` +- Published on MavenCentral. + +The plugin is intended to be applied to a separate project, that's only used to +generate a global coverage report or perform coverage percentage verification. + +## Common coverage config plugin +- Plugin id: `me.proton.core.gradle-plugins.coverage-config` +- Published on MavenCentral. + +Apply the plugin on the root project. +Use the `protonCoverage` extension to configure common coverage settings. +Those settings will be picked up the all the submodules that +use the Coverage plugin (`me.proton.core.gradle-plugins.coverage`). + ## Detekt plugin - Plugin id: `me.proton.core.gradle-plugins.detekt` - Published on MavenCentral. diff --git a/plugins/coverage/build.gradle.kts b/plugins/coverage/build.gradle.kts index 01411ed53..8f08d673c 100644 --- a/plugins/coverage/build.gradle.kts +++ b/plugins/coverage/build.gradle.kts @@ -1,6 +1,6 @@ /* - * Copyright (c) 2023 Proton Technologies AG - * This file is part of Proton Technologies AG and ProtonCore. + * Copyright (c) 2023 Proton AG + * This file is part of Proton AG and ProtonCore. * * ProtonCore is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -44,6 +44,18 @@ gradlePlugin { description = "Plugin to generate coverage reports compatible with GitLab" implementationClass = "me.proton.core.gradle.plugins.coverage.ProtonCoveragePlugin" } + create("globalCoveragePlugin") { + id = "me.proton.core.gradle-plugins.global-coverage" + displayName = "Proton global coverage plugin" + description = "Plugin to generate global coverage reports compatible with GitLab" + implementationClass = "me.proton.core.gradle.plugins.coverage.ProtonGlobalCoveragePlugin" + } + create("coverageCommonConfigPlugin") { + id = "me.proton.core.gradle-plugins.coverage-config" + displayName = "Proton coverage config plugin" + description = "Plugin to configure common coverage settings" + implementationClass = "me.proton.core.gradle.plugins.coverage.ProtonCoverageCommonConfigPlugin" + } } testSourceSets(functionalTest) diff --git a/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProjectExt.kt b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProjectExt.kt new file mode 100644 index 000000000..bd7cb57ca --- /dev/null +++ b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProjectExt.kt @@ -0,0 +1,19 @@ +package me.proton.core.gradle.plugins.coverage + +import org.gradle.api.Project + +private val koverTaskNameRegex = Regex("(.*:)?kover.*") +private val tasksTaskNameRegex = Regex("(.*:)?tasks") + +/** Optional optimization. + * Avoid further configuration if we're not trying to run a `kover` task, + * or display the project's tasks. + * Example: + * > ./gradlew koverHtmlReport # <- The kover plugin will be configured + * > ./gradlew tasks # <- The kover plugin will be configured + * > ./gradlew someOtherTask # <- The kover plugin will NOT be configured + */ +internal fun Project.shouldSkipPluginApplication(): Boolean = + !gradle.startParameter.taskNames.any { + it.matches(koverTaskNameRegex) || it.matches(tasksTaskNameRegex) + } diff --git a/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProtonCoverageCommonConfigPlugin.kt b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProtonCoverageCommonConfigPlugin.kt new file mode 100644 index 000000000..662a993c4 --- /dev/null +++ b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProtonCoverageCommonConfigPlugin.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 Proton AG + * This file is part of Proton AG and ProtonCore. + * + * ProtonCore is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonCore is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonCore. If not, see . + */ + +package me.proton.core.gradle.plugins.coverage + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.create + +/** + * The plugin should be applied on a root project. + * It can be used to provide a common configuration for code coverage. + * The configuration will be picked up by the submodules which use + * the [ProtonCoveragePlugin]. + */ +public class ProtonCoverageCommonConfigPlugin : Plugin { + override fun apply(target: Project) { + if (target != target.rootProject) error("${this::class.simpleName} should be applied on the root project.") + val ext = target.extensions.create("protonCoverage") + + target.afterEvaluate { + ext.finalizeValuesOnRead() + } + } +} diff --git a/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProtonCoverageExtension.kt b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProtonCoverageExtension.kt index 6e7a459b5..6ffb29cf1 100644 --- a/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProtonCoverageExtension.kt +++ b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProtonCoverageExtension.kt @@ -19,6 +19,7 @@ package me.proton.core.gradle.plugins.coverage import kotlinx.kover.gradle.plugin.dsl.KoverReportFilter +import org.gradle.api.Project import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property @@ -67,6 +68,27 @@ public interface ProtonCoverageExtension { public val minLineCoveragePercentage: Property } +internal fun ProtonCoverageExtension.applyConventionsFrom(project: Project) { + project.extensions.findByType(ProtonCoverageExtension::class.java)?.let { otherExt -> + require(otherExt != this) { "Cannot apply the convention from the same object." } + applyConventionsFrom(otherExt) + } +} + +internal fun ProtonCoverageExtension.applyConventionsFrom(other: ProtonCoverageExtension) { + other.androidBuildVariant.orNull?.let { androidBuildVariant.convention(it) } + other.disabled.orNull?.let { disabled.convention(it) } + other.enableAllRules.orNull?.let { enableAllRules.convention(it) } + other.enableAndroidRules.orNull?.let { enableAndroidRules.convention(it) } + other.enableDaggerRules.orNull?.let { enableDaggerRules.convention(it) } + other.enableKotlinParcelizeRules.orNull?.let { enableKotlinParcelizeRules.convention(it) } + other.enableKotlinSerializationRules.orNull?.let { enableKotlinSerializationRules.convention(it) } + other.enableRoomDbRules.orNull?.let { enableRoomDbRules.convention(it) } + other.excludes.orNull?.let { excludes.convention(it) } + other.minBranchCoveragePercentage.orNull?.let { minBranchCoveragePercentage.convention(it) } + other.minLineCoveragePercentage.orNull?.let { minLineCoveragePercentage.convention(it) } +} + internal fun ProtonCoverageExtension.applyGeneralConventions() { disabled.convention(false) excludes.convention(emptyList()) diff --git a/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProtonCoveragePlugin.kt b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProtonCoveragePlugin.kt index 2a935b7ff..6cb14bc27 100644 --- a/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProtonCoveragePlugin.kt +++ b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProtonCoveragePlugin.kt @@ -25,6 +25,11 @@ import kotlinx.kover.gradle.plugin.dsl.KoverReportExtension import kotlinx.kover.gradle.plugin.dsl.KoverReportFilters import kotlinx.kover.gradle.plugin.dsl.KoverVerifyReportConfig import kotlinx.kover.gradle.plugin.dsl.MetricType +import me.proton.core.gradle.plugins.coverage.rules.androidRules +import me.proton.core.gradle.plugins.coverage.rules.daggerRules +import me.proton.core.gradle.plugins.coverage.rules.kotlinParcelizeRules +import me.proton.core.gradle.plugins.coverage.rules.kotlinSerializationRules +import me.proton.core.gradle.plugins.coverage.rules.roomDbRules import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure @@ -38,11 +43,14 @@ private const val DEFAULT_XML_REPORT_FILE = "reports/kover/report.xml" internal const val PROTON_COVERAGE_EXT: String = "protonCoverage" +/** + * The plugin can be applied on a non-root project. + * It can be used to generate coverage HTML/XML report, and verify minimum coverage percentages. + */ public class ProtonCoveragePlugin : Plugin { - private val koverTaskNameRegex = Regex("(.*:)?kover.*") - private val tasksTaskNameRegex = Regex("(.*:)?tasks") - override fun apply(target: Project) { + if (target == target.rootProject) error("${this::class.simpleName} should not be applied on the root project.") + val ext = target.extensions.create(PROTON_COVERAGE_EXT) if (target.shouldSkipPluginApplication()) { @@ -50,6 +58,7 @@ public class ProtonCoveragePlugin : Plugin { } ext.applyGeneralConventions() + ext.applyConventionsFrom(target.rootProject) ext.finalizeValuesOnRead() target.afterEvaluate { @@ -59,31 +68,11 @@ public class ProtonCoveragePlugin : Plugin { } } - /** Optional optimization. - * Avoid further configuration if we're not trying to run a `kover` task, - * or display the project's tasks. - * Example: - * > ./gradlew koverHtmlReport # <- The kover plugin will be configured - * > ./gradlew tasks # <- The kover plugin will be configured - * > ./gradlew someOtherTask # <- The kover plugin will NOT be configured - */ - private fun Project.shouldSkipPluginApplication(): Boolean = - !gradle.startParameter.taskNames.any { - it.matches(koverTaskNameRegex) || it.matches(tasksTaskNameRegex) - } - private fun Project.onAfterEvaluate(ext: ProtonCoverageExtension) { extensions.configure { useKoverTool() } - if (ext.enableAllRules.orNull == true) { - ext.enableAndroidRules.convention(true) - ext.enableDaggerRules.convention(true) - ext.enableKotlinParcelizeRules.convention(true) - ext.enableKotlinSerializationRules.convention(true) - ext.enableRoomDbRules.convention(true) - } if (hasAndroidPlugin()) { ext.androidBuildVariant.convention(DEFAULT_ANDROID_BUILD_VARIANT) ext.enableAndroidRules.convention(true) @@ -98,6 +87,13 @@ public class ProtonCoveragePlugin : Plugin { if (plugins.hasPlugin(PluginIds.kotlinSerialization)) { ext.enableKotlinSerializationRules.convention(true) } + if (ext.enableAllRules.orNull == true) { + ext.enableAndroidRules.convention(true) + ext.enableDaggerRules.convention(true) + ext.enableKotlinParcelizeRules.convention(true) + ext.enableKotlinSerializationRules.convention(true) + ext.enableRoomDbRules.convention(true) + } configureKoverExtension(ext) } @@ -152,53 +148,4 @@ public class ProtonCoveragePlugin : Plugin { ) } } - - private fun KoverReportFilters.androidRules() { - excludes { - classes( - "*Activity", - "*Fragment", - "*.BuildConfig", - "*.R", - "*.R$*" - ) - } - } - - private fun KoverReportFilters.daggerRules() { - excludes { - annotatedBy( - "dagger.internal.DaggerGenerated", - "dagger.Module", - "javax.annotation.processing.Generated" - ) - packages("hilt_aggregated_deps") - } - } - - private fun KoverReportFilters.kotlinSerializationRules() { - excludes { - annotatedBy( - "kotlinx.serialization.SerialName", - "kotlinx.serialization.Serializable" - ) - classes("*\$\$serializer") - } - } - - private fun KoverReportFilters.kotlinParcelizeRules() { - excludes { - annotatedBy("kotlinx.parcelize.Parcelize") - } - } - - private fun KoverReportFilters.roomDbRules() { - excludes { - annotatedBy( - "androidx.room.Dao", - "androidx.room.Entity" - ) - classes("*Database\$Companion*") // For DB migrations - } - } } diff --git a/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProtonGlobalCoveragePlugin.kt b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProtonGlobalCoveragePlugin.kt new file mode 100644 index 000000000..29889999a --- /dev/null +++ b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/ProtonGlobalCoveragePlugin.kt @@ -0,0 +1,51 @@ +package me.proton.core.gradle.plugins.coverage + +import kotlinx.kover.gradle.plugin.KoverGradlePlugin +import kotlinx.kover.gradle.plugin.dsl.KoverNames +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +/** + * The plugin can be applied on a separate project. + * It can generate a combined coverage report from all subprojects + * which use the [ProtonCoveragePlugin]. + */ +public class ProtonGlobalCoveragePlugin : Plugin { + override fun apply(target: Project) { + if (!target.shouldSkipPluginApplication()) { + configureGlobalCoverageReports(target) + } + + target.plugins.apply(ProtonCoveragePlugin::class.java) + } + + private fun configureGlobalCoverageReports(target: Project) { + val rootProject = target.rootProject + + rootProject.subprojects { + if (project == target) return@subprojects + target.evaluationDependsOn(project.path) + } + + target.afterEvaluate { + target.plugins.apply("org.gradle.java-library") + target.plugins.apply("org.jetbrains.kotlin.jvm") + target.plugins.apply(KoverGradlePlugin::class.java) + + target.extensions.configure { + enableAllRules.set(true) + } + + rootProject.subprojects { + if (project == target) return@subprojects + + if (project.plugins.hasPlugin(ProtonCoveragePlugin::class.java) && + project.extensions.findByType(ProtonCoverageExtension::class.java)?.disabled?.get() != true + ) { + target.dependencies.add(KoverNames.DEPENDENCY_CONFIGURATION_NAME, project) + } + } + } + } +} diff --git a/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/androidRules.kt b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/androidRules.kt new file mode 100644 index 000000000..6603b557e --- /dev/null +++ b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/androidRules.kt @@ -0,0 +1,20 @@ +package me.proton.core.gradle.plugins.coverage.rules + +import kotlinx.kover.gradle.plugin.dsl.KoverReportFilters + +internal fun KoverReportFilters.androidRules() { + excludes { + annotatedBy("androidx.compose.runtime.Composable") + classes( + "*Activity", + "*Activity\$*", + "*Binding", + "*.BuildConfig", + "*ComposableSingletons\$*", + "*Fragment", + "*Fragment\$*", + "*.R", + "*.R$*" + ) + } +} diff --git a/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/daggerRules.kt b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/daggerRules.kt new file mode 100644 index 000000000..d472318d5 --- /dev/null +++ b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/daggerRules.kt @@ -0,0 +1,24 @@ +package me.proton.core.gradle.plugins.coverage.rules + +import kotlinx.kover.gradle.plugin.dsl.KoverReportFilters + +internal fun KoverReportFilters.daggerRules() { + excludes { + annotatedBy( + "dagger.hilt.android.AndroidEntryPoint", + "dagger.internal.DaggerGenerated", + "dagger.Binds", + "dagger.Module", + "dagger.Provides", + "javax.annotation.processing.Generated" + ) + classes( + "*Hilt_*", + "*_HiltModules_*", + "*_Provide*Factory", + "*_Provide*Factory\$*", + "*_Factory" + ) + packages("hilt_aggregated_deps") + } +} diff --git a/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/kotlinParcelizeRules.kt b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/kotlinParcelizeRules.kt new file mode 100644 index 000000000..132ebe061 --- /dev/null +++ b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/kotlinParcelizeRules.kt @@ -0,0 +1,9 @@ +package me.proton.core.gradle.plugins.coverage.rules + +import kotlinx.kover.gradle.plugin.dsl.KoverReportFilters + +internal fun KoverReportFilters.kotlinParcelizeRules() { + excludes { + annotatedBy("kotlinx.parcelize.Parcelize") + } +} diff --git a/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/kotlinSerializationRules.kt b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/kotlinSerializationRules.kt new file mode 100644 index 000000000..dfd02660b --- /dev/null +++ b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/kotlinSerializationRules.kt @@ -0,0 +1,13 @@ +package me.proton.core.gradle.plugins.coverage.rules + +import kotlinx.kover.gradle.plugin.dsl.KoverReportFilters + +internal fun KoverReportFilters.kotlinSerializationRules() { + excludes { + annotatedBy( + "kotlinx.serialization.SerialName", + "kotlinx.serialization.Serializable" + ) + classes("*\$\$serializer") + } +} diff --git a/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/roomDbRules.kt b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/roomDbRules.kt new file mode 100644 index 000000000..1129571f1 --- /dev/null +++ b/plugins/coverage/src/main/kotlin/me/proton/core/gradle/plugins/coverage/rules/roomDbRules.kt @@ -0,0 +1,18 @@ +package me.proton.core.gradle.plugins.coverage.rules + +import kotlinx.kover.gradle.plugin.dsl.KoverReportFilters + +internal fun KoverReportFilters.roomDbRules() { + excludes { + annotatedBy( + "androidx.room.Dao", + "androidx.room.Database", + "androidx.room.Entity", + "javax.annotation.processing.Generated" + ) + classes( + "*Dao_Impl", + "*Dao_Impl\$*" + ) + } +}