blob: 3831e44e83f233c38ac7748631fe36b5fbf0ea9c [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.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assume.assumeFalse;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.graph.DexDebugInfo;
import com.android.tools.r8.naming.retrace.StackTrace;
import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
import com.android.tools.r8.utils.BooleanUtils;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@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)
.setPredictiveLineNumbering()
.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)
.addOptionsModification(o -> o.testing.forcePcBasedEncoding = true)
.applyIf(
customSourceFile,
b -> b.getBuilder().setSourceFileProvider(environment -> CUSTOM_SOURCE_FILE))
.run(parameters.getRuntime(), TestClass.class)
.assertFailureWithErrorThatThrows(NullPointerException.class)
.inspectFailure(
i -> {
if (parameters.isDexRuntime()) {
Set<DexDebugInfo> debugInfos =
i.allClasses().stream()
.flatMap(c -> c.allMethods().stream())
.map(m -> m.getMethod().getCode().asDexCode().getDebugInfo())
.collect(Collectors.toSet());
if (isCompileWithPcAsLineNumberSupport() && !customSourceFile) {
// If debug info is stripped all items are null pointers.
assertEquals(Collections.singleton(null), debugInfos);
} else {
// If debug info remains it is two canonical items and one null pointer.
// The presence of 'null' debug info items is for methods with no actual lines at
// all.
assertEquals(3, debugInfos.size());
}
}
})
.inspectOriginalStackTrace(
stackTrace ->
assertThat(
"Unexpected residual stacktrace",
stackTrace,
StackTrace.isSame(getResidualStacktrace())))
.inspectStackTrace(
stacktrace ->
assertThat(
"Unexpected input-source stacktrace",
stacktrace,
StackTrace.isSame(
parameters.isCfRuntime()
? getExpectedInputStacktrace()
: 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) {
if (customSourceFile) {
return line(CUSTOM_SOURCE_FILE, method, line);
}
if (isCompileWithPcAsLineNumberSupport()) {
return line(UNKNOWN_SOURCE_FILE, method, line);
}
return line(DEFAULT_SOURCE_FILE, method, line);
}
private int getPcEncoding(int pc) {
return isCompileWithPcAsLineNumberSupport() && !customSourceFile ? pc : (pc + 1);
}
// A residual line that is either null debug info or pc2pc mapping.
private StackTraceLine residualPcOrNoLine(String method, int pc) {
// If compiling with custom source file the debug info must be non-null and have a pc mapping.
// Add one as pc2pc encoding shifts lines by one.
if (customSourceFile) {
return line(CUSTOM_SOURCE_FILE, method, getPcEncoding(pc));
}
// If debug info is null, then on native pc support VMs it will print "unknown" and pc.
if (isRuntimeWithPcAsLineNumberSupport()) {
return line(UNKNOWN_SOURCE_FILE, method, pc);
}
// On old runtimes it will print "default" and no line info.
return line(DEFAULT_SOURCE_FILE, method, -1);
}
// 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() {
assertFalse(parameters.isCfRuntime());
StackTraceLine fooLine;
if (isRuntimeWithPcAsLineNumberSupport() && !customSourceFile) {
// TODO(b/232212653): Retrace should convert PC 1 to line <noline>/-1/0.
fooLine = inputLine("foo", 1);
} else {
fooLine = inputLine("foo", -1);
}
int position =
isCompileWithPcAsLineNumberSupport() && !customSourceFile ? -1 : getPcEncoding(0);
StackTraceLine barLine = inputLine("bar", position);
StackTraceLine bazLine = inputLine("baz", position);
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();
}
return StackTrace.builder()
// Foo has only <noline> on input and so it is allowed to compile it to a null debug-info.
.add(residualPcOrNoLine("foo", 1))
.add(residualLine("bar", getPcEncoding(0)))
.add(residualLine("baz", getPcEncoding(0)))
.add(residualLine("main", getPcEncoding(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 ***
}
}
}