Improve Kover setup by using only convention plugins (#6213)

* Improve Kover setup using convention plugins.
* Add a new JVM library convention plugin with Kover support
This commit is contained in:
Jorge Martin Espinosa
2026-03-16 15:41:07 +01:00
committed by GitHub
parent 3814085837
commit 949a12f3d2
11 changed files with 92 additions and 80 deletions

View File

@@ -73,7 +73,7 @@ jobs:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: ⚙️ Check coverage for debug variant (includes unit & screenshot tests) - name: ⚙️ Check coverage for debug variant (includes unit & screenshot tests)
run: ./gradlew testDebugUnitTest :tests:uitests:verifyPaparazziDebug :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyAll $CI_GRADLE_ARG_PROPERTIES run: ./gradlew testDebugUnitTest :tests:uitests:verifyPaparazziDebug :koverXmlReportMerged :koverHtmlReportMerged :koverVerifyAll $CI_GRADLE_ARG_PROPERTIES
- name: 🚫 Upload kover failed coverage reports - name: 🚫 Upload kover failed coverage reports
if: failure() if: failure()
@@ -112,5 +112,5 @@ jobs:
with: with:
fail_ci_if_error: true fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
files: app/build/reports/kover/reportGplayDebug.xml files: build/reports/kover/reportMerged.xml
verbose: true verbose: true

View File

@@ -6,6 +6,5 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
plugins { plugins {
alias(libs.plugins.kotlin.jvm) id("io.element.jvm-library")
id("com.android.lint")
} }

View File

@@ -21,10 +21,8 @@ import extension.allFeaturesImpl
import extension.allLibrariesImpl import extension.allLibrariesImpl
import extension.allServicesImpl import extension.allServicesImpl
import extension.buildConfigFieldStr import extension.buildConfigFieldStr
import extension.koverDependencies
import extension.locales import extension.locales
import extension.setupDependencyInjection import extension.setupDependencyInjection
import extension.setupKover
import extension.testCommonDependencies import extension.testCommonDependencies
import java.util.Locale import java.util.Locale
@@ -40,8 +38,6 @@ plugins {
// alias(libs.plugins.gms.google.services) // alias(libs.plugins.gms.google.services)
} }
setupKover()
android { android {
namespace = "io.element.android.x" namespace = "io.element.android.x"
@@ -295,8 +291,6 @@ dependencies {
testCommonDependencies(libs) testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.toolbox.test) testImplementation(projects.services.toolbox.test)
koverDependencies()
} }
tasks.withType<GenerateBuildConfig>().configureEach { tasks.withType<GenerateBuildConfig>().configureEach {

View File

@@ -6,20 +6,7 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
plugins { plugins {
id("java-library") id("io.element.jvm-library")
id("com.android.lint")
alias(libs.plugins.kotlin.jvm)
}
java {
sourceCompatibility = Versions.javaVersion
targetCompatibility = Versions.javaVersion
}
kotlin {
jvmToolchain {
languageVersion = Versions.javaLanguageVersion
}
} }
dependencies { dependencies {

View File

@@ -7,8 +7,7 @@
*/ */
plugins { plugins {
alias(libs.plugins.kotlin.jvm) id("io.element.jvm-library")
id("com.android.lint")
} }
dependencies { dependencies {

View File

@@ -13,10 +13,11 @@ import kotlinx.kover.gradle.plugin.dsl.CoverageUnit
import kotlinx.kover.gradle.plugin.dsl.GroupingEntityType import kotlinx.kover.gradle.plugin.dsl.GroupingEntityType
import kotlinx.kover.gradle.plugin.dsl.KoverProjectExtension import kotlinx.kover.gradle.plugin.dsl.KoverProjectExtension
import kotlinx.kover.gradle.plugin.dsl.KoverVariantCreateConfig import kotlinx.kover.gradle.plugin.dsl.KoverVariantCreateConfig
import org.gradle.api.Action
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.assign import org.gradle.kotlin.dsl.assign
import org.gradle.kotlin.dsl.configure
import java.io.File
enum class KoverVariant(val variantName: String) { enum class KoverVariant(val variantName: String) {
Presenters("presenters"), Presenters("presenters"),
@@ -24,7 +25,7 @@ enum class KoverVariant(val variantName: String) {
Views("views"), Views("views"),
} }
val koverVariants = KoverVariant.values().map { it.variantName } val koverVariants = KoverVariant.entries.map { it.variantName }
val localAarProjects = listOf( val localAarProjects = listOf(
":libraries:rustsdk", ":libraries:rustsdk",
@@ -32,7 +33,7 @@ val localAarProjects = listOf(
) )
val excludedKoverSubProjects = listOf( val excludedKoverSubProjects = listOf(
":app", ":appconfig",
":annotations", ":annotations",
":codegen", ":codegen",
":tests:testutils", ":tests:testutils",
@@ -42,24 +43,63 @@ val excludedKoverSubProjects = listOf(
":libraries:core", ":libraries:core",
":libraries:coroutines", ":libraries:coroutines",
":libraries:di", ":libraries:di",
":tests:detekt-rules",
":tests:konsist",
":tests:testutils",
) + localAarProjects ) + localAarProjects
private fun Project.kover(action: Action<KoverProjectExtension>) { private fun Project.kover(any: Any) {
(this as org.gradle.api.plugins.ExtensionAware).extensions.configure("kover", action) this.dependencies.add("kover", any)
} }
fun Project.setupKover() { fun Project.setupKover() {
// If the project is excluded from Kover, don't apply anything
if (path in excludedKoverSubProjects) return
// Apply the plugin
apply(plugin = "org.jetbrains.kotlinx.kover")
// Create verify all task joining all existing verification tasks // Create verify all task joining all existing verification tasks
tasks.register("koverVerifyAll") { tasks.register("koverVerifyAll") {
group = "verification" group = "verification"
description = "Verifies the code coverage of all subprojects." description = "Verifies the code coverage of all subprojects."
val dependencies = listOf(":app:koverVerifyGplayDebug") + koverVariants.map { ":app:koverVerify${it.replaceFirstChar(Char::titlecase)}" } val dependencies = listOf(":koverVerifyMerged") + koverVariants.map { ":app:koverVerify${it.replaceFirstChar(Char::titlecase)}" }
dependsOn(dependencies) dependsOn(dependencies)
} }
// https://kotlin.github.io/kotlinx-kover/ // https://kotlin.github.io/kotlinx-kover/
// Run `./gradlew :app:koverHtmlReport` to get report at ./app/build/reports/kover // Run `./gradlew :app:koverHtmlReport` to get report at ./app/build/reports/kover
// Run `./gradlew :app:koverXmlReport` to get XML report // Run `./gradlew :app:koverXmlReport` to get XML report
kover { extensions.configure<KoverProjectExtension> {
currentProject {
// Create custom variants for verification
for (variant in koverVariants) {
createVariant(variant) {
defaultVariants(project)
// Using the cache for coverage verification seems to be flaky, so we disable it for now.
val taskName = "koverCachedVerify${variant.replaceFirstChar(Char::titlecase)}"
val cachedTask = project.tasks.findByName(taskName)
cachedTask?.let {
it.outputs.upToDateWhen { false }
}
}
}
// Create merged variant
createVariant("merged") {
defaultVariants(project)
}
}
// If it's the root project, set up kover for subprojects
if (project.path == ":") {
for (project in project.subprojects) {
if (project.path !in excludedKoverSubProjects && File(project.projectDir, "build.gradle.kts").exists()) {
kover(project)
}
}
}
reports { reports {
filters { filters {
excludes { excludes {
@@ -194,54 +234,10 @@ fun Project.setupKover() {
} }
} }
fun Project.applyKoverPluginToAllSubProjects() = rootProject.subprojects {
if (project.path !in localAarProjects) {
apply(plugin = "org.jetbrains.kotlinx.kover")
kover {
currentProject {
for (variant in koverVariants) {
createVariant(variant) {
defaultVariants(project)
}
}
}
}
project.afterEvaluate {
for (variant in koverVariants) {
// Using the cache for coverage verification seems to be flaky, so we disable it for now.
val taskName = "koverCachedVerify${variant.replaceFirstChar(Char::titlecase)}"
val cachedTask = project.tasks.findByName(taskName)
cachedTask?.let {
it.outputs.upToDateWhen { false }
}
}
}
}
}
fun KoverVariantCreateConfig.defaultVariants(project: Project) { fun KoverVariantCreateConfig.defaultVariants(project: Project) {
if (project.name == "app") { if (project.path == ":app") {
addWithDependencies("gplayDebug") addWithDependencies("gplayDebug")
} else { } else {
addWithDependencies("debug", "jvm", optional = true) addWithDependencies("debug", "jvm", optional = true)
} }
} }
fun Project.koverSubprojects() = project.rootProject.subprojects
.filter {
it.project.projectDir.resolve("build.gradle.kts").exists()
}
.map { it.path }
.sorted()
.filter {
it !in excludedKoverSubProjects
}
fun Project.koverDependencies() {
project.koverSubprojects()
.forEach {
// println("Add $it to kover")
dependencies.add("kover", project(it))
}
}

View File

@@ -13,6 +13,7 @@ import extension.androidConfig
import extension.commonDependencies import extension.commonDependencies
import extension.composeConfig import extension.composeConfig
import extension.composeDependencies import extension.composeDependencies
import extension.setupKover
import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.accessors.dm.LibrariesForLibs
val libs = the<LibrariesForLibs>() val libs = the<LibrariesForLibs>()
@@ -37,6 +38,8 @@ kotlin {
} }
} }
setupKover()
dependencies { dependencies {
commonDependencies(libs) commonDependencies(libs)
composeDependencies(libs) composeDependencies(libs)

View File

@@ -13,6 +13,7 @@ import extension.androidConfig
import extension.commonDependencies import extension.commonDependencies
import extension.composeConfig import extension.composeConfig
import extension.composeDependencies import extension.composeDependencies
import extension.setupKover
import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.accessors.dm.LibrariesForLibs
val libs = the<LibrariesForLibs>() val libs = the<LibrariesForLibs>()
@@ -37,6 +38,8 @@ kotlin {
} }
} }
setupKover()
dependencies { dependencies {
commonDependencies(libs) commonDependencies(libs)
composeDependencies(libs) composeDependencies(libs)

View File

@@ -11,6 +11,7 @@
*/ */
import extension.androidConfig import extension.androidConfig
import extension.commonDependencies import extension.commonDependencies
import extension.setupKover
import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.accessors.dm.LibrariesForLibs
val libs = the<LibrariesForLibs>() val libs = the<LibrariesForLibs>()
@@ -33,6 +34,8 @@ kotlin {
} }
} }
setupKover()
dependencies { dependencies {
commonDependencies(libs) commonDependencies(libs)
coreLibraryDesugaring(libs.android.desugar) coreLibraryDesugaring(libs.android.desugar)

View File

@@ -1,7 +1,7 @@
import extension.applyKoverPluginToAllSubProjects import extension.setupKover
plugins { plugins {
id("org.jetbrains.kotlinx.kover") apply false id("org.jetbrains.kotlinx.kover") apply false
} }
applyKoverPluginToAllSubProjects() setupKover()

View File

@@ -0,0 +1,28 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2022-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
/**
* This will generate the plugin "io.element.jvm-library", used in pure JVM libraries.
*/
import extension.setupKover
import org.gradle.accessors.dm.LibrariesForLibs
val libs = the<LibrariesForLibs>()
plugins {
id("org.jetbrains.kotlin.jvm")
id("com.autonomousapps.dependency-analysis")
id("com.android.lint")
}
kotlin {
jvmToolchain {
languageVersion = Versions.javaLanguageVersion
}
}
setupKover()