Skip to content

Commit 384e37d

Browse files
fix: better tests
Signed-off-by: androidacy-user <[email protected]>
1 parent 62fb3d5 commit 384e37d

File tree

13 files changed

+544
-7
lines changed

13 files changed

+544
-7
lines changed

.github/workflows/android.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,34 @@ jobs:
3838
run: |
3939
cd samples
4040
./gradlew :application:assembleRelease :application-global-obfuscate:assembleRelease :library:assembleRelease :library-may-obfuscate:assembleRelease :library-obfuscate:assembleRelease --parallel --configuration-cache
41+
42+
- name: Build testApp release (verify ProGuard rules)
43+
run: ./gradlew :testApp:assembleRelease --configuration-cache
44+
45+
- name: Enable KVM group permissions
46+
run: |
47+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
48+
sudo udevadm control --reload-rules
49+
sudo udevadm trigger --name-match=kvm
50+
51+
- name: Run instrumented tests on managed device
52+
run: ./gradlew :testApp:pixel6api34DebugAndroidTest -Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect --configuration-cache
53+
54+
- name: Upload test results
55+
if: always()
56+
uses: actions/upload-artifact@v4
57+
with:
58+
name: test-results
59+
path: testApp/build/reports/androidTests/
60+
retention-days: 7
61+
62+
- name: Upload test APKs
63+
if: always()
64+
uses: actions/upload-artifact@v4
65+
with:
66+
name: test-apks
67+
path: |
68+
testApp/build/outputs/apk/debug/*.apk
69+
testApp/build/outputs/apk/release/*.apk
70+
retention-days: 7
4171

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ Then in your module's `build.gradle.kts`:
3030
```kotlin
3131
plugins {
3232
id("com.android.application") // or "com.android.library"
33-
id("com.androidacy.lsparanoid") version "0.10.1"
33+
id("com.androidacy.lsparanoid") version "0.10.2"
3434
}
3535

3636
dependencies {
37-
implementation("com.androidacy.lsparanoid:core:0.10.1")
37+
implementation("com.androidacy.lsparanoid:core:0.10.2")
3838
}
3939
```
4040

@@ -51,7 +51,7 @@ Paranoid plugin can be configured using `lsparanoid` extension object.
5151
The following is an example `build.gradle.kts` that configures `lsparanoid` extension object with default values.
5252
```kotlin
5353
plugins {
54-
id("com.androidacy.lsparanoid") version "0.10.1"
54+
id("com.androidacy.lsparanoid") version "0.10.2"
5555
// other plugins...
5656
}
5757

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ plugins {
1010

1111
allprojects {
1212
group = "com.androidacy.lsparanoid"
13-
version = "0.10.1"
13+
version = "0.10.2"
1414

1515
plugins.withType(JavaPlugin::class.java) {
1616
extensions.configure(JavaPluginExtension::class.java) {

core/consumer-rules.pro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Keep Deobfuscator classes and their inner Chunk classes, but allow renaming/obfuscation
33

44
# Keep all Deobfuscator classes (allow obfuscation of class names)
5-
-keep,allowobfuscation class **.Deobfuscator$** {
5+
-keep,allowobfuscation class **.Deobfuscator {
66
# Keep getString method - called from obfuscated code
77
public static java.lang.String getString(long);
88

@@ -13,7 +13,7 @@
1313
}
1414

1515
# Keep Chunk inner classes and their DATA fields
16-
-keep,allowobfuscation class **.Deobfuscator$**$Chunk* {
16+
-keep,allowobfuscation class **.Deobfuscator$Chunk* {
1717
static final byte[] DATA;
1818
}
1919

gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2+
android.useAndroidX=true

settings.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
22

33
pluginManagement {
44
repositories {
5+
mavenLocal()
56
gradlePluginPortal()
67
google()
78
mavenCentral()
@@ -17,4 +18,4 @@ dependencyResolutionManagement {
1718

1819
rootProject.name = "LSParanoid"
1920

20-
include(":core", ":processor", ":gradle-plugin")
21+
include(":core", ":processor", ":gradle-plugin", ":testApp")

testApp/README.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# LSParanoid Test Application
2+
3+
Dedicated test application to verify string obfuscation works correctly in release builds with R8/ProGuard minification enabled.
4+
5+
## Purpose
6+
7+
This test app validates that the ProGuard rules in `core/consumer-rules.pro` are correct and that:
8+
1. Activities with `@Obfuscate` annotation can be launched without crashes
9+
2. Obfuscated strings can be retrieved via reflection (`ensureChunkLoaded` method)
10+
3. The app doesn't crash with `NoSuchMethodException` in minified release builds
11+
4. Chunk loading works correctly for multiple and concurrent string accesses
12+
13+
## ✅ Build Verification (Automated)
14+
15+
The most important verification is that a minified release APK with obfuscation builds successfully:
16+
17+
```bash
18+
./gradlew :testApp:assembleRelease
19+
```
20+
21+
**What this proves:**
22+
- LSParanoid obfuscation is applied to all `@Obfuscate` annotated classes
23+
- R8 minification runs without errors
24+
- ProGuard consumer rules from `core/consumer-rules.pro` are correct
25+
- The `ensureChunkLoaded` method and deobfuscation infrastructure are preserved
26+
27+
**Output:** `testApp/build/outputs/apk/release/testApp-release-unsigned.apk` (~20KB)
28+
29+
**Status**: Successfully tested - release APK builds without errors
30+
31+
## Running Instrumented Tests
32+
33+
### Prerequisites
34+
35+
- Android SDK installed with API 34+ system images
36+
- At least 5GB free disk space (for system image downloads)
37+
- Working emulator or physical device
38+
39+
### Quick Test (Gradle Managed Device - Headless)
40+
41+
Run all tests on a managed emulator (Android 14, API 34):
42+
43+
```bash
44+
./gradlew :testApp:pixel6api34DebugAndroidTest \
45+
-Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect
46+
```
47+
48+
**Note**: First run downloads ~500MB AOSP ATD system image.
49+
50+
### On Physical Device or Connected Emulator
51+
52+
```bash
53+
# Debug build (no minification, tests obfuscation only)
54+
./gradlew :testApp:connectedDebugAndroidTest
55+
56+
# Release build (full minification + obfuscation)
57+
./gradlew :testApp:connectedReleaseAndroidTest
58+
```
59+
60+
### With Display (For Debugging)
61+
62+
To see the emulator while tests run:
63+
64+
```bash
65+
./gradlew :testApp:pixel6api34DebugAndroidTest --enable-display
66+
```
67+
68+
## CI/CD Usage
69+
70+
For GitHub Actions or other CI environments:
71+
72+
```bash
73+
# Build verification (no emulator needed)
74+
./gradlew :testApp:assembleRelease
75+
76+
# Full instrumented tests (requires emulator)
77+
./gradlew :testApp:pixel6api34DebugAndroidTest \
78+
-Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect
79+
```
80+
81+
## Test Configuration
82+
83+
- **Managed Device**: Pixel 6 API 34 (Android 14)
84+
- **System Image**: AOSP ATD (Automated Test Device) optimized for headless CI
85+
- **Build Types**:
86+
- Debug: Obfuscation enabled, no minification (faster tests)
87+
- Release: Full minification + obfuscation (production-like)
88+
- **Test Framework**: AndroidX Test + JUnit4 + Espresso
89+
90+
## What Gets Tested
91+
92+
### ObfuscationTest.java
93+
94+
1. **testApplicationContext**: Verifies basic app initialization with obfuscated code
95+
2. **testObfuscatedActivityLaunches**: Tests activity with multiple obfuscated string constants
96+
3. **testObfuscatedUtilityClass**: Tests static utility class with obfuscated strings
97+
4. **testMultipleObfuscatedStringAccess**: Validates chunk loading with 100+ repeated accesses
98+
5. **testConcurrentStringAccess**: Tests thread safety with 10 threads × 50 accesses each
99+
100+
## Build Outputs
101+
102+
After running tests, check:
103+
- **Debug APK**: `testApp/build/outputs/apk/debug/testApp-debug.apk`
104+
- **Release APK**: `testApp/build/outputs/apk/release/testApp-release-unsigned.apk`
105+
- **ProGuard mapping**: `testApp/build/outputs/mapping/release/mapping.txt`
106+
- **Test results**: `testApp/build/reports/androidTests/`
107+
108+
## Troubleshooting
109+
110+
### NoSuchMethodException in Release Build
111+
112+
If you see `NoSuchMethodException: m32.ensureChunkLoaded [int]`:
113+
- The ProGuard patterns in `core/consumer-rules.pro` don't match generated code
114+
- Verify the pattern is `**.Deobfuscator` (NOT `**.Deobfuscator$**`)
115+
- Check consumer rules are applied: examine the mapping file
116+
117+
### Managed Device Won't Start
118+
119+
- Ensure Android SDK includes API 34 system images
120+
- First run downloads system image (500MB+, several minutes)
121+
- Requires at least 5GB free disk space
122+
- Use `--info` flag for detailed output
123+
124+
### R8 Compilation Errors
125+
126+
If R8 fails with missing class warnings:
127+
- Check `testApp/build/outputs/mapping/debugAndroidTest/missing_rules.txt`
128+
- Add `-dontwarn` rules to `testApp/proguard-rules.pro`
129+
130+
### Tests Pass on Debug but Fail on Release
131+
132+
This indicates a ProGuard configuration issue:
133+
- R8 is removing obfuscation infrastructure in release builds
134+
- Check `core/consumer-rules.pro` is in `META-INF/proguard/`
135+
- Verify rules match actual generated class structure
136+
137+
## Verifying ProGuard Rules
138+
139+
To see what's kept after minification:
140+
141+
```bash
142+
./gradlew :testApp:assembleRelease
143+
grep -i deobfuscator testApp/build/outputs/mapping/release/mapping.txt
144+
```
145+
146+
You should see the Deobfuscator class and `ensureChunkLoaded` method preserved (though renamed).

testApp/build.gradle.kts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
plugins {
2+
id("com.android.application") version "8.13.0"
3+
id("com.androidacy.lsparanoid") version "0.10.2"
4+
}
5+
6+
lsparanoid {
7+
// Enable obfuscation for both debug and release to allow testing
8+
variantFilter = { variant -> true }
9+
}
10+
11+
android {
12+
namespace = "com.androidacy.lsparanoid.testapp"
13+
compileSdk = 35
14+
15+
defaultConfig {
16+
applicationId = "com.androidacy.lsparanoid.testapp"
17+
minSdk = 24
18+
targetSdk = 35
19+
versionCode = 1
20+
versionName = "1.0"
21+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
22+
}
23+
24+
buildTypes {
25+
debug {
26+
// No minification for debug - tests run faster
27+
isMinifyEnabled = false
28+
}
29+
release {
30+
// Enable minification for release to test ProGuard rules
31+
isMinifyEnabled = true
32+
proguardFiles(
33+
getDefaultProguardFile("proguard-android-optimize.txt"),
34+
"proguard-rules.pro"
35+
)
36+
}
37+
}
38+
39+
compileOptions {
40+
sourceCompatibility = JavaVersion.VERSION_17
41+
targetCompatibility = JavaVersion.VERSION_17
42+
}
43+
44+
testOptions {
45+
managedDevices {
46+
localDevices {
47+
create("pixel6api34") {
48+
device = "Pixel 6"
49+
apiLevel = 34
50+
systemImageSource = "aosp-atd"
51+
}
52+
}
53+
}
54+
}
55+
}
56+
57+
dependencies {
58+
implementation(project(":core"))
59+
60+
// Testing dependencies
61+
androidTestImplementation("androidx.test.ext:junit:1.1.5")
62+
androidTestImplementation("androidx.test:runner:1.5.2")
63+
androidTestImplementation("androidx.test:rules:1.5.0")
64+
androidTestImplementation("androidx.test:core:1.5.0")
65+
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
66+
}

testApp/proguard-rules.pro

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# ProGuard rules for testApp
2+
3+
# Keep test activity for instrumentation
4+
-keep class com.androidacy.lsparanoid.testapp.MainActivity {
5+
public <init>(...);
6+
public <methods>;
7+
}
8+
9+
# Keep test utility class
10+
-keep class com.androidacy.lsparanoid.testapp.StringUtils {
11+
public static <methods>;
12+
}
13+
14+
# Standard Android rules
15+
-keepattributes *Annotation*
16+
-keepattributes SourceFile,LineNumberTable
17+
18+
# Fix for missing ErrorProne annotations
19+
-dontwarn com.google.errorprone.annotations.**
20+
-dontwarn org.checkerframework.**
21+
-dontwarn javax.annotation.**
22+
23+
# Note: LSParanoid's consumer rules from core module will be automatically
24+
# applied to keep the Deobfuscator infrastructure intact

0 commit comments

Comments
 (0)