Skip to content

Extract R8 DCE verification into featured-shrinker-tests module#165

Open
kirich1409 wants to merge 1 commit intomainfrom
refactor/extract-shrinker-tests
Open

Extract R8 DCE verification into featured-shrinker-tests module#165
kirich1409 wants to merge 1 commit intomainfrom
refactor/extract-shrinker-tests

Conversation

@kirich1409
Copy link
Copy Markdown
Contributor

Summary

  • Extracted R8 dead-code-elimination tests from featured-gradle-plugin into standalone featured-shrinker-tests module
  • Split monolithic R8EliminationTest (~660 lines) into 8 focused files across 5 packages
  • Removed R8/ASM test dependencies from featured-gradle-plugin
  • Added documentation guide docs/guides/r8-verification.md

Test plan

  • ./gradlew :featured-shrinker-tests:test — 5 tests pass
  • ./gradlew :featured-gradle-plugin:test — 144 tests pass

🤖 Generated with Claude Code

Move R8EliminationTest from featured-gradle-plugin into a dedicated
non-published JVM module. Split the monolithic test (~660 lines) into
focused units: bytecode generators, JAR assembly, ProGuard rules,
test harness, assertions, and two test classes (boolean/int flags).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 10, 2026 14:50
@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Extract R8 dead-code-elimination tests into featured-shrinker-tests module

✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
• Extracted 657-line R8 DCE test into dedicated featured-shrinker-tests module
• Split monolithic test into 8 focused files across 5 packages by concern
• Removed R8/ASM dependencies from featured-gradle-plugin
• Added comprehensive R8 verification guide with test scenarios and usage
Diagram
flowchart LR
  A["featured-gradle-plugin<br/>R8EliminationTest<br/>657 lines"] -->|Extract| B["featured-shrinker-tests<br/>8 focused files"]
  B -->|Bytecode| C["SyntheticBytecodeFactory"]
  B -->|Rules| D["ProguardRulesWriter"]
  B -->|Harness| E["R8TestHarness"]
  B -->|Tests| F["R8BooleanFlagEliminationTest<br/>R8IntFlagEliminationTest"]
  B -->|Assertions| G["JarAssertions"]
  H["docs/guides/r8-verification.md"] -->|Documents| B
Loading

Grey Divider

File Changes

1. featured-gradle-plugin/build.gradle.kts Dependencies +0/-2

Remove R8 and ASM test dependencies

featured-gradle-plugin/build.gradle.kts


2. featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/R8EliminationTest.kt 🧪 Tests +0/-657

Delete monolithic R8 elimination test file

featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/R8EliminationTest.kt


3. featured-shrinker-tests/build.gradle.kts ⚙️ Configuration changes +13/-0

Create new shrinker tests module build config

featured-shrinker-tests/build.gradle.kts


View more (10)
4. featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/assertions/JarAssertions.kt 🧪 Tests +30/-0

JAR inspection assertion utilities for DCE verification

featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/assertions/JarAssertions.kt


5. featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/bytecode/JarAssembler.kt 🧪 Tests +43/-0

JAR assembly functions for boolean and int flag scenarios

featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/bytecode/JarAssembler.kt


6. featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/bytecode/SyntheticBytecodeFactory.kt 🧪 Tests +304/-0

ASM-based synthetic bytecode generation for test classes

featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/bytecode/SyntheticBytecodeFactory.kt


7. featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/harness/R8TestHarness.kt 🧪 Tests +75/-0

Base test harness for R8 invocation and temp directory management

featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/harness/R8TestHarness.kt


8. featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/r8/R8BooleanFlagEliminationTest.kt 🧪 Tests +97/-0

Boolean flag dead-code-elimination test scenarios

featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/r8/R8BooleanFlagEliminationTest.kt


9. featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/r8/R8IntFlagEliminationTest.kt 🧪 Tests +64/-0

Int flag dead-code-elimination test scenarios

featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/r8/R8IntFlagEliminationTest.kt


10. featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/rules/ProguardRulesWriter.kt 🧪 Tests +95/-0

ProGuard rules generation matching plugin output format

featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/rules/ProguardRulesWriter.kt


11. settings.gradle.kts ⚙️ Configuration changes +1/-0

Register featured-shrinker-tests module in build

settings.gradle.kts


12. docs/guides/r8-verification.md 📝 Documentation +78/-0

Comprehensive guide for R8 DCE verification testing

docs/guides/r8-verification.md


13. mkdocs.yml 📝 Documentation +1/-0

Add R8 verification guide to documentation navigation

mkdocs.yml


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Apr 10, 2026

Code Review by Qodo

🐞 Bugs (2)   📘 Rule violations (0)   📎 Requirement gaps (0)   🎨 UX Issues (0)
🐞\ ≡ Correctness (1) ⚙ Maintainability (1)

Grey Divider


Action required

1. DCE tests can false-pass 🐞
Description
writeBooleanRules (and writeIntRules) only protects the expected-surviving class’s sideEffect
field, while the synthetic doWork() has no observable effect other than incrementing that field.
R8 can eliminate the unprotected branch/guarded class even when it is still reachable (by stripping
the write-only field/writes), causing the tests to pass without actually validating that
-assumevalues drove the DCE outcome.
Code

featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/rules/ProguardRulesWriter.kt[R30-47]

+ * `-keepclassmembers` pins the `sideEffect` field of the **surviving** branch class so
+ * that R8 cannot treat the `doWork()` call as a no-op and eliminate the class via
+ * write-only field optimisation. The dead branch class intentionally has no such rule,
+ * so R8 is free to eliminate it once the branch becomes unreachable.
+ */
+internal fun writeBooleanRules(
+    dest: File,
+    returnValue: Boolean,
+) {
+    val survivingClass = if (returnValue) IF_BRANCH_CODE_FQN else ELSE_BRANCH_CODE_FQN
+    dest.writeText(
+        """
+        -assumevalues class $BOOL_EXTENSIONS_FQN {
+            boolean $IS_DARK_MODE_ENABLED($CONFIG_VALUES_FQN) return $returnValue;
+        }
+        -keep class $BIFURCATED_CALLER_FQN { *; }
+        -keepclassmembers class $survivingClass { public static int sideEffect; }
+        -dontwarn **
Evidence
The rules intentionally keep sideEffect only for the expected surviving boolean branch class,
leaving the other branch class unpinned (and writeIntRules doesn’t pin PositiveCountCode at
all). The synthetic branch/guarded classes’ doWork() method only mutates that same sideEffect
field, so if R8 removes that field/writes as write-only, the call becomes a no-op and the class can
be removed independent of -assumevalues actually matching.

featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/rules/ProguardRulesWriter.kt[26-48]
featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/bytecode/SyntheticBytecodeFactory.kt[260-291]
featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/bytecode/JarAssembler.kt[17-42]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The R8 DCE verification tests can produce false positives because a branch/guarded class may be removed due to write-only field optimization, not because `-assumevalues` made the code unreachable.

## Issue Context
`doWork()` only increments a static `sideEffect` field. In the assumevalues scenarios, the ProGuard rules pin `sideEffect` only for the *expected* surviving class (or not at all for the int assumevalues case), so the other class can still be optimized away even if reachable.

## Fix Focus Areas
- featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/rules/ProguardRulesWriter.kt[35-85]

## Concrete fix
- In `writeBooleanRules(...)`, add `-keepclassmembers` for **both** `IfBranchCode` and `ElseBranchCode` (not just the computed `survivingClass`).
 - This prevents write-only stripping from deciding class presence, while still allowing the unreachable class to be removed (because `-keepclassmembers` does not keep the class itself).
- In `writeIntRules(...)`, add `-keepclassmembers class PositiveCountCode { public static int sideEffect; }` so `PositiveCountCode` cannot disappear unless the guarded branch is proven unreachable.
- Keep the existing control tests; after these changes, they will more directly validate that `-assumevalues` is what changes reachability/class presence.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

2. Misleading ASM field value 🐞
Description
sideEffectClassBytes passes an explicit initial value (0) when defining sideEffect, which is
unnecessary for the test and can mislead maintainers into thinking the constant value is relevant to
the optimization being tested. Removing the value argument clarifies intent and avoids relying on
ASM’s handling details.
Code

featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/bytecode/SyntheticBytecodeFactory.kt[R268-272]

+    safeClassWriter()
+        .apply {
+            visit(V1_8, ACC_PUBLIC, internalName, null, OBJECT, null)
+            visitField(ACC_PUBLIC or ACC_STATIC, "sideEffect", "I", null, 0).visitEnd()
+            visitMethod(ACC_PUBLIC, "<init>", "()V", null, null).apply {
Evidence
The synthetic class defines sideEffect with a value parameter even though the test only needs a
mutable static field; the only semantic purpose is writes in doWork(). This parameter adds
confusion without improving the test’s behavior.

featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/bytecode/SyntheticBytecodeFactory.kt[267-286]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The ASM field definition supplies an initial value that is not needed for these tests and can be confusing.

## Issue Context
`sideEffect` is incremented via bytecode; it does not need a ConstantValue-style initializer to start at 0.

## Fix Focus Areas
- featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/bytecode/SyntheticBytecodeFactory.kt[267-273]

## Concrete fix
Change:
- `visitField(ACC_PUBLIC or ACC_STATIC, "sideEffect", "I", null, 0)`
To:
- `visitField(ACC_PUBLIC or ACC_STATIC, "sideEffect", "I", null, null)`
(or omit the value argument if you wrap it).
This keeps the bytecode simpler and the intent clearer.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +30 to +47
* `-keepclassmembers` pins the `sideEffect` field of the **surviving** branch class so
* that R8 cannot treat the `doWork()` call as a no-op and eliminate the class via
* write-only field optimisation. The dead branch class intentionally has no such rule,
* so R8 is free to eliminate it once the branch becomes unreachable.
*/
internal fun writeBooleanRules(
dest: File,
returnValue: Boolean,
) {
val survivingClass = if (returnValue) IF_BRANCH_CODE_FQN else ELSE_BRANCH_CODE_FQN
dest.writeText(
"""
-assumevalues class $BOOL_EXTENSIONS_FQN {
boolean $IS_DARK_MODE_ENABLED($CONFIG_VALUES_FQN) return $returnValue;
}
-keep class $BIFURCATED_CALLER_FQN { *; }
-keepclassmembers class $survivingClass { public static int sideEffect; }
-dontwarn **
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Dce tests can false-pass 🐞 Bug ≡ Correctness

writeBooleanRules (and writeIntRules) only protects the expected-surviving class’s sideEffect
field, while the synthetic doWork() has no observable effect other than incrementing that field.
R8 can eliminate the unprotected branch/guarded class even when it is still reachable (by stripping
the write-only field/writes), causing the tests to pass without actually validating that
-assumevalues drove the DCE outcome.
Agent Prompt
## Issue description
The R8 DCE verification tests can produce false positives because a branch/guarded class may be removed due to write-only field optimization, not because `-assumevalues` made the code unreachable.

## Issue Context
`doWork()` only increments a static `sideEffect` field. In the assumevalues scenarios, the ProGuard rules pin `sideEffect` only for the *expected* surviving class (or not at all for the int assumevalues case), so the other class can still be optimized away even if reachable.

## Fix Focus Areas
- featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/rules/ProguardRulesWriter.kt[35-85]

## Concrete fix
- In `writeBooleanRules(...)`, add `-keepclassmembers` for **both** `IfBranchCode` and `ElseBranchCode` (not just the computed `survivingClass`).
  - This prevents write-only stripping from deciding class presence, while still allowing the unreachable class to be removed (because `-keepclassmembers` does not keep the class itself).
- In `writeIntRules(...)`, add `-keepclassmembers class PositiveCountCode { public static int sideEffect; }` so `PositiveCountCode` cannot disappear unless the guarded branch is proven unreachable.
- Keep the existing control tests; after these changes, they will more directly validate that `-assumevalues` is what changes reachability/class presence.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extracts the R8 dead-code-elimination (DCE) verification tests from :featured-gradle-plugin into a dedicated :featured-shrinker-tests module, keeping the Gradle plugin’s test suite lighter while preserving deterministic verification of -assumevalues behavior.

Changes:

  • Added new :featured-shrinker-tests module with an R8 test harness, ASM-generated bytecode inputs, ProGuard rules writer, and jar assertions.
  • Removed the monolithic R8EliminationTest (and its R8/ASM test deps) from :featured-gradle-plugin.
  • Added an MkDocs guide and navigation entry for R8 DCE verification.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
settings.gradle.kts Registers the new :featured-shrinker-tests module in the build.
mkdocs.yml Adds documentation nav entry for the new R8 verification guide.
featured-shrinker-tests/build.gradle.kts New JVM test-only module with R8 + ASM dependencies.
featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/rules/ProguardRulesWriter.kt Writes test ProGuard rules (including -assumevalues) for the synthetic scenarios.
featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/r8/R8BooleanFlagEliminationTest.kt Boolean-flag DCE verification tests.
featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/r8/R8IntFlagEliminationTest.kt Int-flag DCE verification tests.
featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/harness/R8TestHarness.kt Shared harness for building inputs, invoking R8, and managing temp dirs.
featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/bytecode/SyntheticBytecodeFactory.kt ASM bytecode factory for synthetic class graphs used in tests.
featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/bytecode/JarAssembler.kt Assembles the generated classes into input JARs for R8.
featured-shrinker-tests/src/test/kotlin/dev/androidbroadcast/featured/shrinker/assertions/JarAssertions.kt Asserts presence/absence of classes in the R8 output JAR.
featured-gradle-plugin/src/test/kotlin/dev/androidbroadcast/featured/gradle/R8EliminationTest.kt Deletes the old monolithic R8 test from the plugin module.
featured-gradle-plugin/build.gradle.kts Removes R8/ASM test dependencies now that tests moved modules.
docs/guides/r8-verification.md Adds a guide describing what the shrinker tests verify and how to run/extend them.
.gitignore Ignores swarm-report/ output.

Comment on lines +19 to +20
* write rules files in the exact format `ProguardRulesGenerator` produces, run R8
* programmatically, and assert presence/absence of flag-guarded classes in the output JAR.
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The KDoc says the rules files are written in the exact format produced by ProguardRulesGenerator, but the test rules include additional scaffolding directives (e.g., -keep, -dontwarn) and omit the generator’s header comments. Please reword to clarify that the -assumevalues block matches the generator output and the rest is test-only scaffolding.

Suggested change
* write rules files in the exact format `ProguardRulesGenerator` produces, run R8
* programmatically, and assert presence/absence of flag-guarded classes in the output JAR.
* write test rule files whose `-assumevalues` block matches what `ProguardRulesGenerator`
* produces, add the surrounding `-keep`/`-dontwarn` directives as test-only scaffolding,
* run R8 programmatically, and assert presence/absence of flag-guarded classes in the
* output JAR.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +24
2. **Rules generation** — `ProguardRulesWriter` writes `.pro` files in the exact format
`ProguardRulesGenerator` produces, optionally including the `-assumevalues` block.
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guide states that ProguardRulesWriter writes .pro files in the exact format ProguardRulesGenerator produces, but the test rules intentionally add extra directives (e.g., -keep, -dontwarn) and don’t include the generator’s header comments. Consider clarifying that the -assumevalues block matches the plugin output while the rest is test scaffolding.

Suggested change
2. **Rules generation**`ProguardRulesWriter` writes `.pro` files in the exact format
`ProguardRulesGenerator` produces, optionally including the `-assumevalues` block.
2. **Rules generation**`ProguardRulesWriter` writes test `.pro` files whose
`-assumevalues` block matches the format produced by `ProguardRulesGenerator`,
optionally including that block. The surrounding directives are test scaffolding
(for example `-keep` and `-dontwarn`) rather than a byte-for-byte copy of the
plugin's full output.

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +11
import org.junit.After
import org.junit.Before
import java.io.File
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This harness uses JUnit4 lifecycle annotations (org.junit.Before/After). Most tests in this repo use kotlin.test.BeforeTest/AfterTest instead (e.g. featured-registry/.../FlagRegistryTest.kt:10, providers/javaprefs/...Test.kt:32). Consider switching to the kotlin.test lifecycle annotations for consistency and to avoid depending on JUnit APIs in the test sources.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants