blob: 68e09c1a2ebf0f0c38940e7be6efd2f27900c4b2 [file] [log] [blame]
// 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);
}
}