Browse Source

Merge pull request #1782 from element-hq/feature/bma/upgradeKover

Upgrade kover
pull/2193/head
Benoit Marty 8 months ago committed by GitHub
parent
commit
2bdf2c0bbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .github/renovate.json
  2. 4
      .github/workflows/nightlyReports.yml
  3. 6
      .github/workflows/tests.yml
  4. 215
      app/build.gradle.kts
  5. 176
      build.gradle.kts
  6. 1
      changelog.d/1782.misc
  7. 10
      docs/_developer_onboarding.md
  8. 1
      features/messages/impl/build.gradle.kts
  9. 2
      gradle/libs.versions.toml
  10. 1
      libraries/coroutines/.gitignore
  11. 28
      libraries/coroutines/build.gradle.kts
  12. 28
      libraries/textcomposer/test/build.gradle.kts

6
.github/renovate.json

@ -18,12 +18,6 @@
], ],
"groupName" : "kotlin" "groupName" : "kotlin"
}, },
{
"matchPackageNames" : [
"org.jetbrains.kotlinx.kover"
],
"enabled" : false
},
{ {
"matchPackagePatterns" : [ "matchPackagePatterns" : [
"^org.maplibre" "^org.maplibre"

4
.github/workflows/nightlyReports.yml

@ -33,7 +33,7 @@ jobs:
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
- name: 📈 Generate kover report and verify coverage - name: 📈 Generate kover report and verify coverage
run: ./gradlew koverMergedReport koverMergedVerify $CI_GRADLE_ARG_PROPERTIES -Pci-build=true run: ./gradlew :app:koverHtmlReport :app:koverXmlReport :app:koverVerify $CI_GRADLE_ARG_PROPERTIES -Pci-build=true
- name: ✅ Upload kover report - name: ✅ Upload kover report
if: always() if: always()
@ -41,7 +41,7 @@ jobs:
with: with:
name: kover-results name: kover-results
path: | path: |
**/build/reports/kover/merged **/build/reports/kover
- name: 🔊 Publish results to Sonar - name: 🔊 Publish results to Sonar
env: env:

6
.github/workflows/tests.yml

@ -55,7 +55,7 @@ jobs:
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
- name: 📈Generate kover report and verify coverage - name: 📈Generate kover report and verify coverage
run: ./gradlew koverMergedReport koverMergedVerify $CI_GRADLE_ARG_PROPERTIES -Pci-build=true run: ./gradlew :app:koverHtmlReport :app:koverXmlReport :app:koverVerify $CI_GRADLE_ARG_PROPERTIES -Pci-build=true
- name: 🚫 Upload kover failed coverage reports - name: 🚫 Upload kover failed coverage reports
if: failure() if: failure()
@ -63,7 +63,7 @@ jobs:
with: with:
name: kover-error-report name: kover-error-report
path: | path: |
**/kover/merged/verification/errors.txt app/build/reports/kover/verify.err
- name: ✅ Upload kover report (disabled) - name: ✅ Upload kover report (disabled)
if: always() if: always()
@ -83,4 +83,4 @@ jobs:
if: always() if: always()
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3
# with: # with:
# files: build/reports/kover/merged/xml/report.xml # files: build/reports/kover/xml/report.xml

215
app/build.gradle.kts

@ -20,6 +20,7 @@ import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
import extension.allFeaturesImpl import extension.allFeaturesImpl
import extension.allLibrariesImpl import extension.allLibrariesImpl
import extension.allServicesImpl import extension.allServicesImpl
import org.jetbrains.kotlin.cli.common.toBooleanLenient
plugins { plugins {
id("io.element.android-compose-application") id("io.element.android-compose-application")
@ -190,6 +191,220 @@ knit {
} }
} }
/**
* Kover configuration
*/
dependencies {
// Add all sub projects to kover except some of them
project.rootProject.subprojects
.filter {
it.project.projectDir.resolve("build.gradle.kts").exists()
}
.map { it.path }
.sorted()
.filter {
it !in 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",
":libraries:rustsdk",
":libraries:textcomposer:lib",
)
}
.forEach {
// println("Add $it to kover")
kover(project(it))
}
}
val ciBuildProperty = "ci-build"
val isCiBuild = if (project.hasProperty(ciBuildProperty)) {
val raw = project.property(ciBuildProperty) as? String
raw?.toBooleanLenient() == true || raw?.toIntOrNull() == 1
} else {
false
}
kover {
// When running on the CI, run only debug test variants
if (isCiBuild) {
excludeTests {
// Disable instrumentation for debug test tasks
tasks(
"testDebugUnitTest",
)
}
}
}
// 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.*",
)
annotatedBy(
"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("debug")
mergeWith("release")
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 = 65
// 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 = 75
metric = kotlinx.kover.gradle.plugin.dsl.MetricType.INSTRUCTION
aggregation = kotlinx.kover.gradle.plugin.dsl.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\$*",
)
}
}
bound {
minValue = 85
metric = kotlinx.kover.gradle.plugin.dsl.MetricType.INSTRUCTION
aggregation = kotlinx.kover.gradle.plugin.dsl.AggregationType.COVERED_PERCENTAGE
}
}
// 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$",
)
}
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",
)
}
}
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
}
}
}
}
androidReports("release") {
}
}
dependencies { dependencies {
allLibrariesImpl() allLibrariesImpl()
allServicesImpl() allServicesImpl()

176
build.gradle.kts

@ -1,7 +1,5 @@
import com.google.devtools.ksp.gradle.KspTask import com.google.devtools.ksp.gradle.KspTask
import kotlinx.kover.api.KoverTaskExtension
import org.apache.tools.ant.taskdefs.optional.ReplaceRegExp import org.apache.tools.ant.taskdefs.optional.ReplaceRegExp
import org.jetbrains.kotlin.cli.common.toBooleanLenient
buildscript { buildscript {
dependencies { dependencies {
@ -41,7 +39,7 @@ plugins {
alias(libs.plugins.ktlint) alias(libs.plugins.ktlint)
alias(libs.plugins.dependencygraph) alias(libs.plugins.dependencygraph)
alias(libs.plugins.sonarqube) alias(libs.plugins.sonarqube)
alias(libs.plugins.kover) alias(libs.plugins.kover) apply false
} }
tasks.register<Delete>("clean").configure { tasks.register<Delete>("clean").configure {
@ -164,177 +162,7 @@ allprojects {
} }
allprojects { allprojects {
apply(plugin = "kover") apply(plugin = "org.jetbrains.kotlinx.kover")
}
// https://kotlin.github.io/kotlinx-kover/
// Run `./gradlew koverMergedHtmlReport` to get report at ./build/reports/kover
// Run `./gradlew koverMergedReport` to also get XML report
koverMerged {
enable()
filters {
classes {
excludes.addAll(
listOf(
// 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$*",
// 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.
"io.element.android.libraries.matrix.impl.*",
"*Presenter\$present\$*",
// Forked from compose
"io.element.android.libraries.designsystem.theme.components.bottomsheet.*",
)
)
}
annotations {
excludes.addAll(
listOf(
"*Preview",
)
)
}
projects {
excludes.addAll(
listOf(
":anvilannotations",
":anvilcodegen",
":samples:minimal",
":tests:testutils",
)
)
}
}
// Run ./gradlew koverMergedVerify to check the rules.
verify {
// Does not seems to work, so also run the task manually on the workflow.
onCheck.set(true)
// General rule: minimum code coverage.
rule {
name = "Global minimum code coverage."
target = kotlinx.kover.api.VerificationTarget.ALL
bound {
minValue = 65
// 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 = 75
counter = kotlinx.kover.api.CounterType.INSTRUCTION
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
}
}
// Rule to ensure that coverage of Presenters is sufficient.
rule {
name = "Check code coverage of presenters"
target = kotlinx.kover.api.VerificationTarget.CLASS
overrideClassFilter {
includes += "*Presenter"
excludes += "*Fake*Presenter"
excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*"
// Some options can't be tested at the moment
excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*"
excludes += "*Presenter\$present\$*"
}
bound {
minValue = 85
counter = kotlinx.kover.api.CounterType.INSTRUCTION
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
}
}
// Rule to ensure that coverage of States is sufficient.
rule {
name = "Check code coverage of states"
target = kotlinx.kover.api.VerificationTarget.CLASS
overrideClassFilter {
includes += "^*State$"
excludes += "io.element.android.appnav.root.RootNavState*"
excludes += "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*"
excludes += "io.element.android.libraries.matrix.api.timeline.item.event.EventSendState$*"
excludes += "io.element.android.libraries.matrix.api.room.RoomMembershipState*"
excludes += "io.element.android.libraries.matrix.api.room.MatrixRoomMembersState*"
excludes += "io.element.android.libraries.push.impl.notifications.NotificationState*"
excludes += "io.element.android.features.messages.impl.media.local.pdf.PdfViewerState"
excludes += "io.element.android.features.messages.impl.media.local.LocalMediaViewState"
excludes += "io.element.android.features.location.impl.map.MapState*"
excludes += "io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*"
excludes += "io.element.android.libraries.designsystem.swipe.SwipeableActionsState*"
excludes += "io.element.android.features.messages.impl.timeline.components.ExpandableState*"
excludes += "io.element.android.features.messages.impl.timeline.model.bubble.BubbleState*"
excludes += "io.element.android.libraries.maplibre.compose.CameraPositionState*"
excludes += "io.element.android.libraries.maplibre.compose.SaveableCameraPositionState"
excludes += "io.element.android.libraries.maplibre.compose.SymbolState*"
excludes += "io.element.android.features.ftue.api.state.*"
excludes += "io.element.android.features.ftue.impl.welcome.state.*"
}
bound {
minValue = 90
counter = kotlinx.kover.api.CounterType.INSTRUCTION
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
}
}
// Rule to ensure that coverage of Views is sufficient (deactivated for now).
rule {
name = "Check code coverage of views"
target = kotlinx.kover.api.VerificationTarget.CLASS
overrideClassFilter {
includes += "*ViewKt"
}
bound {
// TODO Update this value, for now there are too many missing tests.
minValue = 0
counter = kotlinx.kover.api.CounterType.INSTRUCTION
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
}
}
}
}
// When running on the CI, run only debug test variants
val ciBuildProperty = "ci-build"
val isCiBuild = if (project.hasProperty(ciBuildProperty)) {
val raw = project.property(ciBuildProperty) as? String
raw?.toBooleanLenient() == true || raw?.toIntOrNull() == 1
} else {
false
}
if (isCiBuild) {
allprojects {
afterEvaluate {
tasks.withType<Test>().configureEach {
extensions.configure<KoverTaskExtension> {
val enabled = name.contains("debug", ignoreCase = true)
isDisabled.set(!enabled)
}
}
}
}
} }
// Register quality check tasks. // Register quality check tasks.

1
changelog.d/1782.misc

@ -0,0 +1 @@
Migrate to Kover 0.7.X

10
docs/_developer_onboarding.md

@ -344,26 +344,26 @@ implementation of our interfaces. Mocking can be used to mock Android framework
[kover](https://github.com/Kotlin/kotlinx-kover) is used to compute code coverage. Only have unit tests can produce code coverage result. Running Maestro does [kover](https://github.com/Kotlin/kotlinx-kover) is used to compute code coverage. Only have unit tests can produce code coverage result. Running Maestro does
not participate to the code coverage results. not participate to the code coverage results.
Kover configuration is defined in the main [build.gradle.kts](../build.gradle.kts) file. Kover configuration is defined in the app [build.gradle.kts](../app/build.gradle.kts) file.
To compute the code coverage, run: To compute the code coverage, run:
```bash ```bash
./gradlew koverMergedReport ./gradlew :app:koverHtmlReport
``` ```
and open the Html report: [../build/reports/kover/merged/html/index.html](../build/reports/kover/merged/html/index.html) and open the Html report: [../app/build/reports/kover/html/index.html](../app/build/reports/kover/html/index.html)
To ensure that the code coverage threshold are OK, you can run To ensure that the code coverage threshold are OK, you can run
```bash ```bash
./gradlew koverMergedVerify ./gradlew :app:koverVerify
``` ```
Note that the CI performs this check on every pull requests. Note that the CI performs this check on every pull requests.
Also, if the rule `Global minimum code coverage.` is in error because code coverage is `> maxValue`, `minValue` and `maxValue` can be updated for this rule in Also, if the rule `Global minimum code coverage.` is in error because code coverage is `> maxValue`, `minValue` and `maxValue` can be updated for this rule in
the file [build.gradle.kts](../build.gradle.kts) (you will see further instructions there). the file [build.gradle.kts](../app/build.gradle.kts) (you will see further instructions there).
### Other points ### Other points

1
features/messages/impl/build.gradle.kts

@ -91,7 +91,6 @@ dependencies {
testImplementation(projects.libraries.mediapickers.test) testImplementation(projects.libraries.mediapickers.test)
testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.textcomposer.test)
testImplementation(projects.libraries.voicerecorder.test) testImplementation(projects.libraries.voicerecorder.test)
testImplementation(projects.libraries.mediaplayer.test) testImplementation(projects.libraries.mediaplayer.test)
testImplementation(projects.libraries.mediaviewer.test) testImplementation(projects.libraries.mediaviewer.test)

2
gradle/libs.versions.toml

@ -215,7 +215,7 @@ dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"
dependencycheck = "org.owasp.dependencycheck:9.0.8" dependencycheck = "org.owasp.dependencycheck:9.0.8"
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" } dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" }
paparazzi = "app.cash.paparazzi:1.3.1" paparazzi = "app.cash.paparazzi:1.3.1"
kover = "org.jetbrains.kotlinx.kover:0.6.1" kover = "org.jetbrains.kotlinx.kover:0.7.5"
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppDistribution" } firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppDistribution" }
knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.0" } knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.0" }

1
libraries/coroutines/.gitignore vendored

@ -1 +0,0 @@
/build

28
libraries/coroutines/build.gradle.kts

@ -1,28 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("java-library")
alias(libs.plugins.kotlin.jvm)
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
dependencies {
implementation(libs.coroutines.core)
}

28
libraries/textcomposer/test/build.gradle.kts

@ -1,28 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("io.element.android-compose-library")
}
android {
namespace = "io.element.android.libraries.textcomposer.test"
}
dependencies {
api(projects.libraries.textcomposer.impl)
implementation(projects.tests.testutils)
}
Loading…
Cancel
Save