/* * 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) { (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)) } }