From 9112a3a4f5cdcc889f14f77587e4d9dba7f34dec Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Fri, 13 Dec 2024 01:09:52 -0800 Subject: [PATCH] Introduce instrumentation tests --- app/core/build.gradle.kts | 5 ++ app/core/proguard-rules.pro | 1 + .../com/topjohnwu/magisk/core/Provider.kt | 3 +- .../com/topjohnwu/magisk/core/TestImpl.kt | 50 ++++++++++++ .../topjohnwu/magisk/core/su/TestHandler.kt | 80 ------------------- app/test/.gitignore | 1 + app/test/build.gradle.kts | 24 ++++++ app/test/src/main/AndroidManifest.xml | 16 ++++ .../com/topjohnwu/magisk/test/Environment.kt | 29 +++++++ .../topjohnwu/magisk/test/MagiskAppTest.kt | 24 ++++++ build.py | 22 +++-- gradle/libs.versions.toml | 3 + scripts/avd_magisk.sh | 4 +- scripts/test_common.sh | 30 +++---- settings.gradle.kts | 2 +- 15 files changed, 190 insertions(+), 104 deletions(-) create mode 100644 app/core/src/main/java/com/topjohnwu/magisk/core/TestImpl.kt delete mode 100644 app/core/src/main/java/com/topjohnwu/magisk/core/su/TestHandler.kt create mode 100644 app/test/.gitignore create mode 100644 app/test/build.gradle.kts create mode 100644 app/test/src/main/AndroidManifest.xml create mode 100644 app/test/src/main/java/com/topjohnwu/magisk/test/Environment.kt create mode 100644 app/test/src/main/java/com/topjohnwu/magisk/test/MagiskAppTest.kt diff --git a/app/core/build.gradle.kts b/app/core/build.gradle.kts index cccf75d7b..0f8f54cb8 100644 --- a/app/core/build.gradle.kts +++ b/app/core/build.gradle.kts @@ -60,4 +60,9 @@ dependencies { implementation(libs.activity) implementation(libs.collection.ktx) implementation(libs.profileinstaller) + + // We also implement all our tests in this module. + // However, we don't want to bundle test dependencies. + // That's why we make it compileOnly. + compileOnly(libs.test.junit) } diff --git a/app/core/proguard-rules.pro b/app/core/proguard-rules.pro index 864ab8ff9..4a12a8234 100644 --- a/app/core/proguard-rules.pro +++ b/app/core/proguard-rules.pro @@ -37,6 +37,7 @@ -flattenpackagehierarchy -allowaccessmodification +-dontwarn org.junit.Assert -dontwarn org.bouncycastle.jsse.BCSSLParameters -dontwarn org.bouncycastle.jsse.BCSSLSocket -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/Provider.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/Provider.kt index ef69615cd..6007d7f3e 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/Provider.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/Provider.kt @@ -3,7 +3,6 @@ package com.topjohnwu.magisk.core import android.os.Bundle import com.topjohnwu.magisk.core.base.BaseProvider import com.topjohnwu.magisk.core.su.SuCallbackHandler -import com.topjohnwu.magisk.core.su.TestHandler class Provider : BaseProvider() { @@ -13,7 +12,7 @@ class Provider : BaseProvider() { SuCallbackHandler.run(context!!, method, extras) Bundle.EMPTY } - else -> TestHandler.run(method) + else -> Bundle.EMPTY } } } diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/TestImpl.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/TestImpl.kt new file mode 100644 index 000000000..391d4798e --- /dev/null +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/TestImpl.kt @@ -0,0 +1,50 @@ +package com.topjohnwu.magisk.core + +import androidx.annotation.Keep +import com.topjohnwu.magisk.core.di.ServiceLocator +import com.topjohnwu.magisk.core.tasks.MagiskInstaller +import com.topjohnwu.magisk.core.utils.RootUtils +import com.topjohnwu.superuser.CallbackList +import com.topjohnwu.superuser.Shell +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertTrue +import timber.log.Timber + +/** + * We implement all test logic here and mark it with @Keep so that our instrumentation package + * can properly run tests on fully obfuscated release APKs. + */ +@Keep +object TestImpl { + + fun before() { + assertTrue("Should have root access", Shell.getShell().isRoot) + // Make sure the root service is running + RootUtils.Connection.await() + } + + object LogList : CallbackList(Runnable::run) { + override fun onAddElement(e: String) { + Timber.i(e) + } + } + + fun setupMagisk() { + runBlocking { + MagiskInstaller.Emulator(LogList, LogList).exec() + } + } + + fun setupShellGrantTest() { + // Clear existing grant for ADB shell + runBlocking { + ServiceLocator.policyDB.delete(2000) + Config.suAutoResponse = Config.Value.SU_AUTO_ALLOW + Config.prefs.edit().commit() + } + } + + fun testZygisk() { + assertTrue("Zygisk should be enabled", Info.isZygiskEnabled) + } +} diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/su/TestHandler.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/su/TestHandler.kt deleted file mode 100644 index bdb070fc3..000000000 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/su/TestHandler.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.topjohnwu.magisk.core.su - -import android.os.Bundle -import com.topjohnwu.magisk.core.Config -import com.topjohnwu.magisk.core.Info -import com.topjohnwu.magisk.core.di.ServiceLocator -import com.topjohnwu.magisk.core.tasks.MagiskInstaller -import com.topjohnwu.magisk.core.utils.RootUtils -import com.topjohnwu.superuser.CallbackList -import com.topjohnwu.superuser.Shell -import kotlinx.coroutines.Runnable -import kotlinx.coroutines.runBlocking -import timber.log.Timber - -object TestHandler { - - object LogList : CallbackList(Runnable::run) { - override fun onAddElement(e: String) { - Timber.i(e) - } - } - - fun run(method: String): Bundle { - var reason: String? = null - - fun prerequisite(): Boolean { - // Make sure the Magisk app can get root - val shell = Shell.getShell() - if (!shell.isRoot) { - reason = "shell not root" - return false - } - - // Make sure the root service is running - RootUtils.Connection.await() - return true - } - - fun setup(): Boolean { - return runBlocking { - MagiskInstaller.Emulator(LogList, LogList).exec() - } - } - - fun test(): Boolean { - // Make sure Zygisk works correctly - if (!Info.isZygiskEnabled) { - reason = "zygisk not enabled" - return false - } - - // Clear existing grant for ADB shell - runBlocking { - ServiceLocator.policyDB.delete(2000) - Config.suAutoResponse = Config.Value.SU_AUTO_ALLOW - Config.prefs.edit().commit() - } - return true - } - - val result = prerequisite() && runCatching { - when (method) { - "setup" -> setup() - "test" -> test() - else -> { - reason = "unknown method" - false - } - } - }.getOrElse { - reason = it.stackTraceToString() - false - } - - return Bundle().apply { - putBoolean("result", result) - if (reason != null) putString("reason", reason) - } - } -} diff --git a/app/test/.gitignore b/app/test/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/app/test/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/test/build.gradle.kts b/app/test/build.gradle.kts new file mode 100644 index 000000000..1ed6b421b --- /dev/null +++ b/app/test/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("com.android.application") + kotlin("android") +} + +android { + namespace = "com.topjohnwu.magisk.test" + + defaultConfig { + applicationId = "com.topjohnwu.magisk.test" + versionCode = 1 + versionName = "1.0" + } +} + +setupAppCommon() + +dependencies { + compileOnly(project(":app:core")) + + implementation(libs.test.runner) + implementation(libs.test.rules) + implementation(libs.test.junit) +} diff --git a/app/test/src/main/AndroidManifest.xml b/app/test/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9f8b98755 --- /dev/null +++ b/app/test/src/main/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/app/test/src/main/java/com/topjohnwu/magisk/test/Environment.kt b/app/test/src/main/java/com/topjohnwu/magisk/test/Environment.kt new file mode 100644 index 000000000..62159dbe7 --- /dev/null +++ b/app/test/src/main/java/com/topjohnwu/magisk/test/Environment.kt @@ -0,0 +1,29 @@ +package com.topjohnwu.magisk.test + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.topjohnwu.magisk.core.TestImpl +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class Environment { + + companion object { + @BeforeClass + @JvmStatic + fun before() { + TestImpl.before() + } + } + + @Test + fun setupMagisk() { + TestImpl.setupMagisk() + } + + @Test + fun setupShellGrantTest() { + TestImpl.setupShellGrantTest() + } +} diff --git a/app/test/src/main/java/com/topjohnwu/magisk/test/MagiskAppTest.kt b/app/test/src/main/java/com/topjohnwu/magisk/test/MagiskAppTest.kt new file mode 100644 index 000000000..109978e51 --- /dev/null +++ b/app/test/src/main/java/com/topjohnwu/magisk/test/MagiskAppTest.kt @@ -0,0 +1,24 @@ +package com.topjohnwu.magisk.test + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.topjohnwu.magisk.core.TestImpl +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MagiskAppTest { + + companion object { + @BeforeClass + @JvmStatic + fun before() { + TestImpl.before() + } + } + + @Test + fun testZygisk() { + TestImpl.testZygisk() + } +} diff --git a/build.py b/build.py index 1a8ddc23f..99de53552 100755 --- a/build.py +++ b/build.py @@ -426,19 +426,20 @@ def build_apk(module: str): source = Path(*paths, "build", "outputs", "apk", build_type, apk) target = config["outdir"] / apk mv(source, target) - header(f"Output: {target}") + return target def build_app(): header("* Building the Magisk app") - build_apk(":app:apk") + apk = build_apk(":app:apk") build_type = "release" if args.release else "debug" # Rename apk-variant.apk to app-variant.apk - source = config["outdir"] / f"apk-{build_type}.apk" - target = config["outdir"] / f"app-{build_type}.apk" + source = apk + target = apk.parent / apk.name.replace("apk-", "app-") mv(source, target) + header(f"Output: {target}") # Stub building is directly integrated into the main app # build process. Copy the stub APK into output directory. @@ -449,7 +450,14 @@ def build_app(): def build_stub(): header("* Building the stub app") - build_apk(":app:stub") + apk = build_apk(":app:stub") + header(f"Output: {apk}") + + +def build_test(): + header("* Building the test app") + apk = build_apk(":app:test") + header(f"Output: {apk}") ################ @@ -491,6 +499,7 @@ def cleanup(): def build_all(): build_native() build_app() + build_test() ############ @@ -719,6 +728,8 @@ def parse_args(): stub_parser = subparsers.add_parser("stub", help="build the stub app") + test_parser = subparsers.add_parser("test", help="build the test app") + clean_parser = subparsers.add_parser("clean", help="cleanup") clean_parser.add_argument( "targets", nargs="*", help="native, cpp, rust, java, or empty to clean all" @@ -757,6 +768,7 @@ def parse_args(): rustup_parser.set_defaults(func=setup_rustup) app_parser.set_defaults(func=build_app) stub_parser.set_defaults(func=build_stub) + test_parser.set_defaults(func=build_test) emu_parser.set_defaults(func=setup_avd) avd_patch_parser.set_defaults(func=patch_avd_file) clean_parser.set_defaults(func=cleanup) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3886b0490..0e259f0a7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,9 @@ transition = { module = "androidx.transition:transition", version = "1.5.1" } collection-ktx = { module = "androidx.collection:collection-ktx", version = "1.4.5" } material = { module = "com.google.android.material:material", version = "1.12.0" } jdk-libs = { module = "com.android.tools:desugar_jdk_libs_nio", version = "2.1.3" } +test-runner = { module = "androidx.test:runner", version = "1.6.2" } +test-rules = { module = "androidx.test:rules", version = "1.6.1" } +test-junit = { module = "androidx.test.ext:junit", version = "1.2.1" } # topjohnwu indeterminate-checkbox = { module = "com.github.topjohnwu:indeterminate-checkbox", version = "1.0.7" } diff --git a/scripts/avd_magisk.sh b/scripts/avd_magisk.sh index a22f7d91f..3bd018125 100755 --- a/scripts/avd_magisk.sh +++ b/scripts/avd_magisk.sh @@ -66,7 +66,7 @@ fi # Stop zygote (and previous setup if exists) magisk --stop 2>/dev/null -stop zygote +stop if [ -d /debug_ramdisk ]; then umount -l /debug_ramdisk 2>/dev/null fi @@ -166,7 +166,7 @@ fi # Boot up $MAGISKTMP/magisk --post-fs-data -start zygote +start $MAGISKTMP/magisk --service # Make sure reset nb prop after zygote starts sleep 2 diff --git a/scripts/test_common.sh b/scripts/test_common.sh index 4f6d0a18a..95a93bdeb 100644 --- a/scripts/test_common.sh +++ b/scripts/test_common.sh @@ -7,6 +7,7 @@ export PATH="$PATH:$ANDROID_HOME/platform-tools" emu="$ANDROID_HOME/emulator/emulator" sdk="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" avd="$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager" +test_pkg='com.topjohnwu.magisk.test' boot_timeout=600 @@ -23,17 +24,12 @@ print_error() { echo -e "\n\033[41;39m${1}\033[0m\n" } -run_content_cmd() { - while true; do - local out=$(adb shell /system/xbin/su 0 content call --uri content://com.topjohnwu.magisk.provider --method $1 | tee /dev/fd/2) - if ! grep -q 'Bundle\[' <<< "$out"; then - # The call failed, wait a while and retry later - sleep 30 - else - grep -q 'result=true' <<< "$out" - return $? - fi - done +run_instrument_tests() { + local out=$(adb shell am instrument -w \ + --user 0 \ + -e class "$1" \ + com.topjohnwu.magisk.test/androidx.test.runner.AndroidJUnitRunner) + grep -q 'OK (' <<< "$out" } test_setup() { @@ -43,12 +39,18 @@ test_setup() { # Install the Magisk app adb install -r -g out/app-${variant}.apk - # Use the app to run setup and reboot - run_content_cmd setup + # Install the test app + adb install -r -g out/test-${variant}.apk + + # Run setup through the test app + run_instrument_tests "$test_pkg.Environment#setupMagisk" } test_app() { # Run app tests - run_content_cmd test + run_instrument_tests "$test_pkg.MagiskAppTest" + + # Test shell su request + run_instrument_tests "$test_pkg.Environment#setupShellGrantTest" adb shell /system/xbin/su 2000 su -c id | tee /dev/fd/2 | grep -q 'uid=0' } diff --git a/settings.gradle.kts b/settings.gradle.kts index 0dbced7f5..9732e55f8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,4 +8,4 @@ dependencyResolutionManagement { } } rootProject.name = "Magisk" -include(":app:apk", ":app:core", ":app:shared", ":app:stub", ":native") +include(":app:apk", ":app:core", ":app:shared", ":app:stub", ":app:test", ":native")