Install and test LSPosed through test app

This commit is contained in:
topjohnwu 2024-12-24 17:11:08 -08:00 committed by John Wu
parent 32faa4ced6
commit 2baedf74d1
8 changed files with 136 additions and 49 deletions

View File

@ -65,4 +65,5 @@ dependencies {
// However, we don't want to bundle test dependencies. // However, we don't want to bundle test dependencies.
// That's why we make it compileOnly. // That's why we make it compileOnly.
compileOnly(libs.test.junit) compileOnly(libs.test.junit)
compileOnly(libs.test.uiautomator)
} }

View File

@ -0,0 +1,57 @@
package com.topjohnwu.magisk.test
import android.app.UiAutomation
import androidx.annotation.Keep
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import org.junit.After
import org.junit.Assert.assertNotNull
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
@Keep
@RunWith(AndroidJUnit4::class)
class AdditionalTest {
companion object {
private const val SHELL_PKG = "com.android.shell"
private const val LSPOSED_CATEGORY = "org.lsposed.manager.LAUNCH_MANAGER"
private const val LSPOSED_PKG = "org.lsposed.manager"
}
private lateinit var uiAutomation: UiAutomation
private lateinit var device: UiDevice
@Before
fun setup() {
val inst = InstrumentationRegistry.getInstrumentation()
uiAutomation = inst.uiAutomation
device = UiDevice.getInstance(inst)
}
@After
fun teardown() {
device.pressHome()
}
@Test
fun testLaunchLsposedManager() {
assumeTrue(Environment.lsposed())
uiAutomation.executeShellCommand(
"am start -c $LSPOSED_CATEGORY $SHELL_PKG/.BugreportWarningActivity"
)
val pattern = Pattern.compile("$LSPOSED_PKG:id/.*")
assertNotNull(
"LSPosed manager launch failed",
device.wait(Until.hasObject(By.res(pattern)), TimeUnit.SECONDS.toMillis(10))
)
}
}

View File

@ -1,17 +1,27 @@
package com.topjohnwu.magisk.test package com.topjohnwu.magisk.test
import android.app.Notification
import android.content.Context
import android.os.Build
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.core.net.toUri
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.topjohnwu.magisk.core.BuildConfig.APP_PACKAGE_NAME import com.topjohnwu.magisk.core.BuildConfig.APP_PACKAGE_NAME
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.di.ServiceLocator import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.download.DownloadNotifier
import com.topjohnwu.magisk.core.download.DownloadProcessor
import com.topjohnwu.magisk.core.ktx.cachedFile
import com.topjohnwu.magisk.core.model.su.SuPolicy import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.core.tasks.AppMigration import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.core.tasks.FlashZip
import com.topjohnwu.magisk.core.tasks.MagiskInstaller import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.CallbackList
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.BeforeClass import org.junit.BeforeClass
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -25,19 +35,55 @@ class Environment {
@BeforeClass @BeforeClass
@JvmStatic @JvmStatic
fun before() = MagiskAppTest.before() fun before() = MagiskAppTest.before()
fun lsposed(): Boolean {
return Build.VERSION.SDK_INT >= 27 && Build.VERSION.SDK_INT <= 34
}
private const val LSPOSED_URL =
"https://github.com/LSPosed/LSPosed/releases/download/v1.9.2/LSPosed-v1.9.2-7024-zygisk-release.zip"
}
object TimberLog : CallbackList<String>(Runnable::run) {
override fun onAddElement(e: String) {
Timber.i(e)
}
}
private lateinit var mContext: Context
@Before
fun setup() {
mContext = InstrumentationRegistry.getInstrumentation().targetContext
} }
@Test @Test
fun setupMagisk() { fun setupMagisk() {
val log = object : CallbackList<String>(Runnable::run) {
override fun onAddElement(e: String) {
Timber.i(e)
}
}
runBlocking { runBlocking {
assertTrue( assertTrue(
"Magisk setup failed", "Magisk setup failed",
MagiskInstaller.Emulator(log, log).exec() MagiskInstaller.Emulator(TimberLog, TimberLog).exec()
)
}
}
@Test
fun setupLsposed() {
assumeTrue(lsposed())
val notify = object : DownloadNotifier {
override val context = mContext
override fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit) {}
}
val processor = DownloadProcessor(notify)
val zip = mContext.cachedFile("lsposed.zip")
runBlocking {
ServiceLocator.networkService.fetchFile(LSPOSED_URL).byteStream().use {
processor.handleModule(it, zip.toUri())
}
assertTrue(
"LSPosed installation failed",
FlashZip(zip.toUri(), TimberLog, TimberLog).exec()
) )
} }
} }
@ -65,7 +111,7 @@ class Environment {
assertTrue( assertTrue(
"App hiding failed", "App hiding failed",
AppMigration.patchAndHide( AppMigration.patchAndHide(
context = InstrumentationRegistry.getInstrumentation().targetContext, context = mContext,
label = "Settings", label = "Settings",
pkg = "repackaged.$APP_PACKAGE_NAME" pkg = "repackaged.$APP_PACKAGE_NAME"
) )
@ -78,9 +124,7 @@ class Environment {
runBlocking { runBlocking {
assertTrue( assertTrue(
"App restoration failed", "App restoration failed",
AppMigration.restoreApp( AppMigration.restoreApp(mContext)
context = InstrumentationRegistry.getInstrumentation().targetContext
)
) )
} }
} }

View File

@ -26,4 +26,5 @@ dependencies {
implementation(libs.test.runner) implementation(libs.test.runner)
implementation(libs.test.rules) implementation(libs.test.rules)
implementation(libs.test.junit) implementation(libs.test.junit)
implementation(libs.test.uiautomator)
} }

View File

@ -6,6 +6,17 @@ import androidx.test.runner.AndroidJUnitRunner
class TestRunner : AndroidJUnitRunner() { class TestRunner : AndroidJUnitRunner() {
override fun onCreate(arguments: Bundle) { override fun onCreate(arguments: Bundle) {
// Support short-hand ".ClassName"
arguments.getString("class")?.let {
val classArg = it.split(",").joinToString(separator = ",") { clz ->
if (clz.startsWith(".")) {
"com.topjohnwu.magisk.test$clz"
} else {
clz
}
}
arguments.putString("class", classArg)
}
// Force using the target context's classloader to run tests // Force using the target context's classloader to run tests
arguments.putString("classLoader", TestClassLoader::class.java.name) arguments.putString("classLoader", TestClassLoader::class.java.name)
super.onCreate(arguments) super.onCreate(arguments)

View File

@ -47,6 +47,7 @@ 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-runner = { module = "androidx.test:runner", version = "1.6.2" }
test-rules = { module = "androidx.test:rules", version = "1.6.1" } test-rules = { module = "androidx.test:rules", version = "1.6.1" }
test-junit = { module = "androidx.test.ext:junit", version = "1.2.1" } test-junit = { module = "androidx.test.ext:junit", version = "1.2.1" }
test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version = "2.3.0" }
# topjohnwu # topjohnwu
indeterminate-checkbox = { module = "com.github.topjohnwu:indeterminate-checkbox", version = "1.0.7" } indeterminate-checkbox = { module = "com.github.topjohnwu:indeterminate-checkbox", version = "1.0.7" }

View File

@ -4,13 +4,10 @@ set -xe
. scripts/test_common.sh . scripts/test_common.sh
emu_args_base="-no-window -no-audio -no-boot-anim -gpu swiftshader_indirect -read-only -no-snapshot -cores $core_count" emu_args_base="-no-window -no-audio -no-boot-anim -gpu swiftshader_indirect -read-only -no-snapshot -cores $core_count"
lsposed_url='https://github.com/LSPosed/LSPosed/releases/download/v1.9.2/LSPosed-v1.9.2-7024-zygisk-release.zip'
emu_pid= emu_pid=
atd_min_api=30 atd_min_api=30
atd_max_api=35 atd_max_api=35
lsposed_min_api=27
lsposed_max_api=34
huge_ram_min_api=26 huge_ram_min_api=26
cleanup() { cleanup() {
@ -81,35 +78,10 @@ test_emu() {
run_setup $variant run_setup $variant
local lsposed
if [ $api -ge $lsposed_min_api -a $api -le $lsposed_max_api ]; then
lsposed=true
else
lsposed=false
fi
# Install LSPosed
if $lsposed; then
adb push out/lsposed.zip /data/local/tmp/lsposed.zip
echo 'PATH=$PATH:/debug_ramdisk magisk --install-module /data/local/tmp/lsposed.zip' | adb shell /system/xbin/su
fi
adb reboot adb reboot
wait_emu wait_for_boot wait_emu wait_for_boot
run_tests run_tests
# Try to launch LSPosed
if $lsposed; then
adb shell rm -f /data/local/tmp/window_dump.xml
adb shell am start -c org.lsposed.manager.LAUNCH_MANAGER com.android.shell/.BugreportWarningActivity
while adb shell '[ ! -f /data/local/tmp/window_dump.xml ]'; do
sleep 10
adb shell uiautomator dump /data/local/tmp/window_dump.xml
done
adb shell grep -q org.lsposed.manager /data/local/tmp/window_dump.xml
adb pull /data/local/tmp/window_dump.xml
fi
} }
test_main() { test_main() {
@ -220,7 +192,6 @@ if [ -n "$FORCE_32_BIT" ]; then
fi fi
yes | "$sdk" --licenses > /dev/null yes | "$sdk" --licenses > /dev/null
curl -L $lsposed_url -o out/lsposed.zip
"$sdk" --channel=3 platform-tools emulator "$sdk" --channel=3 platform-tools emulator
adb kill-server adb kill-server

View File

@ -36,8 +36,7 @@ am_instrument() {
else else
test_pkg=com.topjohnwu.magisk.test test_pkg=com.topjohnwu.magisk.test
fi fi
local out=$(adb shell am instrument -w --user 0 \ local out=$(adb shell am instrument -w --user 0 -e class "$1" \
-e class "com.topjohnwu.magisk.test.$1" \
"$test_pkg/com.topjohnwu.magisk.test.TestRunner") "$test_pkg/com.topjohnwu.magisk.test.TestRunner")
grep -q 'OK (' <<< "$out" grep -q 'OK (' <<< "$out"
} }
@ -59,34 +58,36 @@ run_setup() {
adb install -r -g out/test.apk adb install -r -g out/test.apk
# Run setup through the test app # Run setup through the test app
am_instrument 'Environment#setupMagisk' am_instrument '.Environment#setupMagisk'
# Install LSPosed
am_instrument '.Environment#setupLsposed'
} }
run_tests() { run_tests() {
# Run app tests # Run app tests
am_instrument 'MagiskAppTest' am_instrument '.MagiskAppTest,.AdditionalTest'
# Test shell su request # Test shell su request
am_instrument 'Environment#setupShellGrantTest' am_instrument '.Environment#setupShellGrantTest'
adb shell /system/xbin/su 2000 su -c id | tee /dev/fd/2 | grep -q 'uid=0' adb shell /system/xbin/su 2000 su -c id | tee /dev/fd/2 | grep -q 'uid=0'
adb shell am force-stop com.topjohnwu.magisk adb shell am force-stop com.topjohnwu.magisk
# Test app hiding # Test app hiding
am_instrument 'Environment#setupAppHide' am_instrument '.Environment#setupAppHide'
wait_for_pm com.topjohnwu.magisk wait_for_pm com.topjohnwu.magisk
# Make sure it still works # Make sure it still works
am_instrument 'MagiskAppTest' true am_instrument '.MagiskAppTest' true
# Test shell su request # Test shell su request
am_instrument 'Environment#setupShellGrantTest' true am_instrument '.Environment#setupShellGrantTest' true
adb shell /system/xbin/su 2000 su -c id | tee /dev/fd/2 | grep -q 'uid=0' adb shell /system/xbin/su 2000 su -c id | tee /dev/fd/2 | grep -q 'uid=0'
adb shell am force-stop repackaged.com.topjohnwu.magisk adb shell am force-stop repackaged.com.topjohnwu.magisk
# Test app restore # Test app restore
am_instrument 'Environment#setupAppRestore' true am_instrument '.Environment#setupAppRestore' true
wait_for_pm repackaged.com.topjohnwu.magisk wait_for_pm repackaged.com.topjohnwu.magisk
# Make sure it still works # Make sure it still works
am_instrument 'MagiskAppTest' am_instrument '.MagiskAppTest'
} }