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