Update the retrace test

* Add better utility for extracting stack traces and comparing them
* Refactor to prepare for adding more tests
* Extend current test with attribute combinations of SourceFile and
  LineNumberTable

Bug: 117815862
Change-Id: Ib0a62091bcdbe7fbbcb49327ec96948db667c4aa
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/InliningRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/InliningRetraceTest.java
new file mode 100644
index 0000000..6d7ce25
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/retrace/InliningRetraceTest.java
@@ -0,0 +1,101 @@
+// 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.naming.retrace;
+
+import static com.android.tools.r8.naming.retrace.StackTrace.isSameExceptForFileName;
+import static com.android.tools.r8.naming.retrace.StackTrace.isSameExceptForFileNameAndLineNumber;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.ForceInline;
+import java.util.Collection;
+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 InliningRetraceTest extends RetraceTestBase {
+
+  @Parameters(name = "Backend: {0}, mode: {1}")
+  public static Collection<Object[]> data() {
+    return buildParameters(Backend.values(), CompilationMode.values());
+  }
+
+  public InliningRetraceTest(Backend backend, CompilationMode mode) {
+    super(backend, mode);
+  }
+
+  @Override
+  public Class<?> getMainClass() {
+    return Main.class;
+  }
+
+  private int expectedActualStackTraceHeight() {
+    return mode == CompilationMode.RELEASE ? 1 : 4;
+  }
+
+  @Test
+  public void testSourceFileAndLineNumberTable() throws Exception {
+    runTest(
+        "-keepattributes SourceFile,LineNumberTable",
+        (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
+          // Even when SourceFile is present retrace replaces the file name in the stack trace.
+          assertThat(retracedStackTrace, isSameExceptForFileName(expectedStackTrace));
+          assertEquals(expectedActualStackTraceHeight(), actualStackTrace.size());
+        });
+  }
+
+  @Test
+  public void testLineNumberTableOnly() throws Exception {
+    runTest(
+        "-keepattributes LineNumberTable",
+        (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
+          assertThat(retracedStackTrace, isSameExceptForFileName(expectedStackTrace));
+          assertEquals(expectedActualStackTraceHeight(), actualStackTrace.size());
+        });
+  }
+
+  @Test
+  public void testNoLineNumberTable() throws Exception {
+    runTest(
+        "",
+        (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
+          assertThat(retracedStackTrace, isSameExceptForFileNameAndLineNumber(expectedStackTrace));
+          assertEquals(expectedActualStackTraceHeight(), actualStackTrace.size());
+        });
+  }
+}
+
+class Main {
+
+  @ForceInline
+  public static void method3(long j) {
+    System.out.println("In method3");
+    throw null;
+  }
+
+  @ForceInline
+  public static void method2(int j) {
+    System.out.println("In method2");
+    for (int i = 0; i < 10; i++) {
+      method3((long) j);
+    }
+  }
+
+  @ForceInline
+  public static void method1(String s) {
+    System.out.println("In method1");
+    for (int i = 0; i < 10; i++) {
+      method2(Integer.parseInt(s));
+    }
+  }
+
+  public static void main(String[] args) {
+    System.out.println("In main");
+    method1("1");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/RetraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/RetraceTest.java
deleted file mode 100644
index b110ead..0000000
--- a/src/test/java/com/android/tools/r8/naming/retrace/RetraceTest.java
+++ /dev/null
@@ -1,159 +0,0 @@
-// 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.naming.retrace;
-
-import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.CoreMatchers.not;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
-
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.R8Command;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.DexVm;
-import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.FileUtils;
-import com.android.tools.r8.utils.StringUtils;
-import com.google.common.collect.ImmutableList;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.List;
-import java.util.function.BiConsumer;
-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 RetraceTest extends TestBase {
-  private Backend backend;
-  private CompilationMode mode;
-
-  @Parameters(name = "Backend: {0}, mode: {1}")
-  public static Collection<Object[]> data() {
-    return buildParameters(Backend.values(), CompilationMode.values());
-  }
-
-  public RetraceTest(Backend backend, CompilationMode mode) {
-    this.backend = backend;
-    this.mode = mode;
-  }
-
-  private List<String> retrace(String map, List<String> stackTrace) throws IOException {
-    Path t = temp.newFolder().toPath();
-    Path mapFile = t.resolve("map");
-    Path stackTraceFile = t.resolve("stackTrace");
-    FileUtils.writeTextFile(mapFile, map);
-    FileUtils.writeTextFile(stackTraceFile, stackTrace);
-    return StringUtils.splitLines(ToolHelper.runRetrace(mapFile, stackTraceFile));
-  }
-
-  private boolean isDalvik() {
-    return backend == Backend.DEX && ToolHelper.getDexVm().isOlderThanOrEqual(DexVm.ART_4_4_4_HOST);
-  }
-
-  private List<String> extractStackTrace(ProcessResult result) {
-    ImmutableList.Builder<String> builder = ImmutableList.builder();
-    List<String> stderr = StringUtils.splitLines(result.stderr);
-    Iterator<String> iterator = stderr.iterator();
-
-    // A Dalvik stacktrace looks like this:
-    // W(209693) threadid=1: thread exiting with uncaught exception (group=0xf616cb20)  (dalvikvm)
-    // java.lang.NullPointerException
-    // \tat com.android.tools.r8.naming.retrace.Main.a(:133)
-    // \tat com.android.tools.r8.naming.retrace.Main.a(:139)
-    // \tat com.android.tools.r8.naming.retrace.Main.main(:145)
-    // \tat dalvik.system.NativeStart.main(Native Method)
-    //
-    // An Art 5.1.1 and 6.0.1 stacktrace looks like this:
-    // java.lang.NullPointerException: throw with null exception
-    // \tat com.android.tools.r8.naming.retrace.Main.a(:154)
-    // \tat com.android.tools.r8.naming.retrace.Main.a(:160)
-    // \tat com.android.tools.r8.naming.retrace.Main.main(:166)
-    //
-    // An Art 7.0.0 and latest stacktrace looks like this:
-    // Exception in thread "main" java.lang.NullPointerException: throw with null exception
-    // \tat com.android.tools.r8.naming.retrace.Main.a(:150)
-    // \tat com.android.tools.r8.naming.retrace.Main.a(:156)
-    // \tat com.android.tools.r8.naming.retrace.Main.main(:162)
-    int last = stderr.size();
-    if (isDalvik()) {
-      // Skip the bottom frame "dalvik.system.NativeStart.main".
-      last--;
-    }
-    // Take all lines from the bottom starting with "\tat ".
-    int first = last;
-    while (first - 1 >= 0 && stderr.get(first - 1).startsWith("\tat ")) {
-      first--;
-    }
-    for (int i = first; i < last; i++) {
-      builder.add(stderr.get(i));
-    }
-    return builder.build();
-  }
-
-  public void runTest(Class<?> mainClass, BiConsumer<List<String>, List<String>> checker)
-      throws Exception {
-    StringBuilder proguardMapBuilder = new StringBuilder();
-    AndroidApp output =
-        ToolHelper.runR8(
-            R8Command.builder()
-                .setMode(mode)
-                .addClassProgramData(ToolHelper.getClassAsBytes(mainClass), Origin.unknown())
-                .addProguardConfiguration(
-                    ImmutableList.of(keepMainProguardConfiguration(mainClass)), Origin.unknown())
-                .setProgramConsumer(emptyConsumer(backend))
-                .setProguardMapConsumer((string, ignore) -> proguardMapBuilder.append(string))
-                .build());
-
-    ProcessResult result = runOnVMRaw(output, mainClass, backend);
-    List<String> stackTrace = extractStackTrace(result);
-    List<String> retracesStackTrace = retrace(proguardMapBuilder.toString(), stackTrace);
-    checker.accept(stackTrace, retracesStackTrace);
-  }
-
-  @Test
-  public void test() throws Exception {
-    runTest(
-        Main.class,
-        (List<String> stackTrace, List<String> retracesStackTrace) -> {
-          assertEquals(
-              mode == CompilationMode.RELEASE, stackTrace.size() != retracesStackTrace.size());
-          if (mode == CompilationMode.DEBUG) {
-            assertThat(stackTrace.get(0), not(containsString("method2")));
-            assertThat(stackTrace.get(1), not(containsString("method1")));
-            assertThat(stackTrace.get(2), containsString("main"));
-          }
-          assertEquals(3, retracesStackTrace.size());
-          assertThat(retracesStackTrace.get(0), containsString("method2"));
-          assertThat(retracesStackTrace.get(1), containsString("method1"));
-          assertThat(retracesStackTrace.get(2), containsString("main"));
-        });
-  }
-}
-
-class Main {
-  public static void method2(int i) {
-    System.out.println("In method2");
-    throw null;
-  }
-
-  public static void method1(String s) {
-    System.out.println("In method1");
-    for (int i = 0; i < 10; i++) {
-      method2(Integer.parseInt(s));
-    }
-  }
-
-  public static void main(String[] args) {
-    System.out.println("In main");
-    method1("1");
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java b/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java
new file mode 100644
index 0000000..0d0c21b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java
@@ -0,0 +1,58 @@
+// 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.naming.retrace;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestBase;
+import java.util.function.BiConsumer;
+import org.junit.Before;
+
+public abstract class RetraceTestBase extends TestBase {
+  protected Backend backend;
+  protected CompilationMode mode;
+
+  public RetraceTestBase(Backend backend, CompilationMode mode) {
+    this.backend = backend;
+    this.mode = mode;
+  }
+
+  public StackTrace expectedStackTrace;
+
+  public abstract Class<?> getMainClass();
+
+  @Before
+  public void setup() throws Exception {
+    // Get the expected stack trace by running on the JVM.
+    expectedStackTrace =
+        testForJvm()
+            .addTestClasspath()
+            .run(getMainClass())
+            .assertFailure()
+            .map(StackTrace::extractFromJvm);
+  }
+
+  public void runTest(String keepRule, BiConsumer<StackTrace, StackTrace> checker)
+      throws Exception {
+
+    R8TestRunResult result =
+        testForR8(backend)
+            .setMode(mode)
+            .enableProguardTestOptions()
+            .enableInliningAnnotations()
+            .addProgramClasses(getMainClass())
+            .addKeepMainRule(getMainClass())
+            .addKeepRules(keepRule)
+            .run(getMainClass())
+            .assertFailure();
+
+    // Extract actual stack trace and retraced stack trace from failed run result.
+    StackTrace actualStackTrace = StackTrace.extractFromArt(result.getStdErr());
+    StackTrace retracedStackTrace =
+        actualStackTrace.retrace(result.proguardMap(), temp.newFolder().toPath());
+
+    checker.accept(actualStackTrace, retracedStackTrace);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java b/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java
new file mode 100644
index 0000000..68e09c1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java
@@ -0,0 +1,436 @@
+// 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.naming.retrace;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.base.Equivalence;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+class StackTrace {
+
+  public static String AT_PREFIX = "at ";
+  public static String TAB_AT_PREFIX = "\t" + AT_PREFIX;
+
+  static class StackTraceLine {
+    public final String originalLine;
+    public final String className;
+    public final String methodName;
+    public final String fileName;
+    public final int lineNumber;
+
+    public StackTraceLine(
+        String originalLine, String className, String methodName, String fileName, int lineNumber) {
+      this.originalLine = originalLine;
+      this.className = className;
+      this.methodName = methodName;
+      this.fileName = fileName;
+      this.lineNumber = lineNumber;
+    }
+
+    public static StackTraceLine parse(String line) {
+      String originalLine = line;
+
+      line = line.trim();
+      if (line.startsWith(AT_PREFIX)) {
+        line = line.substring(AT_PREFIX.length());
+      }
+
+      // Expect only one '(', and only one ')' with a ':' in between.
+      int parenBeginIndex = line.indexOf('(');
+      assertTrue(parenBeginIndex > 0);
+      assertEquals(parenBeginIndex, line.lastIndexOf('('));
+      int parenEndIndex = line.indexOf(')');
+      assertTrue(parenBeginIndex < parenEndIndex);
+      assertEquals(parenEndIndex, line.lastIndexOf(')'));
+      int colonIndex = line.indexOf(':');
+      assertTrue(parenBeginIndex < colonIndex && colonIndex < parenEndIndex);
+      assertEquals(parenEndIndex, line.lastIndexOf(')'));
+      String classAndMethod = line.substring(0, parenBeginIndex);
+      int lastDotIndex = classAndMethod.lastIndexOf('.');
+      assertTrue(lastDotIndex > 0);
+      String className = classAndMethod.substring(0, lastDotIndex);
+      String methodName = classAndMethod.substring(lastDotIndex + 1, classAndMethod.length());
+      String fileName = line.substring(parenBeginIndex + 1, colonIndex);
+      int lineNumber = Integer.parseInt(line.substring(colonIndex + 1, parenEndIndex));
+      StackTraceLine result =
+          new StackTraceLine(originalLine, className, methodName, fileName, lineNumber);
+      assertEquals(line, result.toString());
+      return result;
+    }
+
+    @Override
+    public int hashCode() {
+      return className.hashCode() * 31
+          + methodName.hashCode() * 13
+          + fileName.hashCode() * 7
+          + lineNumber;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (this == other) {
+        return true;
+      }
+      if (other instanceof StackTraceLine) {
+        StackTraceLine o = (StackTraceLine) other;
+        return className.equals(o.className)
+            && methodName.equals(o.methodName)
+            && fileName.equals(o.fileName)
+            && lineNumber == o.lineNumber;
+      }
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      return className + '.' + methodName + '(' + fileName + ':' + lineNumber + ')';
+    }
+  }
+
+  private final List<StackTraceLine> stackTraceLines;
+
+  private StackTrace(List<StackTraceLine> stackTraceLines) {
+    assert stackTraceLines.size() > 0;
+    this.stackTraceLines = stackTraceLines;
+  }
+
+  public int size() {
+    return stackTraceLines.size();
+  }
+
+  public StackTraceLine get(int index) {
+    return stackTraceLines.get(index);
+  }
+
+  public static StackTrace extractFromArt(String stderr) {
+    List<StackTraceLine> stackTraceLines = new ArrayList<>();
+    List<String> stderrLines = StringUtils.splitLines(stderr);
+
+    // A Dalvik stacktrace looks like this:
+    // W(209693) threadid=1: thread exiting with uncaught exception (group=0xf616cb20)  (dalvikvm)
+    // java.lang.NullPointerException
+    // \tat com.android.tools.r8.naming.retrace.Main.a(:133)
+    // \tat com.android.tools.r8.naming.retrace.Main.a(:139)
+    // \tat com.android.tools.r8.naming.retrace.Main.main(:145)
+    // \tat dalvik.system.NativeStart.main(Native Method)
+    //
+    // An Art 5.1.1 and 6.0.1 stacktrace looks like this:
+    // java.lang.NullPointerException: throw with null exception
+    // \tat com.android.tools.r8.naming.retrace.Main.a(:154)
+    // \tat com.android.tools.r8.naming.retrace.Main.a(:160)
+    // \tat com.android.tools.r8.naming.retrace.Main.main(:166)
+    //
+    // An Art 7.0.0 and latest stacktrace looks like this:
+    // Exception in thread "main" java.lang.NullPointerException: throw with null exception
+    // \tat com.android.tools.r8.naming.retrace.Main.a(:150)
+    // \tat com.android.tools.r8.naming.retrace.Main.a(:156)
+    // \tat com.android.tools.r8.naming.retrace.Main.main(:162)
+    int last = stderrLines.size();
+    if (ToolHelper.getDexVm().isOlderThanOrEqual(DexVm.ART_4_4_4_HOST)) {
+      // Skip the bottom frame "dalvik.system.NativeStart.main".
+      last--;
+    }
+    // Take all lines from the bottom starting with "\tat ".
+    int first = last;
+    while (first - 1 >= 0 && stderrLines.get(first - 1).startsWith(TAB_AT_PREFIX)) {
+      first--;
+    }
+    for (int i = first; i < last; i++) {
+      stackTraceLines.add(StackTraceLine.parse(stderrLines.get(i)));
+    }
+    return new StackTrace(stackTraceLines);
+  }
+
+  public static StackTrace extractFromJvm(String stderr) {
+    return new StackTrace(
+        StringUtils.splitLines(stderr).stream()
+            .filter(s -> s.startsWith(TAB_AT_PREFIX))
+            .map(StackTraceLine::parse)
+            .collect(Collectors.toList()));
+  }
+
+  public static StackTrace extractFromJvm(TestRunResult result) {
+    assertNotEquals(0, result.getExitCode());
+    return extractFromJvm(result.getStdErr());
+  }
+
+  public StackTrace retrace(String map, Path tempFolder) throws IOException {
+    Path mapFile = tempFolder.resolve("map");
+    Path stackTraceFile = tempFolder.resolve("stackTrace");
+    FileUtils.writeTextFile(mapFile, map);
+    FileUtils.writeTextFile(
+        stackTraceFile,
+        stackTraceLines.stream().map(line -> line.originalLine).collect(Collectors.toList()));
+    return StackTrace.extractFromJvm(ToolHelper.runRetrace(mapFile, stackTraceFile));
+  }
+
+  @Override
+  public int hashCode() {
+    return stackTraceLines.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+    if (other instanceof StackTrace) {
+      return stackTraceLines.equals(((StackTrace) other).stackTraceLines);
+    } else {
+      return false;
+    }
+  }
+
+  @Override
+  public String toString() {
+    return toStringWithPrefix("");
+  }
+
+  public String toStringWithPrefix(String prefix) {
+    StringBuilder builder = new StringBuilder();
+    for (StackTraceLine stackTraceLine : stackTraceLines) {
+      builder.append(prefix).append(stackTraceLine).append(System.lineSeparator());
+    }
+    return builder.toString();
+  }
+
+  public abstract static class StackTraceEquivalence extends Equivalence<StackTrace> {
+    public abstract Equivalence<StackTrace.StackTraceLine> getLineEquivalence();
+
+    @Override
+    protected boolean doEquivalent(StackTrace a, StackTrace b) {
+      if (a.stackTraceLines.size() != b.stackTraceLines.size()) {
+        return false;
+      }
+      for (int i = 0; i < a.stackTraceLines.size(); i++) {
+        if (!getLineEquivalence().equivalent(a.stackTraceLines.get(i), b.stackTraceLines.get(i))) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    @Override
+    protected int doHash(StackTrace stackTrace) {
+      int hashCode = stackTrace.size() * 13;
+      for (StackTrace.StackTraceLine stackTraceLine : stackTrace.stackTraceLines) {
+        hashCode += (hashCode << 4) + getLineEquivalence().hash(stackTraceLine);
+      }
+      return hashCode;
+    }
+  }
+
+  // Equivalence forwarding to default stack trace comparison.
+  public static class EquivalenceFull extends StackTraceEquivalence {
+
+    private static class LineEquivalence extends Equivalence<StackTrace.StackTraceLine> {
+
+      private static final LineEquivalence INSTANCE = new LineEquivalence();
+
+      public static LineEquivalence get() {
+        return INSTANCE;
+      }
+
+      @Override
+      protected boolean doEquivalent(StackTrace.StackTraceLine a, StackTrace.StackTraceLine b) {
+        return a.equals(b);
+      }
+
+      @Override
+      protected int doHash(StackTrace.StackTraceLine stackTraceLine) {
+        return stackTraceLine.hashCode();
+      }
+    }
+
+    private static final EquivalenceFull INSTANCE = new EquivalenceFull();
+
+    public static EquivalenceFull get() {
+      return INSTANCE;
+    }
+
+    @Override
+    public Equivalence<StackTrace.StackTraceLine> getLineEquivalence() {
+      return LineEquivalence.get();
+    }
+
+    @Override
+    protected boolean doEquivalent(StackTrace a, StackTrace b) {
+      return a.equals(b);
+    }
+
+    @Override
+    protected int doHash(StackTrace stackTrace) {
+      return stackTrace.hashCode();
+    }
+  }
+
+  // Equivalence comparing stack traces without taking the file name into account.
+  public static class EquivalenceWithoutFileName extends StackTraceEquivalence {
+
+    private static class LineEquivalence extends Equivalence<StackTrace.StackTraceLine> {
+
+      private static final LineEquivalence INSTANCE = new LineEquivalence();
+
+      public static LineEquivalence get() {
+        return INSTANCE;
+      }
+
+      @Override
+      protected boolean doEquivalent(StackTrace.StackTraceLine a, StackTrace.StackTraceLine b) {
+        return a.className.equals(b.className)
+            && a.methodName.equals(b.methodName)
+            && a.lineNumber == b.lineNumber;
+      }
+
+      @Override
+      protected int doHash(StackTrace.StackTraceLine stackTraceLine) {
+        return stackTraceLine.className.hashCode() * 13
+            + stackTraceLine.methodName.hashCode() * 7
+            + stackTraceLine.lineNumber;
+      }
+    }
+
+    private static final EquivalenceWithoutFileName INSTANCE = new EquivalenceWithoutFileName();
+
+    public static EquivalenceWithoutFileName get() {
+      return INSTANCE;
+    }
+
+    @Override
+    public Equivalence<StackTrace.StackTraceLine> getLineEquivalence() {
+      return LineEquivalence.get();
+    }
+  }
+
+  // Equivalence comparing stack traces without taking the file name and line number into account.
+  public static class EquivalenceWithoutFileNameAndLineNumber extends StackTraceEquivalence {
+
+    private static final EquivalenceWithoutFileNameAndLineNumber INSTANCE =
+        new EquivalenceWithoutFileNameAndLineNumber();
+
+    public static EquivalenceWithoutFileNameAndLineNumber get() {
+      return INSTANCE;
+    }
+
+    public static class LineEquivalence extends Equivalence<StackTrace.StackTraceLine> {
+
+      private static final LineEquivalence INSTANCE = new LineEquivalence();
+
+      public static LineEquivalence get() {
+        return INSTANCE;
+      }
+
+      @Override
+      protected boolean doEquivalent(StackTrace.StackTraceLine a, StackTrace.StackTraceLine b) {
+        return a.className.equals(b.className) && a.methodName.equals(b.methodName);
+      }
+
+      @Override
+      protected int doHash(StackTrace.StackTraceLine stackTraceLine) {
+        return stackTraceLine.className.hashCode() * 13 + stackTraceLine.methodName.hashCode() * 7;
+      }
+    }
+
+    @Override
+    public Equivalence<StackTrace.StackTraceLine> getLineEquivalence() {
+      return LineEquivalence.get();
+    }
+  }
+
+  public static class StackTraceMatcherBase extends TypeSafeMatcher<StackTrace> {
+    private final StackTrace expected;
+    private final StackTraceEquivalence equivalence;
+    private final String comparisonDescription;
+
+    private StackTraceMatcherBase(
+        StackTrace expected, StackTraceEquivalence equivalence, String comparisonDescription) {
+      this.expected = expected;
+      this.equivalence = equivalence;
+      this.comparisonDescription = comparisonDescription;
+    }
+
+    @Override
+    public boolean matchesSafely(StackTrace stackTrace) {
+      return equivalence.equivalent(expected, stackTrace);
+    }
+
+    @Override
+    public void describeTo(Description description) {
+      description
+          .appendText("stacktrace " + comparisonDescription)
+          .appendText(System.lineSeparator())
+          .appendText(expected.toString());
+    }
+
+    @Override
+    public void describeMismatchSafely(final StackTrace stackTrace, Description description) {
+      description.appendText("stacktrace was " + stackTrace);
+      description.appendText(System.lineSeparator());
+      if (expected.size() != stackTrace.size()) {
+        description.appendText("They have different sizes.");
+      } else {
+        for (int i = 0; i < expected.size(); i++) {
+          if (!equivalence.getLineEquivalence().equivalent(expected.get(i), stackTrace.get(i))) {
+            description
+                .appendText("First different entry is index " + i + ":")
+                .appendText(System.lineSeparator())
+                .appendText("Expected: " + expected.get(i))
+                .appendText(System.lineSeparator())
+                .appendText("     Was: " + stackTrace.get(i));
+            return;
+          }
+        }
+      }
+    }
+  }
+
+  public static class StackTraceMatcher extends StackTraceMatcherBase {
+    private StackTraceMatcher(StackTrace expected) {
+      super(expected, EquivalenceFull.get(), "");
+    }
+  }
+
+  public static Matcher<StackTrace> isSame(StackTrace stackTrace) {
+    return new StackTraceMatcher(stackTrace);
+  }
+
+  public static class StackTraceIgnoreFileNameMatcher extends StackTraceMatcherBase {
+    private StackTraceIgnoreFileNameMatcher(StackTrace expected) {
+      super(expected, EquivalenceWithoutFileName.get(), "(ignoring file name)");
+    }
+  }
+
+  public static Matcher<StackTrace> isSameExceptForFileName(StackTrace stackTrace) {
+    return new StackTraceIgnoreFileNameMatcher(stackTrace);
+  }
+
+  public static class StackTraceIgnoreFileNameAndLineNumberMatcher extends StackTraceMatcherBase {
+    private StackTraceIgnoreFileNameAndLineNumberMatcher(StackTrace expected) {
+      super(
+          expected,
+          EquivalenceWithoutFileNameAndLineNumber.get(),
+          "(ignoring file name and line number)");
+    }
+  }
+
+  public static Matcher<StackTrace> isSameExceptForFileNameAndLineNumber(StackTrace stackTrace) {
+    return new StackTraceIgnoreFileNameAndLineNumberMatcher(stackTrace);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/StackTraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/StackTraceTest.java
new file mode 100644
index 0000000..9efa186
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/retrace/StackTraceTest.java
@@ -0,0 +1,66 @@
+// 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.naming.retrace;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+
+public class StackTraceTest {
+
+  private static String oneLineStackTrace = "\tat Test.main(Test.java:10)\n";
+  private static String twoLineStackTrace =
+      "\tat Test.a(Test.java:6)\n" +
+      "\tat Test.main(Test.java:10)\n";
+
+  private void testEquals(String stderr) {
+    StackTrace stackTrace = StackTrace.extractFromJvm(stderr);
+    assertEquals(stackTrace, stackTrace);
+    assertEquals(stackTrace, StackTrace.extractFromJvm(stderr));
+  }
+
+  @Test
+  public void testOneLine() {
+    StackTrace stackTrace = StackTrace.extractFromJvm(oneLineStackTrace);
+    assertEquals(1, stackTrace.size());
+    StackTraceLine stackTraceLine = stackTrace.get(0);
+    assertEquals("Test", stackTraceLine.className);
+    assertEquals("main", stackTraceLine.methodName);
+    assertEquals("Test.java", stackTraceLine.fileName);
+    assertEquals(10, stackTraceLine.lineNumber);
+    assertEquals(StringUtils.splitLines(oneLineStackTrace).get(0), stackTraceLine.originalLine);
+    assertEquals(oneLineStackTrace, stackTrace.toStringWithPrefix(StackTrace.TAB_AT_PREFIX));
+  }
+
+  @Test
+  public void testTwoLine() {
+    StackTrace stackTrace = StackTrace.extractFromJvm(twoLineStackTrace);
+    StackTraceLine stackTraceLine = stackTrace.get(0);
+    assertEquals("Test", stackTraceLine.className);
+    assertEquals("a", stackTraceLine.methodName);
+    assertEquals("Test.java", stackTraceLine.fileName);
+    assertEquals(6, stackTraceLine.lineNumber);
+    assertEquals(StringUtils.splitLines(twoLineStackTrace).get(0), stackTraceLine.originalLine);
+    stackTraceLine = stackTrace.get(1);
+    assertEquals("Test", stackTraceLine.className);
+    assertEquals("main", stackTraceLine.methodName);
+    assertEquals("Test.java", stackTraceLine.fileName);
+    assertEquals(10, stackTraceLine.lineNumber);
+    assertEquals(StringUtils.splitLines(twoLineStackTrace).get(1), stackTraceLine.originalLine);
+    assertEquals(twoLineStackTrace, stackTrace.toStringWithPrefix(StackTrace.TAB_AT_PREFIX));
+  }
+
+  @Test
+  public void testEqualsOneLine() {
+    testEquals(oneLineStackTrace);
+  }
+
+  @Test
+  public void testEqualsTwoLine() {
+    testEquals(twoLineStackTrace);
+  }
+}