-
Notifications
You must be signed in to change notification settings - Fork 75
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add end-to-end emulator test running on CI
- Loading branch information
1 parent
eaf4e6d
commit ec99944
Showing
18 changed files
with
505 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,7 +31,6 @@ else | |
sleep 1 | ||
fi | ||
|
||
echo "Starting emulator..." | ||
$SCRIPT_DIR/start_emulator.sh "$EMULATOR_NAME" | ||
sleep 3 | ||
|
||
|
35 changes: 35 additions & 0 deletions
35
app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestApp.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
app/src/androidTest/java/com/stevesoltys/seedvault/KoinInstrumentationTestRunner.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) | ||
} | ||
} |
139 changes: 139 additions & 0 deletions
139
app/src/androidTest/java/com/stevesoltys/seedvault/e2e/BackupRestoreTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}" | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.