Fix and improve E2E tests
This commit is contained in:
parent
28059a50bd
commit
8297bb7087
9 changed files with 94 additions and 98 deletions
35
.github/scripts/run_tests.sh
vendored
Executable file
35
.github/scripts/run_tests.sh
vendored
Executable file
|
@ -0,0 +1,35 @@
|
||||||
|
adb root
|
||||||
|
sleep 5
|
||||||
|
adb remount
|
||||||
|
|
||||||
|
echo "Installing Seedvault app..."
|
||||||
|
adb shell mkdir -p /system/priv-app/Seedvault
|
||||||
|
adb push app/build/outputs/apk/release/app-release.apk /system/priv-app/Seedvault/Seedvault.apk
|
||||||
|
|
||||||
|
echo "Installing Seedvault permissions..."
|
||||||
|
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
|
||||||
|
|
||||||
|
echo "Setting Seedvault transport..."
|
||||||
|
sleep 10
|
||||||
|
adb shell bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport
|
||||||
|
|
||||||
|
large_test_exit_code=0
|
||||||
|
./gradlew --stacktrace -Pinstrumented_test_size=large :app:connectedAndroidTest || large_test_exit_code=$?
|
||||||
|
|
||||||
|
adb pull /sdcard/seedvault_test_results
|
||||||
|
|
||||||
|
if [ "$large_test_exit_code" -ne 0 ]; then
|
||||||
|
echo 'Large tests failed.'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
medium_test_exit_code=0
|
||||||
|
./gradlew --stacktrace -Pinstrumented_test_size=medium :app:connectedAndroidTest || medium_test_exit_code=$?
|
||||||
|
|
||||||
|
if [ "$medium_test_exit_code" -ne 0 ]; then
|
||||||
|
echo 'Medium tests failed.'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
|
@ -1,6 +1,10 @@
|
||||||
name: Build
|
name: Build
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
|
|
100
.github/workflows/test.yml
vendored
100
.github/workflows/test.yml
vendored
|
@ -13,14 +13,13 @@ concurrency:
|
||||||
jobs:
|
jobs:
|
||||||
instrumentation_tests:
|
instrumentation_tests:
|
||||||
runs-on: macos-11
|
runs-on: macos-11
|
||||||
|
if: github.repository == 'seedvault-app/seedvault'
|
||||||
|
timeout-minutes: 80
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
android_target: [33, 34]
|
android_target: [ 33, 34 ]
|
||||||
emulator_type: [default, google_apis]
|
emulator_type: [ default ]
|
||||||
exclude:
|
|
||||||
- android_target: 34
|
|
||||||
emulator_type: default
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
@ -31,88 +30,33 @@ jobs:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
- name: AVD cache
|
|
||||||
uses: actions/cache@v3
|
|
||||||
id: avd-cache
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.android/avd/*
|
|
||||||
~/.android/adb*
|
|
||||||
key: aosp-${{ matrix.emulator_type }}-${{ matrix.android_target }}-${{ runner.os }}
|
|
||||||
|
|
||||||
- name: Build Release APK
|
- name: Build Release APK
|
||||||
run: ./gradlew :app:assembleRelease
|
run: ./gradlew :app:assembleRelease
|
||||||
|
|
||||||
- name: Create AVD snapshot
|
|
||||||
if: steps.avd-cache.outputs.cache-hit != 'true'
|
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
|
||||||
with:
|
|
||||||
api-level: ${{ matrix.android_target }}
|
|
||||||
target: ${{ matrix.emulator_type }}
|
|
||||||
arch: x86_64
|
|
||||||
force-avd-creation: false
|
|
||||||
emulator-options: -writable-system -no-snapshot-load -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
|
||||||
disable-animations: true
|
|
||||||
script: |
|
|
||||||
./app/development/scripts/provision_emulator.sh "test" "system-images;android-${{ matrix.android_target }};${{ matrix.emulator_type }};x86_64"
|
|
||||||
echo "Generated AVD snapshot for caching."
|
|
||||||
|
|
||||||
- name: Assemble tests
|
- name: Assemble tests
|
||||||
run: ./gradlew :app:assembleAndroidTest
|
run: ./gradlew :app:assembleAndroidTest
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
uses: Wandalen/wretry.action@v1.3.0
|
||||||
with:
|
with:
|
||||||
api-level: ${{ matrix.android_target }}
|
attempt_limit: 3
|
||||||
target: ${{ matrix.emulator_type }}
|
action: reactivecircus/android-emulator-runner@v2
|
||||||
arch: x86_64
|
with: |
|
||||||
force-avd-creation: false
|
api-level: ${{ matrix.android_target }}
|
||||||
emulator-options: -writable-system -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
target: ${{ matrix.emulator_type }}
|
||||||
profile: pixel_6a
|
arch: x86_64
|
||||||
heap-size: '512M'
|
force-avd-creation: true
|
||||||
ram-size: '4096M'
|
emulator-options: -cores 2 -writable-system -no-snapshot-load -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||||
disk-size: '14G'
|
disk-size: '14G'
|
||||||
sdcard-path-or-size: '4096M'
|
sdcard-path-or-size: '4096M'
|
||||||
cores: 3
|
disable-animations: true
|
||||||
disable-animations: false
|
script: |
|
||||||
script: |
|
./app/development/scripts/provision_emulator.sh "test" "system-images;android-${{ matrix.android_target }};${{ matrix.emulator_type }};x86_64"
|
||||||
adb root
|
./.github/scripts/run_tests.sh
|
||||||
sleep 5
|
|
||||||
adb remount
|
|
||||||
|
|
||||||
echo "Installing Seedvault app..."
|
- name: Upload test results
|
||||||
adb shell mkdir -p /system/priv-app/Seedvault
|
|
||||||
adb push app/build/outputs/apk/release/app-release.apk /system/priv-app/Seedvault/Seedvault.apk
|
|
||||||
|
|
||||||
echo "Installing Seedvault permissions..."
|
|
||||||
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
|
|
||||||
|
|
||||||
echo "Setting Seedvault transport..."
|
|
||||||
sleep 10
|
|
||||||
adb shell bmgr transport com.stevesoltys.seedvault.transport.ConfigurableBackupTransport
|
|
||||||
|
|
||||||
wget --quiet https://github.com/seedvault-app/seedvault-test-data/releases/download/1/backup.tar.gz
|
|
||||||
adb shell mkdir -p /sdcard/seedvault_baseline
|
|
||||||
adb push backup.tar.gz /sdcard/seedvault_baseline
|
|
||||||
adb wait-for-device
|
|
||||||
adb shell tar xzf /sdcard/seedvault_baseline/backup.tar.gz --directory=/sdcard/seedvault_baseline
|
|
||||||
adb shell rm /sdcard/seedvault_baseline/backup.tar.gz
|
|
||||||
|
|
||||||
large_test_exit_code=0
|
|
||||||
./gradlew --stacktrace -Pinstrumented_test_size=large :app:connectedAndroidTest || large_test_exit_code=$?
|
|
||||||
|
|
||||||
medium_test_exit_code=0
|
|
||||||
./gradlew --stacktrace -Pinstrumented_test_size=medium :app:connectedAndroidTest || medium_test_exit_code=$?
|
|
||||||
|
|
||||||
adb pull /sdcard/seedvault_test_videos
|
|
||||||
|
|
||||||
if [ $large_test_exit_code -ne 0 ]; then echo 'Gradle test failed.'; exit 0; fi
|
|
||||||
if [ $medium_test_exit_code -ne 0 ]; then echo 'Gradle test failed.'; exit 0; fi
|
|
||||||
|
|
||||||
- name: Upload screenshots and videos
|
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: seedvault_test_videos
|
name: ${{ matrix.emulator_type }}-${{ matrix.android_target }}-results
|
||||||
path: seedvault_test_videos/**/*.mp4
|
path: seedvault_test_results/**/*
|
||||||
|
|
|
@ -162,7 +162,7 @@ dependencies {
|
||||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||||
androidTestImplementation "io.mockk:mockk-android:$mockk_version"
|
androidTestImplementation "io.mockk:mockk-android:1.13.8"
|
||||||
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
|
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,7 +200,7 @@ tasks.register('provisionEmulator', Exec) {
|
||||||
doFirst {
|
doFirst {
|
||||||
commandLine "${project.projectDir}/development/scripts/provision_emulator.sh",
|
commandLine "${project.projectDir}/development/scripts/provision_emulator.sh",
|
||||||
"seedvault",
|
"seedvault",
|
||||||
"system-images;android-34;google_apis;x86_64"
|
"system-images;android-34;default;x86_64"
|
||||||
|
|
||||||
environment "ANDROID_HOME", android.sdkDirectory.absolutePath
|
environment "ANDROID_HOME", android.sdkDirectory.absolutePath
|
||||||
environment "JAVA_HOME", System.properties['java.home']
|
environment "JAVA_HOME", System.properties['java.home']
|
||||||
|
|
|
@ -20,7 +20,7 @@ DEVELOPMENT_DIR=$SCRIPT_DIR/..
|
||||||
ROOT_PROJECT_DIR=$SCRIPT_DIR/../../..
|
ROOT_PROJECT_DIR=$SCRIPT_DIR/../../..
|
||||||
|
|
||||||
echo "Downloading system image..."
|
echo "Downloading system image..."
|
||||||
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install "$SYSTEM_IMAGE"
|
yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install "$SYSTEM_IMAGE"
|
||||||
|
|
||||||
# create AVD if it doesn't exist
|
# create AVD if it doesn't exist
|
||||||
if $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager list avd | grep -q "$EMULATOR_NAME"; then
|
if $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager list avd | grep -q "$EMULATOR_NAME"; then
|
||||||
|
|
|
@ -14,9 +14,5 @@ fi
|
||||||
|
|
||||||
EMULATOR_NAME=$1
|
EMULATOR_NAME=$1
|
||||||
|
|
||||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
|
|
||||||
DEVELOPMENT_DIR=$SCRIPT_DIR/..
|
|
||||||
ROOT_PROJECT_DIR=$SCRIPT_DIR/../../..
|
|
||||||
|
|
||||||
echo "Starting emulator..."
|
echo "Starting emulator..."
|
||||||
nohup $ANDROID_HOME/emulator/emulator -avd "$EMULATOR_NAME" -gpu swiftshader_indirect -writable-system >/dev/null 2>&1 &
|
nohup $ANDROID_HOME/emulator/emulator -avd "$EMULATOR_NAME" -gpu swiftshader_indirect -writable-system >/dev/null 2>&1 &
|
||||||
|
|
|
@ -44,7 +44,7 @@ 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_videos"
|
private const val TEST_VIDEO_FOLDER = "seedvault_test_results"
|
||||||
}
|
}
|
||||||
|
|
||||||
val externalStorageDir: String get() = Environment.getExternalStorageDirectory().absolutePath
|
val externalStorageDir: String get() = Environment.getExternalStorageDirectory().absolutePath
|
||||||
|
@ -106,19 +106,23 @@ internal interface LargeTestBase : KoinComponent {
|
||||||
uiAutomation.executeShellCommand(command).close()
|
uiAutomation.executeShellCommand(command).close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun testResultFilename(testName: String): String {
|
||||||
|
val simpleDateFormat = SimpleDateFormat("yyyyMMdd_hhmmss")
|
||||||
|
val timeStamp = simpleDateFormat.format(Calendar.getInstance().time)
|
||||||
|
return "${timeStamp}_${testName.replace(" ", "_")}"
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
suspend fun startScreenRecord(
|
suspend fun startRecordingTest(
|
||||||
keepRecordingScreen: AtomicBoolean,
|
keepRecordingScreen: AtomicBoolean,
|
||||||
testName: String,
|
testName: String,
|
||||||
) {
|
) {
|
||||||
val simpleDateFormat = SimpleDateFormat("yyyyMMdd_hhmmss")
|
|
||||||
val timeStamp = simpleDateFormat.format(Calendar.getInstance().time)
|
|
||||||
val fileName = "${timeStamp}_${testName.replace(" ", "_")}"
|
|
||||||
|
|
||||||
val folder = testVideoPath
|
val folder = testVideoPath
|
||||||
runCommand("mkdir -p $folder")
|
runCommand("mkdir -p $folder")
|
||||||
|
|
||||||
|
val fileName = testResultFilename(testName)
|
||||||
|
|
||||||
// screen record automatically stops after 3 minutes
|
// screen record automatically stops after 3 minutes
|
||||||
// we need to block on a loop and split it into multiple files
|
// we need to block on a loop and split it into multiple files
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
@ -131,10 +135,16 @@ internal interface LargeTestBase : KoinComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun stopScreenRecord(keepRecordingScreen: AtomicBoolean) {
|
fun stopRecordingTest(
|
||||||
|
keepRecordingScreen: AtomicBoolean,
|
||||||
|
testName: String,
|
||||||
|
) {
|
||||||
keepRecordingScreen.set(false)
|
keepRecordingScreen.set(false)
|
||||||
|
|
||||||
runCommand("pkill -2 screenrecord")
|
runCommand("pkill -2 screenrecord")
|
||||||
|
|
||||||
|
// write logcat to file
|
||||||
|
val fileName = testResultFilename(testName)
|
||||||
|
runCommand("logcat -d -f $testVideoPath/$fileName.log")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun uninstallPackages(packages: Collection<PackageInfo>) {
|
fun uninstallPackages(packages: Collection<PackageInfo>) {
|
||||||
|
|
|
@ -38,13 +38,13 @@ internal abstract class SeedvaultLargeTest :
|
||||||
resetApplicationState()
|
resetApplicationState()
|
||||||
clearTestBackups()
|
clearTestBackups()
|
||||||
|
|
||||||
startScreenRecord(keepRecordingScreen, name.methodName)
|
startRecordingTest(keepRecordingScreen, name.methodName)
|
||||||
restoreBaselineBackup()
|
restoreBaselineBackup()
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
open fun tearDown() {
|
open fun tearDown() {
|
||||||
stopScreenRecord(keepRecordingScreen)
|
stopRecordingTest(keepRecordingScreen, name.methodName)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -11,6 +11,10 @@ import java.lang.Thread.sleep
|
||||||
|
|
||||||
abstract class UiDeviceScreen<T> {
|
abstract class UiDeviceScreen<T> {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SELECTOR_TIMEOUT = 180000L
|
||||||
|
}
|
||||||
|
|
||||||
operator fun invoke(function: T.() -> Unit) {
|
operator fun invoke(function: T.() -> Unit) {
|
||||||
function.invoke(this as T)
|
function.invoke(this as T)
|
||||||
}
|
}
|
||||||
|
@ -18,8 +22,11 @@ abstract class UiDeviceScreen<T> {
|
||||||
fun UiObject.scrollTo(
|
fun UiObject.scrollTo(
|
||||||
scrollSelector: UiSelector = UiSelector().className(ScrollView::class.java),
|
scrollSelector: UiSelector = UiSelector().className(ScrollView::class.java),
|
||||||
): UiObject {
|
): UiObject {
|
||||||
UiScrollable(scrollSelector).scrollIntoView(this)
|
val uiScrollable = UiScrollable(scrollSelector)
|
||||||
waitForExists(15000)
|
uiScrollable.waitForExists(SELECTOR_TIMEOUT)
|
||||||
|
uiScrollable.scrollIntoView(this)
|
||||||
|
waitForExists(SELECTOR_TIMEOUT)
|
||||||
|
|
||||||
sleep(2000)
|
sleep(2000)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -32,6 +39,6 @@ abstract class UiDeviceScreen<T> {
|
||||||
|
|
||||||
private fun device() = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
private fun device() = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||||
.also {
|
.also {
|
||||||
Configurator.getInstance().waitForSelectorTimeout = 60000
|
Configurator.getInstance().waitForSelectorTimeout = SELECTOR_TIMEOUT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue