* Fix `TransactionTooLargeExceptions` caused by Appyx After a long debugging session, we discovered the code Appyx uses to clear the saved state of nodes that have been removed is not working because of a race condition, causing this saved state to grow indefinitely. To fix it, we need to wait until the node has been disposed, which will call `SaveableStateHolder.removeState` once, removing the associated `SaveableStateRegistry`, and *then* call `removeState` again when we detect the node has been removed from the navigation graph. Since these classes and APIs are private in Appyx, we had to copy and modify and use these copies. * Remove ktlint checks on `SafeChildrenTransitionScope.kt` * Don't count the new code for coverage
246 lines
11 KiB
Kotlin
246 lines
11 KiB
Kotlin
/*
|
|
* Copyright (c) 2025 Element Creations Ltd.
|
|
* Copyright 2024, 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.
|
|
*/
|
|
|
|
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.Project
|
|
import org.gradle.kotlin.dsl.apply
|
|
import org.gradle.kotlin.dsl.assign
|
|
import org.gradle.kotlin.dsl.configure
|
|
import java.io.File
|
|
|
|
enum class KoverVariant(val variantName: String) {
|
|
Presenters("presenters"),
|
|
States("states"),
|
|
Views("views"),
|
|
}
|
|
|
|
val koverVariants = KoverVariant.entries.map { it.variantName }
|
|
|
|
val localAarProjects = listOf(
|
|
":libraries:rustsdk",
|
|
":libraries:textcomposer:lib"
|
|
)
|
|
|
|
val excludedKoverSubProjects = listOf(
|
|
":appconfig",
|
|
":annotations",
|
|
":codegen",
|
|
":tests:testutils",
|
|
// Exclude modules which are not Android libraries
|
|
// See https://github.com/Kotlin/kotlinx-kover/issues/312
|
|
":appconfig",
|
|
":libraries:core",
|
|
":libraries:coroutines",
|
|
":libraries:di",
|
|
":tests:detekt-rules",
|
|
":tests:konsist",
|
|
":tests:testutils",
|
|
) + localAarProjects
|
|
|
|
private fun Project.kover(any: Any) {
|
|
this.dependencies.add("kover", any)
|
|
}
|
|
|
|
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
|
|
tasks.register("koverVerifyAll") {
|
|
group = "verification"
|
|
description = "Verifies the code coverage of all subprojects."
|
|
val dependencies = listOf(":koverVerifyMerged") + 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
|
|
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 {
|
|
filters {
|
|
excludes {
|
|
classes(
|
|
// Exclude generated classes.
|
|
"*_Module",
|
|
"*_AssistedFactory",
|
|
"com.airbnb.android.showkase*",
|
|
"io.element.android.libraries.designsystem.showkase.*",
|
|
"*ComposableSingletons$*",
|
|
"*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.*",
|
|
// Konsist code to make test fails
|
|
"io.element.android.tests.konsist.failures",
|
|
// Copied from Appyx
|
|
"io.element.android.libraries.architecture.appyx.SafeChildrenTransitionScope",
|
|
)
|
|
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.features.ftue.api.state.*",
|
|
"io.element.android.features.ftue.impl.welcome.state.*",
|
|
"io.element.android.features.messages.impl.timeline.model.bubble.BubbleState",
|
|
"io.element.android.libraries.designsystem.swipe.SwipeableActionsState",
|
|
"io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomSheetState",
|
|
"io.element.android.libraries.maplibre.compose.CameraPositionState",
|
|
"io.element.android.libraries.maplibre.compose.SymbolState",
|
|
"io.element.android.libraries.matrix.api.room.RoomMembershipState",
|
|
"io.element.android.libraries.matrix.api.room.RoomMembersState",
|
|
"io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*",
|
|
"io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*",
|
|
"io.element.android.libraries.mediaviewer.impl.local.pdf.PdfViewerState",
|
|
"io.element.android.libraries.mediaviewer.impl.local.player.MediaPlayerControllerState",
|
|
"io.element.android.libraries.textcomposer.model.TextEditorState",
|
|
"io.element.android.libraries.textcomposer.components.FormattingOptionState",
|
|
)
|
|
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 KoverVariantCreateConfig.defaultVariants(project: Project) {
|
|
if (project.path == ":app") {
|
|
addWithDependencies("gplayDebug")
|
|
} else {
|
|
addWithDependencies("debug", "jvm", optional = true)
|
|
}
|
|
}
|