* Upgrade the used JDK in the project to v21 * Use it for CI too * Centralise java language version * Fix deprecations, tests and lint issues * Fix coverage taking into account `@Preview` annotated code. --------- Co-authored-by: Benoit Marty <benoit@matrix.org>
269 lines
12 KiB
Kotlin
269 lines
12 KiB
Kotlin
/*
|
|
* Copyright 2024 New Vector Ltd.
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
* Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
package extension
|
|
|
|
import kotlinx.kover.gradle.plugin.dsl.AggregationType
|
|
import kotlinx.kover.gradle.plugin.dsl.CoverageUnit
|
|
import kotlinx.kover.gradle.plugin.dsl.GroupingEntityType
|
|
import kotlinx.kover.gradle.plugin.dsl.KoverProjectExtension
|
|
import kotlinx.kover.gradle.plugin.dsl.KoverVariantCreateConfig
|
|
import org.gradle.api.Action
|
|
import org.gradle.api.Project
|
|
import org.gradle.kotlin.dsl.apply
|
|
import org.gradle.kotlin.dsl.assign
|
|
|
|
enum class KoverVariant(val variantName: String) {
|
|
Presenters("presenters"),
|
|
States("states"),
|
|
Views("views"),
|
|
}
|
|
|
|
val koverVariants = KoverVariant.values().map { it.variantName }
|
|
|
|
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.kover(action: Action<KoverProjectExtension>) {
|
|
(this as org.gradle.api.plugins.ExtensionAware).extensions.configure("kover", action)
|
|
}
|
|
|
|
fun Project.setupKover() {
|
|
// Create verify all task joining all existing verification tasks
|
|
task("koverVerifyAll") {
|
|
group = "verification"
|
|
description = "Verifies the code coverage of all subprojects."
|
|
val dependencies = listOf(":app:koverVerifyGplayDebug") + koverVariants.map { ":app:koverVerify${it.replaceFirstChar(Char::titlecase)}" }
|
|
dependsOn(dependencies)
|
|
|
|
}
|
|
// 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
|
|
kover {
|
|
reports {
|
|
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.*",
|
|
// Test presenters
|
|
"io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter",
|
|
)
|
|
annotatedBy(
|
|
"androidx.compose.ui.tooling.preview.Preview",
|
|
"io.element.android.libraries.architecture.coverage.ExcludeFromCoverage",
|
|
"io.element.android.libraries.designsystem.preview.*",
|
|
)
|
|
}
|
|
}
|
|
|
|
total {
|
|
verify {
|
|
// General rule: minimum code coverage.
|
|
rule("Global minimum code coverage.") {
|
|
groupBy = GroupingEntityType.APPLICATION
|
|
bound {
|
|
minValue = 70
|
|
// 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 = 80
|
|
coverageUnits = CoverageUnit.INSTRUCTION
|
|
aggregationForGroup = AggregationType.COVERED_PERCENTAGE
|
|
}
|
|
}
|
|
}
|
|
}
|
|
variant(KoverVariant.Presenters.variantName) {
|
|
verify {
|
|
// Rule to ensure that coverage of Presenters is sufficient.
|
|
rule("Check code coverage of presenters") {
|
|
groupBy = GroupingEntityType.CLASS
|
|
|
|
bound {
|
|
minValue = 85
|
|
coverageUnits = CoverageUnit.INSTRUCTION
|
|
aggregationForGroup = AggregationType.COVERED_PERCENTAGE
|
|
}
|
|
}
|
|
}
|
|
filters {
|
|
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$*",
|
|
// Need an Activity to use rememberMultiplePermissionsState
|
|
"io.element.android.features.location.impl.common.permissions.DefaultPermissionsPresenter",
|
|
"*Presenter\$present\$*",
|
|
// Too small to be > 85% tested
|
|
"io.element.android.libraries.fullscreenintent.impl.DefaultFullScreenIntentPermissionsPresenter",
|
|
)
|
|
includes.inheritedFrom("io.element.android.libraries.architecture.Presenter")
|
|
}
|
|
}
|
|
variant(KoverVariant.States.variantName) {
|
|
verify {
|
|
// Rule to ensure that coverage of States is sufficient.
|
|
rule("Check code coverage of states") {
|
|
groupBy = GroupingEntityType.CLASS
|
|
bound {
|
|
minValue = 90
|
|
coverageUnits = CoverageUnit.INSTRUCTION
|
|
aggregationForGroup = AggregationType.COVERED_PERCENTAGE
|
|
}
|
|
}
|
|
}
|
|
filters {
|
|
excludes.classes(
|
|
"*State$*", // Exclude inner 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.*",
|
|
"io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomSheetState",
|
|
"io.element.android.libraries.mediaviewer.api.local.pdf.PdfViewerState",
|
|
"io.element.android.libraries.textcomposer.model.TextEditorState",
|
|
)
|
|
includes.classes("*State")
|
|
}
|
|
}
|
|
variant(KoverVariant.Views.variantName) {
|
|
verify {
|
|
// Rule to ensure that coverage of Views is sufficient (deactivated for now).
|
|
rule("Check code coverage of views") {
|
|
groupBy = GroupingEntityType.CLASS
|
|
bound {
|
|
// TODO Update this value, for now there are too many missing tests.
|
|
minValue = 0
|
|
coverageUnits = CoverageUnit.INSTRUCTION
|
|
aggregationForGroup = AggregationType.COVERED_PERCENTAGE
|
|
}
|
|
}
|
|
}
|
|
filters {
|
|
excludes.classes("*ViewKt$*") // Exclude inner classes
|
|
includes.classes("*ViewKt")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
if (project.name == "app") {
|
|
addWithDependencies("gplayDebug")
|
|
} else {
|
|
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))
|
|
}
|
|
}
|