blob: f8e88c6decf1df1f37a9a4f6f0d495a23be87379 [file] [log] [blame]
// 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): Normal line-opt will cause a single-line mapping. Retrace should not
// optimize that to mean it represents a single possible line. (<noline> should not match 1:x).
StackTraceLine barLine = parameters.isCfRuntime() ? inputLine("bar", 100) : inputLine("bar", 0);
// TODO(b/232212653): The retracing in CF where the line table is preserved is incorrect.
// same issue as for bar.
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);
return StackTrace.builder()
.add(fooLine)
.add(residualLine("bar", 0))
.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 ***
}
}
}