Skip to content

Commit

Permalink
Add end-to-end emulator test running on CI
Browse files Browse the repository at this point in the history
  • Loading branch information
stevesoltys committed Sep 12, 2023
1 parent eaf4e6d commit 5c9b76b
Show file tree
Hide file tree
Showing 15 changed files with 442 additions and 21 deletions.
28 changes: 28 additions & 0 deletions .cirrus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
instrumented_task_test:
name: Run Android emulator tests
timeout_in: 30m
only_if: $CIRRUS_BRANCH == "main" || CIRRUS_PR != ""
skip: "!changesInclude('.cirrus.yml', '*.gradle', '*.gradle.kts', '**/*.gradle', '**/*.gradle.kts', '*.properties', '**/*.properties', '**/*.kt', '**/*.xml')"
env:
JAVA_TOOL_OPTIONS: -Xmx6g
GRADLE_OPTS: -Dorg.gradle.daemon=false -Dkotlin.incremental=false -Dkotlin.compiler.execution.strategy=in-process
container:
image: reactivecircus/android-emulator-33:latest
kvm: true
cpu: 8
memory: 24G

create_device_script:
./gradlew :app:provisionEmulator
start_emulator_background_script:
$ANDROID_HOME/emulator/emulator -avd "seedvault" -no-window -gpu swiftshader_indirect -writable-system -no-snapshot -noaudio -no-boot-anim -camera-back none
assemble_instrumented_tests_script:
./gradlew assembleDebugAndroidTest
wait_for_emulator_script:
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 3; done; input keyevent 82'
disable_animations_script: |
adb shell settings put global window_animation_scale 0.0
adb shell settings put global transition_animation_scale 0.0
adb shell settings put global animator_duration_scale 0.0
run_instrumented_tests_script:
./gradlew connectedAndroidTest
6 changes: 3 additions & 3 deletions .idea/runConfigurations/app_emulator.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 16 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ android {
minSdk 32 // leave at 32 for robolectric tests
targetSdk rootProject.ext.targetSdk
versionNameSuffix "-$gitDescribe"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunner "com.stevesoltys.seedvault.KoinInstrumentationTestRunner"
testInstrumentationRunnerArguments disableAnalytics: 'true'
testInstrumentationRunnerArgument 'size', 'large'
}

buildTypes {
Expand Down Expand Up @@ -150,10 +151,13 @@ dependencies {
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit5_version"
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$junit5_version"

androidTestImplementation rootProject.ext.aosp_libs
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation "io.mockk:mockk-android:$mockk_version"

androidTestImplementation 'com.kaspersky.android-components:kaspresso:1.5.3'
}

apply from: "${rootProject.rootDir}/gradle/ktlint.gradle"
Expand Down Expand Up @@ -210,3 +214,14 @@ tasks.register('installEmulatorRelease', Exec) {
environment "JAVA_HOME", System.properties['java.home']
}
}

tasks.register('clearEmulatorAppData', Exec) {
group("emulator")

doFirst {
commandLine "${project.projectDir}/development/scripts/clear_app_data.sh"

environment "ANDROID_SDK_HOME", android.sdkDirectory.absolutePath
environment "JAVA_HOME", System.properties['java.home']
}
}
22 changes: 22 additions & 0 deletions app/development/scripts/clear_app_data.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash

# assert ANDROID_HOME is set
if [ -z "$ANDROID_SDK_HOME" ]; then
echo "ANDROID_SDK_HOME is not set"
exit 1
fi

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
DEVELOPMENT_DIR=$SCRIPT_DIR/..
ROOT_PROJECT_DIR=$SCRIPT_DIR/../../..

EMULATOR_DEVICE_NAME=$($ANDROID_SDK_HOME/platform-tools/adb devices | grep emulator | cut -f1)

if [ -z "$EMULATOR_DEVICE_NAME" ]; then
echo "Emulator device name not found"
exit 1
fi

ADB="$ANDROID_SDK_HOME/platform-tools/adb -s $EMULATOR_DEVICE_NAME"

$ADB shell pm clear com.stevesoltys.seedvault
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.stevesoltys.seedvault

import androidx.test.platform.app.InstrumentationRegistry
import com.stevesoltys.seedvault.restore.RestoreViewModel
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.spyk
import org.koin.core.module.Module
import org.koin.dsl.module

private val spyBackupNotificationManager = spyk(
BackupNotificationManager(
InstrumentationRegistry.getInstrumentation()
.targetContext.applicationContext
)
)

class KoinInstrumentationTestApp : App() {

override fun appModules(): List<Module> {
val testModule = module {
single { spyBackupNotificationManager }

single {
spyk(
RestoreViewModel(
this@KoinInstrumentationTestApp,
get(), get(), get(), get(), get(), get()
)
)
}
}

return super.appModules().plus(testModule)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.stevesoltys.seedvault

import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner

class KoinInstrumentationTestRunner : AndroidJUnitRunner() {

override fun newApplication(
classLoader: ClassLoader?,
className: String?,
context: Context?,
): Application {
return super.newApplication(
classLoader,
KoinInstrumentationTestApp::class.java.name,
context
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package com.stevesoltys.seedvault.e2e

import androidx.test.filters.LargeTest
import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen
import com.stevesoltys.seedvault.restore.RestoreViewModel
import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.every
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.junit.Test
import org.koin.core.component.inject
import java.lang.Thread.sleep

@LargeTest
class BackupRestoreTest : LargeTestBase() {

private val packageService: PackageService by inject()

private val spyBackupNotificationManager: BackupNotificationManager by inject()

private val restoreViewModel: RestoreViewModel by inject()

companion object {
private const val BACKUP_TIMEOUT = 360 * 1000L
}

@Test
fun `back up and restore applications`() = run {
launchBackupActivity()
verifyCode()
waitUntilIdle()

chooseBackupLocation()

val eligiblePackages = launchAllEligibleApps()
performBackup(eligiblePackages)
uninstallAllApps(eligiblePackages)
performRestore()

val packagesAfterRestore = getEligibleApps()
assert(eligiblePackages == packagesAfterRestore)
}

private fun getEligibleApps() = packageService.userApps
.map { it.packageName }.toSet()

private fun launchAllEligibleApps(): Set<String> {
return getEligibleApps().onEach {
val intent = device.targetContext.packageManager.getLaunchIntentForPackage(it)

device.targetContext.startActivity(intent)
waitUntilIdle()
}
}

private fun uninstallAllApps(packages: Set<String>) {
packages.forEach { runCommand("pm uninstall $it") }
}

private fun performBackup(expectedPackages: Set<String>) = run {
var finishedBackup = false

every {
spyBackupNotificationManager.onBackupFinished(any(), any())
} answers {
val success = firstArg<Boolean>()
assert(success) { "Backup failed." }

val packageCount = secondArg<Int>()
assert(packageCount == expectedPackages.size) {
"Expected ${expectedPackages.size} apps, got $packageCount."
}

this.callOriginal()
finishedBackup = true
}

step("Start backup and await completion") {
startBackup()

runBlocking {
withTimeout(BACKUP_TIMEOUT) {
while (!finishedBackup) {
delay(100)
}
}
}
}
}

private fun performRestore() = run {
step("Start restore and await completion") {
RestoreScreen {
startRestore()
waitForInstallResult()
sleep(5000)

nextButton.click()
waitForRestoreResult()
sleep(5000)

finishButton.click()
}
}
}

private fun waitForInstallResult() = runBlocking {
withTimeout(BACKUP_TIMEOUT) {

while (restoreViewModel.installResult.value == null) {
delay(100)
}

val restoreResultValue = restoreViewModel.installResult.value!!
assert(!restoreResultValue.hasFailed) { "Failed to install packages" }
}
}

private fun waitForRestoreResult() = runBlocking {
withTimeout(BACKUP_TIMEOUT) {

while (restoreViewModel.restoreBackupResult.value == null) {
delay(100)
}

val restoreResultValue = restoreViewModel.restoreBackupResult.value!!

assert(!restoreResultValue.hasError()) {
"Restore failed: ${restoreResultValue.errorMsg}"
}
}
}
}
Loading

0 comments on commit 5c9b76b

Please sign in to comment.