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\$*"
+ )
+ }
+}