Add test with inline positions for stack sample retracing

Bug: b/460808033
Change-Id: I27e6109417cd862263fe33baa785a5fa39067c09
diff --git a/src/main/java/com/android/tools/r8/utils/IntBox.java b/src/main/java/com/android/tools/r8/utils/IntBox.java
index 47f2093..3aa8ed7 100644
--- a/src/main/java/com/android/tools/r8/utils/IntBox.java
+++ b/src/main/java/com/android/tools/r8/utils/IntBox.java
@@ -72,6 +72,12 @@
     this.value = value;
   }
 
+  public void setMin(int value) {
+    if (value < get()) {
+      set(value);
+    }
+  }
+
   public void setMax(int value) {
     if (value > get()) {
       set(value);
diff --git a/src/test/java/com/android/tools/r8/retrace/stacksamples/MethodWithInlinePositionsStackSampleRetraceTest.java b/src/test/java/com/android/tools/r8/retrace/stacksamples/MethodWithInlinePositionsStackSampleRetraceTest.java
new file mode 100644
index 0000000..f941c99
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/stacksamples/MethodWithInlinePositionsStackSampleRetraceTest.java
@@ -0,0 +1,117 @@
+// Copyright (c) 2025, 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.retrace.stacksamples;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestCompileResultBase;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.retrace.RetraceMethodElement;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class MethodWithInlinePositionsStackSampleRetraceTest extends StackSampleRetraceTestBase {
+
+  private static final String obfuscatedClassName = "com.android.tools.r8.retrace.stacksamples.a";
+  private static final String obfuscatedMethodName = "a";
+
+  static byte[] programClassFileData;
+
+  // Map the line numbers of the Main class so that the line numbers start from 42.
+  // This ensures that changes to the test does not impact the line numbers of the test data.
+  @BeforeClass
+  public static void setup() throws Exception {
+    int firstLineNumber = getFirstLineNumber(Main.class);
+    programClassFileData = transformer(Main.class).mapLineNumbers(42 - firstLineNumber).transform();
+    assertEquals(42, getFirstLineNumber(programClassFileData));
+  }
+
+  @Test
+  public void test() throws Exception {
+    runTest(
+        testBuilder ->
+            testBuilder.addProgramClassFileData(programClassFileData).enableInliningAnnotations());
+  }
+
+  @Override
+  Class<?> getMainClass() {
+    return Main.class;
+  }
+
+  @Override
+  String getExpectedMap() {
+    return StringUtils.joinLines(
+        "com.android.tools.r8.retrace.stacksamples.MethodWithInlinePositionsStackSampleRetraceTest$Main"
+            + " -> com.android.tools.r8.retrace.stacksamples.a:",
+        "# {\"id\":\"sourceFile\",\"fileName\":\"MethodWithInlinePositionsStackSampleRetraceTest.java\"}",
+        "    1:1:void foo():54:54 -> a",
+        "    1:1:void test():50 -> a",
+        "    2:2:void bar():59:59 -> a",
+        "    2:2:void foo():55 -> a",
+        "    2:2:void test():50 -> a",
+        "    3:3:void baz():64:64 -> a",
+        "    3:3:void bar():60 -> a",
+        "    3:3:void foo():55 -> a",
+        "    3:3:void test():50 -> a",
+        "    1:4:void main(java.lang.String[]):45:45 -> main");
+  }
+
+  @Override
+  String getExpectedOutput() {
+    return StringUtils.lines("foo", "bar", "baz");
+  }
+
+  @Override
+  void inspectCode(CodeInspector inspector) {
+    // Verify all methods have been inlined into the test method.
+    ClassSubject mainClass = inspector.clazz(Main.class);
+    assertEquals(2, mainClass.allMethods().size());
+
+    // Verify Main.test is renamed to a.a.
+    assertEquals(obfuscatedClassName, mainClass.getFinalName());
+    assertEquals(
+        obfuscatedMethodName, mainClass.uniqueMethodWithOriginalName("test").getFinalName());
+  }
+
+  @Override
+  void testRetrace(R8TestCompileResultBase<?> compileResult) throws Exception {
+    // Expected: a.a should retrace to Main.test.
+    RetraceMethodElement retraceMethodElement =
+        getSingleRetraceMethodElement(
+            Reference.classFromTypeName(obfuscatedClassName), obfuscatedMethodName);
+    assertEquals(
+        Reference.methodFromMethod(Main.class.getDeclaredMethod("test")),
+        retraceMethodElement.getRetracedMethod().asKnown().getMethodReference());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      test();
+    }
+
+    @NeverInline
+    static void test() {
+      foo();
+    }
+
+    static void foo() {
+      System.out.println("foo");
+      bar();
+    }
+
+    static void bar() {
+      System.out.println("bar");
+      baz();
+    }
+
+    static void baz() {
+      System.out.println("baz");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/stacksamples/StackSampleRetraceTestBase.java b/src/test/java/com/android/tools/r8/retrace/stacksamples/StackSampleRetraceTestBase.java
new file mode 100644
index 0000000..0b3644b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/stacksamples/StackSampleRetraceTestBase.java
@@ -0,0 +1,136 @@
+// Copyright (c) 2025, 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.retrace.stacksamples;
+
+import static com.android.tools.r8.utils.InternalOptions.ASM_VERSION;
+import static com.android.tools.r8.utils.StringUtils.UNIX_LINE_SEPARATOR;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.R8TestCompileResultBase;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestDiagnosticMessagesImpl;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.retrace.ProguardMapProducer;
+import com.android.tools.r8.retrace.RetraceClassResult;
+import com.android.tools.r8.retrace.RetraceMethodElement;
+import com.android.tools.r8.retrace.RetraceMethodResult;
+import com.android.tools.r8.retrace.Retracer;
+import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.utils.IntBox;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+
+@RunWith(Parameterized.class)
+public abstract class StackSampleRetraceTestBase extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  void runTest(ThrowableConsumer<R8TestBuilder<?, ?, ?>> testBuilderConsumer) throws Exception {
+    testForR8(parameters)
+        .addKeepClassRulesWithAllowObfuscation(getMainClass())
+        .addKeepRules(
+            "-keepclassmembers class " + getMainClass().getTypeName() + " {",
+            "  public static void main(java.lang.String[]);",
+            "}")
+        .apply(testBuilderConsumer)
+        .compile()
+        .inspect(this::inspectCode)
+        .applyIf(parameters.isDexRuntime(), this::inspectMap)
+        .apply(this::testRetrace)
+        .run(parameters.getRuntime(), getMainClass())
+        .assertSuccessWithOutput(getExpectedOutput());
+  }
+
+  abstract Class<?> getMainClass();
+
+  abstract String getExpectedMap();
+
+  abstract String getExpectedOutput();
+
+  abstract void inspectCode(CodeInspector inspector);
+
+  abstract void testRetrace(R8TestCompileResultBase<?> compileResult) throws Exception;
+
+  private void inspectMap(R8TestCompileResultBase<?> compileResult) {
+    assertEquals(getExpectedMap(), getMapWithoutHeader(compileResult));
+  }
+
+  RetraceMethodElement getSingleRetraceMethodElement(
+      ClassReference obfuscatedClassReference, String obfuscatedMethodName) {
+    TestDiagnosticMessages diagnostics = new TestDiagnosticMessagesImpl();
+    Retracer retracer =
+        Retracer.createDefault(ProguardMapProducer.fromString(getExpectedMap()), diagnostics);
+    RetraceClassResult retraceClassResult = retracer.retraceClass(obfuscatedClassReference);
+    RetraceMethodResult retraceMethodResult = retraceClassResult.lookupMethod(obfuscatedMethodName);
+    List<RetraceMethodElement> retraceMethodElements =
+        retraceMethodResult.stream().collect(Collectors.toList());
+    assertEquals(1, retraceMethodElements.size());
+    diagnostics.assertNoMessages();
+    return retraceMethodElements.get(0);
+  }
+
+  static int getFirstLineNumber(Class<?> clazz) throws Exception {
+    return getFirstLineNumber(ToolHelper.getClassAsBytes(clazz));
+  }
+
+  static int getFirstLineNumber(byte[] classFileData) throws Exception {
+    ClassReader reader = new ClassReader(classFileData);
+    IntBox result = new IntBox(Integer.MAX_VALUE);
+    reader.accept(
+        new ClassVisitor(ASM_VERSION) {
+          @Override
+          public MethodVisitor visitMethod(
+              int access, String name, String descriptor, String signature, String[] exceptions) {
+            MethodVisitor subvisitor =
+                super.visitMethod(access, name, descriptor, signature, exceptions);
+            return new MethodVisitor(ASM_VERSION, subvisitor) {
+              @Override
+              public void visitLineNumber(int line, Label start) {
+                super.visitLineNumber(line, start);
+                result.setMin(line);
+              }
+            };
+          }
+        },
+        0);
+    return result.get();
+  }
+
+  static String getMapWithoutHeader(R8TestCompileResultBase<?> compileResult) {
+    BooleanBox pastHeader = new BooleanBox();
+    return StringUtils.splitLines(compileResult.getProguardMap()).stream()
+        .filter(
+            line -> {
+              if (line.startsWith("#") && pastHeader.isFalse()) {
+                return false;
+              } else {
+                pastHeader.set();
+                return true;
+              }
+            })
+        .collect(Collectors.joining(UNIX_LINE_SEPARATOR));
+  }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/testbase/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index c8ed345..376c11b 100644
--- a/src/test/testbase/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/testbase/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -1661,6 +1661,16 @@
         });
   }
 
+  public ClassFileTransformer mapLineNumbers(int delta) {
+    return addMethodTransformer(
+        new MethodTransformer() {
+          @Override
+          public void visitLineNumber(int line, Label start) {
+            super.visitLineNumber(line + delta, start);
+          }
+        });
+  }
+
   public ClassFileTransformer stripDebugLocals(MethodPredicate predicate) {
     return addMethodTransformer(
         new MethodTransformer() {