| // 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.TestBase; | |
| import com.android.tools.r8.TestParameters; | |
| import com.android.tools.r8.TestParametersBuilder; | |
| import com.android.tools.r8.TestRuntime; | |
| import com.android.tools.r8.TestRuntime.DexRuntime; | |
| import com.android.tools.r8.ToolHelper; | |
| import com.android.tools.r8.ToolHelper.ArtCommandBuilder; | |
| import com.android.tools.r8.ToolHelper.DexVm.Version; | |
| import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.Command.NopCommand; | |
| import com.android.tools.r8.graph.ClassAccessFlags; | |
| 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.references.MethodReference; | |
| import com.android.tools.r8.utils.DescriptorUtils; | |
| import com.android.tools.r8.utils.internal.exceptions.Unreachable; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.Lists; | |
| 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.util.ArrayDeque; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collections; | |
| import java.util.Deque; | |
| import java.util.HashMap; | |
| 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.ObjectReferenceCommandSet; | |
| 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.Ignore; | |
| import org.junit.Rule; | |
| import org.junit.rules.TestName; | |
| /** | |
| * Base class for debugging tests. | |
| * | |
| * <p>The protocol messages are described here: | |
| * https://docs.oracle.com/javase/8/docs/platform/jpda/jdwp/jdwp-protocol.html | |
| */ | |
| public abstract class DebugTestBase extends TestBase { | |
| public static TestParametersBuilder getTestParameters() { | |
| return TestBase.getTestParameters().withoutCollapsedCfRuntimes().withoutCollapsedDexRuntimes(); | |
| } | |
| // Set to true to enable verbose logs | |
| private static final boolean DEBUG_TESTS = false; | |
| private static final int TIMEOUT_MS = 40000; | |
| 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; | |
| } | |
| } | |
| public static class DebugTestParameters { | |
| final HashMap<String, DelayedDebugTestConfig> map = new HashMap<>(); | |
| public DebugTestParameters add(String name, DebugTestConfig config) { | |
| return add(name, temp -> config); | |
| } | |
| public DebugTestParameters add(String name, DelayedDebugTestConfig config) { | |
| assert !map.containsKey(name); | |
| map.put(name, config); | |
| return this; | |
| } | |
| // Returns a list of parameters used in most debug tests of the form: name * config. | |
| public List<Object[]> build() { | |
| return map.entrySet().stream() | |
| .map(e -> new Object[] {e.getKey(), e.getValue()}) | |
| .collect(Collectors.toList()); | |
| } | |
| } | |
| public static DebugTestParameters parameters() { | |
| return new DebugTestParameters(); | |
| } | |
| // Returns parameters of the form: parameters * name * config, with the CF config on the default | |
| // CF runtime and a D8 config for each DEX runtime. | |
| static List<Object[]> buildCfAndD8Parameters( | |
| DelayedDebugTestConfig cfConfig, Function<DexRuntime, DelayedDebugTestConfig> d8Config) { | |
| List<Object[]> result = new ArrayList<>(); | |
| for (TestParameters parameters : | |
| getTestParameters().withDefaultCfRuntime().withDexRuntimes().build()) { | |
| if (parameters.isCfRuntime()) { | |
| result.add(new Object[] {parameters, "CF", cfConfig}); | |
| } else { | |
| result.add(new Object[] {parameters, "D8", d8Config.apply(parameters.asDexRuntime())}); | |
| } | |
| } | |
| return result; | |
| } | |
| @Rule public TestName testName = new TestName(); | |
| protected final void runDebugTest( | |
| DebugTestConfig config, Class<?> debuggeeClass, JUnit3Wrapper.Command... commands) | |
| throws Throwable { | |
| runInternal(config, debuggeeClass.getTypeName(), Arrays.asList(commands)); | |
| } | |
| 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 { | |
| Assume.assumeTrue( | |
| "Skipping test " | |
| + testName.getMethodName() | |
| + " because debug tests are not yet supported on Windows", | |
| !ToolHelper.isWindows()); | |
| // TODO(b/199700280): Reenable on 12.0.0 when we have the libjdwp.so file include and the flags | |
| // fixed. | |
| Assume.assumeTrue( | |
| "Skipping test " + testName.getMethodName() + " because debugging not enabled in 12.0.0", | |
| config.getRuntime().isCf() | |
| || !config.getRuntime().asDex().getVersion().isEqualTo(Version.V12_0_0)); | |
| 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(MethodReference method) { | |
| return breakpoint(method.getHolderClass().getTypeName(), method.getMethodName()); | |
| } | |
| protected final JUnit3Wrapper.Command breakpoint(MethodReference method, int line) { | |
| return breakpoint(method.getHolderClass().getTypeName(), method.getMethodName(), line); | |
| } | |
| 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 applyIf( | |
| boolean condition, Supplier<JUnit3Wrapper.Command> action) { | |
| return condition ? action.get() : new NopCommand(); | |
| } | |
| 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(int line) { | |
| return inspect(t -> t.checkLine(null, line)); | |
| } | |
| 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 -> 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 checkFieldOnThis( | |
| String fieldName, String fieldSignature, Value expectedValue) { | |
| return inspect( | |
| t -> { | |
| Value value = t.getFieldOnThis(fieldName, fieldSignature); | |
| Assert.assertEquals( | |
| "Incorrect value for field 'this." + fieldName + "'", expectedValue, value); | |
| }); | |
| } | |
| protected final JUnit3Wrapper.Command inspect(Consumer<JUnit3Wrapper.DebuggeeState> inspector) { | |
| return t -> inspector.accept(t.debuggeeState); | |
| } | |
| protected final JUnit3Wrapper.Command conditional( | |
| Function<JUnit3Wrapper.DebuggeeState, List<JUnit3Wrapper.Command>> conditional) { | |
| return t -> subcommands(conditional.apply(t.debuggeeState)).perform(t); | |
| } | |
| protected final JUnit3Wrapper.Command subcommands(List<JUnit3Wrapper.Command> commands) { | |
| return t -> Lists.reverse(commands).forEach(t.commandsQueue::addFirst); | |
| } | |
| 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); | |
| boolean canUsePcForMissingLineNumberTable(); | |
| } | |
| private class IdentityTranslator implements Translator { | |
| private final boolean usePcForMissingLineTable; | |
| public IdentityTranslator(boolean usePcForMissingLineNumberTable) { | |
| this.usePcForMissingLineTable = usePcForMissingLineNumberTable; | |
| } | |
| @Override | |
| public boolean canUsePcForMissingLineNumberTable() { | |
| return usePcForMissingLineTable; | |
| } | |
| @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, boolean usePcForMissingLineTable) { | |
| super(usePcForMissingLineTable); | |
| 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(config.shouldUsePcForMissingLineNumberTable()); | |
| } else { | |
| this.translator = | |
| new ClassNameMapperTranslator( | |
| classNameMapper, config.shouldUsePcForMissingLineNumberTable()); | |
| } | |
| } | |
| void prepareForStreaming() throws Exception { | |
| if (DEBUG_TESTS) { | |
| System.out.println("Preparing test for stream execution"); | |
| } | |
| setUp(); | |
| } | |
| @Override | |
| protected void runTest() { | |
| 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 = | |
| config | |
| .getRuntime() | |
| .match( | |
| cfRuntime -> false, | |
| (dexRuntime, version) -> { | |
| if (version.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; | |
| return true; | |
| } | |
| } | |
| return false; | |
| }); | |
| 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. | |
| config | |
| .getRuntime() | |
| .match( | |
| cfRuntime -> { | |
| setProperty( | |
| "jpda.settings.debuggeeJavaPath", cfRuntime.getJavaExecutable().toString()); | |
| }, | |
| (dexRuntime, version) -> { | |
| ArtCommandBuilder artCommandBuilder = new ArtCommandBuilder(dexRuntime.getVm()); | |
| if (version.isNewerThan(Version.V5_1_1)) { | |
| artCommandBuilder.appendArtOption("-Xcompiler-option"); | |
| artCommandBuilder.appendArtOption("--debuggable"); | |
| } | |
| if (version.isNewerThanOrEqual(Version.V13_0_0)) { | |
| // TODO(b/199700280): These options may be the same for V12 once the libs are | |
| // added. | |
| artCommandBuilder.appendArtOption("-Xplugin:libopenjdkjvmti.so"); | |
| setProperty("jpda.settings.debuggeeAgentArgument", "-agentpath:"); | |
| setProperty("jpda.settings.debuggeeAgentName", "libjdwp.so"); | |
| } else if (version.isNewerThanOrEqual(Version.V9_0_0)) { | |
| artCommandBuilder.appendArtOption("-XjdwpProvider:internal"); | |
| } | |
| if (DEBUG_TESTS && version.isNewerThan(Version.V4_4_4)) { | |
| artCommandBuilder.appendArtOption("-verbose:jdwp"); | |
| } | |
| String build = artCommandBuilder.build(); | |
| setProperty("jpda.settings.debuggeeJavaPath", 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)); | |
| // Set timeout | |
| setProperty("jpda.settings.timeout", Integer.toString(TIMEOUT_MS)); | |
| setProperty("jpda.settings.waitingTime", Integer.toString(TIMEOUT_MS)); | |
| } | |
| } | |
| 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 setLocal(String localName, Value newValue); | |
| void checkLine(String sourceFile, int line); | |
| } | |
| public static class DebuggeeState implements FrameInspector { | |
| private class DebuggeeFrame implements FrameInspector { | |
| private final JUnit3Wrapper wrapper; | |
| private final long frameId; | |
| private final Location location; | |
| private final Translator translator; | |
| public DebuggeeFrame( | |
| JUnit3Wrapper wrapper, long frameId, Location location, Translator translator) { | |
| this.wrapper = wrapper; | |
| 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(); | |
| if (lines == 0 && translator.canUsePcForMissingLineNumberTable()) { | |
| return (int) location.index; | |
| } | |
| 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 setLocal(String localName, Value newValue) { | |
| Optional<Variable> localVar = getVariableAt(mirror, getLocation(), localName); | |
| Assert.assertTrue("No local '" + localName + "'", localVar.isPresent()); | |
| CommandPacket setValues = | |
| new CommandPacket( | |
| StackFrameCommandSet.CommandSetID, StackFrameCommandSet.SetValuesCommand); | |
| setValues.setNextValueAsThreadID(getThreadId()); | |
| setValues.setNextValueAsFrameID(getFrameId()); | |
| setValues.setNextValueAsInt(1); | |
| setValues.setNextValueAsInt(localVar.get().getSlot()); | |
| setValues.setNextValueAsValue(newValue); | |
| ReplyPacket replyPacket = mirror.performCommand(setValues); | |
| wrapper.checkReplyPacket(replyPacket, "StackFrame.SetValues"); | |
| } | |
| @Override | |
| public void checkLine(String sourceFile, int line) { | |
| sourceFile = sourceFile != null ? sourceFile : getSourceFile(); | |
| 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 TestRuntime getRuntime() { | |
| return config.getRuntime(); | |
| } | |
| 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 setLocal(String localName, Value newValue) { | |
| getTopFrame().setLocal(localName, newValue); | |
| } | |
| @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 internalStaticField(getMirror(), classId, fieldId); | |
| } | |
| public Value getFieldOnThis(String fieldName, String fieldSignature) { | |
| long thisObjectId = getMirror().getThisObject(getThreadId(), getFrameId()); | |
| long classId = getMirror().getReferenceType(thisObjectId); | |
| // TODO(zerny): Search supers too. This will only get the field if directly on the class. | |
| long fieldId = findField(getMirror(), classId, fieldName, fieldSignature); | |
| return internalInstanceField(getMirror(), thisObjectId, fieldId); | |
| } | |
| public void setFieldOnThis(String fieldName, String fieldSignature, Value value) { | |
| long thisObjectId = getMirror().getThisObject(getThreadId(), getFrameId()); | |
| long classId = getMirror().getReferenceType(thisObjectId); | |
| // TODO(zerny): Search supers too. This will only get the field if directly on the class. | |
| long fieldId = findField(getMirror(), classId, fieldName, fieldSignature); | |
| internalSetInstanceField(getMirror(), thisObjectId, fieldId, value); | |
| } | |
| 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 static Value internalStaticField(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; | |
| } | |
| } | |
| private static void internalSetInstanceField( | |
| VmMirror mirror, long objectId, long fieldId, Value value) { | |
| CommandPacket commandPacket = | |
| new CommandPacket( | |
| ObjectReferenceCommandSet.CommandSetID, ObjectReferenceCommandSet.SetValuesCommand); | |
| commandPacket.setNextValueAsObjectID(objectId); | |
| commandPacket.setNextValueAsInt(1); // field count. | |
| commandPacket.setNextValueAsFieldID(fieldId); | |
| commandPacket.setNextValueAsUntaggedValue(value); | |
| ReplyPacket replyPacket = mirror.performCommand(commandPacket); | |
| assert replyPacket.getErrorCode() == Error.NONE : "Error code: " + replyPacket.getErrorCode(); | |
| } | |
| private static Value internalInstanceField(VmMirror mirror, long objectId, long fieldId) { | |
| CommandPacket commandPacket = | |
| new CommandPacket( | |
| ObjectReferenceCommandSet.CommandSetID, ObjectReferenceCommandSet.GetValuesCommand); | |
| commandPacket.setNextValueAsObjectID(objectId); | |
| 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(this, 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"; | |
| } | |
| } | |
| class NopCommand implements Command { | |
| @Override | |
| public void perform(JUnit3Wrapper testBase) { | |
| // Intentionally empty - it is a nop. | |
| } | |
| } | |
| // 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) { | |
| // TODO(b/174809573): These "lambda" specific methods are highly questionable. | |
| // Determine the exact filtering behavior of intellij (which is very likely not name | |
| // based) and update this filter accordingly. | |
| CommandPacket cmd = | |
| new CommandPacket( | |
| ReferenceTypeCommandSet.CommandSetID, ReferenceTypeCommandSet.ModifiersCommand); | |
| cmd.setNextValueAsReferenceTypeID(location.classID); | |
| ReplyPacket reply = mirror.performCommand(cmd); | |
| mirror.checkReply(reply); | |
| int modifiers = reply.getNextValueAsInt(); | |
| ClassAccessFlags flags = ClassAccessFlags.fromCfAccessFlags(modifiers); | |
| if (!flags.isSynthetic()) { | |
| return false; | |
| } | |
| String signature = mirror.getClassSignature(location.classID); | |
| if (signature.contains("-CC")) { | |
| // TODO(b/174809573): The need to return false here indicates a questionable test | |
| // expectation. Either the test is incorrect or there is a bug in our generation of | |
| // -CC classes marked as synthetic as that would lead to unwanted debugger filtering. | |
| return false; | |
| } | |
| return true; | |
| } | |
| private static boolean isLambdaMethod(VmMirror mirror, Location location) { | |
| String methodName = mirror.getMethodName(location.classID, location.methodID); | |
| return methodName.startsWith("lambda$") || methodName.startsWith("$private$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(); | |
| } | |
| } | |
| } | |
| protected void runContinuousTest( | |
| String debuggeeClassName, DebugTestConfig config, String mainMethodName) throws Throwable { | |
| runDebugTest( | |
| config, | |
| debuggeeClassName, | |
| breakpoint(debuggeeClassName, mainMethodName), | |
| run(), | |
| stepUntil( | |
| StepKind.OVER, | |
| StepLevel.INSTRUCTION, | |
| debuggeeState -> { | |
| // Fetch local variables. | |
| Map<String, Value> localValues = debuggeeState.getLocalValues(); | |
| Assert.assertNotNull(localValues); | |
| // Always step until we actually exit the program. | |
| return false; | |
| })); | |
| } | |
| } |