blob: 04477bd5942171fb46520bad973a8c5546fda9b7 [file] [log] [blame]
// Copyright (c) 2017, 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.debug;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.DebuggeeState;
import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.FrameInspector;
import com.android.tools.r8.debug.DebugTestConfig.RuntimeKind;
import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.Pair;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.StringUtils.BraceType;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.harmony.jpda.tests.framework.jdwp.Frame.Variable;
public class DebugStreamComparator {
public static class PrintOptions {
boolean printStates = false;
boolean printClass = false;
boolean printMethod = false;
boolean printVariables = false;
boolean printStack = false;
public static PrintOptions printAll() {
PrintOptions options = new PrintOptions();
options.printStates = true;
options.printClass = true;
options.printMethod = true;
options.printVariables = true;
options.printStack = true;
return options;
}
}
private static class StreamState {
static final int ENTRY_LINE = -1;
static final int PLACEHOLDER_LINE = -2;
final Iterator<DebuggeeState> iterator;
final Deque<Integer> frameEntryLines = new ArrayDeque<>();
int currentLine = ENTRY_LINE;
StreamState(Stream<DebuggeeState> stream) {
iterator = stream.iterator();
}
DebuggeeState next() {
while (true) {
DebuggeeState state = iterator.next();
if (state == null) {
return null;
}
int nextDepth = state.getFrameDepth();
int nextLine = state.getLineNumber();
if (nextDepth == frameEntryLines.size()) {
currentLine = nextLine;
return state;
}
if (nextDepth > frameEntryLines.size()) {
frameEntryLines.push(currentLine);
while (nextDepth > frameEntryLines.size()) {
// If the depth grows by more than one we have entered into filtered out frames, eg,
// java/android internals. In this case push placeholder lines on the stack.
frameEntryLines.push(PLACEHOLDER_LINE);
}
currentLine = nextLine;
assert nextDepth == frameEntryLines.size();
return state;
}
currentLine = nextLine;
while (frameEntryLines.size() > nextDepth + 1) {
// If the depth decreases by more than one we have popped the filtered frames or an
// exception has unwinded the stack.
frameEntryLines.pop();
}
int lineOnEntry = frameEntryLines.pop();
assert nextDepth == frameEntryLines.size();
if (lineOnEntry != nextLine) {
return state;
}
// A frame was popped and the current line is the same as when the frame was entered.
// In this case we advance again to avoid comparing that a function call returns to the
// same line (which may not be the case if no stores are needed after the call).
}
}
}
private boolean verifyLines = true;
private boolean verifyFiles = true;
private boolean verifyMethods = true;
private boolean verifyClasses = true;
private boolean verifyVariables = true;
private boolean verifyStack = false;
private Predicate<DebuggeeState> filter = s -> true;
private final List<String> names = new ArrayList<>();
private final List<Stream<DebuggeeState>> streams = new ArrayList<>();
private final PrintOptions printOptions = new PrintOptions();
private final PrintOptions errorPrintOptions = PrintOptions.printAll();
public DebugStreamComparator add(String name, Stream<DebuggeeState> stream) {
names.add(name);
streams.add(stream);
return this;
}
public DebugStreamComparator setFilter(Predicate<DebuggeeState> filter) {
this.filter = filter;
return this;
}
public DebugStreamComparator setVerifyLines(boolean verifyLines) {
this.verifyLines = verifyLines;
return this;
}
public DebugStreamComparator setVerifyFiles(boolean verifyFiles) {
this.verifyFiles = verifyFiles;
return this;
}
public DebugStreamComparator setVerifyMethods(boolean verifyMethods) {
this.verifyMethods = verifyMethods;
return this;
}
public DebugStreamComparator setVerifyClasses(boolean verifyClasses) {
this.verifyClasses = verifyClasses;
return this;
}
public DebugStreamComparator setVerifyVariables(boolean verifyVariables) {
this.verifyVariables = verifyVariables;
return this;
}
public DebugStreamComparator setVerifyStack(boolean verifyStack) {
this.verifyStack = verifyStack;
return this;
}
public DebugStreamComparator setPrintStates(boolean printStates) {
printOptions.printStates = printStates;
return this;
}
public DebugStreamComparator setPrintClass(boolean printClass) {
printOptions.printClass = printClass;
return this;
}
public DebugStreamComparator setPrintMethod(boolean printMethod) {
printOptions.printMethod = printMethod;
return this;
}
public DebugStreamComparator setPrintStack(boolean printStack) {
printOptions.printStack = printStack;
return this;
}
public DebugStreamComparator setPrintVariables(boolean printVariables) {
printOptions.printVariables = printVariables;
return this;
}
public void run() {
if (streams.size() != 1) {
throw new RuntimeException("Expected single stream to run");
}
internal();
}
public void compare() {
if (streams.size() < 2) {
throw new RuntimeException("Expected multiple streams to compare");
}
internal();
}
private void internal() {
List<StreamState> streamStates =
streams.stream().map(StreamState::new).collect(Collectors.toList());
while (true) {
List<DebuggeeState> states = new ArrayList<>(streamStates.size());
boolean done = false;
for (StreamState streamState : streamStates) {
DebuggeeState state;
do {
state = streamState.next();
} while (state != null && !filter.test(state));
states.add(state);
if (state == null) {
done = true;
}
}
try {
if (done) {
assertTrue(
"Not all streams completed at the same time. "
+ "Set 'DebugTestBase.DEBUG_TEST = true' to aid in diagnosing the issue.",
states.stream().allMatch(Objects::isNull));
return;
} else {
verifyStatesEqual(states);
if (printOptions.printStates) {
System.out.println(prettyPrintState(states.get(0), printOptions));
}
}
} catch (AssertionError e) {
for (int i = 0; i < names.size(); i++) {
System.err.println(
names.get(i) + ":\n" + prettyPrintState(states.get(i), errorPrintOptions));
}
throw e;
}
}
}
public static String prettyPrintState(DebuggeeState state, PrintOptions options) {
StringBuilder builder = new StringBuilder();
if (!options.printStack) {
builder.append(prettyPrintFrame(state, options));
} else {
for (int i = 0; i < state.getFrameDepth(); i++) {
builder.append("f").append(i).append(": ");
builder.append(prettyPrintFrame(state.getFrame(i), options));
builder.append('\n');
}
}
return builder.toString();
}
public static String prettyPrintFrame(FrameInspector frame, PrintOptions options) {
StringBuilder builder =
new StringBuilder()
.append(frame.getSourceFile())
.append(':')
.append(frame.getLineNumber())
.append(' ');
if (options.printClass) {
builder.append("\n class: ").append(frame.getClassName());
}
if (options.printMethod) {
builder
.append("\n method: ")
.append(frame.getMethodName())
.append(frame.getMethodSignature());
}
if (options.printVariables) {
builder.append(prettyPrintVariables(frame.getVisibleVariables(), options));
}
return builder.toString();
}
public static String prettyPrintVariables(List<Variable> variables, PrintOptions options) {
StringBuilder builder = new StringBuilder("\n locals: ");
StringUtils.append(
builder,
ListUtils.map(variables, v -> v.getName() + ':' + v.getSignature()),
", ",
BraceType.NONE);
return builder.toString();
}
private void verifyStatesEqual(List<DebuggeeState> states) {
DebuggeeState reference = states.get(0);
int line = reference.getLineNumber();
String file = reference.getSourceFile();
String clazz = reference.getClassName();
String method = reference.getMethodName();
String sig = reference.getMethodSignature();
List<Variable> variables = reference.getVisibleVariables();
int frameDepth = reference.getFrameDepth();
RuntimeKind referenceRuntime = reference.getConfig().getRuntimeKind();
for (int i = 1; i < states.size(); i++) {
DebuggeeState state = states.get(i);
RuntimeKind stateRuntime = state.getConfig().getRuntimeKind();
if (verifyFiles) {
assertEquals("source file mismatch", file, state.getSourceFile());
}
if (verifyLines) {
assertEquals("line number mismatch", line, state.getLineNumber());
}
if (verifyClasses) {
assertEquals("class name mismatch", clazz, state.getClassName());
}
if (verifyMethods) {
assertEquals(
"method mismatch", method + sig, state.getMethodName() + state.getMethodSignature());
}
if (verifyVariables) {
verifyVariablesEqual(
referenceRuntime,
reference.getVisibleVariables(),
stateRuntime,
state.getVisibleVariables());
}
if (verifyStack) {
assertEquals(frameDepth, state.getFrameDepth());
for (int j = 0; j < frameDepth; j++) {
FrameInspector referenceInspector = reference.getFrame(j);
FrameInspector stateInspector = state.getFrame(j);
verifyVariablesEqual(
referenceRuntime,
referenceInspector.getVisibleVariables(),
stateRuntime,
stateInspector.getVisibleVariables());
}
}
}
}
private static boolean shouldIgnoreVariable(Variable variable, RuntimeKind runtime) {
return runtime == RuntimeKind.DEX && variable.getName().isEmpty();
}
private static void verifyVariablesEqual(
RuntimeKind xRuntime, List<Variable> xs, RuntimeKind yRuntime, List<Variable> ys) {
Map<String, Variable> map = new HashMap<>(xs.size());
for (Variable x : xs) {
if (!shouldIgnoreVariable(x, xRuntime)) {
map.put(x.getName(), x);
}
}
List<Variable> unexpected = new ArrayList<>(ys.size());
List<Pair<Variable, Variable>> different = new ArrayList<>(Math.min(xs.size(), ys.size()));
for (Variable y : ys) {
if (shouldIgnoreVariable(y, yRuntime)) {
continue;
}
Variable x = map.remove(y.getName());
if (x == null) {
unexpected.add(y);
} else if (!isVariableEqual(x, y)) {
different.add(new Pair<>(x, y));
}
}
StringBuilder builder = null;
if (!map.isEmpty() || !unexpected.isEmpty()) {
builder = new StringBuilder();
if (!map.isEmpty()) {
builder.append("Missing variables: ");
for (Variable variable : map.values()) {
builder.append(variable.getName()).append(", ");
}
}
if (!unexpected.isEmpty()) {
builder.append("Unexpected variables: ");
for (Variable variable : unexpected) {
builder.append(variable.getName()).append(", ");
}
}
}
if (!different.isEmpty()) {
if (builder == null) {
builder = new StringBuilder();
}
builder.append("Different variables: ");
for (Pair<Variable, Variable> pair : different) {
Variable x = pair.getFirst();
Variable y = pair.getSecond();
builder.append(x.getName()).append(":").append(x.getType());
if (x.getGenericSignature() != null) {
builder.append('(').append(x.getGenericSignature()).append(')');
}
builder.append(" != ");
builder.append(y.getName()).append(":").append(y.getType());
if (y.getGenericSignature() != null) {
builder.append('(').append(y.getGenericSignature()).append(')');
}
}
}
if (builder != null) {
fail(builder.toString());
}
}
private static boolean isVariableEqual(Variable x, Variable y) {
return x.getName().equals(y.getName())
&& x.getType().equals(y.getType())
&& Objects.equals(x.getGenericSignature(), y.getGenericSignature());
}
}