Test redexing with jumbo const string.
Change-Id: I9fe72bb7bf1a25013e226de1afd5811db013d0fb
diff --git a/src/test/java/com/android/tools/r8/redex/RedexRetraceTest.java b/src/test/java/com/android/tools/r8/redex/RedexRetraceTest.java
new file mode 100644
index 0000000..e1b2086
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/redex/RedexRetraceTest.java
@@ -0,0 +1,173 @@
+// Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.redex;
+
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.R8TestCompileResultBase;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParametersBuilder;
+import com.android.tools.r8.TestRuntime.DexRuntime;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.naming.retrace.StackTrace;
+import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.android.tools.r8.utils.internal.Box;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class RedexRetraceTest extends TestBase {
+
+ private final AndroidApiLevel compilationApiLevel;
+ private final AndroidApiLevel redexApiLevel;
+ private final DexVm.Version runtime;
+
+ public RedexRetraceTest(
+ AndroidApiLevel compilationApiLevel, AndroidApiLevel redexApiLevel, Version runtime) {
+ this.compilationApiLevel = compilationApiLevel;
+ this.redexApiLevel = redexApiLevel;
+ this.runtime = runtime;
+ }
+
+ @Parameters(name = "firstApi:{0}, redexApi:{1}, runtime:{2}")
+ public static Collection<Object[]> data() {
+ // Test a re-dex API level upgrade from api to target with some runtime.
+ // { (api, target, runtime) |
+ // target in {LATEST, Sv2} &
+ // api <= target &
+ // runtime in {target.minVm, target.maxVm} &
+ // }
+
+ // The test assumes that all targets are >= 24
+ List<AndroidApiLevel> targets = ImmutableList.of(AndroidApiLevel.LATEST, AndroidApiLevel.Sv2);
+
+ Version maxVm = Version.V17_0_0;
+
+ List<Object[]> parametersList = new ArrayList<>();
+ for (AndroidApiLevel target : targets) {
+ for (AndroidApiLevel api : AndroidApiLevel.getAndroidApiLevelsSorted()) {
+ if (api.isLessThanOrEqualTo(target)) {
+ Set<Version> runtimes =
+ ImmutableSet.of(ToolHelper.getDexVersionForApiLevel(target), maxVm);
+ for (Version vm : runtimes) {
+ parametersList.add(new Object[] {api, target, vm});
+ }
+ }
+ }
+ }
+ // Verify that valid configurations are actually found.
+ assert parametersList.size() == 102
+ : "Unexpected configuration count: " + parametersList.size();
+
+ return TestParametersBuilder.filterByDexVmVersion(
+ parametersList, params -> (Version) params[2]);
+ }
+
+ @Test
+ public void test() throws Exception {
+ boolean isDirectPcEncodingEnabled =
+ compilationApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.CINNAMON_BUN);
+
+ Box<String> currentFooMethodName = new Box<>("foo");
+ R8TestCompileResultBase<?> compileResult =
+ testForR8(Backend.DEX)
+ .setMinApi(compilationApiLevel)
+ .setMode(CompilationMode.RELEASE)
+ .addProgramClasses(TestClass.class)
+ .addKeepMainRule(TestClass.class)
+ .enableInliningAnnotations()
+ .addOptionsModification(
+ options -> {
+ options.lineNumberOptimization = LineNumberOptimization.ON;
+ })
+ .compile()
+ .inspect(
+ inspector -> {
+ ClassSubject clazz = inspector.clazz(TestClass.class);
+ MethodSubject method =
+ clazz.uniqueMethodWithOriginalName(currentFooMethodName.get());
+ assertTrue(method.isPresent());
+ currentFooMethodName.set(method.getFinalName());
+ });
+
+ Path r8Output = compileResult.writeToZip();
+ String r8Mapping = compileResult.getProguardMap();
+
+ compileResult
+ .run(new DexRuntime(runtime), TestClass.class)
+ .assertFailureWithErrorThatThrows(RuntimeException.class)
+ .inspectOriginalStackTrace(
+ stacktrace -> {
+ assertThat(stacktrace, not(StackTrace.isSame(getExpectedStackTrace())));
+ StackTrace retracedStackTrace = stacktrace.retrace(r8Mapping);
+ assertThat(retracedStackTrace, StackTrace.isSame(getExpectedStackTrace()));
+ });
+
+ testForD8(Backend.DEX)
+ .setMinApi(redexApiLevel)
+ // Make const-strings instructions larger to "bump" the pc.
+ .addOptionsModification(options -> options.testing.forceJumboStringProcessing = true)
+ .addProgramFiles(r8Output)
+ .compile()
+ .inspect(
+ inspector -> {
+ ClassSubject clazz = inspector.clazz(TestClass.class);
+ MethodSubject method = clazz.uniqueMethodWithOriginalName(currentFooMethodName.get());
+ assertTrue(method.isPresent());
+ })
+ .run(new DexRuntime(runtime), TestClass.class)
+ .assertFailureWithErrorThatThrows(RuntimeException.class)
+ .inspectOriginalStackTrace(
+ stacktrace -> {
+ assertThat(stacktrace, not(StackTrace.isSame(getExpectedStackTrace())));
+ StackTrace retracedStackTrace = stacktrace.retrace(r8Mapping);
+ if (isDirectPcEncodingEnabled) {
+ // The original mapping file is invalidated by the jumbo strings since the implicit
+ // debug info is not maintained.
+ assertThat(retracedStackTrace, not(StackTrace.isSame(getExpectedStackTrace())));
+ } else {
+ // The explicit debug info is maintained and preserves the validity of the map.
+ assertThat(retracedStackTrace, StackTrace.isSame(getExpectedStackTrace()));
+ }
+ });
+ }
+
+ private StackTrace getExpectedStackTrace() {
+ String className = TestClass.class.getName();
+ return StackTrace.builder()
+ .add(
+ StackTraceLine.builder()
+ .setClassName(className)
+ .setMethodName("foo")
+ .setFileName("TestClass.java")
+ .setLineNumber(19)
+ .build())
+ .add(
+ StackTraceLine.builder()
+ .setClassName(className)
+ .setMethodName("main")
+ .setFileName("TestClass.java")
+ .setLineNumber(12)
+ .build())
+ .build();
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/redex/TestClass.java b/src/test/java/com/android/tools/r8/redex/TestClass.java
new file mode 100644
index 0000000..3f2f0fc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/redex/TestClass.java
@@ -0,0 +1,23 @@
+// Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.redex;
+
+import com.android.tools.r8.NeverInline;
+import java.util.Arrays;
+
+public class TestClass {
+
+ public static void main(String[] args) {
+ foo(); // This line must be line 12 for the test to pass.
+ }
+
+ @NeverInline
+ public static void foo() {
+ String[] strings = new String[] {"1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1"};
+ if (System.out != null) {
+ throw new RuntimeException("Crash!"); // This line must be line 19 for the test to pass.
+ }
+ throw new RuntimeException("" + Arrays.hashCode(strings));
+ }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/TestParametersBuilder.java b/src/test/testbase/java/com/android/tools/r8/TestParametersBuilder.java
index 0f512c7..8c5c330 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestParametersBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestParametersBuilder.java
@@ -19,6 +19,7 @@
import java.util.Set;
import java.util.TreeSet;
import java.util.function.BiPredicate;
+import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -378,6 +379,19 @@
return System.getProperty("runtimes");
}
+ /** Filters the items based on the build-configured runtime filter. */
+ public static <T> List<T> filterByDexVmVersion(
+ List<T> items, Function<T, DexVm.Version> versionExtractor) {
+ Set<DexVm.Version> allowedVersions =
+ getAvailableRuntimes()
+ .filter(TestRuntime::isDex)
+ .map(r -> r.asDex().getVersion())
+ .collect(Collectors.toSet());
+ return items.stream()
+ .filter(item -> allowedVersions.contains(versionExtractor.apply(item)))
+ .collect(Collectors.toList());
+ }
+
private static Stream<TestRuntime> getUnfilteredAvailableRuntimes() {
// The runtimes are built in a linked hash map to ensure a deterministic order and avoid
// duplicates.