Merge pull request #748 from seedvault-app/feature/cirrus-ci-tests
Run tests in Cirrus CI again
This commit is contained in:
commit
0fa14025b3
24 changed files with 234 additions and 154 deletions
77
.cirrus.yml
77
.cirrus.yml
|
@ -1,13 +1,66 @@
|
||||||
task:
|
container:
|
||||||
name: Build with AOSP
|
image: ghcr.io/cirruslabs/android-sdk:34
|
||||||
only_if: $CIRRUS_PR_LABELS =~ ".*aosp-build.*"
|
kvm: true
|
||||||
timeout_in: 70m
|
cpu: 8
|
||||||
container:
|
memory: 16G
|
||||||
image: ubuntu:23.04
|
|
||||||
cpu: 8
|
instrumentation_tests_task:
|
||||||
memory: 32G
|
name: "Cirrus CI Instrumentation Tests"
|
||||||
build_script:
|
start_avd_background_script:
|
||||||
- ./.github/scripts/build_aosp.sh aosp_arm64 ap1a userdebug android-14.0.0_r29
|
sdkmanager --install "system-images;android-34;default;x86_64" "emulator";
|
||||||
|
echo no | avdmanager create avd -n seedvault -k "system-images;android-34;default;x86_64";
|
||||||
|
$ANDROID_HOME/emulator/emulator
|
||||||
|
-avd seedvault
|
||||||
|
-no-audio
|
||||||
|
-no-boot-anim
|
||||||
|
-gpu swiftshader_indirect
|
||||||
|
-no-snapshot
|
||||||
|
-no-window
|
||||||
|
-writable-system;
|
||||||
|
provision_avd_background_script:
|
||||||
|
wget https://github.com/seedvault-app/seedvault-test-data/releases/download/3/backup.tar.gz;
|
||||||
|
|
||||||
|
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;
|
||||||
|
sleep 5;
|
||||||
|
assemble_script:
|
||||||
|
./gradlew :app:assembleRelease :contacts:assembleRelease assembleAndroidTest
|
||||||
|
install_app_script:
|
||||||
|
timeout 180s bash -c 'while [[ -z $(adb shell mount | grep "/system " | grep "(rw,") ]]; do sleep 1; done;';
|
||||||
|
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';
|
||||||
|
|
||||||
|
adb shell mkdir -p /sdcard/seedvault_baseline;
|
||||||
|
adb push backup.tar.gz /sdcard/seedvault_baseline/backup.tar.gz;
|
||||||
|
adb shell tar xzf /sdcard/seedvault_baseline/backup.tar.gz --directory=/sdcard/seedvault_baseline;
|
||||||
|
|
||||||
|
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 mkdir -p /system/priv-app/ContactsBackup;
|
||||||
|
adb push contactsbackup/build/outputs/apk/release/contactsbackup-release.apk /system/priv-app/ContactsBackup/contactsbackup.apk;
|
||||||
|
adb push contactsbackup/default-permissions_org.calyxos.backup.contacts.xml /system/etc/default-permissions/default-permissions_org.calyxos.backup.contacts.xml;
|
||||||
|
|
||||||
|
adb shell bmgr enable true;
|
||||||
|
adb reboot;
|
||||||
|
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;';
|
||||||
|
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;';
|
||||||
|
run_large_tests_script: ./gradlew -Pandroid.testInstrumentationRunnerArguments.size=large :app:connectedAndroidTest
|
||||||
|
run_other_tests_script: ./gradlew -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest connectedAndroidTest
|
||||||
always:
|
always:
|
||||||
seedvault_artifacts:
|
pull_screenshots_script:
|
||||||
path: Seedvault.apk
|
adb pull /sdcard/seedvault_test_results
|
||||||
|
screenshots_artifacts:
|
||||||
|
path: "seedvault_test_results/**/*.mp4"
|
||||||
|
logcat_artifacts:
|
||||||
|
path: "seedvault_test_results/**/*.log"
|
||||||
|
|
|
@ -32,13 +32,6 @@ android {
|
||||||
versionNameSuffix = "-${gitDescribe()}"
|
versionNameSuffix = "-${gitDescribe()}"
|
||||||
testInstrumentationRunner = "com.stevesoltys.seedvault.KoinInstrumentationTestRunner"
|
testInstrumentationRunner = "com.stevesoltys.seedvault.KoinInstrumentationTestRunner"
|
||||||
testInstrumentationRunnerArguments["disableAnalytics"] = "true"
|
testInstrumentationRunnerArguments["disableAnalytics"] = "true"
|
||||||
|
|
||||||
if (project.hasProperty("instrumented_test_size")) {
|
|
||||||
val testSize = project.property("instrumented_test_size").toString()
|
|
||||||
println("Instrumented test size: $testSize")
|
|
||||||
|
|
||||||
testInstrumentationRunnerArguments["size"] = testSize
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|
|
@ -9,7 +9,6 @@ import android.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import androidx.test.uiautomator.Until
|
import androidx.test.uiautomator.Until
|
||||||
import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept
|
import com.stevesoltys.seedvault.e2e.io.BackupDataInputIntercept
|
||||||
import com.stevesoltys.seedvault.e2e.io.InputStreamIntercept
|
|
||||||
import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen
|
import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen
|
||||||
import com.stevesoltys.seedvault.transport.backup.FullBackup
|
import com.stevesoltys.seedvault.transport.backup.FullBackup
|
||||||
import com.stevesoltys.seedvault.transport.backup.InputFactory
|
import com.stevesoltys.seedvault.transport.backup.InputFactory
|
||||||
|
@ -21,8 +20,10 @@ import io.mockk.every
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import org.calyxos.seedvault.core.toHexString
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
import java.io.ByteArrayOutputStream
|
import java.security.DigestInputStream
|
||||||
|
import java.security.MessageDigest
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.test.fail
|
import kotlin.test.fail
|
||||||
|
|
||||||
|
@ -154,7 +155,8 @@ internal interface LargeBackupTestBase : LargeTestBase {
|
||||||
|
|
||||||
private fun spyOnFullBackupData(backupResult: SeedvaultLargeTestResult) {
|
private fun spyOnFullBackupData(backupResult: SeedvaultLargeTestResult) {
|
||||||
var packageName: String? = null
|
var packageName: String? = null
|
||||||
var dataIntercept = ByteArrayOutputStream()
|
val messageDigest = MessageDigest.getInstance("SHA-256")
|
||||||
|
var digestInputStream: DigestInputStream? = null
|
||||||
|
|
||||||
coEvery {
|
coEvery {
|
||||||
spyFullBackup.performFullBackup(any(), any(), any())
|
spyFullBackup.performFullBackup(any(), any(), any())
|
||||||
|
@ -166,20 +168,19 @@ internal interface LargeBackupTestBase : LargeTestBase {
|
||||||
every {
|
every {
|
||||||
spyInputFactory.getInputStream(any())
|
spyInputFactory.getInputStream(any())
|
||||||
} answers {
|
} answers {
|
||||||
InputStreamIntercept(
|
digestInputStream = DigestInputStream(callOriginal(), messageDigest)
|
||||||
inputStream = callOriginal(),
|
digestInputStream!!
|
||||||
intercept = dataIntercept
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
coEvery {
|
coEvery {
|
||||||
spyFullBackup.finishBackup()
|
spyFullBackup.finishBackup()
|
||||||
} answers {
|
} answers {
|
||||||
val result = callOriginal()
|
val result = callOriginal()
|
||||||
backupResult.full[packageName!!] = dataIntercept.toByteArray().sha256()
|
val digest = digestInputStream?.messageDigest ?: fail("No digestInputStream")
|
||||||
|
backupResult.full[packageName!!] = digest.digest().toHexString()
|
||||||
|
|
||||||
packageName = null
|
packageName = null
|
||||||
dataIntercept = ByteArrayOutputStream()
|
digest.reset()
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -192,9 +193,6 @@ internal interface LargeBackupTestBase : LargeTestBase {
|
||||||
every {
|
every {
|
||||||
spyBackupNotificationManager.onBackupSuccess(any(), any(), any())
|
spyBackupNotificationManager.onBackupSuccess(any(), any(), any())
|
||||||
} answers {
|
} answers {
|
||||||
val success = firstArg<Boolean>()
|
|
||||||
assert(success) { "Backup failed." }
|
|
||||||
|
|
||||||
callOriginal()
|
callOriginal()
|
||||||
completed.set(true)
|
completed.set(true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,13 @@ package com.stevesoltys.seedvault.e2e
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import com.stevesoltys.seedvault.e2e.io.BackupDataOutputIntercept
|
import com.stevesoltys.seedvault.e2e.io.BackupDataOutputIntercept
|
||||||
import com.stevesoltys.seedvault.e2e.io.OutputStreamIntercept
|
|
||||||
import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen
|
import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen
|
||||||
import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen
|
import com.stevesoltys.seedvault.e2e.screen.impl.RestoreScreen
|
||||||
import com.stevesoltys.seedvault.transport.restore.FullRestore
|
import com.stevesoltys.seedvault.transport.restore.FullRestore
|
||||||
import com.stevesoltys.seedvault.transport.restore.KVRestore
|
import com.stevesoltys.seedvault.transport.restore.KVRestore
|
||||||
import com.stevesoltys.seedvault.transport.restore.OutputFactory
|
import com.stevesoltys.seedvault.transport.restore.OutputFactory
|
||||||
|
import io.mockk.Call
|
||||||
|
import io.mockk.MockKAnswerScope
|
||||||
import io.mockk.clearMocks
|
import io.mockk.clearMocks
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
@ -22,8 +23,11 @@ import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import org.calyxos.seedvault.core.toHexString
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
import java.io.ByteArrayOutputStream
|
import java.security.DigestOutputStream
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import kotlin.test.fail
|
||||||
|
|
||||||
internal interface LargeRestoreTestBase : LargeTestBase {
|
internal interface LargeRestoreTestBase : LargeTestBase {
|
||||||
|
|
||||||
|
@ -161,14 +165,26 @@ internal interface LargeRestoreTestBase : LargeTestBase {
|
||||||
|
|
||||||
clearMocks(spyKVRestore)
|
clearMocks(spyKVRestore)
|
||||||
|
|
||||||
coEvery {
|
fun initializeStateBlock(
|
||||||
spyKVRestore.initializeState(any(), any(), any(), any())
|
packageInfoIndex: Int
|
||||||
} answers {
|
): MockKAnswerScope<Unit, Unit>.(Call) -> Unit = {
|
||||||
packageName = arg<PackageInfo>(3).packageName
|
packageName = arg<PackageInfo>(packageInfoIndex).packageName
|
||||||
restoreResult.kv[packageName!!] = mutableMapOf()
|
restoreResult.kv[packageName!!] = mutableMapOf()
|
||||||
callOriginal()
|
callOriginal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
coEvery {
|
||||||
|
spyKVRestore.initializeState(any(), any(), any(), any())
|
||||||
|
} answers initializeStateBlock(1)
|
||||||
|
|
||||||
|
coEvery {
|
||||||
|
spyKVRestore.initializeStateV1(any(), any(), any(), any())
|
||||||
|
} answers initializeStateBlock(2)
|
||||||
|
|
||||||
|
coEvery {
|
||||||
|
spyKVRestore.initializeStateV0(any(), any())
|
||||||
|
} answers initializeStateBlock(1)
|
||||||
|
|
||||||
every {
|
every {
|
||||||
spyOutputFactory.getBackupDataOutput(any())
|
spyOutputFactory.getBackupDataOutput(any())
|
||||||
} answers {
|
} answers {
|
||||||
|
@ -182,47 +198,61 @@ internal interface LargeRestoreTestBase : LargeTestBase {
|
||||||
|
|
||||||
private fun spyOnFullRestoreData(restoreResult: SeedvaultLargeTestResult) {
|
private fun spyOnFullRestoreData(restoreResult: SeedvaultLargeTestResult) {
|
||||||
var packageName: String? = null
|
var packageName: String? = null
|
||||||
var dataIntercept = ByteArrayOutputStream()
|
val messageDigest = MessageDigest.getInstance("SHA-256")
|
||||||
|
var digestOutputStream: DigestOutputStream? = null
|
||||||
|
|
||||||
clearMocks(spyFullRestore)
|
clearMocks(spyFullRestore)
|
||||||
|
|
||||||
coEvery {
|
fun initializeStateBlock(
|
||||||
spyFullRestore.initializeState(any(), any(), any())
|
packageInfoIndex: Int
|
||||||
} answers {
|
): MockKAnswerScope<Unit, Unit>.(Call) -> Unit = {
|
||||||
packageName?.let {
|
packageName?.let {
|
||||||
restoreResult.full[it] = dataIntercept.toByteArray().sha256()
|
// sometimes finishRestore() doesn't get called, so get data from last package here
|
||||||
|
digestOutputStream?.messageDigest?.let { digest ->
|
||||||
|
restoreResult.full[packageName!!] = digest.digest().toHexString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packageName = arg<PackageInfo>(3).packageName
|
packageName = arg<PackageInfo>(packageInfoIndex).packageName
|
||||||
dataIntercept = ByteArrayOutputStream()
|
|
||||||
|
|
||||||
callOriginal()
|
callOriginal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
coEvery {
|
||||||
|
spyFullRestore.initializeState(any(), any(), any())
|
||||||
|
} answers initializeStateBlock(1)
|
||||||
|
|
||||||
|
coEvery {
|
||||||
|
spyFullRestore.initializeStateV1(any(), any(), any())
|
||||||
|
} answers initializeStateBlock(2)
|
||||||
|
|
||||||
|
coEvery {
|
||||||
|
spyFullRestore.initializeStateV0(any(), any())
|
||||||
|
} answers initializeStateBlock(1)
|
||||||
|
|
||||||
every {
|
every {
|
||||||
spyOutputFactory.getOutputStream(any())
|
spyOutputFactory.getOutputStream(any())
|
||||||
} answers {
|
} answers {
|
||||||
OutputStreamIntercept(
|
digestOutputStream = DigestOutputStream(callOriginal(), messageDigest)
|
||||||
outputStream = callOriginal(),
|
digestOutputStream!!
|
||||||
intercept = dataIntercept
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
every {
|
every {
|
||||||
spyFullRestore.abortFullRestore()
|
spyFullRestore.abortFullRestore()
|
||||||
} answers {
|
} answers {
|
||||||
packageName = null
|
packageName = null
|
||||||
dataIntercept = ByteArrayOutputStream()
|
digestOutputStream?.messageDigest?.reset()
|
||||||
callOriginal()
|
callOriginal()
|
||||||
}
|
}
|
||||||
|
|
||||||
every {
|
every {
|
||||||
spyFullRestore.finishRestore()
|
spyFullRestore.finishRestore()
|
||||||
} answers {
|
} answers {
|
||||||
restoreResult.full[packageName!!] = dataIntercept.toByteArray().sha256()
|
val digest = digestOutputStream?.messageDigest ?: fail("No digestOutputStream")
|
||||||
|
restoreResult.full[packageName!!] = digest.digest().toHexString()
|
||||||
|
|
||||||
packageName = null
|
packageName = null
|
||||||
dataIntercept = ByteArrayOutputStream()
|
digest.reset()
|
||||||
callOriginal()
|
callOriginal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,14 +49,14 @@ internal interface LargeTestBase : KoinComponent {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TEST_STORAGE_FOLDER = "seedvault_test"
|
private const val TEST_STORAGE_FOLDER = "seedvault_test"
|
||||||
private const val TEST_VIDEO_FOLDER = "seedvault_test_results"
|
private const val TEST_RESULT_FOLDER = "seedvault_test_results"
|
||||||
}
|
}
|
||||||
|
|
||||||
val externalStorageDir: String get() = Environment.getExternalStorageDirectory().absolutePath
|
val externalStorageDir: String get() = Environment.getExternalStorageDirectory().absolutePath
|
||||||
|
|
||||||
val testStoragePath get() = "$externalStorageDir/$TEST_STORAGE_FOLDER"
|
val testStoragePath get() = "$externalStorageDir/$TEST_STORAGE_FOLDER"
|
||||||
|
|
||||||
val testVideoPath get() = "$externalStorageDir/$TEST_VIDEO_FOLDER"
|
val testResultPath get() = "$externalStorageDir/$TEST_RESULT_FOLDER"
|
||||||
|
|
||||||
val targetContext: Context
|
val targetContext: Context
|
||||||
get() = InstrumentationRegistry.getInstrumentation().targetContext
|
get() = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
@ -123,7 +123,7 @@ internal interface LargeTestBase : KoinComponent {
|
||||||
keepRecordingScreen: AtomicBoolean,
|
keepRecordingScreen: AtomicBoolean,
|
||||||
testName: String,
|
testName: String,
|
||||||
) {
|
) {
|
||||||
val folder = testVideoPath
|
val folder = testResultPath
|
||||||
runCommand("mkdir -p $folder")
|
runCommand("mkdir -p $folder")
|
||||||
|
|
||||||
val fileName = testResultFilename(testName)
|
val fileName = testResultFilename(testName)
|
||||||
|
@ -149,7 +149,7 @@ internal interface LargeTestBase : KoinComponent {
|
||||||
|
|
||||||
// write logcat to file
|
// write logcat to file
|
||||||
val fileName = testResultFilename(testName)
|
val fileName = testResultFilename(testName)
|
||||||
runCommand("logcat -d -f $testVideoPath/$fileName.log")
|
runCommand("logcat -d -f $testResultPath/$fileName.log")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun uninstallPackages(packages: Collection<PackageInfo>) {
|
fun uninstallPackages(packages: Collection<PackageInfo>) {
|
||||||
|
@ -162,7 +162,7 @@ internal interface LargeTestBase : KoinComponent {
|
||||||
|
|
||||||
fun clearTestBackups() {
|
fun clearTestBackups() {
|
||||||
File(testStoragePath).deleteRecursively()
|
File(testStoragePath).deleteRecursively()
|
||||||
File(testVideoPath).deleteRecursively()
|
File(testResultPath).deleteRecursively()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeBackupLocation(
|
fun changeBackupLocation(
|
||||||
|
@ -225,6 +225,7 @@ internal interface LargeTestBase : KoinComponent {
|
||||||
|
|
||||||
fun confirmCode() {
|
fun confirmCode() {
|
||||||
RecoveryCodeScreen {
|
RecoveryCodeScreen {
|
||||||
|
startNewBackupButton.click()
|
||||||
confirmCodeButton.click()
|
confirmCodeButton.click()
|
||||||
|
|
||||||
verifyCodeButton.scrollTo().click()
|
verifyCodeButton.scrollTo().click()
|
||||||
|
|
|
@ -45,9 +45,8 @@ internal abstract class SeedvaultLargeTest :
|
||||||
clearTestBackups()
|
clearTestBackups()
|
||||||
|
|
||||||
runCommand("bmgr enable true")
|
runCommand("bmgr enable true")
|
||||||
sleep(60_000)
|
|
||||||
runCommand("bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport")
|
runCommand("bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport")
|
||||||
sleep(60_000)
|
sleep(5000)
|
||||||
|
|
||||||
startRecordingTest(keepRecordingScreen, name.methodName)
|
startRecordingTest(keepRecordingScreen, name.methodName)
|
||||||
restoreBaselineBackup()
|
restoreBaselineBackup()
|
||||||
|
|
|
@ -5,11 +5,14 @@
|
||||||
|
|
||||||
package com.stevesoltys.seedvault.e2e.impl
|
package com.stevesoltys.seedvault.e2e.impl
|
||||||
|
|
||||||
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
import android.util.Log
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.e2e.SeedvaultLargeTest
|
import com.stevesoltys.seedvault.e2e.SeedvaultLargeTest
|
||||||
import com.stevesoltys.seedvault.e2e.SeedvaultLargeTestResult
|
import com.stevesoltys.seedvault.e2e.SeedvaultLargeTestResult
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState
|
import com.stevesoltys.seedvault.metadata.PackageState
|
||||||
|
import com.stevesoltys.seedvault.transport.backup.isStopped
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
|
@ -29,6 +32,9 @@ internal class BackupRestoreTest : SeedvaultLargeTest() {
|
||||||
changeBackupLocation()
|
changeBackupLocation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
launchStoppedApps()
|
||||||
|
launchBackupActivity()
|
||||||
|
|
||||||
val backupResult = performBackup()
|
val backupResult = performBackup()
|
||||||
assertValidBackupMetadata(backupResult)
|
assertValidBackupMetadata(backupResult)
|
||||||
|
|
||||||
|
@ -58,6 +64,28 @@ internal class BackupRestoreTest : SeedvaultLargeTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun launchStoppedApps() {
|
||||||
|
val packageManager = targetContext.packageManager
|
||||||
|
val notBackedUp = packageService.notBackedUpPackages
|
||||||
|
notBackedUp.forEach { packageInfo ->
|
||||||
|
val i = packageManager.getLaunchIntentForPackage(packageInfo.packageName)?.apply {
|
||||||
|
addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
Log.i("TEST", "Launching $i")
|
||||||
|
try {
|
||||||
|
targetContext.startActivity(i)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("TEST", "Could not launch activity for ${packageInfo.packageName}", e)
|
||||||
|
}
|
||||||
|
waitUntilIdle()
|
||||||
|
}
|
||||||
|
waitUntilIdle()
|
||||||
|
notBackedUp.forEach { packageInfo ->
|
||||||
|
val pi = packageManager.getPackageInfo(packageInfo.packageName, 0)
|
||||||
|
Log.e("TEST", "${packageInfo.packageName} isStopped: ${pi.isStopped()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun assertValidResults(
|
private fun assertValidResults(
|
||||||
backup: SeedvaultLargeTestResult,
|
backup: SeedvaultLargeTestResult,
|
||||||
restore: SeedvaultLargeTestResult,
|
restore: SeedvaultLargeTestResult,
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2023 The Calyx Institute
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.stevesoltys.seedvault.e2e.io
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
class InputStreamIntercept(
|
|
||||||
private val inputStream: InputStream,
|
|
||||||
private val intercept: ByteArrayOutputStream
|
|
||||||
) : InputStream() {
|
|
||||||
|
|
||||||
override fun read(): Int {
|
|
||||||
val byte = inputStream.read()
|
|
||||||
if (byte != -1) {
|
|
||||||
intercept.write(byte)
|
|
||||||
}
|
|
||||||
return byte
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
|
|
||||||
val bytesRead = inputStream.read(buffer, offset, length)
|
|
||||||
if (bytesRead != -1) {
|
|
||||||
intercept.write(buffer, offset, bytesRead)
|
|
||||||
}
|
|
||||||
return bytesRead
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2023 The Calyx Institute
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.stevesoltys.seedvault.e2e.io
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
class OutputStreamIntercept(
|
|
||||||
private val outputStream: OutputStream,
|
|
||||||
private val intercept: ByteArrayOutputStream
|
|
||||||
) : OutputStream() {
|
|
||||||
|
|
||||||
override fun write(byte: Int) {
|
|
||||||
intercept.write(byte)
|
|
||||||
outputStream.write(byte)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun write(buffer: ByteArray, offset: Int, length: Int) {
|
|
||||||
intercept.write(buffer, offset, length)
|
|
||||||
outputStream.write(buffer, offset, length)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -24,7 +24,7 @@ object BackupScreen : UiDeviceScreen<BackupScreen>() {
|
||||||
|
|
||||||
val internalStorageButton = findObject { textContains(Build.MODEL) }
|
val internalStorageButton = findObject { textContains(Build.MODEL) }
|
||||||
|
|
||||||
val useAnywayButton = findObject { text("USE ANYWAY") }
|
val useAnywayButton = findObject { text("Use anyway") }
|
||||||
|
|
||||||
val initializingText: BySelector = By.textContains("Initializing backup location")
|
val initializingText: BySelector = By.textContains("Initializing backup location")
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import com.stevesoltys.seedvault.e2e.screen.UiDeviceScreen
|
||||||
|
|
||||||
object RecoveryCodeScreen : UiDeviceScreen<RecoveryCodeScreen>() {
|
object RecoveryCodeScreen : UiDeviceScreen<RecoveryCodeScreen>() {
|
||||||
|
|
||||||
|
val startNewBackupButton = findObject { text("Start new") }
|
||||||
|
|
||||||
val confirmCodeButton = findObject { text("Confirm code") }
|
val confirmCodeButton = findObject { text("Confirm code") }
|
||||||
|
|
||||||
val verifyCodeButton = findObject { text("Verify") }
|
val verifyCodeButton = findObject { text("Verify") }
|
||||||
|
|
|
@ -9,7 +9,9 @@ import com.stevesoltys.seedvault.e2e.screen.UiDeviceScreen
|
||||||
|
|
||||||
object RestoreScreen : UiDeviceScreen<RestoreScreen>() {
|
object RestoreScreen : UiDeviceScreen<RestoreScreen>() {
|
||||||
|
|
||||||
val backupListItem = findObject { textContains("Last backup") }
|
val backupListItem = findObject {
|
||||||
|
textContains("Android SDK") // device name of test backups
|
||||||
|
}
|
||||||
|
|
||||||
val appsSelectedButton = findObject { text("Restore backup") }
|
val appsSelectedButton = findObject { text("Restore backup") }
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ import android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_ERROR_PREFLIGHT
|
||||||
import android.app.backup.IBackupManagerMonitor
|
import android.app.backup.IBackupManagerMonitor
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Log.DEBUG
|
|
||||||
|
|
||||||
private val TAG = BackupMonitor::class.java.name
|
private val TAG = BackupMonitor::class.java.name
|
||||||
|
|
||||||
|
@ -29,14 +28,11 @@ open class BackupMonitor : IBackupManagerMonitor.Stub() {
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun onEvent(id: Int, category: Int, packageName: String?, bundle: Bundle) {
|
open fun onEvent(id: Int, category: Int, packageName: String?, bundle: Bundle) {
|
||||||
|
Log.d(TAG, "${packageName?.padEnd(64, ' ')} cat: $category id: $id")
|
||||||
if (id == LOG_EVENT_ID_ERROR_PREFLIGHT) {
|
if (id == LOG_EVENT_ID_ERROR_PREFLIGHT) {
|
||||||
val preflightResult = bundle.getLong(EXTRA_LOG_PREFLIGHT_ERROR, -1)
|
val preflightResult = bundle.getLong(EXTRA_LOG_PREFLIGHT_ERROR, -1)
|
||||||
Log.w(TAG, "Pre-flight error from $packageName: $preflightResult")
|
Log.w(TAG, "Pre-flight error from $packageName: $preflightResult")
|
||||||
}
|
}
|
||||||
if (!Log.isLoggable(TAG, DEBUG)) return
|
|
||||||
Log.d(TAG, "ID: $id")
|
|
||||||
Log.d(TAG, "CATEGORY: $category")
|
|
||||||
Log.d(TAG, "PACKAGE: $packageName")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,10 @@ class BackendManager(
|
||||||
backendFactory: BackendFactory,
|
backendFactory: BackendFactory,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@Volatile
|
||||||
private var mBackend: Backend?
|
private var mBackend: Backend?
|
||||||
|
|
||||||
|
@Volatile
|
||||||
private var mBackendProperties: BackendProperties<*>?
|
private var mBackendProperties: BackendProperties<*>?
|
||||||
|
|
||||||
val backend: Backend
|
val backend: Backend
|
||||||
|
@ -81,6 +84,8 @@ class BackendManager(
|
||||||
* IMPORTANT: Do no call this while current plugins are being used,
|
* IMPORTANT: Do no call this while current plugins are being used,
|
||||||
* e.g. while backup/restore operation is still running.
|
* e.g. while backup/restore operation is still running.
|
||||||
*/
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
@Synchronized
|
||||||
fun <T> changePlugins(
|
fun <T> changePlugins(
|
||||||
backend: Backend,
|
backend: Backend,
|
||||||
storageProperties: BackendProperties<T>,
|
storageProperties: BackendProperties<T>,
|
||||||
|
|
|
@ -90,6 +90,7 @@ internal class SafHandler(
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
fun setPlugin(safProperties: SafProperties) {
|
fun setPlugin(safProperties: SafProperties) {
|
||||||
backendManager.changePlugins(
|
backendManager.changePlugins(
|
||||||
backend = backendFactory.createSafBackend(safProperties),
|
backend = backendFactory.createSafBackend(safProperties),
|
||||||
|
|
|
@ -86,6 +86,7 @@ internal class WebDavHandler(
|
||||||
settingsManager.saveWebDavConfig(properties.config)
|
settingsManager.saveWebDavConfig(properties.config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
fun setPlugin(properties: WebDavProperties, backend: Backend) {
|
fun setPlugin(properties: WebDavProperties, backend: Backend) {
|
||||||
backendManager.changePlugins(
|
backendManager.changePlugins(
|
||||||
backend = backend,
|
backend = backend,
|
||||||
|
|
|
@ -7,6 +7,7 @@ package com.stevesoltys.seedvault.repo
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.MODE_APPEND
|
import android.content.Context.MODE_APPEND
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
import com.stevesoltys.seedvault.MemoryLogger
|
import com.stevesoltys.seedvault.MemoryLogger
|
||||||
import com.stevesoltys.seedvault.proto.Snapshot
|
import com.stevesoltys.seedvault.proto.Snapshot
|
||||||
import com.stevesoltys.seedvault.proto.Snapshot.Blob
|
import com.stevesoltys.seedvault.proto.Snapshot.Blob
|
||||||
|
@ -90,6 +91,7 @@ class BlobCache(
|
||||||
* * changing to a different backup to prevent usage of blobs that don't exist there
|
* * changing to a different backup to prevent usage of blobs that don't exist there
|
||||||
* * uploading a new snapshot to prevent the persistent cache from growing indefinitely
|
* * uploading a new snapshot to prevent the persistent cache from growing indefinitely
|
||||||
*/
|
*/
|
||||||
|
@WorkerThread
|
||||||
fun clearLocalCache() {
|
fun clearLocalCache() {
|
||||||
log.info { "Clearing local cache..." }
|
log.info { "Clearing local cache..." }
|
||||||
context.deleteFile(CACHE_FILE_NAME)
|
context.deleteFile(CACHE_FILE_NAME)
|
||||||
|
|
|
@ -161,6 +161,9 @@ internal class KVRestore(
|
||||||
} catch (e: AEADBadTagException) {
|
} catch (e: AEADBadTagException) {
|
||||||
Log.e(TAG, "Decryption failed", e)
|
Log.e(TAG, "Decryption failed", e)
|
||||||
TRANSPORT_ERROR
|
TRANSPORT_ERROR
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unknown error", e)
|
||||||
|
TRANSPORT_ERROR
|
||||||
} finally {
|
} finally {
|
||||||
dbManager.deleteDb(state.packageInfo.packageName, true)
|
dbManager.deleteDb(state.packageInfo.packageName, true)
|
||||||
this.state = null
|
this.state = null
|
||||||
|
|
|
@ -107,8 +107,12 @@ class RecoveryCodeInputFragment : Fragment() {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
view.requireViewById<Toolbar>(R.id.toolbar).apply {
|
view.requireViewById<Toolbar>(R.id.toolbar).apply {
|
||||||
setNavigationOnClickListener {
|
if (viewModel.isRestore) {
|
||||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
visibility = GONE
|
||||||
|
} else {
|
||||||
|
setNavigationOnClickListener {
|
||||||
|
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import android.app.backup.IBackupManager
|
||||||
import android.app.job.JobInfo
|
import android.app.job.JobInfo
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.UiThread
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
|
@ -23,6 +24,7 @@ import com.stevesoltys.seedvault.worker.AppBackupWorker
|
||||||
import com.stevesoltys.seedvault.worker.BackupRequester.Companion.requestFilesAndAppBackup
|
import com.stevesoltys.seedvault.worker.BackupRequester.Companion.requestFilesAndAppBackup
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.calyxos.backup.storage.api.StorageBackup
|
import org.calyxos.backup.storage.api.StorageBackup
|
||||||
import org.calyxos.backup.storage.backup.BackupJobService
|
import org.calyxos.backup.storage.backup.BackupJobService
|
||||||
import org.calyxos.seedvault.core.backends.Backend
|
import org.calyxos.seedvault.core.backends.Backend
|
||||||
|
@ -46,25 +48,38 @@ internal class BackupStorageViewModel(
|
||||||
|
|
||||||
override val isRestoreOperation = false
|
override val isRestoreOperation = false
|
||||||
|
|
||||||
|
@UiThread
|
||||||
override fun onSafUriSet(safProperties: SafProperties) {
|
override fun onSafUriSet(safProperties: SafProperties) {
|
||||||
safHandler.save(safProperties)
|
safHandler.save(safProperties)
|
||||||
safHandler.setPlugin(safProperties)
|
viewModelScope.launch {
|
||||||
if (safProperties.isUsb) {
|
withContext(Dispatchers.IO) {
|
||||||
// disable storage backup if new storage is on USB
|
safHandler.setPlugin(safProperties)
|
||||||
cancelBackupWorkers()
|
}
|
||||||
} else {
|
withContext(Dispatchers.Main) { // UiThread
|
||||||
// enable it, just in case the previous storage was on USB,
|
if (safProperties.isUsb) {
|
||||||
// also to update the network requirement of the new storage
|
// disable storage backup if new storage is on USB
|
||||||
scheduleBackupWorkers()
|
cancelBackupWorkers()
|
||||||
|
} else {
|
||||||
|
// enable it, just in case the previous storage was on USB,
|
||||||
|
// also to update the network requirement of the new storage
|
||||||
|
scheduleBackupWorkers()
|
||||||
|
}
|
||||||
|
onStorageLocationSet(safProperties.isUsb)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onStorageLocationSet(safProperties.isUsb)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onWebDavConfigSet(properties: WebDavProperties, backend: Backend) {
|
override fun onWebDavConfigSet(properties: WebDavProperties, backend: Backend) {
|
||||||
webdavHandler.save(properties)
|
webdavHandler.save(properties)
|
||||||
webdavHandler.setPlugin(properties, backend)
|
viewModelScope.launch {
|
||||||
scheduleBackupWorkers()
|
withContext(Dispatchers.IO) {
|
||||||
onStorageLocationSet(isUsb = false)
|
webdavHandler.setPlugin(properties, backend)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
scheduleBackupWorkers()
|
||||||
|
onStorageLocationSet(isUsb = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onStorageLocationSet(isUsb: Boolean) {
|
private fun onStorageLocationSet(isUsb: Boolean) {
|
||||||
|
|
|
@ -124,6 +124,8 @@ class AppBackupWorker(
|
||||||
Result.retry()
|
Result.retry()
|
||||||
} else {
|
} else {
|
||||||
val result = doBackup()
|
val result = doBackup()
|
||||||
|
// show error notification if backup wasn't successful (maybe only when no retry?)
|
||||||
|
if (result != Result.success()) nm.onBackupError()
|
||||||
// only allow retrying if rescheduling is allowed
|
// only allow retrying if rescheduling is allowed
|
||||||
if (tags.contains(TAG_RESCHEDULE)) return result
|
if (tags.contains(TAG_RESCHEDULE)) return result
|
||||||
else Result.success()
|
else Result.success()
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
* SPDX-FileCopyrightText: 2020 The Calyx Institute
|
* SPDX-FileCopyrightText: 2020 The Calyx Institute
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.util.Properties
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
|
@ -42,23 +40,18 @@ android {
|
||||||
isReturnDefaultValues = true
|
isReturnDefaultValues = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// optional signingConfigs
|
signingConfigs {
|
||||||
// On userdebug builds, you can use the testkey here to update the system app
|
create("aosp") {
|
||||||
val keystorePropertiesFile = project.file("keystore.properties")
|
keyAlias = "android"
|
||||||
if (keystorePropertiesFile.exists()) {
|
keyPassword = "android"
|
||||||
val keystoreProperties = Properties()
|
storePassword = "android"
|
||||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
storeFile = file("testkey.jks")
|
||||||
|
|
||||||
signingConfigs {
|
|
||||||
create("release") {
|
|
||||||
keyAlias = keystoreProperties["keyAlias"] as String
|
|
||||||
keyPassword = keystoreProperties["keyPassword"] as String
|
|
||||||
storeFile = file(keystoreProperties["storeFile"] as String)
|
|
||||||
storePassword = keystoreProperties["storePassword"] as String
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
buildTypes.getByName("release").signingConfig = signingConfigs.getByName("release")
|
}
|
||||||
buildTypes.getByName("debug").signingConfig = signingConfigs.getByName("release")
|
|
||||||
|
buildTypes {
|
||||||
|
getByName("release").signingConfig = signingConfigs.getByName("aosp")
|
||||||
|
getByName("debug").signingConfig = signingConfigs.getByName("aosp")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,6 +67,7 @@ dependencies {
|
||||||
androidTestImplementation(libs.kotlin.stdlib.jdk8)
|
androidTestImplementation(libs.kotlin.stdlib.jdk8)
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
androidTestImplementation(
|
androidTestImplementation(
|
||||||
"androidx.test.espresso:espresso-core:${libs.versions.espresso.get()}")
|
"androidx.test.espresso:espresso-core:${libs.versions.espresso.get()}"
|
||||||
|
)
|
||||||
androidTestImplementation("io.mockk:mockk-android:${libs.versions.mockk.get()}")
|
androidTestImplementation("io.mockk:mockk-android:${libs.versions.mockk.get()}")
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,12 @@
|
||||||
|
|
||||||
package org.calyxos.backup.contacts
|
package org.calyxos.backup.contacts
|
||||||
|
|
||||||
|
import android.Manifest.permission.READ_CONTACTS
|
||||||
|
import android.Manifest.permission.WRITE_CONTACTS
|
||||||
import android.app.backup.BackupAgent
|
import android.app.backup.BackupAgent
|
||||||
import android.app.backup.BackupAgent.TYPE_FILE
|
import android.app.backup.BackupAgent.TYPE_FILE
|
||||||
import android.app.backup.FullBackupDataOutput
|
import android.app.backup.FullBackupDataOutput
|
||||||
|
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
@ -19,6 +22,7 @@ import org.calyxos.backup.contacts.ContactsBackupAgent.BACKUP_FILE
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Assume.assumeTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -42,6 +46,9 @@ class BackupRestoreTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testBackupAndRestore() {
|
fun testBackupAndRestore() {
|
||||||
|
val hasReadPermission = context.checkSelfPermission(READ_CONTACTS) == PERMISSION_GRANTED
|
||||||
|
val hasWritePermission = context.checkSelfPermission(WRITE_CONTACTS) == PERMISSION_GRANTED
|
||||||
|
assumeTrue(hasReadPermission && hasWritePermission)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"Test will remove *all* contacts and thus requires empty address book",
|
"Test will remove *all* contacts and thus requires empty address book",
|
||||||
0,
|
0,
|
||||||
|
|
BIN
contactsbackup/testkey.jks
Normal file
BIN
contactsbackup/testkey.jks
Normal file
Binary file not shown.
Loading…
Reference in a new issue