blob: 99222d0609f822ea32cabb11bfeb5911068f74ea [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 com.android.tools.r8.ToolHelper;
import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
import com.android.tools.r8.ToolHelper.DexVm;
import com.android.tools.r8.ToolHelper.DexVm.Version;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.naming.ClassNameMapper;
import com.android.tools.r8.naming.ClassNamingForNameMapper;
import com.android.tools.r8.naming.ClassNamingForNameMapper.MappedRange;
import com.android.tools.r8.naming.MemberNaming;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
import com.android.tools.r8.naming.MemberNaming.Signature;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.TestDescriptionWatcher;
import com.google.common.collect.ImmutableList;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.harmony.jpda.tests.framework.TestErrorException;
import org.apache.harmony.jpda.tests.framework.jdwp.CommandPacket;
import org.apache.harmony.jpda.tests.framework.jdwp.Event;
import org.apache.harmony.jpda.tests.framework.jdwp.EventBuilder;
import org.apache.harmony.jpda.tests.framework.jdwp.EventPacket;
import org.apache.harmony.jpda.tests.framework.jdwp.Frame.Variable;
import org.apache.harmony.jpda.tests.framework.jdwp.JDWPCommands;
import org.apache.harmony.jpda.tests.framework.jdwp.JDWPCommands.ReferenceTypeCommandSet;
import org.apache.harmony.jpda.tests.framework.jdwp.JDWPCommands.StackFrameCommandSet;
import org.apache.harmony.jpda.tests.framework.jdwp.JDWPConstants;
import org.apache.harmony.jpda.tests.framework.jdwp.JDWPConstants.Error;
import org.apache.harmony.jpda.tests.framework.jdwp.JDWPConstants.EventKind;
import org.apache.harmony.jpda.tests.framework.jdwp.JDWPConstants.StepDepth;
import org.apache.harmony.jpda.tests.framework.jdwp.JDWPConstants.StepSize;
import org.apache.harmony.jpda.tests.framework.jdwp.JDWPConstants.SuspendPolicy;
import org.apache.harmony.jpda.tests.framework.jdwp.JDWPConstants.TypeTag;
import org.apache.harmony.jpda.tests.framework.jdwp.Location;
import org.apache.harmony.jpda.tests.framework.jdwp.Method;
import org.apache.harmony.jpda.tests.framework.jdwp.ParsedEvent;
import org.apache.harmony.jpda.tests.framework.jdwp.ParsedEvent.EventThread;
import org.apache.harmony.jpda.tests.framework.jdwp.ReplyPacket;
import org.apache.harmony.jpda.tests.framework.jdwp.Value;
import org.apache.harmony.jpda.tests.framework.jdwp.VmMirror;
import org.apache.harmony.jpda.tests.jdwp.share.JDWPTestCase;
import org.apache.harmony.jpda.tests.share.JPDATestOptions;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.TestName;
/**
* Base class for debugging tests.
*
* The protocol messages are described here:
* https://docs.oracle.com/javase/8/docs/platform/jpda/jdwp/jdwp-protocol.html
*/
public abstract class DebugTestBase {
// Set to true to enable verbose logs
private static final boolean DEBUG_TESTS = false;
// Build-time compiled debug-test resources for Java SDK < 8. See build.gradle
public static final Path DEBUGGEE_JAR =
Paths.get(ToolHelper.BUILD_DIR, "test", "debug_test_resources.jar");
// Build-time compiled debug-test resources for Java SDK 8. See build.gradle
public static final Path DEBUGGEE_JAVA8_JAR =
Paths.get(ToolHelper.BUILD_DIR, "test", "debug_test_resources_java8.jar");
public static final StepFilter NO_FILTER = new StepFilter.NoStepFilter();
public static final StepFilter INTELLIJ_FILTER = new StepFilter.IntelliJStepFilter();
public static final StepFilter ANDROID_FILTER = new StepFilter.AndroidRuntimeStepFilter();
private static final StepFilter DEFAULT_FILTER = NO_FILTER;
private static final int FIRST_LINE = -1;
static class SignatureAndLine {
final String signature;
int line;
SignatureAndLine(String signature, int line) {
this.signature = signature;
this.line = line;
}
}
@ClassRule
public static TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
@Rule
public TestName testName = new TestName();
@Rule
public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
protected static final boolean supportsDefaultMethod(DebugTestConfig config) {
return config.isCfRuntime()
|| ToolHelper.getMinApiLevelForDexVm().getLevel() >= AndroidApiLevel.N.getLevel();
}
protected final void runDebugTest(
DebugTestConfig config, String debuggeeClass, JUnit3Wrapper.Command... commands)
throws Throwable {
runInternal(config, debuggeeClass, Arrays.asList(commands));
}
protected final void runDebugTest(
DebugTestConfig config, String debuggeeClass, List<JUnit3Wrapper.Command> commands)
throws Throwable {
runInternal(config, debuggeeClass, commands);
}
private void runInternal(
DebugTestConfig config, String debuggeeClass, List<JUnit3Wrapper.Command> commands)
throws Throwable {
getDebugTestRunner(config, debuggeeClass, commands).runBare();
}
protected DebugTestRunner getDebugTestRunner(
DebugTestConfig config, String debuggeeClass, JUnit3Wrapper.Command... commands)
throws Throwable {
return getDebugTestRunner(config, debuggeeClass, Arrays.asList(commands));
}
protected DebugTestRunner getDebugTestRunner(
DebugTestConfig config, String debuggeeClass, List<JUnit3Wrapper.Command> commands)
throws Throwable {
// Skip test due to unsupported runtime.
Assume.assumeTrue("Skipping test " + testName.getMethodName() + " because ART is not supported",
ToolHelper.artSupported());
Assume.assumeTrue("Skipping test " + testName.getMethodName()
+ " because debug tests are not yet supported on Windows",
!ToolHelper.isWindows());
Assume.assumeTrue("Skipping test " + testName.getMethodName()
+ " because debug tests are not yet supported on device",
ToolHelper.getDexVm().getKind() == ToolHelper.DexVm.Kind.HOST);
ClassNameMapper classNameMapper =
config.getProguardMap() == null
? null
: ClassNameMapper.mapperFromFile(
config.getProguardMap(), config.getMissingProguardMapAction());
return new JUnit3Wrapper(config, debuggeeClass, commands, classNameMapper);
}
/**
* Lazily debug-step an execution starting from main(String[]) in {@code debuggeeClass}.
*
* @return A stream of successive debuggee states.
*/
public Stream<JUnit3Wrapper.DebuggeeState> streamDebugTest(
DebugTestConfig config, String debuggeeClass, StepFilter filter) throws Exception {
return streamDebugTest(
config,
debuggeeClass,
breakpoint(debuggeeClass, "main", "([Ljava/lang/String;)V"),
filter);
}
/**
* Lazily debug-step an execution starting from {@code breakpoint}.
*
* @return A stream of successive debuggee states.
*/
public Stream<JUnit3Wrapper.DebuggeeState> streamDebugTest(
DebugTestConfig config,
String debuggeeClass,
JUnit3Wrapper.Command breakpoint,
StepFilter filter)
throws Exception {
assert breakpoint instanceof JUnit3Wrapper.Command.BreakpointCommand;
// Continuous single-step command.
// The execution of the command pushes itself onto the command queue ensuring the next step.
JUnit3Wrapper.Command streamCommand =
new JUnit3Wrapper.Command() {
@Override
public void perform(JUnit3Wrapper testBase) {
if (DEBUG_TESTS) {
System.out.println("Running stream stepping command");
}
stepInto(filter).perform(testBase);
assert testBase.commandsQueue.isEmpty();
testBase.commandsQueue.push(this);
}
};
// Initial setup of the debug tests. Assumes execution starts at the "main" method of the class.
final JUnit3Wrapper wrapper =
new JUnit3Wrapper(
config,
debuggeeClass,
ImmutableList.of(breakpoint, run()),
null);
// Setup the initial state for the JDWP test base and run the program to the initial breakpoint.
wrapper.prepareForStreaming();
boolean running = true;
while (running
&& !(wrapper.commandsQueue.isEmpty()
&& wrapper.state == JUnit3Wrapper.State.ProcessCommand)) {
if (DEBUG_TESTS) {
System.out.println("Running stream initialization step");
}
running = wrapper.mainLoopStep();
}
if (DEBUG_TESTS) {
System.out.println("Finished initialization of stream");
}
// Add the "infinite streaming" command.
wrapper.commandsQueue.addLast(streamCommand);
final boolean initiallyRunning = running;
// Construct an infinite stream of states. Each element denotes the next debuggee state reached
// by single-stepping the program. On and after exit, all elements are null.
return Stream.generate(
new Supplier<JUnit3Wrapper.DebuggeeState>() {
private boolean initial = true;
private boolean running = initiallyRunning;
@Override
public JUnit3Wrapper.DebuggeeState get() {
if (wrapper.state == JUnit3Wrapper.State.Exit) {
return null;
}
assert verifyStateLocation(wrapper.getDebuggeeState());
if (initial) {
if (DEBUG_TESTS) {
System.out.println("Request for initial stream state");
}
initial = false;
return wrapper.getDebuggeeState();
}
if (DEBUG_TESTS) {
System.out.println("Request for next stream state");
}
while (running) {
running = wrapper.mainLoopStep();
JUnit3Wrapper.DebuggeeState state = wrapper.getDebuggeeState();
if (state != null && !state.frames.isEmpty()) {
return state;
}
if (DEBUG_TESTS) {
System.out.println("Continuing search for next stream state");
}
}
if (DEBUG_TESTS) {
System.out.println("Debuggee exited, no next stream state");
}
return null;
}
// When the set of streaming tests includes a Dalvik runtime it appears to interfere with
// the state of other tests. In such a case, the below check fails and the 'state' of
// the streams (both CF or DEX) appear to have become invalid.
private boolean verifyStateLocation(JUnit3Wrapper.DebuggeeState state) {
Location thisLocation = state.getLocation();
Location procLocation =
state.getMirror().getAllThreadFrames(state.threadId).get(0).getLocation();
assert thisLocation.methodID == procLocation.methodID;
return true;
}
});
}
protected final JUnit3Wrapper.Command run() {
return new JUnit3Wrapper.Command.RunCommand();
}
protected final JUnit3Wrapper.Command breakpoint(String className, String methodName) {
return breakpoint(className, methodName, null);
}
protected final JUnit3Wrapper.Command breakpoint(String className, String methodName, int line) {
return breakpoint(className, methodName, null, line);
}
protected final JUnit3Wrapper.Command breakpoint(String className, String methodName,
String methodSignature) {
return breakpoint(className, methodName, methodSignature, FIRST_LINE);
}
protected final JUnit3Wrapper.Command breakpoint(String className, String methodName,
String methodSignature, int line) {
return new JUnit3Wrapper.Command.BreakpointCommand(className, methodName, methodSignature, line);
}
protected final JUnit3Wrapper.Command breakOnException(String className, String methodName,
boolean caught, boolean uncaught) {
return new JUnit3Wrapper.Command.BreakOnExceptionCommand(
className, methodName, caught, uncaught);
}
protected final JUnit3Wrapper.Command stepOver() {
return stepOver(DEFAULT_FILTER);
}
protected final JUnit3Wrapper.Command stepOver(StepFilter stepFilter) {
return step(StepKind.OVER, stepFilter);
}
protected final JUnit3Wrapper.Command stepOut() {
return stepOut(DEFAULT_FILTER);
}
protected final JUnit3Wrapper.Command stepOut(StepFilter stepFilter) {
return step(StepKind.OUT, stepFilter);
}
protected final JUnit3Wrapper.Command stepInto() {
return stepInto(DEFAULT_FILTER);
}
protected final JUnit3Wrapper.Command stepInto(StepFilter stepFilter) {
return step(StepKind.INTO, stepFilter);
}
protected static List<Variable> getVisibleKotlinInlineVariables(
JUnit3Wrapper.DebuggeeState debuggeeState) {
return debuggeeState.getVisibleVariables().stream()
.filter(v -> v.getName().matches("^\\$i\\$f\\$.*$")).collect(Collectors.toList());
}
public enum StepKind {
INTO(StepDepth.INTO),
OVER(StepDepth.OVER),
OUT(StepDepth.OUT);
private final byte jdwpValue;
StepKind(byte jdwpValue) {
this.jdwpValue = jdwpValue;
}
}
public enum StepLevel {
LINE(StepSize.LINE),
INSTRUCTION(StepSize.MIN);
private final byte jdwpValue;
StepLevel(byte jdwpValue) {
this.jdwpValue = jdwpValue;
}
}
private JUnit3Wrapper.Command step(StepKind stepKind, StepFilter stepFilter) {
return step(stepKind, StepLevel.LINE, stepFilter);
}
private JUnit3Wrapper.Command step(StepKind stepKind, StepLevel stepLevel,
StepFilter stepFilter) {
return new JUnit3Wrapper.Command.StepCommand(stepKind, stepLevel, stepFilter);
}
protected JUnit3Wrapper.Command stepUntil(StepKind stepKind, StepLevel stepLevel,
Function<JUnit3Wrapper.DebuggeeState, Boolean> stepUntil) {
return stepUntil(stepKind, stepLevel, stepUntil, DEFAULT_FILTER);
}
protected JUnit3Wrapper.Command stepUntil(StepKind stepKind, StepLevel stepLevel,
Function<JUnit3Wrapper.DebuggeeState, Boolean> stepUntil, StepFilter stepFilter) {
// We create an extension to the given step filter which will also check whether we need to
// step again according to the given stepUntil function.
StepFilter stepUntilFilter = new StepFilter() {
@Override
public List<String> getExcludedClasses() {
return stepFilter.getExcludedClasses();
}
@Override
public boolean skipLocation(JUnit3Wrapper.DebuggeeState debuggeeState, JUnit3Wrapper wrapper,
JUnit3Wrapper.Command.StepCommand stepCommand) {
if (stepFilter.skipLocation(debuggeeState, wrapper, stepCommand)) {
return true;
}
if (stepUntil.apply(debuggeeState) == Boolean.FALSE) {
// We did not reach the expected location so step again.
wrapper.enqueueCommandFirst(stepCommand);
return true;
}
return false;
}
};
return new JUnit3Wrapper.Command.StepCommand(stepKind, stepLevel, stepUntilFilter);
}
protected final JUnit3Wrapper.Command checkLocal(String localName) {
return inspect(t -> t.checkLocal(localName));
}
protected final JUnit3Wrapper.Command checkLocals(String... localNames) {
return inspect(t -> {
for (String str : localNames) {
t.checkLocal(str);
}
});
}
protected final JUnit3Wrapper.Command checkLocal(String localName, Value expectedValue) {
return inspect(t -> t.checkLocal(localName, expectedValue));
}
protected final JUnit3Wrapper.Command checkNoLocal(String localName) {
return inspect(t -> t.checkNoLocal(localName));
}
protected final JUnit3Wrapper.Command checkNoLocals(String... localNames) {
return inspect(t -> {
for (String str : localNames) {
t.checkNoLocal(str);
}
});
}
protected final JUnit3Wrapper.Command checkNoLocal() {
return inspect(t -> {
List<String> localNames = t.getLocalNames();
Assert.assertTrue("Local variables: " + String.join(",", localNames), localNames.isEmpty());
});
}
protected final JUnit3Wrapper.Command checkLine(String sourceFile, int line) {
return inspect(t -> t.checkLine(sourceFile, line));
}
protected final JUnit3Wrapper.Command checkInlineFrames(List<SignatureAndLine> expectedFrames) {
return inspect(
t -> {
List<SignatureAndLine> actualFrames = t.getInlineFrames();
Assert.assertEquals(expectedFrames.size(), actualFrames.size());
for (int i = 0; i < expectedFrames.size(); ++i) {
SignatureAndLine expectedFrame = expectedFrames.get(i);
SignatureAndLine actualFrame = actualFrames.get(i);
Assert.assertEquals(expectedFrame.signature, actualFrame.signature);
Assert.assertEquals(expectedFrame.line, actualFrame.line);
}
});
}
protected final JUnit3Wrapper.Command checkMethod(String className, String methodName) {
return checkMethod(className, methodName, null);
}
protected final JUnit3Wrapper.Command checkMethod(String className, String methodName,
String methodSignature) {
return inspect(t -> {
Assert.assertEquals("Incorrect class name", className, t.getClassName());
Assert.assertEquals("Incorrect method name", methodName, t.getMethodName());
if (methodSignature != null) {
Assert.assertEquals("Incorrect method signature", methodSignature,
t.getMethodSignature());
}
});
}
protected final JUnit3Wrapper.Command checkStaticFieldClinitSafe(
String className, String fieldName, String fieldSignature, Value expectedValue) {
return inspect(t -> {
// TODO(65148874): The current Art from AOSP master hangs when requesting static fields
// when breaking in <clinit>. Last known good version is 7.0.0.
Assume.assumeTrue(
"Skipping test " + testName.getMethodName() + " because ART version is not supported",
t.isCfRuntime() || ToolHelper.getDexVm().getVersion().isOlderThanOrEqual(Version.V7_0_0));
checkStaticField(className, fieldName, fieldSignature, expectedValue);
});
}
protected final JUnit3Wrapper.Command checkStaticField(
String className, String fieldName, String fieldSignature, Value expectedValue) {
return inspect(t -> {
Value value = t.getStaticField(className, fieldName, fieldSignature);
Assert.assertEquals("Incorrect value for static '" + className + "." + fieldName + "'",
expectedValue, value);
});
}
protected final JUnit3Wrapper.Command inspect(Consumer<JUnit3Wrapper.DebuggeeState> inspector) {
return t -> inspector.accept(t.debuggeeState);
}
protected final JUnit3Wrapper.Command setLocal(String localName, Value newValue) {
return new JUnit3Wrapper.Command.SetLocalCommand(localName, newValue);
}
protected final JUnit3Wrapper.Command getLocal(String localName, Consumer<Value> inspector) {
return t -> inspector.accept(t.debuggeeState.getLocalValues().get(localName));
}
protected interface DebugTestRunner {
void enqueueCommandFirst(JUnit3Wrapper.Command command);
void runBare() throws Throwable;
}
@Ignore("Prevents Gradle from running the wrapper as a test.")
public static class JUnit3Wrapper extends JDWPTestCase implements DebugTestRunner {
private final String debuggeeClassName;
// Initially, the runtime is suspended so we're ready to process commands.
private State state = State.ProcessCommand;
/**
* Represents the context of the debuggee suspension. This is {@code null} when the debuggee is
* not suspended.
*/
private DebuggeeState debuggeeState = null;
private final Deque<Command> commandsQueue;
private final Translator translator;
// Active event requests.
private final Map<Integer, EventHandler> events = new TreeMap<>();
private final DebugTestConfig config;
/**
* The Translator interface provides mapping between the class and method names and line numbers
* found in the binary file and their original forms.
*
* <p>Terminology:
*
* <p>The term 'original' refers to the names and line numbers found in the original source
* code. The term 'obfuscated' refers to the names and line numbers in the binary. Note that
* they may not actually be obfuscated:
*
* <p>- The obfuscated class and method names can be identical to the original ones if
* minification is disabled or they are 'keep' classes/methods. - The obfuscated line numbers
* can be identical to the original ones if neither inlining nor line number remapping took
* place.
*/
private interface Translator {
String getOriginalClassName(String obfuscatedClassName);
String getOriginalClassNameForLine(
String obfuscatedClassName, String obfuscatedMethodName, int obfuscatedLineNumber);
String getOriginalMethodName(
String obfuscatedClassName, String obfuscatedMethodName, String methodSignature);
String getOriginalMethodNameForLine(
String obfuscatedClassName,
String obfuscatedMethodName,
String methodSignature,
int obfuscatedLineNumber);
int getOriginalLineNumber(
String obfuscatedClassName, String obfuscatedMethodName, int obfuscatedLineNumber);
List<SignatureAndLine> getInlineFramesForLine(
String obfuscatedClassName, String obfuscatedMethodName, int obfuscatedLineNumber);
String getObfuscatedClassName(String originalClassName);
String getObfuscatedMethodName(
String originalClassName, String originalMethodName, String methodSignature);
}
private class IdentityTranslator implements Translator {
@Override
public String getOriginalClassName(String obfuscatedClassName) {
return obfuscatedClassName;
}
@Override
public String getOriginalClassNameForLine(
String obfuscatedClassName, String obfuscatedMethodName, int obfuscatedLineNumber) {
return obfuscatedClassName;
}
@Override
public String getOriginalMethodName(
String obfuscatedClassName, String obfuscatedMethodName, String methodSignature) {
return obfuscatedMethodName;
}
@Override
public String getOriginalMethodNameForLine(
String obfuscatedClassName,
String obfuscatedMethodName,
String methodSignature,
int obfuscatedLineNumber) {
return obfuscatedMethodName;
}
@Override
public int getOriginalLineNumber(
String obfuscatedClassName, String obfuscatedMethodName, int obfuscatedLineNumber) {
return obfuscatedLineNumber;
}
@Override
public List<SignatureAndLine> getInlineFramesForLine(
String obfuscatedClassName, String obfuscatedMethodName, int obfuscatedLineNumber) {
return null;
}
@Override
public String getObfuscatedClassName(String originalClassName) {
return originalClassName;
}
@Override
public String getObfuscatedMethodName(
String originalClassName, String originalMethodName, String methodSignature) {
return originalMethodName;
}
}
private class ClassNameMapperTranslator extends IdentityTranslator {
private final ClassNameMapper classNameMapper;
public ClassNameMapperTranslator(ClassNameMapper classNameMapper) {
this.classNameMapper = classNameMapper;
}
@Override
public String getOriginalClassName(String obfuscatedClassName) {
return classNameMapper.deobfuscateClassName(obfuscatedClassName);
}
private ClassNamingForNameMapper.MappedRange getMappedRangeForLine(
String obfuscatedClassName, String obfuscatedMethodName, int obfuscatedLineNumber) {
ClassNamingForNameMapper classNaming = classNameMapper.getClassNaming(obfuscatedClassName);
if (classNaming == null) {
return null;
}
ClassNamingForNameMapper.MappedRangesOfName ranges =
classNaming.mappedRangesByRenamedName.get(obfuscatedMethodName);
if (ranges == null) {
return null;
}
return ranges.firstRangeForLine(obfuscatedLineNumber);
}
@Override
public String getOriginalClassNameForLine(
String obfuscatedClassName, String obfuscatedMethodName, int obfuscatedLineNumber) {
ClassNamingForNameMapper.MappedRange range =
getMappedRangeForLine(obfuscatedClassName, obfuscatedMethodName, obfuscatedLineNumber);
if (range == null) {
return obfuscatedClassName;
}
return range.signature.type;
}
@Override
public String getOriginalMethodName(
String obfuscatedClassName, String obfuscatedMethodName, String methodSignature) {
MemberNaming memberNaming =
getMemberNaming(obfuscatedClassName, obfuscatedMethodName, methodSignature);
if (memberNaming == null) {
return obfuscatedMethodName;
}
Signature originalSignature = memberNaming.getOriginalSignature();
return originalSignature.name;
}
@Override
public String getOriginalMethodNameForLine(
String obfuscatedClassName,
String obfuscatedMethodName,
String methodSignature,
int obfuscatedLineNumber) {
ClassNamingForNameMapper.MappedRange range =
getMappedRangeForLine(obfuscatedClassName, obfuscatedMethodName, obfuscatedLineNumber);
if (range == null) {
return obfuscatedMethodName;
}
return range.signature.name;
}
@Override
public int getOriginalLineNumber(
String obfuscatedClassName, String obfuscatedMethodName, int obfuscatedLineNumber) {
ClassNamingForNameMapper.MappedRange range =
getMappedRangeForLine(obfuscatedClassName, obfuscatedMethodName, obfuscatedLineNumber);
if (range == null) {
return obfuscatedLineNumber;
}
return range.getOriginalLineNumber(obfuscatedLineNumber);
}
@Override
public List<SignatureAndLine> getInlineFramesForLine(
String obfuscatedClassName, String obfuscatedMethodName, int obfuscatedLineNumber) {
ClassNamingForNameMapper classNaming = classNameMapper.getClassNaming(obfuscatedClassName);
if (classNaming == null) {
return null;
}
ClassNamingForNameMapper.MappedRangesOfName ranges =
classNaming.mappedRangesByRenamedName.get(obfuscatedMethodName);
if (ranges == null) {
return null;
}
List<MappedRange> mappedRanges = ranges.allRangesForLine(obfuscatedLineNumber);
if (mappedRanges.isEmpty()) {
return null;
}
List<SignatureAndLine> lines = new ArrayList<>(mappedRanges.size());
for (MappedRange range : mappedRanges) {
lines.add(
new SignatureAndLine(
range.signature.toString(), range.getOriginalLineNumber(obfuscatedLineNumber)));
}
return lines;
}
@Override
public String getObfuscatedClassName(String originalClassName) {
// TODO(tamaskenez) Watch for inline methods (we can be in a different class).
String obfuscatedClassName =
classNameMapper.getObfuscatedToOriginalMapping().inverse().get(originalClassName);
return obfuscatedClassName == null ? originalClassName : obfuscatedClassName;
}
@Override
public String getObfuscatedMethodName(
String originalClassName, String originalMethodName, String methodSignatureOrNull) {
ClassNamingForNameMapper naming;
String obfuscatedClassName =
classNameMapper.getObfuscatedToOriginalMapping().inverse().get(originalClassName);
if (obfuscatedClassName != null) {
naming = classNameMapper.getClassNaming(obfuscatedClassName);
} else {
return originalMethodName;
}
if (methodSignatureOrNull == null) {
List<MemberNaming> memberNamings = naming.lookupByOriginalName(originalMethodName);
if (memberNamings.isEmpty()) {
return originalMethodName;
} else if (memberNamings.size() == 1) {
return memberNamings.get(0).getRenamedName();
} else
throw new RuntimeException(
String.format(
"Looking up method %s.%s without signature is ambiguous (%d candidates).",
originalClassName, originalMethodName, memberNamings.size()));
} else {
MethodSignature originalSignature =
MethodSignature.fromSignature(originalMethodName, methodSignatureOrNull);
MemberNaming memberNaming = naming.lookupByOriginalSignature(originalSignature);
if (memberNaming == null) {
return originalMethodName;
}
return memberNaming.getRenamedName();
}
}
/** Assumes classNameMapper is valid. Return null if no member naming found. */
private MemberNaming getMemberNaming(
String obfuscatedClassName, String obfuscatedMethodName, String genericMethodSignature) {
ClassNamingForNameMapper classNaming = classNameMapper.getClassNaming(obfuscatedClassName);
if (classNaming == null) {
return null;
}
MethodSignature renamedSignature =
MethodSignature.fromSignature(obfuscatedMethodName, genericMethodSignature);
return classNaming.lookup(renamedSignature);
}
}
JUnit3Wrapper(
DebugTestConfig config,
String debuggeeClassName,
List<Command> commands,
ClassNameMapper classNameMapper) {
this.config = config;
this.debuggeeClassName = debuggeeClassName;
this.commandsQueue = new ArrayDeque<>(commands);
if (classNameMapper == null) {
this.translator = new IdentityTranslator();
} else {
this.translator = new ClassNameMapperTranslator(classNameMapper);
}
}
void prepareForStreaming() throws Exception {
if (DEBUG_TESTS) {
System.out.println("Preparing test for stream execution");
}
setUp();
}
@Override
protected void runTest() throws Throwable {
if (DEBUG_TESTS) {
logWriter.println("Starts loop with " + commandsQueue.size() + " command(s) to process");
}
while (mainLoopStep()) {
// Continue stepping until mainLoopStep exits with false.
}
if (config.mustProcessAllCommands()) {
assertTrue(
"All commands have NOT been processed for config: " + config, commandsQueue.isEmpty());
}
logWriter.println("Finish loop");
}
private boolean mainLoopStep() {
if (DEBUG_TESTS) {
logWriter.println("Loop on state " + state.name());
}
switch (state) {
case ProcessCommand: {
Command command = commandsQueue.poll();
assert command != null;
if (DEBUG_TESTS) {
logWriter.println("Process command " + command.toString());
}
try {
command.perform(this);
} catch (TestErrorException e) {
boolean ignoreException = false;
if (config.isDexRuntime()
&& ToolHelper.getDexVm().getVersion().isOlderThanOrEqual(Version.V4_4_4)) {
// Dalvik has flaky synchronization issue on shutdown. The workaround is to ignore
// the exception if and only if we know that it's the final resume command.
if (debuggeeState == null && commandsQueue.isEmpty()) {
// We should receive the VMDeath event and transition to the Exit state here.
processEvents();
assert state == State.Exit;
ignoreException = true;
}
}
if (!ignoreException) {
throw e;
}
}
break;
}
case WaitForEvent:
processEvents();
break;
case Exit:
return false;
default:
throw new AssertionError();
}
return true;
}
@Override
protected String getDebuggeeClassName() {
return debuggeeClassName;
}
private enum State {
/**
* Process next command
*/
ProcessCommand,
/**
* Wait for the next event
*/
WaitForEvent,
/**
* The debuggee has exited
*/
Exit
}
// We expect to have either a single event or two events with one being an installed breakpoint.
private ParsedEvent getPrimaryEvent(ParsedEvent[] events) {
assertTrue(events.length == 1 || events.length == 2);
if (events.length == 1) {
return events[0];
}
assertEquals(2, events.length);
for (ParsedEvent event : events) {
if (event.getEventKind() == EventKind.BREAKPOINT) {
return event;
}
}
fail("Expected breakpoint when receiving multiple events.");
throw new Unreachable();
}
private void processEvents() {
EventPacket eventPacket = getMirror().receiveEvent();
ParsedEvent[] parsedEvents = ParsedEvent.parseEventPacket(eventPacket);
if (DEBUG_TESTS) {
logWriter.println("Received " + parsedEvents.length + " event(s)");
for (int i = 0; i < parsedEvents.length; ++i) {
String msg =
String.format(
"#%d: %s (id=%d)",
i,
JDWPConstants.EventKind.getName(parsedEvents[i].getEventKind()),
parsedEvents[i].getRequestID());
logWriter.println(msg);
}
}
ParsedEvent parsedEvent = getPrimaryEvent(parsedEvents);
byte eventKind = parsedEvent.getEventKind();
int requestID = parsedEvent.getRequestID();
if (eventKind == JDWPConstants.EventKind.VM_DEATH) {
// Special event when debuggee is about to terminate.
assertEquals(0, requestID);
setState(State.Exit);
} else {
assert parsedEvent.getSuspendPolicy() == SuspendPolicy.ALL;
// Capture the context of the event suspension.
updateEventContext((EventThread) parsedEvent);
if (DEBUG_TESTS && debuggeeState.getLocation() != null) {
// Dump location
String classSig = getMirror().getClassSignature(debuggeeState.getLocation().classID);
String methodName = VmMirrorUtils
.getMethodName(getMirror(), debuggeeState.getLocation().classID,
debuggeeState.getLocation().methodID);
String methodSig = VmMirrorUtils
.getMethodSignature(getMirror(), debuggeeState.getLocation().classID,
debuggeeState.getLocation().methodID);
String msg =
String.format(
"Suspended in %s#%s%s@0x%x",
classSig, methodName, methodSig, debuggeeState.getLocation().index);
if (debuggeeState.getLocation().index >= 0) {
msg += " (line " + debuggeeState.getLineNumber() + ")";
}
System.out.println(msg);
}
// Handle event.
EventHandler eh = events.get(requestID);
assert eh != null;
eh.handle(this);
}
}
@Override
protected JPDATestOptions createTestOptions() {
// Override properties to run debuggee with ART/Dalvik.
class ArtTestOptions extends JPDATestOptions {
ArtTestOptions(String[] debuggeePath) {
// Set debuggee command-line.
if (config.isDexRuntime()) {
ArtCommandBuilder artCommandBuilder = new ArtCommandBuilder(ToolHelper.getDexVm());
if (ToolHelper.getDexVm().getVersion().isNewerThan(DexVm.Version.V5_1_1)) {
artCommandBuilder.appendArtOption("-Xcompiler-option");
artCommandBuilder.appendArtOption("--debuggable");
}
if (DEBUG_TESTS && ToolHelper.getDexVm().getVersion().isNewerThan(Version.V4_4_4)) {
artCommandBuilder.appendArtOption("-verbose:jdwp");
}
setProperty("jpda.settings.debuggeeJavaPath", artCommandBuilder.build());
}
// Set debuggee classpath
String debuggeeClassPath = String.join(File.pathSeparator, debuggeePath);
setProperty("jpda.settings.debuggeeClasspath", debuggeeClassPath);
// Force to localhost (required for continuous testing configuration). Use port '0'
// for automatic selection (required when tests are executed in parallel).
setProperty("jpda.settings.transportAddress", "127.0.0.1:0");
// Set verbosity
setProperty("jpda.settings.verbose", Boolean.toString(DEBUG_TESTS));
}
}
return new ArtTestOptions(
config.getPaths().stream().map(Path::toString).toArray(String[]::new));
}
public void enqueueCommandFirst(Command command) {
commandsQueue.addFirst(command);
}
public void enqueueCommandsFirst(List<Command> commands) {
for (int i = commands.size() - 1; i >= 0; --i) {
enqueueCommandFirst(commands.get(i));
}
}
//
// Inspection
//
public interface FrameInspector {
long getFrameId();
Location getLocation();
int getLineNumber();
List<SignatureAndLine> getInlineFrames();
String getSourceFile();
String getClassName();
String getClassSignature();
String getMethodName();
String getMethodSignature();
// Locals
List<Variable> getVisibleVariables();
/**
* Returns the names of all local variables visible at the current location
*/
List<String> getLocalNames();
/**
* Returns the values of all locals visible at the current location.
*/
Map<String, Value> getLocalValues();
void checkNoLocal(String localName);
void checkLocal(String localName);
void checkLocal(String localName, Value expectedValue);
void checkLine(String sourceFile, int line);
}
public static class DebuggeeState implements FrameInspector {
private class DebuggeeFrame implements FrameInspector {
private final long frameId;
private final Location location;
private final Translator translator;
public DebuggeeFrame(long frameId, Location location, Translator translator) {
this.frameId = frameId;
this.location = location;
this.translator = translator;
}
public long getFrameId() {
return frameId;
}
public Location getLocation() {
return location;
}
private int getObfuscatedLineNumber() {
Location location = getLocation();
ReplyPacket reply = getMirror().getLineTable(location.classID, location.methodID);
if (reply.getErrorCode() != 0) {
return -1;
}
long startCodeIndex = reply.getNextValueAsLong();
long endCodeIndex = reply.getNextValueAsLong();
int lines = reply.getNextValueAsInt();
int line = -1;
long previousLineCodeIndex = -1;
for (int i = 0; i < lines; ++i) {
long currentLineCodeIndex = reply.getNextValueAsLong();
int currentLineNumber = reply.getNextValueAsInt();
// Code indices are in ascending order.
assert currentLineCodeIndex >= startCodeIndex;
assert currentLineCodeIndex <= endCodeIndex;
assert currentLineCodeIndex >= previousLineCodeIndex;
previousLineCodeIndex = currentLineCodeIndex;
if (location.index >= currentLineCodeIndex) {
line = currentLineNumber;
} else {
break;
}
}
return line;
}
public int getLineNumber() {
return translator.getOriginalLineNumber(
getObfuscatedClassName(), getObfuscatedMethodName(), getObfuscatedLineNumber());
}
public List<SignatureAndLine> getInlineFrames() {
return translator.getInlineFramesForLine(
getObfuscatedClassName(), getObfuscatedMethodName(), getObfuscatedLineNumber());
}
public String getSourceFile() {
// TODO(shertz) support JSR-45
Location location = getLocation();
CommandPacket sourceFileCommand = new CommandPacket(
JDWPCommands.ReferenceTypeCommandSet.CommandSetID,
JDWPCommands.ReferenceTypeCommandSet.SourceFileCommand);
sourceFileCommand.setNextValueAsReferenceTypeID(location.classID);
ReplyPacket replyPacket = getMirror().performCommand(sourceFileCommand);
if (replyPacket.getErrorCode() != 0) {
return null;
} else {
return replyPacket.getNextValueAsString();
}
}
@Override
public List<Variable> getVisibleVariables() {
// Get variable table and keep only variables visible at this location.
Location frameLocation = getLocation();
return getVariables(getMirror(), frameLocation.classID, frameLocation.methodID).stream()
.filter(v -> inScope(frameLocation.index, v))
.collect(Collectors.toList());
}
public List<String> getLocalNames() {
return getVisibleVariables().stream().map(Variable::getName).collect(Collectors.toList());
}
@Override
public Map<String, Value> getLocalValues() {
return JUnit3Wrapper.getVariablesAt(mirror, location)
.stream()
.collect(
Collectors.toMap(
Variable::getName,
v -> {
// Get local value
CommandPacket commandPacket =
new CommandPacket(
JDWPCommands.StackFrameCommandSet.CommandSetID,
JDWPCommands.StackFrameCommandSet.GetValuesCommand);
commandPacket.setNextValueAsThreadID(getThreadId());
commandPacket.setNextValueAsFrameID(getFrameId());
commandPacket.setNextValueAsInt(1);
commandPacket.setNextValueAsInt(v.getSlot());
commandPacket.setNextValueAsByte(v.getTag());
ReplyPacket replyPacket = getMirror().performCommand(commandPacket);
int valuesCount = replyPacket.getNextValueAsInt();
assert valuesCount == 1;
return replyPacket.getNextValueAsValue();
}));
}
private String convertCurrentLocationToString() {
return getSourceFile() + ":" + getLineNumber()
+ " (pc 0x" + Long.toHexString(location.index) + ")";
}
private void failNoLocal(String localName) {
String locationString = convertCurrentLocationToString();
Assert.fail("expected local '" + localName + "' is not present at " + locationString);
}
private void checkIncorrectLocal(String localName, Value expected, Value actual) {
if (expected.equals(actual)) {
return;
}
String locationString = convertCurrentLocationToString();
Assert.fail(
"Incorrect value for local '"
+ localName
+ "' at "
+ locationString
+ ", expected "
+ expected
+ ", actual "
+ actual);
}
@Override
public void checkNoLocal(String localName) {
Optional<Variable> localVar = getVariableAt(mirror, getLocation(), localName);
if (localVar.isPresent()) {
String locationString = convertCurrentLocationToString();
Assert.fail("unexpected local '" + localName + "' is present at " + locationString);
}
}
public void checkLocal(String localName) {
Optional<Variable> localVar = getVariableAt(mirror, getLocation(), localName);
if (!localVar.isPresent()) {
failNoLocal(localName);
}
}
public void checkLocal(String localName, Value expectedValue) {
Optional<Variable> localVar = getVariableAt(mirror, getLocation(), localName);
if (!localVar.isPresent()) {
failNoLocal(localName);
}
// Get value
CommandPacket commandPacket = new CommandPacket(
JDWPCommands.StackFrameCommandSet.CommandSetID,
JDWPCommands.StackFrameCommandSet.GetValuesCommand);
commandPacket.setNextValueAsThreadID(getThreadId());
commandPacket.setNextValueAsFrameID(getFrameId());
commandPacket.setNextValueAsInt(1);
commandPacket.setNextValueAsInt(localVar.get().getSlot());
commandPacket.setNextValueAsByte(localVar.get().getTag());
ReplyPacket replyPacket = getMirror().performCommand(commandPacket);
int valuesCount = replyPacket.getNextValueAsInt();
assert valuesCount == 1;
Value localValue = replyPacket.getNextValueAsValue();
checkIncorrectLocal(localName, expectedValue, localValue);
}
@Override
public void checkLine(String sourceFile, int line) {
if (!Objects.equals(sourceFile, getSourceFile()) || line != getLineNumber()) {
String locationString = convertCurrentLocationToString();
Assert.fail(
"Incorrect line at " + locationString + ", expected " + sourceFile + ":" + line);
}
}
/**
* Return class name, as found in the binary. If it has not been obfuscated (minified) it's
* identical to the original class name. Otherwise, it's the obfuscated one.
*/
private String getObfuscatedClassName() {
String classSignature = getClassSignature();
assert classSignature.charAt(0) == 'L';
// Remove leading 'L' and trailing ';'
classSignature = classSignature.substring(1, classSignature.length() - 1);
// Return fully qualified name
return classSignature.replace('/', '.');
}
public String getClassName() {
return translator.getOriginalClassName(getObfuscatedClassName());
}
public String getClassSignature() {
Location location = getLocation();
return getMirror().getClassSignature(location.classID);
}
// Return method name as found in the binary. Can be obfuscated (minified).
private String getObfuscatedMethodName() {
Location location = getLocation();
return getMirror().getMethodName(location.classID, location.methodID);
}
// Return original method name.
public String getMethodName() {
return translator.getOriginalMethodName(
getObfuscatedClassName(), getObfuscatedMethodName(), getMethodSignature());
}
public String getMethodSignature() {
Location location = getLocation();
CommandPacket command = new CommandPacket(ReferenceTypeCommandSet.CommandSetID,
ReferenceTypeCommandSet.MethodsWithGenericCommand);
command.setNextValueAsReferenceTypeID(location.classID);
ReplyPacket reply = getMirror().performCommand(command);
assert reply.getErrorCode() == Error.NONE;
int methods = reply.getNextValueAsInt();
for (int i = 0; i < methods; ++i) {
long methodId = reply.getNextValueAsMethodID();
reply.getNextValueAsString(); // skip name
String methodSignature = reply.getNextValueAsString();
reply.getNextValueAsString(); // skip generic signature
reply.getNextValueAsInt(); // skip modifiers
if (methodId == location.methodID) {
return methodSignature;
}
}
throw new AssertionError("No method info for the current location");
}
}
private final DebugTestConfig config;
private final VmMirror mirror;
private final long threadId;
private final List<DebuggeeFrame> frames;
public DebuggeeState(
DebugTestConfig config, VmMirror mirror, long threadId, List<DebuggeeFrame> frames) {
this.config = config;
this.mirror = mirror;
this.threadId = threadId;
this.frames = frames;
}
public DebugTestConfig getConfig() {
return config;
}
public boolean isCfRuntime() {
return getConfig().isCfRuntime();
}
public boolean isDexRuntime() {
return getConfig().isDexRuntime();
}
public VmMirror getMirror() {
return mirror;
}
public long getThreadId() {
return threadId;
}
public int getFrameDepth() {
return frames.size();
}
public FrameInspector getFrame(int index) {
return frames.get(index);
}
public FrameInspector getTopFrame() {
return getFrame(0);
}
@Override
public long getFrameId() {
return getTopFrame().getFrameId();
}
@Override
public Location getLocation() {
return frames.isEmpty() ? null : getTopFrame().getLocation();
}
@Override
public void checkNoLocal(String localName) {
getTopFrame().checkNoLocal(localName);
}
@Override
public void checkLocal(String localName) {
getTopFrame().checkLocal(localName);
}
@Override
public void checkLocal(String localName, Value expectedValue) {
getTopFrame().checkLocal(localName, expectedValue);
}
@Override
public void checkLine(String sourceFile, int line) {
getTopFrame().checkLine(sourceFile, line);
}
@Override
public int getLineNumber() {
return getTopFrame().getLineNumber();
}
@Override
public List<SignatureAndLine> getInlineFrames() {
return getTopFrame().getInlineFrames();
}
@Override
public String getSourceFile() {
return getTopFrame().getSourceFile();
}
@Override
public List<String> getLocalNames() {
return getTopFrame().getLocalNames();
}
@Override
public Map<String, Value> getLocalValues() {
return getTopFrame().getLocalValues();
}
@Override
public String getClassName() {
return getTopFrame().getClassName();
}
@Override
public String getClassSignature() {
return getTopFrame().getClassSignature();
}
@Override
public String getMethodName() {
return getTopFrame().getMethodName();
}
@Override
public String getMethodSignature() {
return getTopFrame().getMethodSignature();
}
@Override
public List<Variable> getVisibleVariables() {
return getTopFrame().getVisibleVariables();
}
public Value getStaticField(String className, String fieldName, String fieldSignature) {
String classSignature = DescriptorUtils.javaTypeToDescriptor(className);
byte typeTag = TypeTag.CLASS;
long classId = getMirror().getClassID(classSignature);
Assert.assertFalse("No class named " + className + " found", classId == -1);
// The class is available, lookup and read the field.
long fieldId = findField(getMirror(), classId, fieldName, fieldSignature);
return getField(getMirror(), classId, fieldId);
}
private long findField(VmMirror mirror, long classId, String fieldName,
String fieldSignature) {
boolean withGenericSignature = true;
CommandPacket commandPacket = new CommandPacket(ReferenceTypeCommandSet.CommandSetID,
ReferenceTypeCommandSet.FieldsWithGenericCommand);
commandPacket.setNextValueAsReferenceTypeID(classId);
ReplyPacket replyPacket = mirror.performCommand(commandPacket);
if (replyPacket.getErrorCode() != Error.NONE) {
// Retry with older command ReferenceType.Fields.
withGenericSignature = false;
commandPacket.setCommand(ReferenceTypeCommandSet.FieldsCommand);
replyPacket = mirror.performCommand(commandPacket);
assert replyPacket.getErrorCode() == Error.NONE;
}
int fieldsCount = replyPacket.getNextValueAsInt();
LongList matchingFieldIds = new LongArrayList();
for (int i = 0; i < fieldsCount; ++i) {
long currentFieldId = replyPacket.getNextValueAsFieldID();
String currentFieldName = replyPacket.getNextValueAsString();
String currentFieldSignature = replyPacket.getNextValueAsString();
if (withGenericSignature) {
replyPacket.getNextValueAsString(); // Skip generic signature.
}
replyPacket.getNextValueAsInt(); // Skip modifiers.
// Filter fields based on name (and signature if there is).
if (fieldName.equals(currentFieldName)) {
if (fieldSignature == null || fieldSignature.equals(currentFieldSignature)) {
matchingFieldIds.add(currentFieldId);
}
}
}
Assert.assertTrue(replyPacket.isAllDataRead());
Assert.assertFalse("No field named " + fieldName + " found", matchingFieldIds.isEmpty());
// There must be only one matching field.
Assert.assertEquals("More than 1 field found: please specify a signature", 1,
matchingFieldIds.size());
return matchingFieldIds.getLong(0);
}
private Value getField(VmMirror mirror, long classId, long fieldId) {
CommandPacket commandPacket = new CommandPacket(ReferenceTypeCommandSet.CommandSetID,
ReferenceTypeCommandSet.GetValuesCommand);
commandPacket.setNextValueAsReferenceTypeID(classId);
commandPacket.setNextValueAsInt(1);
commandPacket.setNextValueAsFieldID(fieldId);
ReplyPacket replyPacket = mirror.performCommand(commandPacket);
assert replyPacket.getErrorCode() == Error.NONE;
int fieldsCount = replyPacket.getNextValueAsInt();
assert fieldsCount == 1;
Value result = replyPacket.getNextValueAsValue();
Assert.assertTrue(replyPacket.isAllDataRead());
return result;
}
}
public static Optional<Variable> getVariableAt(VmMirror mirror, Location location,
String localName) {
return getVariablesAt(mirror, location).stream()
.filter(v -> localName.equals(v.getName()))
.findFirst();
}
protected static boolean inScope(long index, Variable var) {
long varStart = var.getCodeIndex();
long varEnd = varStart + var.getLength();
return index >= varStart && index < varEnd;
}
private static List<Variable> getVariablesAt(VmMirror mirror, Location location) {
// Get variable table and keep only variables visible at this location.
return getVariables(mirror, location.classID, location.methodID).stream()
.filter(v -> inScope(location.index, v))
.collect(Collectors.toList());
}
private static List<Variable> getVariables(VmMirror mirror, long classID, long methodID) {
List<Variable> list = mirror.getVariableTable(classID, methodID);
return list != null ? list : Collections.emptyList();
}
private void setState(State state) {
this.state = state;
}
public DebuggeeState getDebuggeeState() {
return debuggeeState;
}
private void updateEventContext(EventThread event) {
final long threadId = event.getThreadID();
final List<JUnit3Wrapper.DebuggeeState.DebuggeeFrame> frames = new ArrayList<>();
debuggeeState = new DebuggeeState(config, getMirror(), threadId, frames);
// ART returns an error if we ask for frames when there is none. Workaround by asking the
// frame count first.
int frameCount = getMirror().getFrameCount(threadId);
if (frameCount > 0) {
ReplyPacket replyPacket = getMirror().getThreadFrames(threadId, 0, frameCount);
int number = replyPacket.getNextValueAsInt();
assertEquals(frameCount, number);
for (int i = 0; i < frameCount; ++i) {
long frameId = replyPacket.getNextValueAsFrameID();
Location location = replyPacket.getNextValueAsLocation();
frames.add(debuggeeState.new DebuggeeFrame(frameId, location, translator));
}
assertAllDataRead(replyPacket);
}
}
private VmMirror getMirror() {
return debuggeeWrapper.vmMirror;
}
private void resume() {
debuggeeState = null;
getMirror().resume();
setState(State.WaitForEvent);
}
private LongList getMethodCodeIndex(long classId, long breakpointMethodId, int lineToSearch) {
LongList pcs = new LongArrayList();
ReplyPacket replyPacket = getMirror().getLineTable(classId, breakpointMethodId);
checkReplyPacket(replyPacket, "Failed to get method line table");
long start = replyPacket.getNextValueAsLong(); // start
replyPacket.getNextValueAsLong(); // end
int linesCount = replyPacket.getNextValueAsInt();
if (linesCount == 0) {
if (lineToSearch == FIRST_LINE) {
// There is no line table but we are not looking for a specific line. Therefore just
// set the breakpoint on the 1st instruction.
pcs.add(start);
} else {
pcs.add(-1L);
}
} else {
if (lineToSearch == FIRST_LINE) {
// Read only the 1st line because code indices are in ascending order
pcs.add(replyPacket.getNextValueAsLong());
} else {
for (int entry = 0; entry < linesCount; entry++) {
long pc = replyPacket.getNextValueAsLong();
long lineNumber = replyPacket.getNextValueAsInt();
if (lineNumber == lineToSearch) {
pcs.add(pc);
}
}
}
}
return pcs;
}
//
// Command processing
//
public interface Command {
void perform(JUnit3Wrapper testBase);
class RunCommand implements Command {
@Override
public void perform(JUnit3Wrapper testBase) {
testBase.resume();
}
@Override
public String toString() {
return "run";
}
}
// Break on exceptions thrown in className.methodName.
class BreakOnExceptionCommand implements Command {
private static final int ALL_EXCEPTIONS = 0;
private final String className;
private final String methodName;
private final boolean caught;
private final boolean uncaught;
public BreakOnExceptionCommand(
String className, String methodName, boolean caught, boolean uncaught) {
this.className = className;
this.methodName = methodName;
this.caught = caught;
this.uncaught = uncaught;
}
@Override
public void perform(JUnit3Wrapper testBase) {
ReplyPacket replyPacket =
testBase.getMirror().setException(ALL_EXCEPTIONS, caught, uncaught);
assert replyPacket.getErrorCode() == Error.NONE;
int breakpointId = replyPacket.getNextValueAsInt();
testBase.events.put(
Integer.valueOf(breakpointId),
new BreakOnExceptionHandler(className, methodName));
}
@Override
public String toString() {
return "breakOnException";
}
}
class BreakpointCommand implements Command {
private final String className;
private final String methodName;
private final String methodSignature;
private boolean requestedClassPrepare = false;
private int line;
public BreakpointCommand(String className, String methodName,
String methodSignature, int line) {
assert className != null;
assert methodName != null;
this.className = className;
this.methodName = methodName;
this.methodSignature = methodSignature;
this.line = line;
}
@Override
public void perform(JUnit3Wrapper testBase) {
VmMirror mirror = testBase.getMirror();
String obfuscatedClassName = testBase.translator.getObfuscatedClassName(className);
String classSignature = getClassSignature(obfuscatedClassName);
byte typeTag = TypeTag.CLASS;
long classId = mirror.getClassID(classSignature);
if (classId == -1) {
// Is it an interface ?
classId = mirror.getInterfaceID(classSignature);
typeTag = TypeTag.INTERFACE;
}
if (classId == -1) {
// The class is not ready yet. Request a CLASS_PREPARE to delay the installation of the
// breakpoint.
assert !requestedClassPrepare : "Already requested class prepare";
requestedClassPrepare = true;
ReplyPacket replyPacket = mirror.setClassPrepared(obfuscatedClassName);
final int classPrepareRequestId = replyPacket.getNextValueAsInt();
testBase.events.put(
Integer.valueOf(classPrepareRequestId),
wrapper -> {
// Remove the CLASS_PREPARE
wrapper.events.remove(Integer.valueOf(classPrepareRequestId));
wrapper
.getMirror()
.clearEvent(JDWPConstants.EventKind.CLASS_PREPARE, classPrepareRequestId);
// Breakpoint then resume.
wrapper.enqueueCommandsFirst(
Arrays.asList(
BreakpointCommand.this, new JUnit3Wrapper.Command.RunCommand()));
// Set wrapper ready to process next command.
wrapper.setState(State.ProcessCommand);
});
} else {
// The class is available: lookup the method then set the breakpoint.
String obfuscatedMethodName =
testBase.translator.getObfuscatedMethodName(className, methodName, methodSignature);
long breakpointMethodId =
findMethod(mirror, classId, obfuscatedMethodName, methodSignature);
LongList pcs = testBase.getMethodCodeIndex(classId, breakpointMethodId, line);
for (long pc : pcs) {
Assert.assertTrue("No code in method", pc >= 0);
ReplyPacket replyPacket = testBase.getMirror().setBreakpoint(
new Location(typeTag, classId, breakpointMethodId, pc), SuspendPolicy.ALL);
assert replyPacket.getErrorCode() == Error.NONE;
int breakpointId = replyPacket.getNextValueAsInt();
testBase.events.put(Integer.valueOf(breakpointId), new DefaultEventHandler());
}
}
}
private static long findMethod(VmMirror mirror, long classId, String methodName,
String methodSignature) {
boolean withGenericSignature = true;
CommandPacket commandPacket = new CommandPacket(ReferenceTypeCommandSet.CommandSetID,
ReferenceTypeCommandSet.MethodsWithGenericCommand);
commandPacket.setNextValueAsReferenceTypeID(classId);
ReplyPacket replyPacket = mirror.performCommand(commandPacket);
if (replyPacket.getErrorCode() != Error.NONE) {
// Retry with older command ReferenceType.Methods
withGenericSignature = false;
commandPacket.setCommand(ReferenceTypeCommandSet.MethodsCommand);
replyPacket = mirror.performCommand(commandPacket);
assert replyPacket.getErrorCode() == Error.NONE;
}
int methodsCount = replyPacket.getNextValueAsInt();
LongSet matchingMethodIds = new LongOpenHashSet();
for (int i = 0; i < methodsCount; ++i) {
long currentMethodId = replyPacket.getNextValueAsMethodID();
String currentMethodName = replyPacket.getNextValueAsString();
String currentMethodSignature = replyPacket.getNextValueAsString();
if (withGenericSignature) {
replyPacket.getNextValueAsString(); // skip generic signature
}
replyPacket.getNextValueAsInt(); // skip modifiers
// Filter methods based on name (and signature if there is).
if (methodName.equals(currentMethodName)) {
if (methodSignature == null || methodSignature.equals(currentMethodSignature)) {
matchingMethodIds.add(currentMethodId);
}
}
}
Assert.assertTrue(replyPacket.isAllDataRead());
Assert
.assertFalse("No method named " + methodName + " found", matchingMethodIds.isEmpty());
// There must be only one matching method
Assert.assertEquals("More than 1 method found: please specify a signature", 1,
matchingMethodIds.size());
return matchingMethodIds.iterator().nextLong();
}
@Override
public String toString() {
return String.format(
"breakpoint class=%s method=%s signature=%s line=%s",
className, methodName, methodSignature, line);
}
}
class StepCommand implements Command {
private final StepKind stepDepth;
private final StepLevel stepSize;
private final StepFilter stepFilter;
public StepCommand(StepKind stepDepth, StepLevel stepSize, StepFilter stepFilter) {
this.stepDepth = stepDepth;
this.stepSize = stepSize;
this.stepFilter = stepFilter;
}
@Override
public void perform(JUnit3Wrapper testBase) {
long threadId = testBase.getDebuggeeState().getThreadId();
int stepRequestID;
{
EventBuilder eventBuilder = Event.builder(EventKind.SINGLE_STEP, SuspendPolicy.ALL);
eventBuilder.setStep(threadId, stepSize.jdwpValue, stepDepth.jdwpValue);
stepFilter.getExcludedClasses().forEach(eventBuilder::setClassExclude);
ReplyPacket replyPacket = testBase.getMirror().setEvent(eventBuilder.build());
stepRequestID = replyPacket.getNextValueAsInt();
testBase.assertAllDataRead(replyPacket);
}
testBase.events.put(stepRequestID, new StepEventHandler(this, stepRequestID, stepFilter));
// Resume all threads.
testBase.resume();
}
@Override
public String toString() {
return String.format("step %s/%s", JDWPConstants.StepDepth.getName(stepDepth.jdwpValue),
JDWPConstants.StepSize.getName(stepSize.jdwpValue));
}
}
class SetLocalCommand implements Command {
private final String localName;
private final Value newValue;
public SetLocalCommand(String localName, Value newValue) {
this.localName = localName;
this.newValue = newValue;
}
@Override
public void perform(JUnit3Wrapper testBase) {
Optional<Variable> localVar =
getVariableAt(testBase.getMirror(), testBase.debuggeeState.getLocation(), localName);
Assert.assertTrue("No local '" + localName + "'", localVar.isPresent());
CommandPacket setValues = new CommandPacket(StackFrameCommandSet.CommandSetID,
StackFrameCommandSet.SetValuesCommand);
setValues.setNextValueAsThreadID(testBase.getDebuggeeState().getThreadId());
setValues.setNextValueAsFrameID(testBase.getDebuggeeState().getFrameId());
setValues.setNextValueAsInt(1);
setValues.setNextValueAsInt(localVar.get().getSlot());
setValues.setNextValueAsValue(newValue);
ReplyPacket replyPacket = testBase.getMirror().performCommand(setValues);
testBase.checkReplyPacket(replyPacket, "StackFrame.SetValues");
}
}
}
//
// Event handling
//
private interface EventHandler {
void handle(JUnit3Wrapper testBase);
}
private static class DefaultEventHandler implements EventHandler {
@Override
public void handle(JUnit3Wrapper testBase) {
testBase.setState(State.ProcessCommand);
}
}
private static class BreakOnExceptionHandler extends DefaultEventHandler {
private final String className;
private final String methodName;
BreakOnExceptionHandler(String className, String methodName) {
this.className = className;
this.methodName = methodName;
}
@Override
public void handle(JUnit3Wrapper testBase) {
boolean inMethod =
testBase.getDebuggeeState().getTopFrame().getMethodName().equals(methodName);
boolean inClass =
testBase.getDebuggeeState().getTopFrame().getClassName().equals(className);
if (!(inClass && inMethod)) {
// Not the right place, continue until the next exception.
testBase.enqueueCommandFirst(new JUnit3Wrapper.Command.RunCommand());
}
testBase.setState(State.ProcessCommand);
}
}
private static class StepEventHandler extends DefaultEventHandler {
private final JUnit3Wrapper.Command.StepCommand stepCommand;
private final int stepRequestID;
private final StepFilter stepFilter;
private StepEventHandler(
JUnit3Wrapper.Command.StepCommand stepCommand,
int stepRequestID,
StepFilter stepFilter) {
this.stepCommand = stepCommand;
this.stepRequestID = stepRequestID;
this.stepFilter = stepFilter;
}
@Override
public void handle(JUnit3Wrapper testBase) {
// Clear step event.
testBase.getMirror().clearEvent(EventKind.SINGLE_STEP, stepRequestID);
testBase.events.remove(Integer.valueOf(stepRequestID));
// Let the filtering happen.
// Note: we don't need to know whether the location was skipped or not because we are
// going to process the next command(s) in the queue anyway.
stepFilter.skipLocation(testBase.getDebuggeeState(), testBase, stepCommand);
super.handle(testBase);
}
}
}
//
// Step filtering
//
interface StepFilter {
/**
* Provides a list of class name to be skipped when single stepping. This can be a fully
* qualified name (like java.lang.String) or a subpackage (like java.util.*).
*/
List<String> getExcludedClasses();
/**
* Indicates whether the given location must be skipped.
*/
boolean skipLocation(JUnit3Wrapper.DebuggeeState debuggeeState, JUnit3Wrapper wrapper,
JUnit3Wrapper.Command.StepCommand stepCommand);
/**
* A {@link StepFilter} that does not filter anything.
*/
class NoStepFilter implements StepFilter {
@Override
public List<String> getExcludedClasses() {
return Collections.emptyList();
}
@Override
public boolean skipLocation(JUnit3Wrapper.DebuggeeState debuggeeState, JUnit3Wrapper wrapper,
JUnit3Wrapper.Command.StepCommand stepCommand) {
return false;
}
}
/**
* A {@link StepFilter} that matches the default behavior of IntelliJ regarding single
* stepping.
*/
class IntelliJStepFilter implements StepFilter {
// This is the value specified by JDWP in documentation of ReferenceType.Methods command.
private static final int SYNTHETIC_FLAG = 0xF0000000;
@Override
public List<String> getExcludedClasses() {
return ImmutableList.of(
"com.sun.*",
"java.*",
"javax.*",
"org.omg.*",
"sun.*",
"jdk.internal.*",
"junit.*",
"com.intellij.rt.*",
"com.yourkit.runtime.*",
"com.springsource.loaded.*",
"org.springsource.loaded.*",
"javassist.*",
"org.apache.webbeans.*",
"com.ibm.ws.*",
"kotlin.*");
}
@Override
public boolean skipLocation(JUnit3Wrapper.DebuggeeState debuggeeState, JUnit3Wrapper wrapper,
JUnit3Wrapper.Command.StepCommand stepCommand) {
VmMirror mirror = debuggeeState.getMirror();
Location location = debuggeeState.getLocation();
// Skip synthetic methods.
if (isLambdaMethod(mirror, location)) {
// Lambda methods are synthetic but we do want to stop there.
if (DEBUG_TESTS) {
System.out.println("NOT skipping lambda implementation method");
}
return false;
}
if (isInLambdaClass(mirror, location)) {
// Lambda classes must be skipped since they are only wrappers around lambda code.
if (DEBUG_TESTS) {
System.out.println("Skipping lambda class wrapper method");
}
wrapper.enqueueCommandFirst(stepCommand);
return true;
}
if (isSyntheticMethod(mirror, location)) {
if (DEBUG_TESTS) {
System.out.println("Skipping synthetic method");
}
wrapper.enqueueCommandFirst(stepCommand);
return true;
}
if (isClassLoader(mirror, location)) {
if (DEBUG_TESTS) {
System.out.println("Skipping class loader");
}
wrapper.enqueueCommandFirst(
new JUnit3Wrapper.Command.StepCommand(StepKind.OUT, StepLevel.LINE, this));
return true;
}
return false;
}
private static boolean isClassLoader(VmMirror mirror, Location location) {
final long classLoaderClassID = mirror.getClassID("Ljava/lang/ClassLoader;");
assert classLoaderClassID != -1;
long classID = location.classID;
while (classID != 0) {
if (classID == classLoaderClassID) {
return true;
}
classID = mirror.getSuperclassId(classID);
}
return false;
}
private static boolean isSyntheticMethod(VmMirror mirror, Location location) {
// We must gather the modifiers of the method. This is only possible using
// ReferenceType.Methods command which gather information about all methods in a class.
Method[] methods = mirror.getMethods(location.classID);
for (Method method : methods) {
if (method.getMethodID() == location.methodID &&
((method.getModBits() & SYNTHETIC_FLAG) != 0)) {
return true;
}
}
return false;
}
private static boolean isInLambdaClass(VmMirror mirror, Location location) {
String classSig = mirror.getClassSignature(location.classID);
return classSig.contains("$$Lambda$");
}
private static boolean isLambdaMethod(VmMirror mirror, Location location) {
String methodName = mirror.getMethodName(location.classID, location.methodID);
return methodName.startsWith("lambda$");
}
}
/**
* IntelliJ derived step filter that also skips all android runtime specific libraries.
*/
class AndroidRuntimeStepFilter extends IntelliJStepFilter {
@Override
public List<String> getExcludedClasses() {
return ImmutableList.<String>builder()
.addAll(super.getExcludedClasses())
.add("libcore.*")
.add("dalvik.*")
.add("com.android.dex.*") // Android 6.0.1 - 7.0.0 use this for reflection.
.build();
}
}
}
}