Retrace: First cut at actually parsing stack traces and use mapping

The first cut should work for all cases that do not have ambiguous
retraced lines. This CL adds several synthetic tests for renaming and
functional tests based on obfuscated and retraced stack traces from
our bots.

There is no support for verbose yet.

Bug: 132850880
Change-Id: I876f010f9ba2e8c74c0b39965ec0b3675f0b02ed
diff --git a/src/main/java/com/android/tools/r8/retrace/Retrace.java b/src/main/java/com/android/tools/r8/retrace/Retrace.java
index 834e843..d31a838 100644
--- a/src/main/java/com/android/tools/r8/retrace/Retrace.java
+++ b/src/main/java/com/android/tools/r8/retrace/Retrace.java
@@ -114,7 +114,7 @@
       RetraceResult result =
           new RetraceCore(classNameMapper, command.stackTrace, command.diagnosticsHandler)
               .retrace();
-      command.retracedStackTraceConsumer.accept(result.toList());
+      command.retracedStackTraceConsumer.accept(result.toListOfStrings());
     } catch (IOException ex) {
       command.diagnosticsHandler.error(
           new StringDiagnostic("Could not open mapping input stream: " + ex.getMessage()));
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceCore.java b/src/main/java/com/android/tools/r8/retrace/RetraceCore.java
index 4eca518..311a0c4 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceCore.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceCore.java
@@ -4,51 +4,88 @@
 
 package com.android.tools.r8.retrace;
 
+import static com.google.common.base.Predicates.not;
+
 import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.naming.ClassNameMapper;
-import java.util.ArrayDeque;
+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.utils.DescriptorUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.Files;
 import java.util.ArrayList;
-import java.util.Deque;
+import java.util.Comparator;
 import java.util.List;
+import java.util.function.Predicate;
 
 public final class RetraceCore {
 
   public static class StackTraceNode {
-    private final List<StackTraceNode> children = new ArrayList<>();
-    // TODO(b/132850880): This is not the final design of a node, but we need a placeholder for
-    //  writing tests.
-    public String line;
+
+    private final List<StackTraceLine> lines;
+
+    StackTraceNode(List<StackTraceLine> lines) {
+      this.lines = lines;
+      assert !lines.isEmpty();
+      assert lines.size() == 1 || lines.stream().allMatch(StackTraceLine::isAtLine);
+    }
 
     @Override
     public String toString() {
-      return line;
+      assert !lines.isEmpty();
+      if (lines.size() == 1) {
+        return lines.get(0).toString();
+      }
+      // This is a stack trace line that have multiple origins that we cannot disambiguate.
+      // TODO(b/132850880): Write tests for inlining, class-inlining etc.
+      StringBuilder sb = new StringBuilder();
+      lines.sort(new AtStackTraceLineComparator());
+      for (StackTraceLine line : lines) {
+        assert line.isAtLine();
+        sb.append(line.toString());
+        sb.append(System.lineSeparator());
+      }
+      return sb.toString();
     }
   }
 
-  public static class RetraceResult {
+  static class AtStackTraceLineComparator implements Comparator<StackTraceLine> {
 
-    StackTraceNode root;
+    @Override
+    public int compare(StackTraceLine o1, StackTraceLine o2) {
+      AtLine a1 = (AtLine) o1;
+      AtLine a2 = (AtLine) o2;
+      int compare = a1.clazz.compareTo(a2.clazz);
+      if (compare != 0) {
+        return compare;
+      }
+      compare = a1.method.compareTo(a2.clazz);
+      if (compare != 0) {
+        return compare;
+      }
+      compare = a1.fileName.compareTo(a2.fileName);
+      if (compare != 0) {
+        return compare;
+      }
+      return Integer.compare(a1.linePosition, a2.linePosition);
+    }
+  }
 
-    RetraceResult(StackTraceNode root) {
-      this.root = root;
+  static class RetraceResult {
+
+    private final List<StackTraceNode> nodes;
+
+    RetraceResult(List<StackTraceNode> nodes) {
+      this.nodes = nodes;
     }
 
-    public List<String> toList() {
-      ArrayList<String> stackTrace = new ArrayList<>();
-      if (root == null) {
-        return stackTrace;
+    List<String> toListOfStrings() {
+      List<String> strings = new ArrayList<>(nodes.size());
+      for (StackTraceNode node : nodes) {
+        strings.add(node.toString());
       }
-      Deque<StackTraceNode> nodes = new ArrayDeque<>();
-      nodes.addLast(root);
-      while (!nodes.isEmpty()) {
-        StackTraceNode currentNode = nodes.removeFirst();
-        stackTrace.add(currentNode.line);
-        for (StackTraceNode child : currentNode.children) {
-          assert child != null;
-          nodes.addLast(child);
-        }
-      }
-      return stackTrace;
+      return strings;
     }
   }
 
@@ -56,7 +93,7 @@
   private final List<String> stackTrace;
   private final DiagnosticsHandler diagnosticsHandler;
 
-  public RetraceCore(
+  RetraceCore(
       ClassNameMapper classNameMapper,
       List<String> stackTrace,
       DiagnosticsHandler diagnosticsHandler) {
@@ -66,19 +103,417 @@
   }
 
   public RetraceResult retrace() {
-    return new RetraceResult(retraceLine(stackTrace, 0));
+    ArrayList<StackTraceNode> result = new ArrayList<>();
+    retraceLine(stackTrace, 0, result);
+    return new RetraceResult(result);
   }
 
-  private StackTraceNode retraceLine(List<String> stackTrace, int index) {
+  private void retraceLine(List<String> stackTrace, int index, List<StackTraceNode> result) {
     if (stackTrace.size() <= index) {
+      return;
+    }
+    StackTraceLine stackTraceLine = parseLine(index + 1, stackTrace.get(index));
+    List<StackTraceLine> retraced = stackTraceLine.retrace(classNameMapper);
+    StackTraceNode node = new StackTraceNode(retraced);
+    result.add(node);
+    retraceLine(stackTrace, index + 1, result);
+  }
+
+  abstract static class StackTraceLine {
+    abstract List<StackTraceLine> retrace(ClassNameMapper mapper);
+
+    static int firstNonWhiteSpaceCharacterFromIndex(String line, int index) {
+      return firstFromIndex(line, index, not(Character::isWhitespace));
+    }
+
+    static int firstCharFromIndex(String line, int index, char ch) {
+      return firstFromIndex(line, index, c -> c == ch);
+    }
+
+    static int firstFromIndex(String line, int index, Predicate<Character> predicate) {
+      for (int i = index; i < line.length(); i++) {
+        if (predicate.test(line.charAt(i))) {
+          return i;
+        }
+      }
+      return line.length();
+    }
+
+    AtLine asAtLine() {
       return null;
     }
-    StackTraceNode node = new StackTraceNode();
-    node.line = stackTrace.get(index);
-    StackTraceNode childNode = retraceLine(stackTrace, index + 1);
-    if (childNode != null) {
-      node.children.add(childNode);
+
+    boolean isAtLine() {
+      return false;
     }
-    return node;
+  }
+
+  /**
+   * Captures a stack trace line of the following formats:
+   *
+   * <ul>
+   *   <li>com.android.r8.R8Exception
+   *   <li>com.android.r8.R8Exception: Problem when compiling program
+   *   <li>Caused by: com.android.r8.R8InnerException: You have to write the program first
+   *   <li>com.android.r8.R8InnerException: You have to write the program first
+   * </ul>
+   *
+   * <p>This will also contains false positives, such as
+   *
+   * <pre>
+   *   W( 8207) VFY: unable to resolve static method 11: Lprivateinterfacemethods/I$-CC;....
+   * </pre>
+   *
+   * <p>The only invalid chars for type-identifiers for a java type-name is ';', '[' and '/', so we
+   * cannot really disregard the above line.
+   *
+   * <p>Caused by and Suppressed seems to not change based on locale, so we use these as markers.
+   */
+  static class ExceptionLine extends StackTraceLine {
+
+    private static final String CAUSED_BY = "Caused by: ";
+    private static final String SUPPRESSED = "Suppressed: ";
+
+    private final String initialWhiteSpace;
+    private final String description;
+    private final String exceptionClass;
+    private final String message;
+
+    ExceptionLine(
+        String initialWhiteSpace, String description, String exceptionClass, String message) {
+      this.initialWhiteSpace = initialWhiteSpace;
+      this.description = description;
+      this.exceptionClass = exceptionClass;
+      this.message = message;
+    }
+
+    static ExceptionLine tryParse(String line) {
+      if (line.isEmpty()) {
+        return null;
+      }
+      int firstNonWhiteSpaceChar = firstNonWhiteSpaceCharacterFromIndex(line, 0);
+      String description = "";
+      if (line.startsWith(CAUSED_BY, firstNonWhiteSpaceChar)) {
+        description = CAUSED_BY;
+      } else if (line.startsWith(SUPPRESSED, firstNonWhiteSpaceChar)) {
+        description = SUPPRESSED;
+      }
+      int exceptionStartIndex = firstNonWhiteSpaceChar + description.length();
+      int messageStartIndex = firstCharFromIndex(line, exceptionStartIndex, ':');
+      String className = line.substring(exceptionStartIndex, messageStartIndex);
+      if (!DescriptorUtils.isValidJavaType(className)) {
+        return null;
+      }
+      return new ExceptionLine(
+          line.substring(0, firstNonWhiteSpaceChar),
+          description,
+          className,
+          line.substring(messageStartIndex));
+    }
+
+    @Override
+    List<StackTraceLine> retrace(ClassNameMapper mapper) {
+      ClassNamingForNameMapper classNaming = mapper.getClassNaming(exceptionClass);
+      String retracedExceptionClass = exceptionClass;
+      if (classNaming != null) {
+        retracedExceptionClass = classNaming.originalName;
+      }
+      return ImmutableList.of(
+          new ExceptionLine(initialWhiteSpace, description, retracedExceptionClass, message));
+    }
+
+    @Override
+    public String toString() {
+      return initialWhiteSpace + description + exceptionClass + message;
+    }
+  }
+
+  /**
+   * Captures a stack trace line on the following form
+   *
+   * <ul>
+   *   <li>at dalvik.system.NativeStart.main(NativeStart.java:99)
+   *   <li>at dalvik.system.NativeStart.main(:99)
+   *   <li>dalvik.system.NativeStart.main(Foo.java:)
+   *   <li>at dalvik.system.NativeStart.main(Native Method)
+   * </ul>
+   *
+   * <p>Empirical evidence suggests that the "at" string is never localized.
+   */
+  static class AtLine extends StackTraceLine {
+
+    private static final int NO_POSITION = -2;
+    private static final int INVALID_POSITION = -1;
+
+    private final String startingWhitespace;
+    private final String at;
+    private final String clazz;
+    private final String method;
+    private final String fileName;
+    private final int linePosition;
+
+    private AtLine(
+        String startingWhitespace,
+        String at,
+        String clazz,
+        String method,
+        String fileName,
+        int linePosition) {
+      this.startingWhitespace = startingWhitespace;
+      this.at = at;
+      this.clazz = clazz;
+      this.method = method;
+      this.fileName = fileName;
+      this.linePosition = linePosition;
+    }
+
+    static AtLine tryParse(String line) {
+      // Check that the line is indented with some amount of white space.
+      if (line.length() == 0 || !Character.isWhitespace(line.charAt(0))) {
+        return null;
+      }
+      // Find the first non-white space character and check that we have the sequence 'a', 't', ' '.
+      int firstNonWhiteSpace = firstNonWhiteSpaceCharacterFromIndex(line, 0);
+      if (firstNonWhiteSpace + 2 >= line.length()
+          || line.charAt(firstNonWhiteSpace) != 'a'
+          || line.charAt(firstNonWhiteSpace + 1) != 't'
+          || line.charAt(firstNonWhiteSpace + 2) != ' ') {
+        return null;
+      }
+      int classStartIndex = firstNonWhiteSpaceCharacterFromIndex(line, firstNonWhiteSpace + 2);
+      if (classStartIndex >= line.length() || classStartIndex != firstNonWhiteSpace + 3) {
+        return null;
+      }
+      int parensStart = firstCharFromIndex(line, classStartIndex, '(');
+      if (parensStart >= line.length()) {
+        return null;
+      }
+      int parensEnd = firstCharFromIndex(line, parensStart, ')');
+      if (parensEnd >= line.length()) {
+        return null;
+      }
+      if (firstNonWhiteSpaceCharacterFromIndex(line, parensEnd) == line.length()) {
+        return null;
+      }
+      int methodSeparator = line.lastIndexOf('.', parensStart);
+      if (methodSeparator <= classStartIndex) {
+        return null;
+      }
+      // Check if we have a filename and position.
+      String fileName = "";
+      int position = NO_POSITION;
+      int separatorIndex = firstCharFromIndex(line, parensStart, ':');
+      if (separatorIndex < parensEnd) {
+        fileName = line.substring(parensStart + 1, separatorIndex);
+        try {
+          String positionAsString = line.substring(separatorIndex + 1, parensEnd);
+          position = Integer.parseInt(positionAsString);
+        } catch (NumberFormatException e) {
+          position = INVALID_POSITION;
+        }
+      } else {
+        fileName = line.substring(parensStart + 1, parensEnd);
+      }
+      return new AtLine(
+          line.substring(0, firstNonWhiteSpace),
+          line.substring(firstNonWhiteSpace, classStartIndex),
+          line.substring(classStartIndex, methodSeparator),
+          line.substring(methodSeparator + 1, parensStart),
+          fileName,
+          position);
+    }
+
+    @Override
+    List<StackTraceLine> retrace(ClassNameMapper mapper) {
+      ClassNamingForNameMapper classNaming = mapper.getClassNaming(clazz);
+      List<StackTraceLine> lines = new ArrayList<>();
+      if (classNaming == null) {
+        lines.add(
+            new AtLine(
+                startingWhitespace, at, clazz, method, retracedFileName(null), linePosition));
+        return lines;
+      }
+      String retraceClazz = classNaming.originalName;
+      MappedRangesOfName mappedRangesOfName = classNaming.mappedRangesByRenamedName.get(method);
+      if (mappedRangesOfName == null) {
+        lines.add(
+            new AtLine(
+                startingWhitespace,
+                at,
+                retraceClazz,
+                method,
+                retracedFileName(retraceClazz),
+                linePosition));
+        return lines;
+      }
+      List<MappedRange> mappedRanges = mappedRangesOfName.allRangesForLine(linePosition);
+      if (mappedRanges == null || mappedRanges.isEmpty()) {
+        lines.add(
+            new AtLine(
+                startingWhitespace,
+                at,
+                retraceClazz,
+                method,
+                retracedFileName(retraceClazz),
+                linePosition));
+        return lines;
+      }
+      for (MappedRange mappedRange : mappedRanges) {
+        // TODO(b/132850880): What if we have a class-merged or inlined line here?
+        int retracedLinePosition = linePosition;
+        if (linePosition > 0) {
+          retracedLinePosition = mappedRange.getOriginalLineNumber(linePosition);
+        }
+        lines.add(
+            new AtLine(
+                startingWhitespace,
+                at,
+                retraceClazz,
+                mappedRange.signature.name,
+                retracedFileName(retraceClazz),
+                retracedLinePosition));
+      }
+      assert !lines.isEmpty();
+      return lines;
+    }
+
+    private String retracedFileName(String retracedClazz) {
+      if (retracedClazz == null) {
+        // We have no new information, only rewrite filename if it is empty or SourceFile.
+        // PG-retrace will always rewrite the filename, but that seems a bit to harsh to do.
+        if (fileName.isEmpty() || fileName.equals("SourceFile") || fileName.equals("Unknown")) {
+          return getClassSimpleName(clazz) + ".java";
+        }
+        return fileName;
+      }
+      String newFileName = getClassSimpleName(retracedClazz);
+      String extension = Files.getFileExtension(fileName);
+      if (extension.isEmpty()) {
+        extension = ".java";
+      }
+      return newFileName + extension;
+    }
+
+    private String getClassSimpleName(String clazz) {
+      int lastIndexOfPeriod = clazz.lastIndexOf('.');
+      if (lastIndexOfPeriod > -1) {
+        // Check if we can find a subclass separator.
+        int endIndex = firstCharFromIndex(clazz, lastIndexOfPeriod, '$');
+        return clazz.substring(lastIndexOfPeriod + 1, endIndex);
+      } else {
+        return clazz;
+      }
+    }
+
+    @Override
+    public String toString() {
+      StringBuilder sb = new StringBuilder(startingWhitespace);
+      sb.append(at);
+      sb.append(clazz);
+      sb.append(".");
+      sb.append(method);
+      sb.append("(");
+      sb.append(fileName);
+      if (linePosition != NO_POSITION) {
+        sb.append(":");
+      }
+      if (linePosition > INVALID_POSITION) {
+        sb.append(linePosition);
+      }
+      sb.append(")");
+      return sb.toString();
+    }
+
+    int methodIndex() {
+      return at.length() + clazz.length() + 1;
+    }
+
+    @Override
+    boolean isAtLine() {
+      return true;
+    }
+
+    @Override
+    AtLine asAtLine() {
+      return this;
+    }
+  }
+
+  static class MoreLine extends StackTraceLine {
+    private final String line;
+
+    MoreLine(String line) {
+      this.line = line;
+    }
+
+    static StackTraceLine tryParse(String line) {
+      int dotsSeen = 0;
+      boolean isWhiteSpaceAllowed = true;
+      for (int i = 0; i < line.length(); i++) {
+        char ch = line.charAt(i);
+        if (Character.isWhitespace(ch) && isWhiteSpaceAllowed) {
+          continue;
+        }
+        isWhiteSpaceAllowed = false;
+        if (ch != '.') {
+          return null;
+        }
+        if (++dotsSeen == 3) {
+          return new MoreLine(line);
+        }
+      }
+      return null;
+    }
+
+    @Override
+    List<StackTraceLine> retrace(ClassNameMapper mapper) {
+      return ImmutableList.of(new MoreLine(line));
+    }
+
+    @Override
+    public String toString() {
+      return line;
+    }
+  }
+
+  static class UnknownLine extends StackTraceLine {
+    private final String line;
+
+    UnknownLine(String line) {
+      this.line = line;
+    }
+
+    @Override
+    List<StackTraceLine> retrace(ClassNameMapper mapper) {
+      return ImmutableList.of(new UnknownLine(line));
+    }
+
+    @Override
+    public String toString() {
+      return line;
+    }
+  }
+
+  private StackTraceLine parseLine(int lineNumber, String line) {
+    if (line == null) {
+      diagnosticsHandler.error(RetraceInvalidStackTraceLineDiagnostics.createNull(lineNumber));
+      throw new Retrace.RetraceAbortException();
+    }
+    // Most lines are 'at lines' so attempt to parse it first.
+    StackTraceLine parsedLine = AtLine.tryParse(line);
+    if (parsedLine != null) {
+      return parsedLine;
+    }
+    parsedLine = ExceptionLine.tryParse(line);
+    if (parsedLine != null) {
+      return parsedLine;
+    }
+    parsedLine = MoreLine.tryParse(line);
+    if (parsedLine == null) {
+      diagnosticsHandler.warning(
+          RetraceInvalidStackTraceLineDiagnostics.createParse(lineNumber, line));
+    }
+    parsedLine = new UnknownLine(line);
+    return parsedLine;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceInvalidStackTraceLineDiagnostics.java b/src/main/java/com/android/tools/r8/retrace/RetraceInvalidStackTraceLineDiagnostics.java
new file mode 100644
index 0000000..b595f87
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceInvalidStackTraceLineDiagnostics.java
@@ -0,0 +1,51 @@
+// Copyright (c) 2019, 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.retrace;
+
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.position.TextPosition;
+
+@Keep
+public class RetraceInvalidStackTraceLineDiagnostics implements Diagnostic {
+
+  private static final String NULL_STACK_TRACE_LINE_MESSAGE = "The stack trace line is <null>";
+  private static final String PARSE_STACK_TRACE_LINE_MESSAGE =
+      "Could not parse the stack trace line '%s'";
+
+  private final int lineNumber;
+  private final String message;
+
+  private RetraceInvalidStackTraceLineDiagnostics(int lineNumber, String message) {
+    this.lineNumber = lineNumber;
+    this.message = message;
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return Origin.unknown();
+  }
+
+  @Override
+  public Position getPosition() {
+    return new TextPosition(0, lineNumber, TextPosition.UNKNOWN_COLUMN);
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return message;
+  }
+
+  public static RetraceInvalidStackTraceLineDiagnostics createNull(int lineNumber) {
+    return new RetraceInvalidStackTraceLineDiagnostics(lineNumber, NULL_STACK_TRACE_LINE_MESSAGE);
+  }
+
+  public static RetraceInvalidStackTraceLineDiagnostics createParse(int lineNumber, String line) {
+    return new RetraceInvalidStackTraceLineDiagnostics(
+        lineNumber, String.format(PARSE_STACK_TRACE_LINE_MESSAGE, line));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceTests.java b/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
new file mode 100644
index 0000000..9cd242a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
@@ -0,0 +1,116 @@
+// Copyright (c) 2019, 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.retrace;
+
+import static junit.framework.TestCase.assertEquals;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessagesImpl;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.retrace.Retrace.RetraceAbortException;
+import com.android.tools.r8.retrace.stacktraces.ActualBotStackTraceBase;
+import com.android.tools.r8.retrace.stacktraces.ActualIdentityStackTrace;
+import com.android.tools.r8.retrace.stacktraces.ActualRetraceBotStackTrace;
+import com.android.tools.r8.retrace.stacktraces.FileNameExtensionStackTrace;
+import com.android.tools.r8.retrace.stacktraces.InvalidStackTrace;
+import com.android.tools.r8.retrace.stacktraces.NullStackTrace;
+import com.android.tools.r8.retrace.stacktraces.ObfucatedExceptionClassStackTrace;
+import com.android.tools.r8.retrace.stacktraces.StackTraceForTest;
+import com.android.tools.r8.retrace.stacktraces.SuppressedStackTrace;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class RetraceTests extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withCfRuntimes().build();
+  }
+
+  public RetraceTests(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testCanMapExceptionClass() {
+    runRetraceTest(new ObfucatedExceptionClassStackTrace());
+  }
+
+  @Test
+  public void testSuppressedStackTrace() {
+    runRetraceTest(new SuppressedStackTrace());
+  }
+
+  @Test
+  public void testFileNameStackTrace() {
+    runRetraceTest(new FileNameExtensionStackTrace());
+  }
+
+  @Test
+  public void testNullLineTrace() {
+    TestDiagnosticMessagesImpl diagnosticsHandler = new TestDiagnosticMessagesImpl();
+    NullStackTrace nullStackTrace = new NullStackTrace();
+    RetraceCommand retraceCommand =
+        RetraceCommand.builder(diagnosticsHandler)
+            .setProguardMapProducer(nullStackTrace::mapping)
+            .setStackTrace(nullStackTrace.obfuscatedStackTrace())
+            .setRetracedStackTraceConsumer(retraced -> fail())
+            .build();
+    try {
+      Retrace.run(retraceCommand);
+      fail();
+    } catch (RetraceAbortException e) {
+      diagnosticsHandler.assertOnlyErrors();
+      diagnosticsHandler.assertErrorsCount(1);
+      assertThat(
+          diagnosticsHandler.getErrors().get(0).getDiagnosticMessage(),
+          containsString("The stack trace line is <null>"));
+    }
+  }
+
+  @Test
+  public void testInvalidStackTraceLineWarnings() {
+    InvalidStackTrace invalidStackTraceTest = new InvalidStackTrace();
+    TestDiagnosticMessagesImpl diagnosticsHandler = runRetraceTest(invalidStackTraceTest);
+    diagnosticsHandler.assertOnlyWarnings();
+    diagnosticsHandler.assertWarningsCount(invalidStackTraceTest.expectedWarnings());
+    assertThat(
+        diagnosticsHandler.getWarnings().get(0).getDiagnosticMessage(),
+        containsString(". . . 7 more"));
+  }
+
+  @Test
+  public void testActualStackTraces() {
+    List<ActualBotStackTraceBase> stackTraces =
+        ImmutableList.of(new ActualIdentityStackTrace(), new ActualRetraceBotStackTrace());
+    for (ActualBotStackTraceBase stackTrace : stackTraces) {
+      runRetraceTest(stackTrace).assertWarningsCount(stackTrace.expectedWarnings());
+    }
+  }
+
+  private TestDiagnosticMessagesImpl runRetraceTest(StackTraceForTest stackTraceForTest) {
+    TestDiagnosticMessagesImpl diagnosticsHandler = new TestDiagnosticMessagesImpl();
+    RetraceCommand retraceCommand =
+        RetraceCommand.builder(diagnosticsHandler)
+            .setProguardMapProducer(stackTraceForTest::mapping)
+            .setStackTrace(stackTraceForTest.obfuscatedStackTrace())
+            .setRetracedStackTraceConsumer(
+                retraced -> assertEquals(stackTraceForTest.retracedStackTrace(), retraced))
+            .build();
+    Retrace.run(retraceCommand);
+    return diagnosticsHandler;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualBotStackTraceBase.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualBotStackTraceBase.java
new file mode 100644
index 0000000..b1b8e8b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualBotStackTraceBase.java
@@ -0,0 +1,23 @@
+// Copyright (c) 2019, 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.retrace.stacktraces;
+
+import com.android.tools.r8.ToolHelper;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public abstract class ActualBotStackTraceBase implements StackTraceForTest {
+
+  public String r8MappingFromGitSha(String sha) {
+    Path resolve = ToolHelper.RETRACE_MAPS_DIR.resolve(sha + "-r8lib.jar.map");
+    try {
+      return new String(Files.readAllBytes(resolve));
+    } catch (IOException e) {
+      e.printStackTrace();
+      throw new RuntimeException("Could not read mapping file " + resolve.toString());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualIdentityStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualIdentityStackTrace.java
new file mode 100644
index 0000000..a36feae
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualIdentityStackTrace.java
@@ -0,0 +1,61 @@
+// Copyright (c) 2019, 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.retrace.stacktraces;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class ActualIdentityStackTrace extends ActualBotStackTraceBase {
+
+  @Override
+  public List<String> obfuscatedStackTrace() {
+    return Arrays.asList(
+        "java.lang.AssertionError: Unexpected failure: 'I( 8207) Could not find method"
+            + " privateinterfacemethods.I$-CC.$default$iFoo, referenced from method"
+            + " privateinterfacemethods.I$-CC.$default$dFoo  (dalvikvm)",
+        "    W( 8207) VFY: unable to resolve static method 11:"
+            + " Lprivateinterfacemethods/I$-CC;.$default$iFoo"
+            + " (Lprivateinterfacemethods/I;Z)Ljava/lang/String;  (dalvikvm)",
+        "    I( 8207) Could not find method privateinterfacemethods.I$-CC.$default$iFoo,"
+            + " referenced from method privateinterfacemethods.I$-CC.lambda$lFoo$0  (dalvikvm)",
+        "    W( 8207) VFY: unable to resolve static method 11:"
+            + " Lprivateinterfacemethods/I$-CC;.$default$iFoo"
+            + " (Lprivateinterfacemethods/I;Z)Ljava/lang/String;  (dalvikvm)",
+        "    I( 8207) Could not find method privateinterfacemethods.I$-CC.$default$iFoo,"
+            + " referenced from method privateinterfacemethods.I$-CC.sFoo  (dalvikvm)",
+        "    W( 8207) VFY: unable to resolve static method 11:"
+            + " Lprivateinterfacemethods/I$-CC;.$default$iFoo"
+            + " (Lprivateinterfacemethods/I;Z)Ljava/lang/String;  (dalvikvm)",
+        "    W( 8207) Exception Ljava/lang/NoSuchMethodError; thrown while initializing"
+            + " Lprivateinterfacemethods/I;  (dalvikvm)",
+        "    W( 8207) threadid=1: thread exiting with uncaught exception (group=0xf653e180) "
+            + " (dalvikvm)",
+        "    java.lang.ExceptionInInitializerError",
+        "    \tat"
+            + " privateinterfacemethods.PrivateInterfaceMethods.main(PrivateInterfaceMethods.java:9)",
+        "    \tat dalvik.system.NativeStart.main(Native Method)",
+        "    Caused by: java.lang.NoSuchMethodError: privateinterfacemethods.I$-CC.$default$iFoo",
+        "    \tat privateinterfacemethods.I$-CC.sFoo(PrivateInterfaceMethods.java:40)",
+        "    \tat privateinterfacemethods.I$-CC.access$000(PrivateInterfaceMethods.java:28)",
+        "    \tat privateinterfacemethods.I$1.<init>(PrivateInterfaceMethods.java:31)",
+        "    \tat privateinterfacemethods.I.<clinit>(PrivateInterfaceMethods.java:30)",
+        "    \t... 2 more");
+  }
+
+  @Override
+  public String mapping() {
+    return r8MappingFromGitSha("82710798b61fd70910d76d23a71e436356becb66");
+  }
+
+  @Override
+  public List<String> retracedStackTrace() {
+    return obfuscatedStackTrace();
+  }
+
+  @Override
+  public int expectedWarnings() {
+    return 1;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualRetraceBotStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualRetraceBotStackTrace.java
new file mode 100644
index 0000000..2c057d3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/ActualRetraceBotStackTrace.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2019, 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.retrace.stacktraces;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class ActualRetraceBotStackTrace extends ActualBotStackTraceBase {
+
+  @Override
+  public List<String> obfuscatedStackTrace() {
+    return Arrays.asList(
+        "com.android.tools.r8.CompilationFailedException: Compilation failed to complete",
+        "\tat com.android.tools.r8.BaseCommand$Builder.build(:6)",
+        "\tat com.android.tools.r8.R8TestBuilder.internalCompile(R8TestBuilder.java:104)",
+        "\tat com.android.tools.r8.R8TestBuilder.internalCompile(R8TestBuilder.java:29)",
+        "\tat com.android.tools.r8.TestCompilerBuilder.compile(TestCompilerBuilder.java:89)",
+        "\tat com.android.tools.r8.TestCompilerBuilder.run(TestCompilerBuilder.java:113)",
+        "\tat com.android.tools.r8.TestBuilder.run(TestBuilder.java:49)",
+        "\tat com.android.tools.r8.ir.optimize.classinliner.ClassInlinerTest.testCodeSample(ClassInlinerTest.java:289)",
+        "",
+        "Caused by:",
+        "com.android.tools.r8.utils.b: Error: offset: 158, line: 2, column: 33, Unexpected"
+            + " attribute at <no file>:2:33",
+        "-keepattributes -keepattributes LineNumberTable",
+        "                                ^",
+        "\tat com.android.tools.r8.utils.t0.a(:21)",
+        "\tat com.android.tools.r8.shaking.ProguardConfigurationParser.parse(:19)",
+        "\tat com.android.tools.r8.R8Command$Builder.i(:16)",
+        "\tat com.android.tools.r8.R8Command$Builder.b(:11)",
+        "\tat com.android.tools.r8.R8Command$Builder.b(:1)",
+        "\tat com.android.tools.r8.BaseCommand$Builder.build(:2)",
+        "\t... 6 more");
+  }
+
+  @Override
+  public String mapping() {
+    return r8MappingFromGitSha("dab96bbe5948133f0ae6e0a88fc133464421cf47");
+  }
+
+  @Override
+  public List<String> retracedStackTrace() {
+    return Arrays.asList(
+        "com.android.tools.r8.CompilationFailedException: Compilation failed to complete",
+        "\tat com.android.tools.r8.BaseCommand$Builder.build(BaseCommand.java:143)",
+        "\tat com.android.tools.r8.R8TestBuilder.internalCompile(R8TestBuilder.java:104)",
+        "\tat com.android.tools.r8.R8TestBuilder.internalCompile(R8TestBuilder.java:29)",
+        "\tat com.android.tools.r8.TestCompilerBuilder.compile(TestCompilerBuilder.java:89)",
+        "\tat com.android.tools.r8.TestCompilerBuilder.run(TestCompilerBuilder.java:113)",
+        "\tat com.android.tools.r8.TestBuilder.run(TestBuilder.java:49)",
+        "\tat com.android.tools.r8.ir.optimize.classinliner.ClassInlinerTest.testCodeSample(ClassInlinerTest.java:289)",
+        "",
+        "Caused by:",
+        "com.android.tools.r8.utils.AbortException: Error: offset: 158, line: 2, column: 33,"
+            + " Unexpected attribute at <no file>:2:33",
+        "-keepattributes -keepattributes LineNumberTable",
+        "                                ^",
+        "\tat com.android.tools.r8.utils.Reporter.failIfPendingErrors(Reporter.java:101)",
+        "\tat com.android.tools.r8.shaking.ProguardConfigurationParser.parse(ProguardConfigurationParser.java:187)",
+        "\tat com.android.tools.r8.R8Command$Builder.makeR8Command(R8Command.java:432)",
+        "\tat com.android.tools.r8.R8Command$Builder.makeCommand(R8Command.java:413)",
+        "\tat com.android.tools.r8.R8Command$Builder.makeCommand(R8Command.java:61)",
+        "\tat com.android.tools.r8.BaseCommand$Builder.build(BaseCommand.java:139)",
+        "\t... 6 more");
+  }
+
+  @Override
+  public int expectedWarnings() {
+    return 1;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/FileNameExtensionStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/FileNameExtensionStackTrace.java
new file mode 100644
index 0000000..71a3e64
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/FileNameExtensionStackTrace.java
@@ -0,0 +1,57 @@
+// Copyright (c) 2019, 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.retrace.stacktraces;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class FileNameExtensionStackTrace implements StackTraceForTest {
+
+  @Override
+  public List<String> obfuscatedStackTrace() {
+    return Arrays.asList(
+        "a.b.c: Problem when compiling program",
+        "    at R8.main(App:800)",
+        "    at R8.main(Native Method)",
+        "    at R8.main(Main.java:)",
+        "    at R8.main(Main.kt:1)",
+        "    at R8.main(Main.foo)",
+        "    at R8.main()",
+        "    at R8.main(Unknown)",
+        "    at R8.main(SourceFile)",
+        "    at R8.main(SourceFile:1)",
+        "Suppressed: a.b.c: You have to write the program first",
+        "    at R8.retrace(App:184)",
+        "    ... 7 more");
+  }
+
+  @Override
+  public String mapping() {
+    return "foo.bar.baz -> a.b.c:";
+  }
+
+  @Override
+  public List<String> retracedStackTrace() {
+    return Arrays.asList(
+        "foo.bar.baz: Problem when compiling program",
+        "    at R8.main(App:800)",
+        "    at R8.main(Native Method)",
+        "    at R8.main(Main.java:)",
+        "    at R8.main(Main.kt:1)",
+        "    at R8.main(Main.foo)",
+        "    at R8.main(R8.java)",
+        "    at R8.main(R8.java)",
+        "    at R8.main(R8.java)",
+        "    at R8.main(R8.java:1)",
+        "Suppressed: foo.bar.baz: You have to write the program first",
+        "    at R8.retrace(App:184)",
+        "    ... 7 more");
+  }
+
+  @Override
+  public int expectedWarnings() {
+    return 0;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/InvalidStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/InvalidStackTrace.java
new file mode 100644
index 0000000..87d30d5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/InvalidStackTrace.java
@@ -0,0 +1,41 @@
+// Copyright (c) 2019, 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.retrace.stacktraces;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class InvalidStackTrace implements StackTraceForTest {
+
+  @Override
+  public List<String> obfuscatedStackTrace() {
+    return Arrays.asList(
+        "  hulubulu",
+        "  XXX, where are you",
+        "a.b.c: Problem when compiling program",
+        " . . . 7 more",
+        "  ... 7 more");
+  }
+
+  @Override
+  public String mapping() {
+    return "foo.bar.baz -> a.b.c:";
+  }
+
+  @Override
+  public List<String> retracedStackTrace() {
+    return Arrays.asList(
+        "  hulubulu",
+        "  XXX, where are you",
+        "foo.bar.baz: Problem when compiling program",
+        " . . . 7 more",
+        "  ... 7 more");
+  }
+
+  @Override
+  public int expectedWarnings() {
+    return 1;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/NullStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/NullStackTrace.java
new file mode 100644
index 0000000..99aa584
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/NullStackTrace.java
@@ -0,0 +1,40 @@
+// Copyright (c) 2019, 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.retrace.stacktraces;
+
+import static org.junit.Assert.fail;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class NullStackTrace implements StackTraceForTest {
+
+  @Override
+  public List<String> obfuscatedStackTrace() {
+    return Arrays.asList(
+        "a.b.c: Problem when compiling program",
+        "    at r8.main(App:800)",
+        null,
+        "    at r8.retrace(App:184)",
+        "    ... 7 more");
+  }
+
+  @Override
+  public String mapping() {
+    return "foo.bar.baz -> a.b.c:";
+  }
+
+  @Override
+  public List<String> retracedStackTrace() {
+    // The obfuscated stack trace should never parse because of the null-line.
+    fail();
+    return null;
+  }
+
+  @Override
+  public int expectedWarnings() {
+    return 0;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/ObfucatedExceptionClassStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/ObfucatedExceptionClassStackTrace.java
new file mode 100644
index 0000000..707874d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/ObfucatedExceptionClassStackTrace.java
@@ -0,0 +1,41 @@
+// Copyright (c) 2019, 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.retrace.stacktraces;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class ObfucatedExceptionClassStackTrace implements StackTraceForTest {
+
+  @Override
+  public List<String> obfuscatedStackTrace() {
+    return Arrays.asList(
+        "a.b.c: Problem when compiling program",
+        "    at r8.main(App:800)",
+        "Caused by: a.b.c: You have to write the program first",
+        "    at r8.retrace(App:184)",
+        "    ... 7 more");
+  }
+
+  @Override
+  public String mapping() {
+    return "foo.bar.baz -> a.b.c:";
+  }
+
+  @Override
+  public List<String> retracedStackTrace() {
+    return Arrays.asList(
+        "foo.bar.baz: Problem when compiling program",
+        "    at r8.main(App:800)",
+        "Caused by: foo.bar.baz: You have to write the program first",
+        "    at r8.retrace(App:184)",
+        "    ... 7 more");
+  }
+
+  @Override
+  public int expectedWarnings() {
+    return 0;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/StackTraceForTest.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/StackTraceForTest.java
new file mode 100644
index 0000000..e9783e6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/StackTraceForTest.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2019, 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.retrace.stacktraces;
+
+import java.util.List;
+
+public interface StackTraceForTest {
+
+  List<String> obfuscatedStackTrace();
+
+  String mapping();
+
+  List<String> retracedStackTrace();
+
+  int expectedWarnings();
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/SuppressedStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/SuppressedStackTrace.java
new file mode 100644
index 0000000..eb44d96
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/SuppressedStackTrace.java
@@ -0,0 +1,41 @@
+// Copyright (c) 2019, 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.retrace.stacktraces;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class SuppressedStackTrace implements StackTraceForTest {
+
+  @Override
+  public List<String> obfuscatedStackTrace() {
+    return Arrays.asList(
+        "a.b.c: Problem when compiling program",
+        "    at r8.main(App:800)",
+        "Suppressed: a.b.c: You have to write the program first",
+        "    at r8.retrace(App:184)",
+        "    ... 7 more");
+  }
+
+  @Override
+  public String mapping() {
+    return "foo.bar.baz -> a.b.c:";
+  }
+
+  @Override
+  public List<String> retracedStackTrace() {
+    return Arrays.asList(
+        "foo.bar.baz: Problem when compiling program",
+        "    at r8.main(App:800)",
+        "Suppressed: foo.bar.baz: You have to write the program first",
+        "    at r8.retrace(App:184)",
+        "    ... 7 more");
+  }
+
+  @Override
+  public int expectedWarnings() {
+    return 0;
+  }
+}