Regression test for various missing line-number issues.
Bug: b/232212653
Change-Id: Ie50d27ffd797d22a2ffdd610d784c4946f8d0c33
diff --git a/src/test/java/com/android/tools/r8/debuginfo/NoLineInfoTest.java b/src/test/java/com/android/tools/r8/debuginfo/NoLineInfoTest.java
new file mode 100644
index 0000000..921d4d9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debuginfo/NoLineInfoTest.java
@@ -0,0 +1,263 @@
+// Copyright (c) 2022, 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.debuginfo;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeFalse;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.naming.retrace.StackTrace;
+import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.transformers.MethodTransformer;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.Label;
+
+@RunWith(Parameterized.class)
+public class NoLineInfoTest extends TestBase {
+
+ private static final String INPUT_SOURCE_FILE = "InputSourceFile.java";
+ private static final String CUSTOM_SOURCE_FILE = "TaggedSourceFile";
+ private static final String DEFAULT_SOURCE_FILE = "SourceFile";
+ private static final String UNKNOWN_SOURCE_FILE = "Unknown Source";
+
+ private final TestParameters parameters;
+ private final boolean customSourceFile;
+
+ @Parameterized.Parameters(name = "{0}, custom-sf:{1}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.values());
+ }
+
+ public NoLineInfoTest(TestParameters parameters, boolean customSourceFile) {
+ this.parameters = parameters;
+ this.customSourceFile = customSourceFile;
+ }
+
+ private byte[] getTestClassTransformed() throws IOException {
+ return transformer(TestClass.class)
+ .setSourceFile(INPUT_SOURCE_FILE)
+ .addMethodTransformer(
+ new MethodTransformer() {
+ private final Map<MethodReference, Integer> lines = new HashMap<>();
+
+ @Override
+ public void visitLineNumber(int line, Label start) {
+ Integer nextLine = lines.getOrDefault(getContext().getReference(), 0);
+ if (nextLine > 0) {
+ super.visitLineNumber(nextLine, start);
+ }
+ // Increment the actual line content by 100 so that each one is clearly distinct
+ // from a PC value for any of the methods.
+ lines.put(getContext().getReference(), nextLine + 100);
+ }
+ })
+ .transform();
+ }
+
+ public boolean isRuntimeWithPcAsLineNumberSupport() {
+ return parameters.isDexRuntime()
+ && parameters
+ .getRuntime()
+ .maxSupportedApiLevel()
+ .isGreaterThanOrEqualTo(apiLevelWithPcAsLineNumberSupport());
+ }
+
+ public boolean isCompileWithPcAsLineNumberSupport() {
+ return parameters.isDexRuntime()
+ && parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevelWithPcAsLineNumberSupport());
+ }
+
+ @Test
+ public void testReference() throws Exception {
+ assumeFalse(customSourceFile);
+ testForRuntime(parameters)
+ .addProgramClassFileData(getTestClassTransformed())
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertFailureWithErrorThatThrows(NullPointerException.class)
+ .inspectStackTrace(
+ stacktrace -> {
+ if (isRuntimeWithPcAsLineNumberSupport()) {
+ // On VMs with PC support the lack of a line will emit the PC instead.
+ assertThat(stacktrace, StackTrace.isSame(getExpectedInputStacktraceOnPcVms()));
+ } else {
+ assertThat(stacktrace, StackTrace.isSame(getExpectedInputStacktrace()));
+ }
+ });
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .addProgramClassFileData(getTestClassTransformed())
+ .addKeepClassAndMembersRules(TestClass.class)
+ .addKeepAttributeSourceFile()
+ .addKeepAttributeLineNumberTable()
+ .setMinApi(parameters.getApiLevel())
+ .addOptionsModification(o -> o.testing.forcePcBasedEncoding = true)
+ .applyIf(
+ customSourceFile,
+ b -> b.getBuilder().setSourceFileProvider(environment -> CUSTOM_SOURCE_FILE))
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertFailureWithErrorThatThrows(NullPointerException.class)
+ .inspectOriginalStackTrace(
+ stackTrace ->
+ assertThat(
+ "Unexpected residual stacktrace",
+ stackTrace,
+ StackTrace.isSame(getResidualStacktrace())))
+ .inspectStackTrace(
+ stacktrace ->
+ assertThat(
+ "Unexpected input-source stacktrace",
+ stacktrace,
+ StackTrace.isSame(getUnexpectedRetracedStacktrace())));
+ }
+
+ private StackTraceLine line(String file, String method, int line) {
+ return StackTraceLine.builder()
+ .setClassName(typeName(TestClass.class))
+ .setFileName(file)
+ .setMethodName(method)
+ .setLineNumber(line)
+ .build();
+ }
+
+ // Normal reference line from the input.
+ private StackTraceLine inputLine(String method, int line) {
+ return line(INPUT_SOURCE_FILE, method, line);
+ }
+
+ // Line as printed for PC supporting VMs when the line is absent (D8 compilation).
+ private StackTraceLine inputPcLine(String method, int pc) {
+ return line(UNKNOWN_SOURCE_FILE, method, pc);
+ }
+
+ // Residual line depends on the source file parameter.
+ private StackTraceLine residualLine(String method, int line) {
+ String defaultFile =
+ isCompileWithPcAsLineNumberSupport() ? UNKNOWN_SOURCE_FILE : DEFAULT_SOURCE_FILE;
+ String file = customSourceFile ? CUSTOM_SOURCE_FILE : defaultFile;
+ return line(file, method, line);
+ }
+
+ // This is the real "reference" stack trace as given by JVM on inputs and should be retraced to.
+ private StackTrace getExpectedInputStacktrace() {
+ return StackTrace.builder()
+ .add(inputLine("foo", -1))
+ .add(inputLine("bar", -1))
+ .add(inputLine("baz", -1))
+ .add(inputLine("main", 200))
+ .build();
+ }
+
+ // When D8 compiling reference inputs directly there is (currently) no way to recover from the PC
+ // printing. Thus, this is the expected stack trace on those VMs.
+ private StackTrace getExpectedInputStacktraceOnPcVms() {
+ return StackTrace.builder()
+ .add(inputPcLine("foo", 1))
+ .add(inputPcLine("bar", 0))
+ .add(inputPcLine("baz", 0))
+ .add(inputLine("main", 200))
+ .build();
+ }
+
+ // TODO(b/232212653): The retraced stack trace should be the same as `getExpectedInputStacktrace`.
+ private StackTrace getUnexpectedRetracedStacktrace() {
+
+ // TODO(b/232212653): Retracing the PC 1 preserves it but it should map to <noline>
+ StackTraceLine fooLine =
+ isRuntimeWithPcAsLineNumberSupport() ? inputLine("foo", 1) : inputLine("foo", -1);
+
+ // TODO(b/232212653): Retracing builds with stripped line table will retrace incorrectly.
+ StackTraceLine barLine =
+ (isRuntimeWithPcAsLineNumberSupport() && customSourceFile)
+ || !isCompileWithPcAsLineNumberSupport()
+ ? inputLine("bar", 100)
+ : inputLine("bar", 0);
+
+ // TODO(b/232212653): The retracing in CF where the line table is preserved is incorrect.
+ StackTraceLine bazLine = parameters.isCfRuntime() ? inputLine("baz", 100) : inputLine("baz", 0);
+
+ return StackTrace.builder()
+ .add(fooLine)
+ .add(barLine)
+ .add(bazLine)
+ .add(inputLine("main", 200))
+ .build();
+ }
+
+ private StackTrace getResidualStacktrace() {
+ if (parameters.isCfRuntime()) {
+ // For CF compilation the line number increments are used and each preamble is retained as
+ // such. This is the expected output.
+ return StackTrace.builder()
+ .add(residualLine("foo", -1))
+ .add(residualLine("bar", -1))
+ .add(residualLine("baz", -1))
+ .add(residualLine("main", 101)) // TODO(b/232212653) Why is this 101?
+ .build();
+ }
+
+ // TODO(b/232212653): The correct line should be with CUSTOM_SOURCE_FILE and PC 1.
+ // When compiling with debug info encoding PCs this is almost the expected output. The issue
+ // being that even "foo" should have PC based encoding too to ensure the SF remains on
+ // newer VMs too.
+ StackTraceLine fooLine =
+ isRuntimeWithPcAsLineNumberSupport()
+ ? line(UNKNOWN_SOURCE_FILE, "foo", 1)
+ : residualLine("foo", -1);
+
+ // TODO(b/232212653): If not using a custom source file, then the single line identification
+ // strips the line table.
+ StackTraceLine barLine =
+ isRuntimeWithPcAsLineNumberSupport() && !customSourceFile
+ ? line(UNKNOWN_SOURCE_FILE, "bar", 0)
+ : residualLine("bar", customSourceFile ? 0 : -1);
+
+ return StackTrace.builder()
+ .add(fooLine)
+ .add(barLine)
+ .add(residualLine("baz", 0))
+ .add(residualLine("main", 6))
+ .build();
+ }
+
+ // Test with a call stack where each initial line is stripped (see getTestClassTransformed)
+ // Line numbers are indicated in comments. The stacktrace is marked by ***.
+ static class TestClass {
+
+ public static void nop() {}
+
+ public static void foo() {
+ throw null; // noline ***
+ }
+
+ public static void bar() {
+ foo(); // noline ***
+ nop(); // 100
+ }
+
+ public static void baz() {
+ bar(); // noline ***
+ nop(); // 100
+ nop(); // 200
+ }
+
+ public static void main(String[] args) {
+ nop(); // noline
+ nop(); // 100
+ baz(); // 200 ***
+ }
+ }
+}