| // 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); |
| } |
| } |