Merge branch 'develop' into feature/bma/upgradeSdk
This commit is contained in:
2
.github/workflows/nightlyReports.yml
vendored
2
.github/workflows/nightlyReports.yml
vendored
@@ -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()
|
||||
|
||||
147
.github/workflows/quality.yml
vendored
147
.github/workflows/quality.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon --warn
|
||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon
|
||||
|
||||
jobs:
|
||||
checkScript:
|
||||
@@ -33,12 +33,13 @@ jobs:
|
||||
- name: Search for invalid screenshot files
|
||||
run: ./tools/test/checkInvalidScreenshots.py
|
||||
|
||||
check:
|
||||
name: Project Check Suite
|
||||
# Code checks
|
||||
konsist:
|
||||
name: Konsist tests
|
||||
runs-on: ubuntu-latest
|
||||
# Allow all jobs on main and develop. Just one per PR.
|
||||
concurrency:
|
||||
group: ${{ github.ref == 'refs/heads/main' && format('check-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-develop-{0}', github.sha) || format('check-{0}', github.ref) }}
|
||||
group: ${{ github.ref == 'refs/heads/main' && format('check-konsist-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-konsist-develop-{0}', github.sha) || format('check-konsist-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -55,8 +56,40 @@ jobs:
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Run code quality check suite
|
||||
run: ./gradlew runQualityChecks $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Run Konsist tests
|
||||
run: ./gradlew :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --no-daemon
|
||||
- name: Upload reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: konsist-report
|
||||
path: |
|
||||
**/build/reports/**/*.*
|
||||
|
||||
lint:
|
||||
name: Android lint check
|
||||
runs-on: ubuntu-latest
|
||||
# Allow all jobs on main and develop. Just one per PR.
|
||||
concurrency:
|
||||
group: ${{ github.ref == 'refs/heads/main' && format('check-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-lint-develop-{0}', github.sha) || format('check-lint-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Run lint
|
||||
run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Upload reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -64,6 +97,108 @@ jobs:
|
||||
name: linting-report
|
||||
path: |
|
||||
**/build/reports/**/*.*
|
||||
|
||||
detekt:
|
||||
name: Detekt checks
|
||||
runs-on: ubuntu-latest
|
||||
# Allow all jobs on main and develop. Just one per PR.
|
||||
concurrency:
|
||||
group: ${{ github.ref == 'refs/heads/main' && format('check-detekt-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-detekt-develop-{0}', github.sha) || format('check-detekt-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Run Detekt
|
||||
run: ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES --no-daemon
|
||||
- name: Upload reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: detekt-report
|
||||
path: |
|
||||
**/build/reports/**/*.*
|
||||
|
||||
ktlint:
|
||||
name: Ktlint checks
|
||||
runs-on: ubuntu-latest
|
||||
# Allow all jobs on main and develop. Just one per PR.
|
||||
concurrency:
|
||||
group: ${{ github.ref == 'refs/heads/main' && format('check-ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-ktlint-develop-{0}', github.sha) || format('check-ktlint-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Run Ktlint check
|
||||
run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES
|
||||
- name: Upload reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ktlint-report
|
||||
path: |
|
||||
**/build/reports/**/*.*
|
||||
|
||||
knit:
|
||||
name: Knit checks
|
||||
runs-on: ubuntu-latest
|
||||
# Allow all jobs on main and develop. Just one per PR.
|
||||
concurrency:
|
||||
group: ${{ github.ref == 'refs/heads/main' && format('check-knit-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-knit-develop-{0}', github.sha) || format('check-knit-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Configure gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||
- name: Run Knit
|
||||
run: ./gradlew knitCheck $CI_GRADLE_ARG_PROPERTIES
|
||||
|
||||
upload_reports:
|
||||
name: Project Check Suite
|
||||
runs-on: ubuntu-latest
|
||||
needs: [konsist, lint, ktlint, detekt]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Ensure we are building the branch and not the branch after being merged on develop
|
||||
# https://github.com/actions/checkout/issues/881
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
- name: Download reports from previous jobs
|
||||
uses: actions/download-artifact@v4
|
||||
- name: Prepare Danger
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -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()
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -45,6 +45,7 @@ captures/
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/compiler.xml
|
||||
.idea/deploymentTargetDropDown.xml
|
||||
.idea/deploymentTargetSelector.xml
|
||||
.idea/gradle.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/misc.xml
|
||||
|
||||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.23" />
|
||||
<option name="version" value="1.9.24" />
|
||||
</component>
|
||||
</project>
|
||||
1
changelog.d/2291.bugfix
Normal file
1
changelog.d/2291.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Make sure explicit links in messages take priority over links found by linkification (urls, emails, phone numbers, etc.)
|
||||
@@ -268,12 +268,16 @@ class TimelineItemContentMessageFactory @Inject constructor(
|
||||
}
|
||||
// Find and set as URLSpans any links present in the text
|
||||
LinkifyCompat.addLinks(this, Linkify.WEB_URLS or Linkify.PHONE_NUMBERS or Linkify.EMAIL_ADDRESSES)
|
||||
// Restore old spans if they don't conflict with the new ones
|
||||
// Restore old spans, remove new ones if there is a conflict
|
||||
for ((urlSpan, location) in oldURLSpans) {
|
||||
val (start, end) = location
|
||||
if (getSpans<URLSpan>(start, end).isEmpty()) {
|
||||
setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
val addedSpans = getSpans<URLSpan>(start, end).orEmpty()
|
||||
if (addedSpans.isNotEmpty()) {
|
||||
for (span in addedSpans) {
|
||||
removeSpan(span)
|
||||
}
|
||||
}
|
||||
setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -69,12 +69,14 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_TRANSACTION_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
||||
@@ -456,6 +458,31 @@ class MessagesPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle action redact message in error, in this case the message is just cancelled`() = runTest {
|
||||
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
val matrixRoom = FakeMatrixRoom()
|
||||
val presenter = createMessagesPresenter(matrixRoom = matrixRoom, coroutineDispatchers = coroutineDispatchers)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(
|
||||
MessagesEvents.HandleAction(
|
||||
action = TimelineItemAction.Redact,
|
||||
event = aMessageEvent(
|
||||
transactionId = A_TRANSACTION_ID,
|
||||
sendState = LocalEventSendState.SendingFailed("Failed to send message")
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(matrixRoom.cancelSendCount).isEqualTo(1)
|
||||
assertThat(matrixRoom.redactEventEventIdParam).isNull()
|
||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle action report content`() = runTest {
|
||||
val navigator = FakeMessagesNavigator()
|
||||
|
||||
@@ -28,6 +28,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
@@ -38,6 +39,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal fun aMessageEvent(
|
||||
eventId: EventId? = AN_EVENT_ID,
|
||||
transactionId: TransactionId? = null,
|
||||
isMine: Boolean = true,
|
||||
isEditable: Boolean = true,
|
||||
content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, formattedBody = null, isEdited = false),
|
||||
@@ -48,6 +50,7 @@ internal fun aMessageEvent(
|
||||
) = TimelineItem.Event(
|
||||
id = eventId?.value.orEmpty(),
|
||||
eventId = eventId,
|
||||
transactionId = transactionId,
|
||||
senderId = A_USER_ID,
|
||||
senderProfile = aProfileTimelineDetailsReady(displayName = A_USER_NAME),
|
||||
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME, size = AvatarSize.TimelineSender),
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.factories.event
|
||||
|
||||
import android.net.Uri
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.URLSpan
|
||||
import androidx.core.text.buildSpannedString
|
||||
@@ -46,6 +48,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.media.ThumbnailInfo
|
||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||
@@ -75,6 +78,7 @@ import org.robolectric.RobolectricTestRunner
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class TimelineItemContentMessageFactoryTest {
|
||||
@Test
|
||||
@@ -641,6 +645,31 @@ class TimelineItemContentMessageFactoryTest {
|
||||
assertThat((result as TimelineItemEmoteContent).formattedBody).isEqualTo(SpannableString("* Bob formatted"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a message with existing URLSpans keeps it after linkification`() = runTest {
|
||||
val expectedSpanned = SpannableStringBuilder().apply {
|
||||
append("Test ")
|
||||
inSpans(URLSpan("https://www.example.org")) {
|
||||
append("me@matrix.org")
|
||||
}
|
||||
}
|
||||
val sut = createTimelineItemContentMessageFactory(
|
||||
htmlConverterTransform = { expectedSpanned },
|
||||
permalinkParser = FakePermalinkParser { PermalinkData.FallbackLink(Uri.EMPTY) }
|
||||
)
|
||||
val result = sut.create(
|
||||
content = createMessageContent(
|
||||
type = TextMessageType(
|
||||
body = "Test [me@matrix.org](https://www.example.org)",
|
||||
formatted = FormattedBody(MessageFormat.HTML, "Test <a href=\"https://www.example.org\">me@matrix.org</a>")
|
||||
)
|
||||
),
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(expectedSpanned)
|
||||
}
|
||||
|
||||
private fun createMessageContent(
|
||||
body: String = "Body",
|
||||
inReplyTo: InReplyTo? = null,
|
||||
@@ -660,12 +689,13 @@ class TimelineItemContentMessageFactoryTest {
|
||||
private fun createTimelineItemContentMessageFactory(
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
htmlConverterTransform: (String) -> CharSequence = { it },
|
||||
permalinkParser: FakePermalinkParser = FakePermalinkParser(),
|
||||
) = TimelineItemContentMessageFactory(
|
||||
fileSizeFormatter = FakeFileSizeFormatter(),
|
||||
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
|
||||
featureFlagService = featureFlagService,
|
||||
htmlConverterProvider = FakeHtmlConverterProvider(htmlConverterTransform),
|
||||
permalinkParser = FakePermalinkParser(),
|
||||
permalinkParser = permalinkParser,
|
||||
)
|
||||
|
||||
private fun createStickerContent(
|
||||
|
||||
@@ -59,3 +59,6 @@ android.enableBuildConfigAsBytecode=true
|
||||
|
||||
# By default, the plugin applies itself to all subprojects, but we don't want that as it would cause issues with builds using local AARs
|
||||
dependency.analysis.autoapply=false
|
||||
|
||||
# Disable new R8 shrinking for local dependencies as it causes issues with release builds
|
||||
android.disableMinifyLocalDependenciesForLibraries=false
|
||||
|
||||
@@ -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
|
||||
@@ -136,7 +136,7 @@ test_arch_core = "androidx.arch.core:core-testing:2.2.0"
|
||||
test_junit = "junit:junit:4.13.2"
|
||||
test_runner = "androidx.test:runner:1.5.2"
|
||||
test_junitext = "androidx.test.ext:junit:1.1.5"
|
||||
test_mockk = "io.mockk:mockk:1.13.10"
|
||||
test_mockk = "io.mockk:mockk:1.13.11"
|
||||
test_konsist = "com.lemonappdev:konsist:0.13.0"
|
||||
test_turbine = "app.cash.turbine:turbine:1.1.0"
|
||||
test_truth = "com.google.truth:truth:1.4.2"
|
||||
|
||||
@@ -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,175 @@ val excludedKoverSubProjects = listOf(
|
||||
":libraries:di",
|
||||
) + localAarProjects
|
||||
|
||||
private fun Project.koverReport(action: Action<KoverReportExtension>) {
|
||||
(this as org.gradle.api.plugins.ExtensionAware).extensions.configure("koverReport", action)
|
||||
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.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 {
|
||||
// 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
|
||||
|
||||
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 {
|
||||
// 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 {
|
||||
// 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 +234,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))
|
||||
|
||||
Reference in New Issue
Block a user