Skip to content

Commit 395e16f

Browse files
authored
Merge pull request #943 from synonymdev/test/ai-dev-tests-suite-setup
test: add android test lanes
2 parents 6a61dfe + cb4b76b commit 395e16f

28 files changed

Lines changed: 216 additions & 14 deletions

.github/scripts/run-ui-tests.sh

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
adb wait-for-device
6+
echo "Waiting for boot to complete..."
7+
adb shell 'while [ -z "$(getprop sys.boot_completed)" ]; do sleep 1; done'
8+
sleep 10
9+
10+
echo "Emulator ABI:"
11+
adb shell getprop ro.product.cpu.abi
12+
13+
./gradlew installDevDebug
14+
15+
suites="${ANDROID_TEST_SUITES:-all}"
16+
suites="$(echo "$suites" | tr -d '[:space:]')"
17+
18+
if [[ -z "$suites" || "$suites" == "all" ]]; then
19+
./gradlew connectedDevDebugAndroidTest
20+
exit 0
21+
fi
22+
23+
IFS=',' read -ra requested_suites <<< "$suites"
24+
for suite in "${requested_suites[@]}"; do
25+
if [[ "$suite" == *.* || ! "$suite" =~ ^[A-Z][A-Za-z0-9]*$ ]]; then
26+
echo "::error::Invalid Android test annotation '$suite'. Use all or comma-separated simple annotation names such as ComposeUi."
27+
exit 1
28+
fi
29+
30+
./gradlew connectedDevDebugAndroidTest -PbitkitAndroidTestAnnotation="$suite"
31+
done

.github/workflows/ui-tests.yml

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ on:
55
branches: [ "master" ]
66

77
workflow_dispatch:
8+
inputs:
9+
suites:
10+
description: "Android test annotations: all or comma-separated simple annotation names"
11+
required: false
12+
default: "all"
13+
type: string
814

915
concurrency:
1016
group: ${{ github.workflow }}-${{ github.ref }}
@@ -81,27 +87,16 @@ jobs:
8187

8288
- name: Run UI tests on Android Emulator
8389
uses: reactivecircus/android-emulator-runner@v2
90+
env:
91+
ANDROID_TEST_SUITES: ${{ github.event.inputs.suites || 'all' }}
8492
with:
8593
api-level: 30
8694
arch: x86_64
8795
profile: pixel_4
8896
force-avd-creation: false
8997
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
9098
disable-animations: true
91-
script: |
92-
# Wait for emulator to be fully ready
93-
adb wait-for-device
94-
echo "Waiting for boot to complete..."
95-
adb shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done'
96-
sleep 10
97-
98-
# Verify emulator ABI matches app
99-
echo "Emulator ABI:"
100-
adb shell getprop ro.product.cpu.abi
101-
102-
# Install and run tests
103-
./gradlew installDevDebug
104-
./gradlew connectedDevDebugAndroidTest
99+
script: bash .github/scripts/run-ui-tests.sh
105100

106101
- name: Upload UI test report
107102
if: always()

app/build.gradle.kts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,97 @@ val e2eBackendEnv = System.getenv("E2E_BACKEND") ?: "local"
5050
val e2eHomegateUrlEnv = System.getenv("E2E_HOMEGATE_URL") ?: "http://127.0.0.1:6288"
5151
val trezorBridgeEnv = System.getenv("TREZOR_BRIDGE")?.toBoolean()?.toString() ?: "false"
5252
val trezorBridgeUrlEnv = System.getenv("TREZOR_BRIDGE_URL") ?: "http://10.0.2.2:21325"
53+
val androidTestAnnotationPackage = "to.bitkit.test.annotations"
54+
val androidTestTaskPrefix = "connectedDevDebug"
55+
val androidTestTaskSuffix = "AndroidTest"
56+
val baseAndroidTestTaskName = "$androidTestTaskPrefix$androidTestTaskSuffix"
57+
val androidTestAnnotationNames = file("src/androidTest/java/to/bitkit/test/annotations")
58+
.listFiles()
59+
?.mapNotNull { file ->
60+
file.nameWithoutExtension.takeIf {
61+
file.isFile &&
62+
file.extension == "kt"
63+
}
64+
}
65+
?.sorted()
66+
.orEmpty()
67+
val requestedTaskNames = gradle.startParameter.taskNames.map { it.substringAfterLast(":") }
68+
69+
fun androidTestTaskName(annotationName: String): String {
70+
return "$androidTestTaskPrefix$annotationName$androidTestTaskSuffix"
71+
}
72+
73+
fun isTaskNameAbbreviation(taskName: String, fullTaskName: String): Boolean {
74+
if (taskName.isEmpty() || taskName == fullTaskName) return false
75+
return fullTaskName.startsWith(taskName) ||
76+
taskName.any { it.isUpperCase() } &&
77+
taskName.isSubsequenceOf(fullTaskName)
78+
}
79+
80+
fun String.isSubsequenceOf(value: String): Boolean {
81+
var searchIndex = 0
82+
for (char in this) {
83+
searchIndex = value.indexOf(char, startIndex = searchIndex)
84+
if (searchIndex == -1) return false
85+
searchIndex++
86+
}
87+
return true
88+
}
89+
90+
val androidTestTaskNames = androidTestAnnotationNames.map { androidTestTaskName(it) }
91+
val requestedBaseAndroidTestTaskNames = requestedTaskNames.filter { taskName ->
92+
taskName == baseAndroidTestTaskName ||
93+
isTaskNameAbbreviation(taskName, baseAndroidTestTaskName)
94+
}
95+
val abbreviatedAndroidTestTaskNames = requestedTaskNames.filter { taskName ->
96+
taskName !in requestedBaseAndroidTestTaskNames &&
97+
taskName !in androidTestTaskNames &&
98+
androidTestTaskNames.any { isTaskNameAbbreviation(taskName, it) }
99+
}
100+
require(abbreviatedAndroidTestTaskNames.isEmpty()) {
101+
"Use full generated Android test lane task names. Abbreviated lane tasks are unsupported: " +
102+
abbreviatedAndroidTestTaskNames.joinToString(", ")
103+
}
104+
val requestedAndroidTestAnnotationTaskNames = requestedTaskNames.filter { taskName ->
105+
taskName in androidTestTaskNames
106+
}
107+
require(requestedBaseAndroidTestTaskNames.isEmpty() || requestedAndroidTestAnnotationTaskNames.isEmpty()) {
108+
"Do not combine '$baseAndroidTestTaskName' with generated Android test lane tasks. Requested lanes: " +
109+
requestedAndroidTestAnnotationTaskNames.joinToString(", ")
110+
}
111+
require(requestedAndroidTestAnnotationTaskNames.size <= 1) {
112+
"Run only one generated Android test lane per Gradle invocation. Requested lanes: " +
113+
requestedAndroidTestAnnotationTaskNames.joinToString(", ")
114+
}
115+
val requestedAndroidTestAnnotationTask = requestedAndroidTestAnnotationTaskNames.singleOrNull()
116+
val requestedAndroidTestAnnotationTaskName = requestedAndroidTestAnnotationTask?.let { taskName ->
117+
androidTestAnnotationNames.first { androidTestTaskName(it) == taskName }
118+
}
119+
val requestedAndroidTestAnnotation = providers.gradleProperty("bitkitAndroidTestAnnotation")
120+
.orNull
121+
?.trim()
122+
?.takeIf { it.isNotEmpty() }
123+
?.also {
124+
require('.' !in it) {
125+
"Use a simple Android test annotation name, e.g. 'ComposeUi'."
126+
}
127+
require(it in androidTestAnnotationNames) {
128+
"Unsupported bitkitAndroidTestAnnotation '$it'. Supported annotations: " +
129+
androidTestAnnotationNames.joinToString(", ")
130+
}
131+
}
132+
requestedAndroidTestAnnotationTaskName?.let { annotationName ->
133+
requestedAndroidTestAnnotation?.let {
134+
require(it == annotationName) {
135+
"Do not combine bitkitAndroidTestAnnotation '$it' with generated lane '$annotationName'."
136+
}
137+
}
138+
}
139+
val bitkitAndroidTestAnnotationName = requestedAndroidTestAnnotation
140+
?: requestedAndroidTestAnnotationTaskName
141+
val bitkitAndroidTestAnnotation = bitkitAndroidTestAnnotationName?.let {
142+
"$androidTestAnnotationPackage.$it"
143+
}
53144

54145
android {
55146
namespace = "to.bitkit"
@@ -61,6 +152,9 @@ android {
61152
versionCode = 181
62153
versionName = "2.2.0"
63154
testInstrumentationRunner = "to.bitkit.test.HiltTestRunner"
155+
bitkitAndroidTestAnnotation?.let {
156+
testInstrumentationRunnerArguments["annotation"] = it
157+
}
64158
vectorDrawables {
65159
useSupportLibrary = true
66160
}
@@ -367,4 +461,12 @@ tasks.withType<Test>().configureEach {
367461
jvmArgs("-XX:+EnableDynamicAgentLoading")
368462
}
369463

464+
androidTestAnnotationNames.forEach { annotationName ->
465+
tasks.register(androidTestTaskName(annotationName)) {
466+
group = "verification"
467+
description = "Runs devDebug Android tests annotated with '$annotationName'."
468+
dependsOn("connectedDevDebugAndroidTest")
469+
}
470+
}
471+
370472
// endregion

app/src/androidTest/java/to/bitkit/data/keychain/KeychainTest.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@ import org.junit.runner.RunWith
1414
import to.bitkit.data.AppDb
1515
import to.bitkit.data.entities.ConfigEntity
1616
import to.bitkit.test.BaseAndroidTest
17+
import to.bitkit.test.annotations.DeviceIntegration
18+
import to.bitkit.test.annotations.DeviceStorageIntegration
1719
import kotlin.test.assertEquals
1820
import kotlin.test.assertFailsWith
1921
import kotlin.test.assertNull
2022
import kotlin.test.assertTrue
2123

2224
@RunWith(AndroidJUnit4::class)
25+
@DeviceIntegration
26+
@DeviceStorageIntegration
2327
class KeychainTest : BaseAndroidTest() {
2428

2529
private val appContext by lazy { ApplicationProvider.getApplicationContext<Context>() }

app/src/androidTest/java/to/bitkit/services/BlocktankTest.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@ import org.junit.Before
1515
import org.junit.Rule
1616
import org.junit.Test
1717
import to.bitkit.env.Env
18+
import to.bitkit.test.annotations.CoreServiceIntegration
19+
import to.bitkit.test.annotations.DeviceIntegration
1820
import javax.inject.Inject
1921
import kotlin.test.assertEquals
2022
import kotlin.test.assertNotEquals
2123
import kotlin.test.assertNotNull
2224
import kotlin.test.assertTrue
2325

2426
@HiltAndroidTest
27+
@DeviceIntegration
28+
@CoreServiceIntegration
2529
class BlocktankTest {
2630
@get:Rule
2731
var hiltRule = HiltAndroidRule(this)

app/src/androidTest/java/to/bitkit/services/OnchainServiceTests.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ import org.junit.Test
88
import org.junit.runner.RunWith
99
import org.lightningdevkit.ldknode.Network
1010
import to.bitkit.models.toDerivationPath
11+
import to.bitkit.test.annotations.CoreServiceIntegration
12+
import to.bitkit.test.annotations.DeviceIntegration
1113
import kotlin.test.assertEquals
1214
import kotlin.test.assertNotNull
1315
import kotlin.test.assertTrue
1416

1517
@RunWith(AndroidJUnit4::class)
18+
@DeviceIntegration
19+
@CoreServiceIntegration
1620
class OnchainServiceTests {
1721
private lateinit var onchainService: OnchainService
1822

app/src/androidTest/java/to/bitkit/services/RoutingFeeEstimationTest.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import to.bitkit.data.CacheStore
1818
import to.bitkit.data.keychain.Keychain
1919
import to.bitkit.env.Env
2020
import to.bitkit.repositories.WalletRepo
21+
import to.bitkit.test.annotations.CoreServiceIntegration
22+
import to.bitkit.test.annotations.DeviceIntegration
2123
import to.bitkit.utils.LdkError
2224
import javax.inject.Inject
2325
import kotlin.test.assertEquals
@@ -27,6 +29,8 @@ import kotlin.test.assertTrue
2729

2830
@HiltAndroidTest
2931
@RunWith(AndroidJUnit4::class)
32+
@DeviceIntegration
33+
@CoreServiceIntegration
3034
class RoutingFeeEstimationTest {
3135

3236
companion object {

app/src/androidTest/java/to/bitkit/services/TxBumpingTests.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import org.junit.runner.RunWith
1515
import to.bitkit.data.keychain.Keychain
1616
import to.bitkit.env.Env
1717
import to.bitkit.repositories.WalletRepo
18+
import to.bitkit.test.annotations.CoreServiceIntegration
19+
import to.bitkit.test.annotations.DeviceIntegration
1820
import javax.inject.Inject
1921
import kotlin.test.assertEquals
2022
import kotlin.test.assertFalse
@@ -23,6 +25,8 @@ import kotlin.test.assertTrue
2325

2426
@HiltAndroidTest
2527
@RunWith(AndroidJUnit4::class)
28+
@DeviceIntegration
29+
@CoreServiceIntegration
2630
class TxBumpingTests {
2731

2832
@get:Rule

app/src/androidTest/java/to/bitkit/services/UtxoSelectionTests.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import org.lightningdevkit.ldknode.CoinSelectionAlgorithm
1616
import to.bitkit.data.keychain.Keychain
1717
import to.bitkit.env.Env
1818
import to.bitkit.repositories.WalletRepo
19+
import to.bitkit.test.annotations.CoreServiceIntegration
20+
import to.bitkit.test.annotations.DeviceIntegration
1921
import javax.inject.Inject
2022
import kotlin.test.assertEquals
2123
import kotlin.test.assertFalse
@@ -25,6 +27,8 @@ import kotlin.test.fail
2527

2628
@HiltAndroidTest
2729
@RunWith(AndroidJUnit4::class)
30+
@DeviceIntegration
31+
@CoreServiceIntegration
2832
class UtxoSelectionTests {
2933

3034
@get:Rule
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package to.bitkit.test.annotations
2+
3+
@Retention(AnnotationRetention.RUNTIME)
4+
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
5+
annotation class ComposeUi

0 commit comments

Comments
 (0)