blob: 3e60ef17db8021ad89b055671394cd4d96ea6bbe [file] [log] [blame]
// Copyright (c) 2018, 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.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import com.android.tools.r8.CompilationMode;
import com.android.tools.r8.OutputMode;
import com.android.tools.r8.R8Command;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.debuginfo.InliningWithoutPositionsTestSourceDump.Location;
import com.android.tools.r8.naming.ClassNameMapper;
import com.android.tools.r8.naming.ClassNamingForNameMapper;
import com.android.tools.r8.naming.ClassNamingForNameMapper.MappedRange;
import com.android.tools.r8.naming.ClassNamingForNameMapper.MappedRangesOfName;
import com.android.tools.r8.naming.Range;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.google.common.collect.ImmutableList;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class InliningWithoutPositionsTestRunner {
enum Backend {
CF,
DEX
}
private static final String TEST_CLASS = "InliningWithoutPositionsTestSource";
private static final String TEST_PACKAGE = "com.android.tools.r8.debuginfo";
private static final String MAIN_CLASS = TEST_PACKAGE + "." + TEST_CLASS;
@ClassRule public static TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
private final Backend backend;
private final boolean mainPos;
private final boolean foo1Pos;
private final boolean barPos;
private final boolean foo2Pos;
private final Location throwLocation;
@Parameters(name = "{0}: main/foo1/bar/foo2 positions: {1}/{2}/{3}/{4}, throwLocation: {5}")
public static Collection<Object[]> data() {
List<Object[]> testCases = new ArrayList<>();
for (Backend backend : Backend.values()) {
for (int i = 0; i < 16; ++i) {
for (Location throwLocation : Location.values()) {
if (throwLocation != Location.MAIN) {
testCases.add(
new Object[] {
backend, (i & 1) != 0, (i & 2) != 0, (i & 4) != 0, (i & 8) != 0, throwLocation
});
}
}
}
}
return testCases;
}
public InliningWithoutPositionsTestRunner(
Backend backend,
boolean mainPos,
boolean foo1Pos,
boolean barPos,
boolean foo2Pos,
Location throwLocation) {
this.backend = backend;
this.mainPos = mainPos;
this.foo1Pos = foo1Pos;
this.barPos = barPos;
this.foo2Pos = foo2Pos;
this.throwLocation = throwLocation;
}
@Test
public void testStackTrace() throws Exception {
// See InliningWithoutPositionsTestSourceDump for the code compiled here.
Path testClassDir = temp.newFolder().toPath();
Path testClassPath = testClassDir.resolve(TEST_CLASS + ".class");
Path outputPath = temp.newFolder().toPath();
Files.write(
testClassPath,
InliningWithoutPositionsTestSourceDump.dump(
mainPos, foo1Pos, barPos, foo2Pos, throwLocation));
Path proguardMapPath = testClassDir.resolve("proguard.map");
R8Command.Builder builder =
R8Command.builder()
.addProgramFiles(testClassPath)
.setMode(CompilationMode.RELEASE)
.setProguardMapOutputPath(proguardMapPath);
if (backend == Backend.DEX) {
AndroidApiLevel minSdk = ToolHelper.getMinApiLevelForDexVm();
builder
.setMinApiLevel(minSdk.getLevel())
.addLibraryFiles(ToolHelper.getAndroidJar(minSdk))
.setOutput(outputPath, OutputMode.DexIndexed);
} else {
assert (backend == Backend.CF);
builder
.addLibraryFiles(ToolHelper.getJava8RuntimeJar())
.setOutput(outputPath, OutputMode.ClassFile);
}
builder
.addProguardConfiguration(
ImmutableList.of(
"-keep class " + MAIN_CLASS + " { public static void main(java.lang.String[]); }"),
Origin.unknown())
.setDisableMinification(true)
.addProguardConfiguration(
ImmutableList.of("-keepattributes SourceFile,LineNumberTable"), Origin.unknown());
ToolHelper.runR8(builder.build(), options -> options.inliningInstructionLimit = 40);
ProcessResult result;
if (backend == Backend.DEX) {
ArtCommandBuilder artCommandBuilder = new ArtCommandBuilder();
artCommandBuilder.appendClasspath(outputPath.resolve("classes.dex").toString());
artCommandBuilder.setMainClass(MAIN_CLASS);
result = ToolHelper.runArtRaw(artCommandBuilder);
} else {
result = ToolHelper.runJava(outputPath, MAIN_CLASS);
}
assertNotEquals(result.exitCode, 0);
// Verify stack trace.
// result.stderr looks like this:
//
// Exception in thread "main" java.lang.RuntimeException: <FOO1-exception>
// at
// com.android.tools.r8.debuginfo.InliningWithoutPositionsTestSource.main(InliningWithoutPositionsTestSource.java:1)
String[] lines = result.stderr.split("\n");
// The line containing 'java.lang.RuntimeException' should contain the expected message, which
// is "LOCATIONCODE-exception>"
int i = 0;
boolean foundException = false;
for (; i < lines.length && !foundException; ++i) {
boolean hasExpectedException = lines[i].contains("<" + throwLocation + "-exception>");
if (lines[i].contains("java.lang.RuntimeException")) {
assertTrue(hasExpectedException);
foundException = true;
} else {
assertFalse(hasExpectedException);
}
}
assertTrue(foundException);
// The next line, the stack trace, must always be the same, indicating 'main' and line = 1 or 2.
Assert.assertTrue(i < lines.length);
String line = lines[i].trim();
assertTrue(line.startsWith("at " + TEST_PACKAGE + "." + TEST_CLASS + "." + "main"));
// It must contain the '<source-file>:1' or ':2', if we're throwing at foo2.
int expectedLineNumber = throwLocation == Location.FOO2 ? 2 : 1;
String expectedFilePos = TEST_CLASS + ".java:" + expectedLineNumber;
int idx = line.indexOf(expectedFilePos);
assertTrue(idx >= 0);
// And the next character must be a non-digit or nothing.
int idxAfter = idx + expectedFilePos.length();
assertTrue(idxAfter == line.length() || !Character.isDigit(line.charAt(idxAfter)));
// Reading the Proguard map. An example map (only the relevant part, 'main'):
//
// 1:1:void bar():0:0 -> main
// 1:1:void foo(boolean):0 -> main
// 1:1:void main(java.lang.String[]):0 -> main
ClassNameMapper mapper = ClassNameMapper.mapperFromFile(proguardMapPath);
assertNotNull(mapper);
ClassNamingForNameMapper classNaming = mapper.getClassNaming(TEST_PACKAGE + "." + TEST_CLASS);
assertNotNull(classNaming);
MappedRangesOfName rangesForMain = classNaming.mappedRangesByRenamedName.get("main");
assertNotNull(rangesForMain);
List<MappedRange> frames = rangesForMain.allRangesForLine(expectedLineNumber);
switch (throwLocation) {
case FOO1:
assertEquals(2, frames.size());
assertFrame(true, "foo", Location.FOO1, foo1Pos, frames.get(0));
break;
case BAR:
assertEquals(3, frames.size());
assertFrame(true, "bar", Location.BAR, barPos, frames.get(0));
assertFrame(false, "foo", Location.FOO1, foo1Pos, frames.get(1));
break;
case FOO2:
assertEquals(2, frames.size());
// If there's no foo2Pos then we expect foo1 pos at this location.
if (foo2Pos) {
assertFrame(true, "foo", Location.FOO2, true, frames.get(0));
} else {
assertFrame(true, "foo", Location.FOO1, foo1Pos, frames.get(0));
}
break;
default:
Assert.fail();
}
assertFrame(false, "main", Location.MAIN, mainPos, frames.get(frames.size() - 1));
}
private void assertFrame(
boolean innermostFrame,
String function,
Location location,
boolean hasPosition,
MappedRange range) {
assertEquals(function, range.signature.name);
int expectedLineNumber = hasPosition ? location.line : 0;
Object expectedOriginalRange =
innermostFrame ? new Range(expectedLineNumber, expectedLineNumber) : expectedLineNumber;
assertEquals(expectedOriginalRange, range.originalRange);
}
}