Redesign test APK architecture

The test APK and the main APK share the same process and classloader,
and in the non-hidden case, the test APK's classes take precedence over
the ones in the main APK. This causes issues because the test APK and
main APK share some dependencies, but don't always use the same
version. This is especially problematic for the Kotlin stdlib and
AndroidX dependencies.

The solution here is to rely on R8's obfuscation feature and repackage
all potentially conflicting classes into a separate package in the test
APK. To ensure that the test classes are always using the same classes
as the main APK, we have to directly implement all tests inside the main
APK, making the test APK purely a "test runner with test dependencies".

As a result, the test APK can only be used when built in release mode,
because R8 no longer allow class obfuscation to be enabled when building
for debug versions.
This commit is contained in:
topjohnwu 2024-12-25 20:17:57 -08:00
parent ccdb0b5d13
commit 32faa4ced6
6 changed files with 37 additions and 9 deletions

View File

@ -10,14 +10,19 @@ android {
applicationId = "com.topjohnwu.magisk.test"
versionCode = 1
versionName = "1.0"
proguardFile("proguard-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = true
}
}
}
setupAppCommon()
dependencies {
compileOnly(project(":app:core"))
implementation(libs.test.runner)
implementation(libs.test.rules)
implementation(libs.test.junit)

13
app/test/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,13 @@
# Keep all test dependencies
-keep class org.junit.** { *; }
-keep class androidx.test.** { *; }
# Make sure the classloader constructor is kept
-keepclassmembers class com.topjohnwu.magisk.test.TestClassLoader { <init>(); }
# Repackage dependencies
-repackageclasses 'deps'
-allowaccessmodification
# Keep attributes for stacktrace
-keepattributes *

View File

@ -2,8 +2,6 @@
<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>

View File

@ -1,5 +1,6 @@
#!/usr/bin/env python3
import argparse
import copy
import glob
import lzma
import multiprocessing
@ -455,9 +456,18 @@ def build_stub():
def build_test():
header("* Building the test app")
apk = build_apk(":app:test")
header(f"Output: {apk}")
global args
args_bak = copy.copy(args)
# Test APK has to be built as release to prevent classname clash
args.release = True
try:
header("* Building the test app")
source = build_apk(":app:test")
target = source.parent / "test.apk"
mv(source, target)
header(f"Output: {target}")
finally:
args = args_bak
################

View File

@ -17,7 +17,6 @@ import org.gradle.api.Action
import org.gradle.api.DefaultTask
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Copy
@ -300,6 +299,9 @@ fun Project.setupAppCommon() {
defaultConfig {
targetSdk = 35
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt")
)
}
buildTypes {

View File

@ -56,7 +56,7 @@ run_setup() {
adb install -r -g out/app-${variant}.apk
# Install the test app
adb install -r -g out/test-${variant}.apk
adb install -r -g out/test.apk
# Run setup through the test app
am_instrument 'Environment#setupMagisk'