blob: 1426a0f3d5e6b4908eb80a3c3eb5f08d54395321 [file] [log] [blame]
// Copyright (c) 2016, 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.optimize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.android.tools.r8.CompilationException;
import com.android.tools.r8.R8Command;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.graph.DexCode;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.naming.ClassNameMapper;
import com.android.tools.r8.naming.ClassNaming;
import com.android.tools.r8.naming.MemberNaming;
import com.android.tools.r8.naming.MemberNaming.InlineInformation;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
import com.android.tools.r8.naming.MemberNaming.Signature;
import com.android.tools.r8.naming.ProguardMapReader;
import com.android.tools.r8.shaking.ProguardRuleParserException;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.DexInspector;
import com.android.tools.r8.utils.DexInspector.ClassSubject;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.io.Closer;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class R8DebugStrippingTest {
private static final String ROOT = ToolHelper.EXAMPLES_BUILD_DIR;
private static final String EXAMPLE_DEX = "throwing/classes.dex";
private static final String EXAMPLE_MAP = "throwing/throwing.map";
private static final String EXAMPLE_CLASS = "throwing.Throwing";
private static final String EXAMPLE_JAVA = "Throwing.java";
private static final String MAIN_NAME = "main";
private static final String[] MAIN_PARAMETERS = new String[]{"java.lang.String[]"};
private static final String VOID_RETURN = "void";
private static final String OTHER_NAME = "throwInAFunctionThatIsNotInlinedAndCalledTwice";
private static final String[] NO_PARAMETERS = new String[0];
private static final String INT_RETURN = "int";
private static final String THIRD_NAME = "aFunctionThatCallsAnInlinedMethodThatThrows";
private static final String[] LIST_PARAMETER = new String[]{"java.util.List"};
private static final String FORTH_NAME = "anotherFunctionThatCallsAnInlinedMethodThatThrows";
private static final String[] STRING_PARAMETER = new String[]{"java.lang.String"};
private static final Map<String, Signature> SIGNATURE_MAP = ImmutableMap.of(
MAIN_NAME, new MethodSignature(MAIN_NAME, VOID_RETURN, MAIN_PARAMETERS),
OTHER_NAME, new MethodSignature(OTHER_NAME, INT_RETURN, NO_PARAMETERS),
THIRD_NAME, new MethodSignature(THIRD_NAME, INT_RETURN, LIST_PARAMETER),
FORTH_NAME, new MethodSignature(FORTH_NAME, INT_RETURN, STRING_PARAMETER)
);
private ClassNameMapper mapper;
@Parameter(0)
public boolean compressRanges;
@Rule
public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
@Before
public void loadRangeInformation() throws IOException {
mapper = ProguardMapReader.mapperFromFile(Paths.get(ROOT, EXAMPLE_MAP));
}
@Parameters(name = "compressLineNumers={0}")
public static Object[] parameters() {
return new Object[]{true, false};
}
@Test
public void testStackTraces()
throws IOException, ProguardRuleParserException, ExecutionException, CompilationException {
// Temporary directory for R8 output.
Path out = temp.getRoot().toPath();
R8Command command =
R8Command.builder()
.addProgramFiles(Paths.get(ROOT, EXAMPLE_DEX))
.setOutputPath(out)
.setProguardMapFile(Paths.get(ROOT, EXAMPLE_MAP))
.build();
// Generate R8 processed version.
AndroidApp result =
ToolHelper.runR8(command, (options) -> options.skipDebugLineNumberOpt = !compressRanges);
ClassNameMapper classNameMapper;
try (InputStream is = result.getProguardMap()) {
classNameMapper = ProguardMapReader.mapperFromInputStream(is);
}
if (compressRanges) {
classNameMapper.forAllClassNamings(this::ensureRangesAreUniquePerClass);
}
if (!ToolHelper.artSupported()) {
return;
}
// Run art on original.
String originalOutput =
ToolHelper.runArtNoVerificationErrors(ROOT + EXAMPLE_DEX, EXAMPLE_CLASS);
// Run art on R8 processed version.
String otherOutput =
ToolHelper.runArtNoVerificationErrors(out + "/classes.dex", EXAMPLE_CLASS);
// Check that exceptions are in same range
assertStacktracesMatchRanges(originalOutput, otherOutput, classNameMapper);
// Check that we have debug information in all the places required.
DexInspector inspector = new DexInspector(out.resolve("classes.dex"));
BiMap<String, String> obfuscationMap
= classNameMapper.getObfuscatedToOriginalMapping().inverse();
ClassSubject overloaded = inspector.clazz(obfuscationMap.get("throwing.Overloaded"));
assertTrue(overloaded.isPresent());
ensureDebugInfosExist(overloaded);
}
private void ensureDebugInfosExist(ClassSubject overloaded) {
final Map<DexString, Boolean> hasDebugInfo = Maps.newIdentityHashMap();
overloaded.forAllMethods(method -> {
if (!method.isAbstract()) {
DexCode code = method.getMethod().getCode().asDexCode();
DexString name = method.getMethod().method.name;
Boolean previous = hasDebugInfo.get(name);
boolean current = code.getDebugInfo() != null;
// If we have seen one before, it should be the same as now.
assertTrue(previous == null || (previous == current));
hasDebugInfo.put(name, current);
}
}
);
}
private void ensureRangesAreUniquePerClass(ClassNaming naming) {
final Map<String, Set<Integer>> rangeMap = new HashMap<>();
naming.forAllMemberNaming(memberNaming -> {
if (memberNaming.isMethodNaming()) {
if (memberNaming.topLevelRange != MemberNaming.fakeZeroRange) {
int startLine = memberNaming.topLevelRange.from;
Set<Integer> used = rangeMap
.computeIfAbsent(memberNaming.getRenamedName(), any -> new HashSet<>());
assertFalse(used.contains(startLine));
used.add(startLine);
}
}
});
}
private String extractRangeIndex(String line, ClassNameMapper mapper) {
int position = line.lastIndexOf(EXAMPLE_JAVA);
assertNotSame("Malformed stackframe: " + line, -1, position);
String numberPart = line.substring(position + EXAMPLE_JAVA.length() + 1, line.lastIndexOf(')'));
int number = Integer.parseInt(numberPart);
// Search the signature map for all signatures that actually match. We do this by first looking
// up the renamed signature and then checking whether it is contained in the line. We prepend
// the class name to make sure we do not match random characters in the line.
for (Entry<String, Signature> entry : SIGNATURE_MAP.entrySet()) {
MemberNaming naming = mapper.getClassNaming(EXAMPLE_CLASS)
.lookupByOriginalSignature(entry.getValue());
if (!line.contains(EXAMPLE_CLASS + "." + naming.getRenamedName())) {
continue;
}
if (naming.topLevelRange.contains(number)) {
return entry.getKey() + ":" + 0;
}
int rangeNo = 1;
for (InlineInformation inlineInformation : naming.inlineInformation) {
if (inlineInformation.inlinedRange.contains(number)) {
return entry.getKey() + ":" + rangeNo;
}
rangeNo++;
}
}
fail("Number not in any range " + number);
return null;
}
private MemberNaming selectRanges(String line, ClassNameMapper mapper) {
Signature signature;
for (Entry<String, Signature> entry : SIGNATURE_MAP.entrySet()) {
if (line.contains(entry.getKey())) {
return mapper.getClassNaming(EXAMPLE_CLASS).lookup(entry.getValue());
}
}
Assert.fail("unknown method in line " + line);
return null;
}
private void assertStacktracesMatchRanges(String before, String after,
ClassNameMapper newMapper) {
String[] beforeLines = before.split("\n");
String[] afterLines = after.split("\n");
assertEquals("Output length differs", beforeLines.length,
afterLines.length);
for (int i = 0; i < beforeLines.length; i++) {
if (!beforeLines[i].startsWith("FRAME:")) {
continue;
}
String beforeLine = beforeLines[i];
String expected = extractRangeIndex(beforeLine, mapper);
String afterLine = afterLines[i];
String generated = extractRangeIndex(afterLine, newMapper);
assertEquals("Ranges match", expected, generated);
}
}
}