chore: Run basic integration test on each commit/PR

This commit is contained in:
Artem Chepurnyi 2024-12-10 11:52:36 +02:00
parent 4d377ce833
commit b9b7360686
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
13 changed files with 309 additions and 45 deletions

53
.github/workflows/check_tests.yaml vendored Normal file
View File

@ -0,0 +1,53 @@
name: "✔️ Check Tests"
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
check-android-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: "Set up JDK 17"
id: setup-java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: "Enable KVM group perms"
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: "Prepare env"
run: |
echo ${{ secrets.KEYSTORE_B64 }} | base64 -d | zcat >> androidApp/keyguard-release.keystore
echo ${{ secrets.KEYSTORE_PROPS_B64 }} | base64 -d | zcat >> androidApp/keyguard-release.properties
echo ${{ secrets.GOOGLE_SERVICES }} | base64 -d | zcat >> androidApp/google-services.json
echo ${{ secrets.SERVICE_ACCOUNT_B64 }} | base64 -d | zcat >> service-account-google.json
python .github/setup_gradle_properties_release.py tag=${{ github.ref_name }}
- name: "Setup Gradle"
uses: gradle/actions/setup-gradle@v4
env:
JAVA_HOME: ${{ steps.setup-java.outputs.path }}
- name: "Check and Build licenses"
run: ./gradlew :androidApp:licenseeAndroidPlayStoreRelease
- name: "Move licenses"
run: |
mv -f androidApp/build/reports/licensee/androidPlayStoreRelease/artifacts.json common/src/commonMain/composeResources/files/licenses.json
- name: "Run tests"
id: baseline-profiles
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 30
arch: x86_64
disable-animations: true
disk-size: 4G
script: |
adb root
./gradlew :androidApp:connectedPlayStoreDebugAndroidTest

View File

@ -52,6 +52,11 @@ android {
versionName = versionInfo.marketingVersion
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// The following argument makes the Android Test Orchestrator run its
// "pm clear" command after each test invocation. This command ensures
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments["clearPackageData"] = "true"
vectorDrawables {
useSupportLibrary = true
}
@ -67,6 +72,10 @@ android {
isCoreLibraryDesugaringEnabled = true
}
testOptions {
execution = "ANDROIDX_TEST_ORCHESTRATOR"
}
androidResources {
@Suppress("UnstableApiUsage")
generateLocaleConfig = true
@ -149,6 +158,20 @@ dependencies {
implementation(project(":common"))
baselineProfile(project(":androidBenchmark"))
coreLibraryDesugaring(libs.android.desugarjdklibs)
// Android tests
androidTestImplementation(project(":androidTest"))
androidTestImplementation(libs.androidx.arch.core.testing)
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.test.rules)
androidTestImplementation(libs.androidx.test.core)
androidTestImplementation(libs.androidx.test.core.ktx)
androidTestImplementation(libs.androidx.test.uiautomator)
androidTestImplementation(libs.androidx.test.espresso.core)
androidTestImplementation(libs.androidx.test.espresso.web)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.ext.junit.ktx)
androidTestUtil(libs.androidx.test.orchestrator)
}
kotlin {

View File

@ -0,0 +1,21 @@
package com.artemchep.keyguard.test
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.junit.Before
abstract class BaseTest {
lateinit var device: UiDevice
lateinit var context: Context
@Before
fun init() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
device = UiDevice.getInstance(instrumentation)
context = ApplicationProvider.getApplicationContext<Context>()
}
}

View File

@ -0,0 +1,17 @@
package com.artemchep.keyguard.test
import com.artemchep.keyguard.BuildConfig
/**
* Convenience parameter to use proper package name
* with regards to build type and build flavor.
*/
val PACKAGE_NAME = StringBuilder("com.artemchep.keyguard").apply {
val hasSuffix = when (BuildConfig.BUILD_TYPE) {
"debug" -> true
else -> false
}
if (hasSuffix) {
append(".${BuildConfig.BUILD_TYPE}")
}
}.toString()

View File

@ -0,0 +1,22 @@
package com.artemchep.keyguard.test.core
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.artemchep.keyguard.test.BaseTest
import com.artemchep.keyguard.test.PACKAGE_NAME
import com.artemchep.test.feature.coreFeature
import com.artemchep.test.feature.ensureMainScreen
import com.artemchep.test.feature.launchDefaultActivityAndWait
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class CreateVaultTest : BaseTest() {
@Test
@Throws(Exception::class)
fun createVaultTest() {
device.coreFeature.launchDefaultActivityAndWait(PACKAGE_NAME)
device.coreFeature.ensureMainScreen()
}
}

View File

@ -52,9 +52,10 @@ baselineProfile {
}
dependencies {
implementation(project(":androidTest"))
implementation(libs.androidx.benchmark.macro.junit4)
implementation(libs.androidx.espresso.core)
implementation(libs.androidx.junit)
implementation(libs.androidx.uiautomator)
implementation(libs.androidx.test.espresso.core)
implementation(libs.androidx.test.ext.junit)
implementation(libs.androidx.test.uiautomator)
implementation(libs.androidx.profileinstaller)
}

View File

@ -4,7 +4,8 @@ import android.os.Build
import androidx.annotation.RequiresApi
import androidx.benchmark.macro.junit4.BaselineProfileRule
import com.artemchep.macrobenchmark.PACKAGE_NAME
import com.artemchep.macrobenchmark.ui.keyguard.createVaultAndWait
import com.artemchep.test.feature.coreFeature
import com.artemchep.test.feature.ensureMainScreen
import org.junit.Rule
import org.junit.Test
@ -29,6 +30,6 @@ class BaselineProfileGenerator {
pressHome()
startActivityAndWait()
createVaultAndWait()
device.coreFeature.ensureMainScreen()
}
}

View File

@ -1,36 +0,0 @@
package com.artemchep.macrobenchmark.ui.keyguard
import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import java.util.regex.Pattern
fun MacrobenchmarkScope.waitForMainScreen() = kotlin.run {
val search = kotlin.run {
val pattern = Pattern.compile("nav:(setup|unlock|main)")
val selector = By.res(pattern)
Until.findObject(selector)
}
device.wait(search, 30_000)
}
fun MacrobenchmarkScope.createVaultAndWait() = kotlin.run {
val screen = waitForMainScreen()
when (screen.resourceName) {
"nav:setup",
"nav:unlock",
-> {
// Create or unlock existing vault
val password = screen
.findObject(By.res("field:password"))
password.text = "111111"
val btn = screen
.findObject(By.res("btn:go"))
btn.wait(Until.clickable(true), 1000L)
btn.click()
// wait till main screen is loaded
device.wait(Until.findObject(By.res("nav:main")), 30_000)
}
else -> screen
}
}

View File

@ -0,0 +1,41 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}
android {
compileSdk = libs.versions.androidCompileSdk.get().toInt()
namespace = "com.artemchep.test"
defaultConfig {
minSdk = libs.versions.androidMinSdk.get().toInt()
targetSdk = libs.versions.androidTargetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
val accountManagementDimension = "accountManagement"
flavorDimensions += accountManagementDimension
productFlavors {
maybeCreate("playStore").apply {
dimension = accountManagementDimension
}
maybeCreate("none").apply {
dimension = accountManagementDimension
}
}
}
dependencies {
implementation(libs.androidx.test.espresso.core)
implementation(libs.androidx.test.ext.junit)
implementation(libs.androidx.test.uiautomator)
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest></manifest>

View File

@ -0,0 +1,104 @@
package com.artemchep.test.feature
import android.content.Context
import android.content.Intent
import androidx.test.core.app.ApplicationProvider
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
import java.util.regex.Pattern
@JvmInline
value class FeatureCore(
val device: UiDevice,
)
val UiDevice.coreFeature get() = FeatureCore(this)
enum class RootScreen(
val res: String,
) {
SETUP("setup"),
UNLOCK("unlock"),
MAIN("main");
companion object {
const val RES_PREFIX = "nav:"
}
}
fun RootScreen.resourceName() = RootScreen.RES_PREFIX + res
fun FeatureCore.waitForRootScreen(
vararg screens: RootScreen,
): UiObject2? = kotlin.run {
require(screens.isNotEmpty()) {
"You must provide at least one screen to wait for!"
}
val selector = kotlin.run {
val variants = screens
.joinToString(separator = "|") { it.res }
val pattern = Pattern.compile("nav:($variants)")
val selector = By.res(pattern)
Until.findObject(selector)
}
device.wait(selector, 30_000)
}
fun FeatureCore.ensureMainScreen(): UiObject2 = kotlin.run {
val screen = waitForRootScreen(
RootScreen.MAIN,
RootScreen.UNLOCK,
RootScreen.SETUP,
)
requireNotNull(screen) {
"Could not find root screen! " +
"Is the app really open?"
}
when (screen.resourceName) {
RootScreen.UNLOCK.resourceName(),
RootScreen.SETUP.resourceName(),
-> {
// Create or unlock existing vault
val password = screen
.findObject(By.res("field:password"))
password.text = "111111"
val btn = screen
.findObject(By.res("btn:go"))
btn.wait(Until.clickable(true), 3000L)
btn.click()
// Wait till main screen is loaded
val mainScreen = waitForRootScreen(
RootScreen.MAIN,
)
requireNotNull(mainScreen)
}
else -> screen
}
}
/**
* Starts the default activity of the app, waiting for
* it to become visible.
*/
fun FeatureCore.launchDefaultActivityAndWait(
packageName: String,
) {
// Launch the app
val context = ApplicationProvider.getApplicationContext<Context>()
val intent = context.packageManager.getLaunchIntentForPackage(packageName)?.apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
context.startActivity(intent)
// Wait for the app to appear
device.wait(
Until.hasObject(By.pkg(packageName).depth(0)),
10_000,
)
}

View File

@ -36,8 +36,13 @@ androidxProfileInstaller = "1.4.1"
androidxRoom = "2.6.1"
androidxSecurityCryptoKtx = "1.1.0-alpha06"
androidxTestEspresso = "3.6.1"
androidxTestExtJUnit = "1.2.1"
coreTestingVersion = "2.2.0"
androidxTestCore = "1.6.1"
androidxTestUiAutomator = "2.3.0"
androidxTestRunner = "1.6.2"
androidxTestRule = "1.6.1"
androidxTestJUnit = "1.2.1"
androidxTestOrchestrator = "1.5.1"
androidxWork = "2.10.0"
# https://github.com/harawata/appdirs
appDirs = "1.2.2"
@ -163,8 +168,6 @@ androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", versi
androidx-core-shortcuts = { module = "androidx.core:core-google-shortcuts", version.ref = "androidxCoreShortcuts" }
androidx-credentials = { module = "androidx.credentials:credentials", version.ref = "androidxCredentials" }
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "androidxDatastore" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidxTestEspresso" }
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidxTestExtJUnit" }
androidx-lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "androidxLifecycle" }
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "androidxLifecycle" }
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
@ -177,9 +180,20 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref =
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidxRoom" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidxRoom" }
androidx-security-crypto-ktx = { module = "androidx.security:security-crypto-ktx", version.ref = "androidxSecurityCryptoKtx" }
androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androidxTestUiAutomator" }
androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "androidxWork" }
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidxWork" }
androidx-arch-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "coreTestingVersion" }
androidx-test-core = { module = "androidx.test:core", version.ref = "androidxTestCore" }
androidx-test-core-ktx = { module = "androidx.test:core-ktx", version.ref = "androidxTestCore" }
androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidxTestEspresso" }
androidx-test-espresso-uiautomator = { module = "androidx.test.espresso:espresso-uiautomator", version.ref = "androidxTestEspresso" }
androidx-test-espresso-web = { module = "androidx.test.espresso:espresso-web", version.ref = "androidxTestEspresso" }
androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidxTestJUnit" }
androidx-test-ext-junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "androidxTestJUnit" }
androidx-test-orchestrator = { module = "androidx.test:orchestrator", version.ref = "androidxTestOrchestrator" }
androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidxTestRule" }
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxTestRunner" }
androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androidxTestUiAutomator" }
arrow-arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" }
arrow-arrow-optics = { module = "io.arrow-kt:arrow-optics", version.ref = "arrow" }
arrow-arrow-optics-ksp-plugin = { module = "io.arrow-kt:arrow-optics-ksp-plugin", version.ref = "arrow" }

View File

@ -27,5 +27,6 @@ include ':common'
// apps
include ':androidApp'
include ':androidBenchmark'
include ':androidTest'
include ':desktopApp'