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.