Drive app migration tests through instrumentation

Make tests less flaky
This commit is contained in:
topjohnwu 2024-12-25 04:29:02 -08:00 committed by John Wu
parent 08ea937f7c
commit 9e2b59060d
4 changed files with 121 additions and 21 deletions

View File

@ -2,13 +2,22 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<queries tools:node="removeAll" />
<application tools:node="replace">
<uses-library android:name="android.test.runner" />
</application>
<instrumentation
android:name="com.topjohnwu.magisk.test.AppTestRunner"
android:targetPackage="com.topjohnwu.magisk" />
<instrumentation
android:name="com.topjohnwu.magisk.test.TestRunner"
android:targetPackage="com.topjohnwu.magisk"
android:label="Tests for Magisk" />
android:targetPackage="com.topjohnwu.magisk.test" />
</manifest>

View File

@ -0,0 +1,88 @@
package com.topjohnwu.magisk.test
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.ParcelFileDescriptor.AutoCloseInputStream
import androidx.annotation.Keep
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@Keep
@RunWith(AndroidJUnit4::class)
class AppMigrationTest {
companion object {
private const val APP_PKG = "com.topjohnwu.magisk"
private const val STUB_PKG = "repackaged.$APP_PKG"
private const val RECEIVER_TIMEOUT = 20L
}
private val instrumentation get() = InstrumentationRegistry.getInstrumentation()
private val context get() = instrumentation.context
private val uiAutomation get() = instrumentation.uiAutomation
private val registeredReceivers = mutableListOf<BroadcastReceiver>()
class PackageRemoveMonitor(
context: Context,
private val packageName: String
) : BroadcastReceiver() {
val latch = CountDownLatch(1)
init {
val filter = IntentFilter(Intent.ACTION_PACKAGE_REMOVED)
filter.addDataScheme("package")
context.registerReceiver(this, filter)
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_PACKAGE_REMOVED)
return
val data = intent.data ?: return
val pkg = data.schemeSpecificPart
if (pkg == packageName) latch.countDown()
}
}
@After
fun tearDown() {
registeredReceivers.forEach(context::unregisterReceiver)
}
private fun testAppMigration(pkg: String, method: String) {
val receiver = PackageRemoveMonitor(context, pkg)
registeredReceivers.add(receiver)
// Trigger the test to run migration
val pfd = uiAutomation.executeShellCommand(
"am instrument -w --user 0 -e class .Environment#$method " +
"$pkg.test/${AppTestRunner::class.java.name}"
)
val output = AutoCloseInputStream(pfd).reader().use { it.readText() }
assertTrue("$method failed, inst out: $output", output.contains("OK ("))
// Wait for migration to complete
assertTrue(
"$pkg uninstallation failed",
receiver.latch.await(RECEIVER_TIMEOUT, TimeUnit.SECONDS)
)
}
@Test
fun testAppHide() {
testAppMigration(APP_PKG, "setupAppHide")
}
@Test
fun testAppRestore() {
testAppMigration(STUB_PKG, "setupAppRestore")
}
}

View File

@ -4,7 +4,7 @@ import android.os.Bundle
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnitRunner
class TestRunner : AndroidJUnitRunner() {
open class TestRunner : AndroidJUnitRunner() {
override fun onCreate(arguments: Bundle) {
// Support short-hand ".ClassName"
arguments.getString("class")?.let {
@ -17,6 +17,12 @@ class TestRunner : AndroidJUnitRunner() {
}
arguments.putString("class", classArg)
}
super.onCreate(arguments)
}
}
class AppTestRunner : TestRunner() {
override fun onCreate(arguments: Bundle) {
// Force using the target context's classloader to run tests
arguments.putString("classLoader", TestClassLoader::class.java.name)
super.onCreate(arguments)

View File

@ -28,16 +28,9 @@ print_error() {
}
# $1 = TestClass#method
# $2: boolean = isRepackaged
# $2 = component
am_instrument() {
local test_pkg
if [ -n "$2" -a "$2" ]; then
test_pkg="repackaged.com.topjohnwu.magisk.test"
else
test_pkg=com.topjohnwu.magisk.test
fi
local out=$(adb shell am instrument -w --user 0 -e class "$1" \
"$test_pkg/com.topjohnwu.magisk.test.TestRunner")
local out=$(adb shell am instrument -w --user 0 -e class "$1" "$2")
grep -q 'OK (' <<< "$out"
}
@ -57,27 +50,31 @@ run_setup() {
# Install the test app
adb install -r -g out/test.apk
local app='com.topjohnwu.magisk.test/com.topjohnwu.magisk.test.AppTestRunner'
# Run setup through the test app
am_instrument '.Environment#setupMagisk'
am_instrument '.Environment#setupMagisk' $app
# Install LSPosed
am_instrument '.Environment#setupLsposed'
am_instrument '.Environment#setupLsposed' $app
}
run_tests() {
local self='com.topjohnwu.magisk.test/com.topjohnwu.magisk.test.TestRunner'
local app='com.topjohnwu.magisk.test/com.topjohnwu.magisk.test.AppTestRunner'
local stub='repackaged.com.topjohnwu.magisk.test/com.topjohnwu.magisk.test.AppTestRunner'
# Run app tests
am_instrument '.MagiskAppTest,.AdditionalTest'
am_instrument '.MagiskAppTest,.AdditionalTest' $app
# Test app hiding
am_instrument '.Environment#setupAppHide'
wait_for_pm com.topjohnwu.magisk
am_instrument '.AppMigrationTest#testAppHide' $self
# Make sure it still works
am_instrument '.MagiskAppTest' true
am_instrument '.MagiskAppTest' $stub
# Test app restore
am_instrument '.Environment#setupAppRestore' true
wait_for_pm repackaged.com.topjohnwu.magisk
am_instrument '.AppMigrationTest#testAppRestore' $self
# Make sure it still works
am_instrument '.MagiskAppTest'
am_instrument '.MagiskAppTest' $app
}