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 ec99944
Show file tree
Hide file tree
Showing 18 changed files with 505 additions and 22 deletions.
49 changes: 49 additions & 0 deletions .cirrus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
container:
image: ghcr.io/cirruslabs/android-sdk:33
kvm: true
cpu: 8
memory: 16G

check_android_task:
skip: "!changesInclude('.cirrus.yml', '*.gradle', '*.gradle.kts', '**/*.gradle', '**/*.gradle.kts', '*.properties', '**/*.properties', '**/*.kt', '**/*.xml')"
create_avd_script:
sdkmanager --install "system-images;android-33;google_apis;x86_64";
echo no | avdmanager create avd -n seedvault -k "system-images;android-33;google_apis;x86_64"
start_avd_background_script:
$ANDROID_HOME/emulator/emulator
-avd seedvault
-no-audio
-no-boot-anim
-gpu swiftshader_indirect
-no-snapshot
-no-window
-writable-system;
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';
adb root;
sleep 5;
adb remount;
adb reboot;
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';
adb root;
sleep 5;
adb remount;
wget --output-document etar.apk https://f-droid.org/repo/ws.xsoh.etar_35.apk;
adb install etar.apk
assemble_release_script:
./gradlew assembleRelease
provision_script:
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';
adb shell mkdir -p /system/priv-app/Seedvault;
adb push app/build/outputs/apk/release/app-release.apk /system/priv-app/Seedvault/Seedvault.apk;
adb push permissions_com.stevesoltys.seedvault.xml /system/etc/permissions/privapp-permissions-seedvault.xml;
adb push allowlist_com.stevesoltys.seedvault.xml /system/etc/sysconfig/allowlist-seedvault.xml;
adb shell bmgr enable true;
adb shell bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport;
adb reboot;
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';
check_script: ./gradlew :app:connectedAndroidTest
always:
pull_screenshots_script:
adb pull /sdcard/Documents/screenshots
screenshots_artifacts:
path: "screenshots/**/*.png"
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
1 change: 0 additions & 1 deletion app/development/scripts/provision_emulator.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ else
sleep 1
fi

echo "Starting emulator..."
$SCRIPT_DIR/start_emulator.sh "$EMULATOR_NAME"
sleep 3

Expand Down
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,139 @@
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.util.concurrent.atomic.AtomicBoolean

@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
private const val RESTORE_TIMEOUT = 360 * 1000L
}

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

val eligiblePackages = launchAllEligibleApps()
performBackup(eligiblePackages)
uninstallPackages(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 performBackup(expectedPackages: Set<String>) = run {
val backupResult = spyOnBackup(expectedPackages)
startBackup()
waitForBackupResult(backupResult)
screenshot("backup result")
}

private fun spyOnBackup(expectedPackages: Set<String>): AtomicBoolean {
val finishedBackup = AtomicBoolean(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.set(true)
}

return finishedBackup
}

private fun waitForBackupResult(finishedBackup: AtomicBoolean) = run {
step("Wait for backup completion") {
runBlocking {
withTimeout(BACKUP_TIMEOUT) {
while (!finishedBackup.get()) {
delay(100)
}
}
}
}
}

private fun performRestore() = run {
step("Start restore and await completion") {
RestoreScreen {
startRestore()
waitForInstallResult()
screenshot("restore app apks result")

nextButton.click()
waitForRestoreResult()
screenshot("restore app data result")

finishButton.click()
}
}
}

private fun waitForInstallResult() = runBlocking {
withTimeout(RESTORE_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(RESTORE_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 ec99944

Please sign in to comment.