Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions .github/scripts/run-ui-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash

set -euo pipefail

adb wait-for-device
echo "Waiting for boot to complete..."
adb shell 'while [ -z "$(getprop sys.boot_completed)" ]; do sleep 1; done'
sleep 10

echo "Emulator ABI:"
adb shell getprop ro.product.cpu.abi

./gradlew installDevDebug

suites="${ANDROID_TEST_SUITES:-all}"
suites="$(echo "$suites" | tr -d '[:space:]')"

if [[ -z "$suites" || "$suites" == "all" ]]; then
./gradlew connectedDevDebugAndroidTest
exit 0
fi

IFS=',' read -ra requested_suites <<< "$suites"
for suite in "${requested_suites[@]}"; do
if [[ "$suite" == *.* || ! "$suite" =~ ^[A-Z][A-Za-z0-9]*$ ]]; then
echo "::error::Invalid Android test annotation '$suite'. Use all or comma-separated simple annotation names such as ComposeUi."
exit 1
fi

./gradlew connectedDevDebugAndroidTest -PbitkitAndroidTestAnnotation="$suite"
done
23 changes: 9 additions & 14 deletions .github/workflows/ui-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ on:
branches: [ "master" ]

workflow_dispatch:
inputs:
suites:
description: "Android test annotations: all or comma-separated simple annotation names"
required: false
default: "all"
type: string

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand Down Expand Up @@ -81,27 +87,16 @@ jobs:

- name: Run UI tests on Android Emulator
uses: reactivecircus/android-emulator-runner@v2
env:
ANDROID_TEST_SUITES: ${{ github.event.inputs.suites || 'all' }}
with:
api-level: 30
arch: x86_64
profile: pixel_4
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: |
# Wait for emulator to be fully ready
adb wait-for-device
echo "Waiting for boot to complete..."
adb shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done'
sleep 10

# Verify emulator ABI matches app
echo "Emulator ABI:"
adb shell getprop ro.product.cpu.abi

# Install and run tests
./gradlew installDevDebug
./gradlew connectedDevDebugAndroidTest
script: bash .github/scripts/run-ui-tests.sh

- name: Upload UI test report
if: always()
Expand Down
98 changes: 98 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,93 @@ val e2eBackendEnv = System.getenv("E2E_BACKEND") ?: "local"
val e2eHomegateUrlEnv = System.getenv("E2E_HOMEGATE_URL") ?: "http://127.0.0.1:6288"
val trezorBridgeEnv = System.getenv("TREZOR_BRIDGE")?.toBoolean()?.toString() ?: "false"
val trezorBridgeUrlEnv = System.getenv("TREZOR_BRIDGE_URL") ?: "http://10.0.2.2:21325"
val androidTestAnnotationPackage = "to.bitkit.test.annotations"
val androidTestTaskPrefix = "connectedDevDebug"
val androidTestTaskSuffix = "AndroidTest"
val baseAndroidTestTaskName = "$androidTestTaskPrefix$androidTestTaskSuffix"
val androidTestAnnotationNames = file("src/androidTest/java/to/bitkit/test/annotations")
.listFiles()
?.mapNotNull { file ->
file.nameWithoutExtension.takeIf {
file.isFile &&
file.extension == "kt"
}
}
?.sorted()
.orEmpty()
val requestedTaskNames = gradle.startParameter.taskNames.map { it.substringAfterLast(":") }

fun androidTestTaskName(annotationName: String): String {
return "$androidTestTaskPrefix$annotationName$androidTestTaskSuffix"
}

fun isTaskNameAbbreviation(taskName: String, fullTaskName: String): Boolean {
if (taskName.isEmpty() || taskName == fullTaskName) return false
return fullTaskName.startsWith(taskName) ||
taskName.any { it.isUpperCase() } &&
taskName.isSubsequenceOf(fullTaskName)
}

fun String.isSubsequenceOf(value: String): Boolean {
var searchIndex = 0
for (char in this) {
searchIndex = value.indexOf(char, startIndex = searchIndex)
if (searchIndex == -1) return false
searchIndex++
}
return true
}

val androidTestTaskNames = androidTestAnnotationNames.map { androidTestTaskName(it) }
val abbreviatedAndroidTestTaskNames = requestedTaskNames.filter { taskName ->
taskName != baseAndroidTestTaskName &&
taskName !in androidTestTaskNames &&
androidTestTaskNames.any { isTaskNameAbbreviation(taskName, it) }
Comment on lines +92 to +94
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude base-task abbreviations from lane rejection

Gradle explicitly supports task-name abbreviation (including camel-case patterns like mAL:cT), but this guard checks every requested task against lane task names and rejects any subsequence match. As a result, abbreviations for the full suite task (for example cDDAT for connectedDevDebugAndroidTest) are misclassified as lane abbreviations and fail with “Use full generated Android test lane task names,” even when no lane was requested. That introduces a regression for valid Gradle CLI usage and can break local/CI scripts that rely on abbreviations.

Useful? React with 👍 / 👎.

}
require(abbreviatedAndroidTestTaskNames.isEmpty()) {
"Use full generated Android test lane task names. Abbreviated lane tasks are unsupported: " +
abbreviatedAndroidTestTaskNames.joinToString(", ")
}
val requestedAndroidTestAnnotationTaskNames = requestedTaskNames.filter { taskName ->
taskName in androidTestTaskNames
}
require(baseAndroidTestTaskName !in requestedTaskNames || requestedAndroidTestAnnotationTaskNames.isEmpty()) {
"Do not combine '$baseAndroidTestTaskName' with generated Android test lane tasks. Requested lanes: " +
requestedAndroidTestAnnotationTaskNames.joinToString(", ")
}
require(requestedAndroidTestAnnotationTaskNames.size <= 1) {
"Run only one generated Android test lane per Gradle invocation. Requested lanes: " +
requestedAndroidTestAnnotationTaskNames.joinToString(", ")
}
val requestedAndroidTestAnnotationTask = requestedAndroidTestAnnotationTaskNames.singleOrNull()
val requestedAndroidTestAnnotationTaskName = requestedAndroidTestAnnotationTask?.let { taskName ->
androidTestAnnotationNames.first { androidTestTaskName(it) == taskName }
}
val requestedAndroidTestAnnotation = providers.gradleProperty("bitkitAndroidTestAnnotation")
.orNull
?.trim()
?.takeIf { it.isNotEmpty() }
?.also {
require('.' !in it) {
"Use a simple Android test annotation name, e.g. 'ComposeUi'."
}
require(it in androidTestAnnotationNames) {
"Unsupported bitkitAndroidTestAnnotation '$it'. Supported annotations: " +
androidTestAnnotationNames.joinToString(", ")
}
}
requestedAndroidTestAnnotationTaskName?.let { annotationName ->
requestedAndroidTestAnnotation?.let {
require(it == annotationName) {
"Do not combine bitkitAndroidTestAnnotation '$it' with generated lane '$annotationName'."
}
}
}
val bitkitAndroidTestAnnotationName = requestedAndroidTestAnnotation
?: requestedAndroidTestAnnotationTaskName
Comment thread
ovitrif marked this conversation as resolved.
val bitkitAndroidTestAnnotation = bitkitAndroidTestAnnotationName?.let {
"$androidTestAnnotationPackage.$it"
}

android {
namespace = "to.bitkit"
Expand All @@ -61,6 +148,9 @@ android {
versionCode = 181
versionName = "2.2.0"
testInstrumentationRunner = "to.bitkit.test.HiltTestRunner"
bitkitAndroidTestAnnotation?.let {
testInstrumentationRunnerArguments["annotation"] = it
}
vectorDrawables {
useSupportLibrary = true
}
Expand Down Expand Up @@ -367,4 +457,12 @@ tasks.withType<Test>().configureEach {
jvmArgs("-XX:+EnableDynamicAgentLoading")
}

androidTestAnnotationNames.forEach { annotationName ->
tasks.register(androidTestTaskName(annotationName)) {
group = "verification"
description = "Runs devDebug Android tests annotated with '$annotationName'."
dependsOn("connectedDevDebugAndroidTest")
}
}

// endregion
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@ import org.junit.runner.RunWith
import to.bitkit.data.AppDb
import to.bitkit.data.entities.ConfigEntity
import to.bitkit.test.BaseAndroidTest
import to.bitkit.test.annotations.DeviceIntegration
import to.bitkit.test.annotations.DeviceStorageIntegration
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
import kotlin.test.assertTrue

@RunWith(AndroidJUnit4::class)
@DeviceIntegration
@DeviceStorageIntegration
class KeychainTest : BaseAndroidTest() {

private val appContext by lazy { ApplicationProvider.getApplicationContext<Context>() }
Expand Down
4 changes: 4 additions & 0 deletions app/src/androidTest/java/to/bitkit/services/BlocktankTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import to.bitkit.env.Env
import to.bitkit.test.annotations.CoreServiceIntegration
import to.bitkit.test.annotations.DeviceIntegration
import javax.inject.Inject
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

@HiltAndroidTest
@DeviceIntegration
@CoreServiceIntegration
class BlocktankTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.lightningdevkit.ldknode.Network
import to.bitkit.models.toDerivationPath
import to.bitkit.test.annotations.CoreServiceIntegration
import to.bitkit.test.annotations.DeviceIntegration
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

@RunWith(AndroidJUnit4::class)
@DeviceIntegration
@CoreServiceIntegration
class OnchainServiceTests {
private lateinit var onchainService: OnchainService

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import to.bitkit.data.CacheStore
import to.bitkit.data.keychain.Keychain
import to.bitkit.env.Env
import to.bitkit.repositories.WalletRepo
import to.bitkit.test.annotations.CoreServiceIntegration
import to.bitkit.test.annotations.DeviceIntegration
import to.bitkit.utils.LdkError
import javax.inject.Inject
import kotlin.test.assertEquals
Expand All @@ -27,6 +29,8 @@ import kotlin.test.assertTrue

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
@DeviceIntegration
@CoreServiceIntegration
class RoutingFeeEstimationTest {

companion object {
Expand Down
4 changes: 4 additions & 0 deletions app/src/androidTest/java/to/bitkit/services/TxBumpingTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import org.junit.runner.RunWith
import to.bitkit.data.keychain.Keychain
import to.bitkit.env.Env
import to.bitkit.repositories.WalletRepo
import to.bitkit.test.annotations.CoreServiceIntegration
import to.bitkit.test.annotations.DeviceIntegration
import javax.inject.Inject
import kotlin.test.assertEquals
import kotlin.test.assertFalse
Expand All @@ -23,6 +25,8 @@ import kotlin.test.assertTrue

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
@DeviceIntegration
@CoreServiceIntegration
class TxBumpingTests {

@get:Rule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import org.lightningdevkit.ldknode.CoinSelectionAlgorithm
import to.bitkit.data.keychain.Keychain
import to.bitkit.env.Env
import to.bitkit.repositories.WalletRepo
import to.bitkit.test.annotations.CoreServiceIntegration
import to.bitkit.test.annotations.DeviceIntegration
import javax.inject.Inject
import kotlin.test.assertEquals
import kotlin.test.assertFalse
Expand All @@ -25,6 +27,8 @@ import kotlin.test.fail

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
@DeviceIntegration
@CoreServiceIntegration
class UtxoSelectionTests {

@get:Rule
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package to.bitkit.test.annotations

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class ComposeUi
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package to.bitkit.test.annotations

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class CoreServiceIntegration
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package to.bitkit.test.annotations

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class DeviceIntegration
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package to.bitkit.test.annotations

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class DeviceStorageIntegration
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import androidx.compose.ui.test.performClick
import org.junit.Rule
import org.junit.Test
import to.bitkit.models.NodeLifecycleState
import to.bitkit.test.annotations.ComposeUi
import to.bitkit.viewmodels.SendMethod
import to.bitkit.viewmodels.SendUiState
import to.bitkit.viewmodels.previewAmountInputViewModel

@ComposeUi
class SendAmountContentTest {

@get:Rule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import org.junit.Rule
import org.junit.Test
import to.bitkit.test.annotations.ComposeUi
import to.bitkit.ui.theme.AppThemeSurface

@ComposeUi
class BlockCardTest {

@get:Rule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import org.junit.Rule
import org.junit.Test
import to.bitkit.test.annotations.ComposeUi
import to.bitkit.ui.theme.AppThemeSurface

@ComposeUi
class FactsCardTest {

@get:Rule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import org.junit.Rule
import org.junit.Test
import to.bitkit.test.annotations.ComposeUi
import to.bitkit.ui.theme.AppThemeSurface

@ComposeUi
class FactsPreviewContentTest {

@get:Rule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import org.junit.Rule
import org.junit.Test
import to.bitkit.test.annotations.ComposeUi
import to.bitkit.ui.theme.AppThemeSurface

@ComposeUi
class HeadlineCardTest {

@get:Rule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import org.junit.Rule
import org.junit.Test
import to.bitkit.models.widget.ArticleModel
import to.bitkit.models.widget.HeadlinePreferences
import to.bitkit.test.annotations.ComposeUi
import to.bitkit.ui.theme.AppThemeSurface

@ComposeUi
class HeadlinesEditContentTest {

@get:Rule
Expand Down
Loading
Loading