1// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file 2// for details. All rights reserved. Use of this source code is governed by a 3// BSD-style license that can be found in the LICENSE file. 4package com.android.tools.r8.optimize; 5 6import static org.junit.Assert.assertEquals; 7import static org.junit.Assert.assertFalse; 8import static org.junit.Assert.assertNotSame; 9import static org.junit.Assert.assertTrue; 10import static org.junit.Assert.fail; 11 12import com.android.tools.r8.CompilationException; 13import com.android.tools.r8.R8Command; 14import com.android.tools.r8.ToolHelper; 15import com.android.tools.r8.graph.DexCode; 16import com.android.tools.r8.graph.DexString; 17import com.android.tools.r8.naming.ClassNameMapper; 18import com.android.tools.r8.naming.ClassNaming; 19import com.android.tools.r8.naming.MemberNaming; 20import com.android.tools.r8.naming.MemberNaming.InlineInformation; 21import com.android.tools.r8.naming.MemberNaming.MethodSignature; 22import com.android.tools.r8.naming.MemberNaming.Signature; 23import com.android.tools.r8.naming.ProguardMapReader; 24import com.android.tools.r8.shaking.ProguardRuleParserException; 25import com.android.tools.r8.utils.AndroidApp; 26import com.android.tools.r8.utils.DexInspector; 27import com.android.tools.r8.utils.DexInspector.ClassSubject; 28import com.google.common.collect.BiMap; 29import com.google.common.collect.ImmutableMap; 30import com.google.common.collect.Maps; 31import com.google.common.io.Closer; 32import java.io.IOException; 33import java.nio.file.Path; 34import java.nio.file.Paths; 35import java.util.HashMap; 36import java.util.HashSet; 37import java.util.Map; 38import java.util.Map.Entry; 39import java.util.Set; 40import java.util.concurrent.ExecutionException; 41import org.junit.Assert; 42import org.junit.Before; 43import org.junit.Rule; 44import org.junit.Test; 45import org.junit.rules.TemporaryFolder; 46import org.junit.runner.RunWith; 47import org.junit.runners.Parameterized; 48import org.junit.runners.Parameterized.Parameter; 49import org.junit.runners.Parameterized.Parameters; 50 51@RunWith(Parameterized.class) 52public class R8DebugStrippingTest { 53 54 private static final String ROOT = ToolHelper.EXAMPLES_BUILD_DIR; 55 private static final String EXAMPLE_DEX = "throwing/classes.dex"; 56 private static final String EXAMPLE_MAP = "throwing/throwing.map"; 57 private static final String EXAMPLE_CLASS = "throwing.Throwing"; 58 private static final String EXAMPLE_JAVA = "Throwing.java"; 59 60 private static final String MAIN_NAME = "main"; 61 private static final String[] MAIN_PARAMETERS = new String[]{"java.lang.String[]"}; 62 private static final String VOID_RETURN = "void"; 63 64 private static final String OTHER_NAME = "throwInAFunctionThatIsNotInlinedAndCalledTwice"; 65 private static final String[] NO_PARAMETERS = new String[0]; 66 private static final String INT_RETURN = "int"; 67 68 private static final String THIRD_NAME = "aFunctionThatCallsAnInlinedMethodThatThrows"; 69 private static final String[] LIST_PARAMETER = new String[]{"java.util.List"}; 70 71 private static final String FORTH_NAME = "anotherFunctionThatCallsAnInlinedMethodThatThrows"; 72 private static final String[] STRING_PARAMETER = new String[]{"java.lang.String"}; 73 74 private static final Map<String, Signature> SIGNATURE_MAP = ImmutableMap.of( 75 MAIN_NAME, new MethodSignature(MAIN_NAME, VOID_RETURN, MAIN_PARAMETERS), 76 OTHER_NAME, new MethodSignature(OTHER_NAME, INT_RETURN, NO_PARAMETERS), 77 THIRD_NAME, new MethodSignature(THIRD_NAME, INT_RETURN, LIST_PARAMETER), 78 FORTH_NAME, new MethodSignature(FORTH_NAME, INT_RETURN, STRING_PARAMETER) 79 ); 80 81 private ClassNameMapper mapper; 82 83 @Parameter(0) 84 public boolean compressRanges; 85 86 @Rule 87 public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest(); 88 89 @Before 90 public void loadRangeInformation() throws IOException { 91 mapper = ProguardMapReader.mapperFromFile(Paths.get(ROOT, EXAMPLE_MAP)); 92 } 93 94 @Parameters(name = "compressLineNumers={0}") 95 public static Object[] parameters() { 96 return new Object[]{true, false}; 97 } 98 99 @Test 100 public void testStackTraces() 101 throws IOException, ProguardRuleParserException, ExecutionException, CompilationException { 102 103 // Temporary directory for R8 output. 104 Path out = temp.getRoot().toPath(); 105 106 R8Command command = 107 R8Command.builder() 108 .addProgramFiles(Paths.get(ROOT, EXAMPLE_DEX)) 109 .setOutputPath(out) 110 .setProguardMapFile(Paths.get(ROOT, EXAMPLE_MAP)) 111 .build(); 112 113 // Generate R8 processed version. 114 AndroidApp result = 115 ToolHelper.runR8(command, (options) -> options.skipDebugLineNumberOpt = !compressRanges); 116 117 ClassNameMapper classNameMapper; 118 try (Closer closer = Closer.create()) { 119 classNameMapper = ProguardMapReader.mapperFromInputStream(result.getProguardMap(closer)); 120 } 121 if (compressRanges) { 122 classNameMapper.forAllClassNamings(this::ensureRangesAreUniquePerClass); 123 } 124 125 if (!ToolHelper.artSupported()) { 126 return; 127 } 128 // Run art on original. 129 String originalOutput = 130 ToolHelper.runArtNoVerificationErrors(ROOT + EXAMPLE_DEX, EXAMPLE_CLASS); 131 // Run art on R8 processed version. 132 String otherOutput = 133 ToolHelper.runArtNoVerificationErrors(out + "/classes.dex", EXAMPLE_CLASS); 134 // Check that exceptions are in same range 135 assertStacktracesMatchRanges(originalOutput, otherOutput, classNameMapper); 136 137 // Check that we have debug information in all the places required. 138 DexInspector inspector = new DexInspector(out.resolve("classes.dex")); 139 BiMap<String, String> obfuscationMap 140 = classNameMapper.getObfuscatedToOriginalMapping().inverse(); 141 ClassSubject overloaded = inspector.clazz(obfuscationMap.get("throwing.Overloaded")); 142 assertTrue(overloaded.isPresent()); 143 ensureDebugInfosExist(overloaded); 144 } 145 146 private void ensureDebugInfosExist(ClassSubject overloaded) { 147 final Map<DexString, Boolean> hasDebugInfo = Maps.newIdentityHashMap(); 148 overloaded.forAllMethods(method -> { 149 if (!method.isAbstract()) { 150 DexCode code = method.getMethod().getCode().asDexCode(); 151 DexString name = method.getMethod().method.name; 152 Boolean previous = hasDebugInfo.get(name); 153 boolean current = code.getDebugInfo() != null; 154 // If we have seen one before, it should be the same as now. 155 assertTrue(previous == null || (previous == current)); 156 hasDebugInfo.put(name, current); 157 } 158 } 159 ); 160 } 161 162 private void ensureRangesAreUniquePerClass(ClassNaming naming) { 163 final Map<String, Set<Integer>> rangeMap = new HashMap<>(); 164 naming.forAllMemberNaming(memberNaming -> { 165 if (memberNaming.isMethodNaming()) { 166 if (memberNaming.topLevelRange != MemberNaming.fakeZeroRange) { 167 int startLine = memberNaming.topLevelRange.from; 168 Set<Integer> used = rangeMap 169 .computeIfAbsent(memberNaming.getRenamedName(), any -> new HashSet<>()); 170 assertFalse(used.contains(startLine)); 171 used.add(startLine); 172 } 173 } 174 }); 175 } 176 177 private String extractRangeIndex(String line, ClassNameMapper mapper) { 178 int position = line.lastIndexOf(EXAMPLE_JAVA); 179 assertNotSame("Malformed stackframe: " + line, -1, position); 180 String numberPart = line.substring(position + EXAMPLE_JAVA.length() + 1, line.lastIndexOf(')')); 181 int number = Integer.parseInt(numberPart); 182 // Search the signature map for all signatures that actually match. We do this by first looking 183 // up the renamed signature and then checking whether it is contained in the line. We prepend 184 // the class name to make sure we do not match random characters in the line. 185 for (Entry<String, Signature> entry : SIGNATURE_MAP.entrySet()) { 186 MemberNaming naming = mapper.getClassNaming(EXAMPLE_CLASS) 187 .lookupByOriginalSignature(entry.getValue()); 188 if (!line.contains(EXAMPLE_CLASS + "." + naming.getRenamedName())) { 189 continue; 190 } 191 if (naming.topLevelRange.contains(number)) { 192 return entry.getKey() + ":" + 0; 193 } 194 int rangeNo = 1; 195 for (InlineInformation inlineInformation : naming.inlineInformation) { 196 if (inlineInformation.inlinedRange.contains(number)) { 197 return entry.getKey() + ":" + rangeNo; 198 } 199 rangeNo++; 200 } 201 } 202 fail("Number not in any range " + number); 203 return null; 204 } 205 206 private MemberNaming selectRanges(String line, ClassNameMapper mapper) { 207 Signature signature; 208 for (Entry<String, Signature> entry : SIGNATURE_MAP.entrySet()) { 209 if (line.contains(entry.getKey())) { 210 return mapper.getClassNaming(EXAMPLE_CLASS).lookup(entry.getValue()); 211 } 212 } 213 Assert.fail("unknown method in line " + line); 214 return null; 215 } 216 217 private void assertStacktracesMatchRanges(String before, String after, 218 ClassNameMapper newMapper) { 219 String[] beforeLines = before.split("\n"); 220 String[] afterLines = after.split("\n"); 221 assertEquals("Output length differs", beforeLines.length, 222 afterLines.length); 223 for (int i = 0; i < beforeLines.length; i++) { 224 if (!beforeLines[i].startsWith("FRAME:")) { 225 continue; 226 } 227 String beforeLine = beforeLines[i]; 228 String expected = extractRangeIndex(beforeLine, mapper); 229 String afterLine = afterLines[i]; 230 String generated = extractRangeIndex(afterLine, newMapper); 231 assertEquals("Ranges match", expected, generated); 232 } 233 } 234} 235