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);
+ }
+}