diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index 65a2717f36..b27183cc59 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -33,7 +33,7 @@ jobs: run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES - name: 📈 Generate kover report and verify coverage - run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyGplayDebug $CI_GRADLE_ARG_PROPERTIES + run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyAll $CI_GRADLE_ARG_PROPERTIES - name: ✅ Upload kover report if: always() diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4952ac435c..a678cb54cb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,7 +55,7 @@ jobs: run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES - name: 📈Generate kover report and verify coverage - run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyGplayDebug $CI_GRADLE_ARG_PROPERTIES + run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyAll $CI_GRADLE_ARG_PROPERTIES - name: 🚫 Upload kover failed coverage reports if: failure() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aaa82ef687..38f879f28b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,7 +57,7 @@ autoservice = "1.1.1" junit = "4.13.2" androidx-test-ext-junit = "1.1.5" espresso-core = "3.5.1" -kover = "0.7.6" +kover = "0.8.0" [libraries] # Project diff --git a/plugins/src/main/kotlin/extension/KoverExtension.kt b/plugins/src/main/kotlin/extension/KoverExtension.kt index e107291918..68f89f300f 100644 --- a/plugins/src/main/kotlin/extension/KoverExtension.kt +++ b/plugins/src/main/kotlin/extension/KoverExtension.kt @@ -16,10 +16,24 @@ package extension -import kotlinx.kover.gradle.plugin.dsl.KoverReportExtension +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.configurationcache.extensions.capitalized 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", @@ -44,160 +58,179 @@ val excludedKoverSubProjects = listOf( ":libraries:di", ) + localAarProjects -private fun Project.koverReport(action: Action) { - (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("koverReport", action) +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.capitalized()}" } + 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 - koverReport { - 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 presenter - "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.PreviewsDayNight", - "io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight", - ) + 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 presenter + "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.PreviewsDayNight", + "io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight", + ) + } } - } - defaults { - // add reports of both 'debug' and 'release' Android build variants to default reports - mergeWith("gplayDebug") + total { + verify { + onCheck = true + // 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 { + onCheck = true + // Rule to ensure that coverage of Presenters is sufficient. + rule("Check code coverage of presenters") { + groupBy = GroupingEntityType.CLASS - verify { - onCheck = true - // General rule: minimum code coverage. - rule("Global minimum code coverage.") { - isEnabled = true - entity = kotlinx.kover.gradle.plugin.dsl.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 - metric = kotlinx.kover.gradle.plugin.dsl.MetricType.INSTRUCTION - aggregation = kotlinx.kover.gradle.plugin.dsl.AggregationType.COVERED_PERCENTAGE + bound { + minValue = 85 + coverageUnits = CoverageUnit.INSTRUCTION + aggregationForGroup = AggregationType.COVERED_PERCENTAGE + } } } - // Rule to ensure that coverage of Presenters is sufficient. - rule("Check code coverage of presenters") { - isEnabled = true - entity = kotlinx.kover.gradle.plugin.dsl.GroupingEntityType.CLASS - filters { - includes { - classes( - "*Presenter", - ) - } - 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$*", - "*Presenter\$present\$*", - ) - } + filters { + includes { + classes( + "*Presenter", + ) } - bound { - minValue = 85 - metric = kotlinx.kover.gradle.plugin.dsl.MetricType.INSTRUCTION - aggregation = kotlinx.kover.gradle.plugin.dsl.AggregationType.COVERED_PERCENTAGE + 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$*", + "*Presenter\$present\$*", + ) } } - // Rule to ensure that coverage of States is sufficient. - rule("Check code coverage of states") { - isEnabled = true - entity = kotlinx.kover.gradle.plugin.dsl.GroupingEntityType.CLASS - filters { - includes { - classes( - "^*State$", - ) + } + variant(KoverVariant.States.variantName) { + verify { + onCheck = true + // 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 } - excludes { - 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.*", - ) - } - } - bound { - minValue = 90 - metric = kotlinx.kover.gradle.plugin.dsl.MetricType.INSTRUCTION - aggregation = kotlinx.kover.gradle.plugin.dsl.AggregationType.COVERED_PERCENTAGE } } - // Rule to ensure that coverage of Views is sufficient (deactivated for now). - rule("Check code coverage of views") { - isEnabled = true - entity = kotlinx.kover.gradle.plugin.dsl.GroupingEntityType.CLASS - filters { - includes { - classes( - "*ViewKt", - ) + filters { + includes { + classes( + "^*State$", + ) + } + excludes { + 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.*", + ) + } + } + } + variant(KoverVariant.Views.variantName) { + verify { + onCheck = true + // 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 } } - bound { - // TODO Update this value, for now there are too many missing tests. - minValue = 0 - metric = kotlinx.kover.gradle.plugin.dsl.MetricType.INSTRUCTION - aggregation = kotlinx.kover.gradle.plugin.dsl.AggregationType.COVERED_PERCENTAGE + } + filters { + includes { + classes( + "*ViewKt", + ) } } } @@ -205,22 +238,37 @@ fun Project.setupKover() { } } -fun Project.applyKoverPluginToAllSubProjects() = rootProject.allprojects { +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() + } + } + } + } } } +fun KoverVariantCreateConfig.defaultVariants() { + addWithDependencies("gplayDebug", "debug", 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.rootProject.subprojects - .filter { - it.project.projectDir.resolve("build.gradle.kts").exists() - } - .map { it.path } - .sorted() - .filter { - it !in excludedKoverSubProjects - } + project.koverSubprojects() .forEach { // println("Add $it to kover") dependencies.add("kover", project(it))