Introduce instrumentation tests

This commit is contained in:
topjohnwu 2024-12-13 01:09:52 -08:00 committed by John Wu
parent 24615afda1
commit 9112a3a4f5
15 changed files with 190 additions and 104 deletions

View File

@ -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)
}

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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<String>(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)
}
}

View File

@ -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<String>(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)
}
}
}

1
app/test/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

24
app/test/build.gradle.kts Normal file
View File

@ -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)
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission tools:node="removeAll" />
<application tools:node="replace">
<uses-library android:name="android.test.runner" />
</application>
<instrumentation
android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.topjohnwu.magisk"
android:label="Tests for Magisk" />
</manifest>

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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)

View File

@ -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" }

View File

@ -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

View File

@ -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'
}

View File

@ -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")