Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
46f5cea
Add test coverage tasks
RankoR May 29, 2026
d1c6977
Add shared unit test utilities
RankoR May 31, 2026
13b0664
Add core domain unit tests
RankoR May 31, 2026
6226055
Add data layer unit tests
RankoR May 31, 2026
e5ea3b9
Add settings unit tests
RankoR May 31, 2026
0c7c2a6
Add conversation composer unit tests
RankoR May 31, 2026
cbc7de6
Add conversation entry unit tests
RankoR May 31, 2026
986de76
Add conversation media unit tests
RankoR May 31, 2026
6598345
Add conversation messages unit tests
RankoR May 31, 2026
b332468
Add conversation screen unit tests
RankoR May 31, 2026
9757183
Remove dead code
RankoR May 31, 2026
0befae0
Add test tags to the UI elements
RankoR May 31, 2026
b640402
Enable Compose Robolectric unit tests
RankoR Jun 1, 2026
dfb4556
Move instrumentation helpers to the Kotlin source set
RankoR Jun 1, 2026
1da275c
Refresh existing instrumentation tests
RankoR Jun 1, 2026
f009dce
Add shared conversation Compose test fixtures
RankoR Jun 1, 2026
910e875
Cover app settings and utility logic
RankoR Jun 1, 2026
dbec297
Cover conversation composer and top-level UI
RankoR Jun 1, 2026
36d174c
Cover conversation media picker UI
RankoR Jun 1, 2026
2672581
Cover conversation message rendering
RankoR Jun 1, 2026
ffc364d
Cover recipient picker UI
RankoR Jun 1, 2026
a2673b5
Cover conversation screen flows
RankoR Jun 1, 2026
c2eba0b
Add focused Compose instrumentation tests
RankoR Jun 1, 2026
f7f5bae
Add JaCoCo rule lists
RankoR Jun 1, 2026
41629ee
Clean up the JaCoCo Gradle script
RankoR Jun 1, 2026
9ffe802
Add pull request CI checks
RankoR Jun 2, 2026
715bccd
Add unit coverage gate
RankoR Jun 2, 2026
fe12f56
Add Android 16 instrumented PR tests
RankoR Jun 2, 2026
564bf5e
Add instrumented coverage gate
RankoR Jun 2, 2026
dad58a5
Cover draft attachment limit validation
RankoR Jun 4, 2026
d82fcc9
Cover conversation segment counter
RankoR Jun 4, 2026
366fc91
Upgrade mockk
RankoR Jun 4, 2026
ac41ebb
Update pinned deps hashes
RankoR Jun 9, 2026
85c671c
Cache AVD snapshot for tests
RankoR Jun 9, 2026
e2006e5
Address tests review comments
RankoR Jun 9, 2026
a11c548
Update deps pins
RankoR Jun 11, 2026
1510d5b
Fix tests after rebasing
RankoR Jun 11, 2026
d04b9cf
Fix flaky participants list test
RankoR Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 197 additions & 9 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,208 @@
name: Build application
name: Pull request checks

on: [pull_request, push]
on:
pull_request:
types:
- opened
- reopened
- synchronize

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
build:
build-debug-apk:
name: Build debug APK
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Check out sources
uses: actions/checkout@v6
with:
submodules: true

- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
cache: gradle

- name: Assemble debug APK
run: ./gradlew :app:assembleDebug --no-daemon --stacktrace --console=plain

ktlint:
name: ktlintCheck
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Check out sources
uses: actions/checkout@v6
with:
submodules: true

- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
cache: gradle

- name: Run ktlintCheck
run: ./gradlew ktlintCheck --no-daemon --stacktrace --console=plain

- name: Upload ktlint reports
if: failure()
uses: actions/upload-artifact@v4
with:
name: ktlint-reports
path: |
build/reports/ktlint/
app/build/reports/ktlint/
if-no-files-found: ignore

detekt:
name: Detekt
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Check out sources
uses: actions/checkout@v6
with:
submodules: true

- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
cache: gradle

- name: Run Detekt
run: ./gradlew :app:detekt --no-daemon --stacktrace --console=plain

- name: Upload Detekt reports
if: failure()
uses: actions/upload-artifact@v4
with:
name: detekt-reports
path: app/build/reports/detekt/
if-no-files-found: ignore

unit-tests:
name: Unit tests
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- uses: actions/checkout@v5
- name: Check out sources
uses: actions/checkout@v6
with:
submodules: true
- name: Set up JDK 21

- name: Set up JDKs
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 21
distribution: temurin
java-version: |
17
21
cache: gradle
- name: Build with Gradle
run: ./gradlew build --no-daemon

- name: Run unit tests and coverage gate
run: >
./gradlew :app:jacocoUnitTestVerification
-PunitTestMinCoverage=80
-PunitTestMinBranchCoverage=60
--no-daemon --stacktrace --console=plain

- name: Upload unit test reports
if: failure()
uses: actions/upload-artifact@v4
with:
name: unit-test-reports
path: |
app/build/reports/jacoco/jacocoUnitTestReport/
app/build/reports/tests/testDebugUnitTest/
app/build/test-results/testDebugUnitTest/
if-no-files-found: ignore

instrumented-tests:
name: Instrumented tests (Android 16 x86_64)
runs-on: ubuntu-latest
timeout-minutes: 45

steps:
- name: Check out sources
uses: actions/checkout@v6
with:
submodules: true

- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
cache: gradle

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Restore AVD cache
id: avd-cache
uses: actions/cache@v4
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-36-x86_64-default

- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a
with:
api-level: 36
arch: x86_64
target: default
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
script: echo "Generated AVD snapshot for caching."

- name: Run instrumented tests and coverage gate
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a
with:
api-level: 36
arch: x86_64
target: default
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: >
./gradlew :app:connectedDebugAndroidTest :app:jacocoAndroidTestVerification
-PandroidTestCoverage=true
-PandroidTestMinCoverage=80
-PandroidTestMinBranchCoverage=50
--no-daemon --stacktrace --console=plain

- name: Upload instrumented test reports
if: failure()
uses: actions/upload-artifact@v4
with:
name: instrumented-test-reports
path: |
app/build/outputs/code_coverage/debugAndroidTest/connected/
app/build/outputs/androidTest-results/connected/
app/build/reports/jacoco/jacocoAndroidTestReport/
app/build/reports/androidTests/connected/
if-no-files-found: ignore
23 changes: 20 additions & 3 deletions .github/workflows/validate-gradle-wrapper.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
name: Validate Gradle Wrapper

on: [pull_request, push]
on:
pull_request:
types:
- opened
- reopened
- synchronize

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
validation:
name: Validation
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- uses: actions/checkout@v5
- uses: gradle/actions/wrapper-validation@v6
- name: Check out sources
uses: actions/checkout@v6

- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v6
38 changes: 37 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.detekt)
alias(libs.plugins.hilt)
jacoco
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
Expand All @@ -19,6 +20,10 @@ java {
}
}

val unitTestJavaLauncher = javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(21))
}

detekt {
basePath.set(rootDir)
buildUponDefaultConfig = true
Expand Down Expand Up @@ -78,6 +83,14 @@ android {
res.srcDir("../res")
}

sourceSets.getByName("test") {
kotlin.srcDir("src/sharedTest/kotlin")
}

sourceSets.getByName("androidTest") {
kotlin.srcDir("src/sharedTest/kotlin")
}

val keystorePropertiesFile = rootProject.file("keystore.properties")
val useKeystoreProperties = keystorePropertiesFile.canRead()
val keystoreProperties = Properties()
Expand All @@ -104,7 +117,7 @@ android {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"../proguard.flags",
"../proguard-release.flags"
"../proguard-release.flags",
)

if (useKeystoreProperties) {
Expand All @@ -115,6 +128,8 @@ android {
getByName("debug") {
applicationIdSuffix = ".debug"
resValue("string", "app_name", "Messaging d")
enableUnitTestCoverage = true
enableAndroidTestCoverage = true
}

create("perf") {
Expand All @@ -126,6 +141,23 @@ android {
}
}

testCoverage {
jacocoVersion = libs.versions.jacoco.get()
}

testOptions {
unitTests {
isIncludeAndroidResources = true
all { unitTest ->
unitTest.javaLauncher.set(unitTestJavaLauncher)
unitTest.extensions.configure(JacocoTaskExtension::class.java) {
isIncludeNoLocationClasses = true
excludes = listOf("jdk.internal.*")
}
}
}
}

lint {
abortOnError = false
}
Expand Down Expand Up @@ -188,6 +220,8 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.compose.ui.tooling)

testImplementation(platform(libs.androidx.compose.bom))
testImplementation(libs.androidx.compose.ui.test.junit4)
testImplementation(libs.junit4)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.mockk)
Expand All @@ -212,3 +246,5 @@ dependencies {
androidTestImplementation(libs.mockk.android)
androidTestImplementation(libs.turbine)
}

apply(from = "jacoco.gradle.kts")
15 changes: 15 additions & 0 deletions app/jacoco-rules/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# JaCoCo Rules

Human-maintained coverage policy lives in these text files. The Gradle script
loads them at configuration time and fails fast on missing files, duplicate
rules, backslash separators, or source/class-pattern mixups.

Format:

- One rule per line.
- Blank lines and lines starting with `#` are ignored.
- `*-class-patterns.txt` files use Gradle `fileTree` patterns against compiled class directories.
- `*-sources.txt` files are paths relative to `src/com/android/messaging/ui`.

The script still owns generated synthetic-class patterns and source-to-class pattern derivation.
Keep only human-maintained coverage policy here.
19 changes: 19 additions & 0 deletions app/jacoco-rules/common/excluded-class-patterns.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Common class-directory exclude patterns for unit and instrumented JaCoCo tracks.

# Dependency injection generated classes.
**/Dagger*
**/Hilt_*
**/_Factory*
**/_GeneratedInjector*
**/_HiltModules*
**/_MembersInjector*

# Compose and data-holder noise.
**/*ComposableSingletons*
**/model/**

# Kotlin compiler-generated classes.
**/*$DefaultImpls*
**/*$invokeSuspend$*
**/*$$serializer.class
**/*$$inlined$*
12 changes: 12 additions & 0 deletions app/jacoco-rules/instrumented/excluded-class-patterns.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Instrumented-test-only class-directory exclude patterns.
**/ui/conversation/mediapicker/ConversationCaptureMode.class
**/ui/conversation/mediapicker/ConversationMediaPickerSavedState.class
**/ui/conversation/mediapicker/ConversationMediaPickerSavedState$*.class
**/ui/conversation/mediapicker/ConversationMediaPickerState.class
**/ui/conversation/mediapicker/ConversationMediaPickerState$*.class
**/ui/conversation/mediapicker/camera/ConversationCameraController.class
**/ui/conversation/mediapicker/camera/ConversationCameraControllerImpl*.class
**/ui/conversation/navigation/ConversationNavRouteState.class
**/ui/conversation/navigation/ConversationPendingLaunchPayload.class
**/ui/conversation/screen/ConversationSimSheetState.class
**/ui/conversation/screen/ConversationSimSheetState$*.class
5 changes: 5 additions & 0 deletions app/jacoco-rules/instrumented/included-class-patterns.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Instrumented-test class-directory include patterns.
com/android/messaging/ui/appsettings/**
com/android/messaging/ui/contact/**
com/android/messaging/ui/conversation/**
com/android/messaging/ui/core/**
2 changes: 2 additions & 0 deletions app/jacoco-rules/instrumented/measured-composable-sources.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Composable source files intentionally measured by the instrumented coverage track.
# Currently empty; composables measured by the unit track are excluded from this track.
Loading
Loading