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() {