Support continuous stepping in debug tests

This CL adds continuous stepping support to the debug test
infrastructure. It is now possible to step through an entire program
by stepping on each line (or instruction) until the runtime exits.

At each step, we do fetch each local variable that is visible (both
its name and value) of the current stack frame.

For now we only add a couple test doing continuous step of existing
debuggee classes. In the future we can add more sophisticated classes
for testing and fetch more data (source file, line number, ...) at
each step in the program.

Bug: 37731620
Change-Id: I3e9f35533f2c59b216e4777a08cd9b3dd7830d4f
diff --git a/src/test/java/com/android/tools/r8/debug/ContinuousSteppingTest.java b/src/test/java/com/android/tools/r8/debug/ContinuousSteppingTest.java
new file mode 100644
index 0000000..ad94a53
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/ContinuousSteppingTest.java
@@ -0,0 +1,38 @@
+// 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 java.util.Map;
+import org.apache.harmony.jpda.tests.framework.jdwp.Value;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ContinuousSteppingTest extends DebugTestBase {
+
+  @Test
+  public void testArithmetic() throws Throwable {
+    runContinuousTest("Arithmetic");
+  }
+
+  @Test
+  public void testLocals() throws Throwable {
+    runContinuousTest("Locals");
+  }
+
+  private void runContinuousTest(String debuggeeClassName) throws Throwable {
+    runDebugTest(debuggeeClassName,
+        breakpoint(debuggeeClassName, "main"),
+        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;
+        }));
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index a272eca..be25097 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -18,10 +18,10 @@
 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.Optional;
-import java.util.Queue;
 import java.util.TreeMap;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -341,7 +341,7 @@
      */
     private DebuggeeState debuggeeState = null;
 
-    private final Queue<Command> commandsQueue;
+    private final Deque<Command> commandsQueue;
 
     // Active event requests.
     private final Map<Integer, EventHandler> events = new TreeMap<>();
@@ -473,6 +473,9 @@
               artCommandBuilder.appendArtOption("-Xcompiler-option");
               artCommandBuilder.appendArtOption("--compiler-filter=interpret-only");
             }
+            if (DEBUG_TESTS) {
+              artCommandBuilder.appendArtOption("-verbose:jdwp");
+            }
             setProperty("jpda.settings.debuggeeJavaPath", artCommandBuilder.build());
           }
 
@@ -1031,8 +1034,8 @@
             stepRequestID = replyPacket.getNextValueAsInt();
             testBase.assertAllDataRead(replyPacket);
           }
-          testBase.events.put(stepRequestID, new StepEventHandler(stepRequestID, stepFilter,
-              stepUntil));
+          testBase.events
+              .put(stepRequestID, new StepEventHandler(this, stepRequestID, stepFilter, stepUntil));
 
           // Resume all threads.
           testBase.resume();
@@ -1092,13 +1095,17 @@
 
     private static class StepEventHandler extends DefaultEventHandler {
 
+      private final JUnit3Wrapper.Command.StepCommand stepCommand;
       private final int stepRequestID;
       private final StepFilter stepFilter;
       private final Function<DebuggeeState, Boolean> stepUntil;
 
-      private StepEventHandler(int stepRequestID,
+      private StepEventHandler(
+          JUnit3Wrapper.Command.StepCommand stepCommand,
+          int stepRequestID,
           StepFilter stepFilter,
           Function<DebuggeeState, Boolean> stepUntil) {
+        this.stepCommand = stepCommand;
         this.stepRequestID = stepRequestID;
         this.stepFilter = stepFilter;
         this.stepUntil = stepUntil;
@@ -1106,19 +1113,23 @@
 
       @Override
       public void handle(JUnit3Wrapper testBase) {
+        // Clear step event.
+        testBase.getMirror().clearEvent(EventKind.SINGLE_STEP, stepRequestID);
+        testBase.events.remove(Integer.valueOf(stepRequestID));
+
+        // Do we need to step again ?
+        boolean repeatStep = false;
         if (stepFilter
             .skipLocation(testBase.getMirror(), testBase.getDebuggeeState().getLocation())) {
-          // Keep the step active and resume so that we do another step.
-          testBase.resume();
+          repeatStep = true;
         } else if (stepUntil.apply(testBase.getDebuggeeState()) == Boolean.FALSE) {
-          // We must not stop yet.
-          testBase.resume();
-        } else {
-          // When hit, the single step must be cleared.
-          testBase.getMirror().clearEvent(EventKind.SINGLE_STEP, stepRequestID);
-          testBase.events.remove(Integer.valueOf(stepRequestID));
-          super.handle(testBase);
+          repeatStep = true;
         }
+        if (repeatStep) {
+          // In order to repeat the step now, we need to add it at the beginning of the queue.
+          testBase.commandsQueue.addFirst(stepCommand);
+        }
+        super.handle(testBase);
       }
     }