Support stepping over/out Kotlin inline functions
Bug: 63608278
Change-Id: I5904eb873fdf191e9ef32ad88b5002909ca05088
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 c89e792..9a28fac 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -12,8 +12,10 @@
import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.IteratorUtils;
import com.android.tools.r8.utils.OffOrAuto;
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 java.io.File;
@@ -269,6 +271,12 @@
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),
@@ -317,10 +325,7 @@
}
protected final JUnit3Wrapper.Command checkNoLocal(String localName) {
- return inspect(t -> {
- List<String> localNames = t.getLocalNames();
- Assert.assertFalse("Unexpected local: " + localName, localNames.contains(localName));
- });
+ return inspect(t -> t.checkNoLocal(localName));
}
protected final JUnit3Wrapper.Command checkNoLocal() {
@@ -508,8 +513,9 @@
.getMethodSignature(debuggeeState.getLocation().classID,
debuggeeState.getLocation().methodID);
System.out.println(String
- .format("Suspended in %s#%s%s@0x%x", classSig, methodName, methodSig,
- Long.valueOf(debuggeeState.getLocation().index)));
+ .format("Suspended in %s#%s%s@0x%x (line=%d)", classSig, methodName, methodSig,
+ Long.valueOf(debuggeeState.getLocation().index),
+ Integer.valueOf(debuggeeState.getLineNumber())));
}
// Handle event.
@@ -555,6 +561,16 @@
return new ArtTestOptions(debuggeePath);
}
+ 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
//
@@ -572,6 +588,8 @@
// Locals
+ List<Variable> getVisibleVariables();
+
/**
* Returns the names of all local variables visible at the current location
*/
@@ -581,6 +599,7 @@
* 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);
}
@@ -624,7 +643,7 @@
// Code indices are in ascending order.
assert currentLineCodeIndex >= startCodeIndex;
assert currentLineCodeIndex <= endCodeIndex;
- assert currentLineCodeIndex > previousLineCodeIndex;
+ assert currentLineCodeIndex >= previousLineCodeIndex;
previousLineCodeIndex = currentLineCodeIndex;
if (location.index >= currentLineCodeIndex) {
@@ -652,13 +671,19 @@
}
}
- public List<String> getLocalNames() {
- Location location = getLocation();
- return JUnit3Wrapper.getVariablesAt(mirror, location).stream()
- .map(v -> v.getName())
+ @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(v -> v.getName()).collect(Collectors.toList());
+ }
+
@Override
public Map<String, Value> getLocalValues() {
return JUnit3Wrapper.getVariablesAt(mirror, location).stream()
@@ -687,6 +712,13 @@
"line " + getLineNumber() + ": Expected local '" + localName + "' not present");
}
+ @Override
+ public void checkNoLocal(String localName) {
+ Optional<Variable> localVar = JUnit3Wrapper
+ .getVariableAt(mirror, getLocation(), localName);
+ Assert.assertFalse("Unexpected local: " + localName, localVar.isPresent());
+ }
+
public void checkLocal(String localName) {
Optional<Variable> localVar = JUnit3Wrapper
.getVariableAt(mirror, getLocation(), localName);
@@ -780,6 +812,10 @@
return threadId;
}
+ public int getFrameDepth() {
+ return frames.size();
+ }
+
public FrameInspector getFrame(int index) {
return frames.get(index);
}
@@ -799,6 +835,11 @@
}
@Override
+ public void checkNoLocal(String localName) {
+ getTopFrame().checkNoLocal(localName);
+ }
+
+ @Override
public void checkLocal(String localName) {
getTopFrame().checkLocal(localName);
}
@@ -848,6 +889,11 @@
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;
@@ -920,19 +966,19 @@
}
}
- private static boolean inScope(long index, Variable var) {
- long varStart = var.getCodeIndex();
- long varEnd = varStart + var.getLength();
- return index >= varStart && index < varEnd;
- }
-
- private static Optional<Variable> getVariableAt(VmMirror mirror, Location location,
+ 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()
@@ -1059,10 +1105,9 @@
wrapper.getMirror().clearEvent(JDWPConstants.EventKind.CLASS_PREPARE,
classPrepareRequestId);
- // Breakpoint then resume. Note: we add them at the beginning of the queue (to be the
- // next commands to be processed), thus they need to be pushed in reverse order.
- wrapper.commandsQueue.addFirst(new JUnit3Wrapper.Command.RunCommand());
- wrapper.commandsQueue.addFirst(BreakpointCommand.this);
+ // Breakpoint then resume.
+ wrapper.enqueueCommandsFirst(
+ Arrays.asList(BreakpointCommand.this, new JUnit3Wrapper.Command.RunCommand()));
// Set wrapper ready to process next command.
wrapper.setState(State.ProcessCommand);
@@ -1262,7 +1307,7 @@
}
if (repeatStep) {
// In order to repeat the step now, we need to add it at the beginning of the queue.
- testBase.commandsQueue.addFirst(stepCommand);
+ testBase.enqueueCommandFirst(stepCommand);
}
super.handle(testBase);
}
diff --git a/src/test/java/com/android/tools/r8/debug/KotlinDebugTestBase.java b/src/test/java/com/android/tools/r8/debug/KotlinDebugTestBase.java
new file mode 100644
index 0000000..a0a9c3f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/KotlinDebugTestBase.java
@@ -0,0 +1,83 @@
+// 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.Arrays;
+import java.util.List;
+import org.apache.harmony.jpda.tests.framework.jdwp.Frame.Variable;
+import org.apache.harmony.jpda.tests.framework.jdwp.Location;
+
+/**
+ * A specialization for Kotlin-based tests which provides extra commands.
+ */
+public abstract class KotlinDebugTestBase extends DebugTestBase {
+
+ protected final JUnit3Wrapper.Command kotlinStepOver() {
+ return testBaseBeforeStep -> {
+ final JUnit3Wrapper.DebuggeeState debuggeeStateBeforeStep = testBaseBeforeStep
+ .getDebuggeeState();
+ final int frameDepthBeforeStep = debuggeeStateBeforeStep.getFrameDepth();
+ final Location locationBeforeStep = debuggeeStateBeforeStep.getLocation();
+ final List<Variable> kotlinLvsBeforeStep = getVisibleKotlinInlineVariables(
+ debuggeeStateBeforeStep);
+
+ // This is the command that will be executed after the initial (normal) step over. If we
+ // reach an inlined location, this command will step until reaching a non-inlined location.
+ JUnit3Wrapper.Command commandAfterStep = testBaseAfterStep -> {
+ // Get the new debuggee state (previous one is stale).
+ JUnit3Wrapper.DebuggeeState debuggeeStateAfterStep = testBaseBeforeStep.getDebuggeeState();
+
+ // Are we in the same frame ?
+ final int frameDepthAfterStep = debuggeeStateAfterStep.getFrameDepth();
+ final Location locationAfterStep = debuggeeStateAfterStep.getLocation();
+ if (frameDepthBeforeStep == frameDepthAfterStep
+ && locationBeforeStep.classID == locationAfterStep.classID
+ && locationBeforeStep.methodID == locationAfterStep.methodID) {
+ // We remain in the same method. Do we step into an inlined section ?
+ List<Variable> kotlinLvsAfterStep = getVisibleKotlinInlineVariables(
+ debuggeeStateAfterStep);
+ if (kotlinLvsBeforeStep.isEmpty() && !kotlinLvsAfterStep.isEmpty()) {
+ assert kotlinLvsAfterStep.size() == 1;
+
+ // We're located in an inlined section. Instead of doing a classic step out, we must
+ // jump out of the inlined section.
+ Variable inlinedSectionLv = kotlinLvsAfterStep.get(0);
+ testBaseAfterStep.enqueueCommandFirst(stepUntilOutOfInlineScope(inlinedSectionLv));
+ }
+ }
+ };
+
+ // Step over then check whether we need to continue stepping.
+ testBaseBeforeStep.enqueueCommandsFirst(Arrays.asList(stepOver(), commandAfterStep));
+ };
+ }
+
+ protected final JUnit3Wrapper.Command kotlinStepOut() {
+ return wrapper -> {
+ final List<Variable> kotlinLvsBeforeStep = getVisibleKotlinInlineVariables(
+ wrapper.getDebuggeeState());
+
+ JUnit3Wrapper.Command nextCommand;
+ if (!kotlinLvsBeforeStep.isEmpty()) {
+ // We are in an inline section. We need to step until being out of inline scope.
+ assert kotlinLvsBeforeStep.size() == 1;
+ final Variable inlinedSectionLv = kotlinLvsBeforeStep.get(0);
+ nextCommand = stepUntilOutOfInlineScope(inlinedSectionLv);
+ } else {
+ nextCommand = stepOut();
+ }
+ wrapper.enqueueCommandFirst(nextCommand);
+ };
+ }
+
+ private JUnit3Wrapper.Command stepUntilOutOfInlineScope(Variable inlineScopeLv) {
+ return stepUntil(StepKind.OVER, StepLevel.LINE, debuggeeState -> {
+ boolean inInlineScope = JUnit3Wrapper
+ .inScope(debuggeeState.getLocation().index, inlineScopeLv);
+ return !inInlineScope;
+ });
+ }
+
+}
diff --git a/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java b/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java
index e62a17b..8e9d145 100644
--- a/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java
+++ b/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java
@@ -10,9 +10,9 @@
import org.junit.Ignore;
import org.junit.Test;
-public class KotlinInlineTest extends DebugTestBase {
+// TODO check double-depth inline (an inline in another inline)
+public class KotlinInlineTest extends KotlinDebugTestBase {
- @Ignore("Requires kotlin-specific stepping behavior")
@Test
public void testStepOverInline() throws Throwable {
String methodName = "singleInline";
@@ -26,7 +26,6 @@
assertEquals(41, s.getLineNumber());
s.checkLocal("this");
}),
- // TODO(shertz) stepping over must take kotlin inline range into account.
stepOver(),
inspect(s -> {
assertEquals("KotlinInline", s.getClassName());
@@ -35,7 +34,7 @@
assertEquals(42, s.getLineNumber());
s.checkLocal("this");
}),
- stepOver(),
+ kotlinStepOver(),
inspect(s -> {
assertEquals("KotlinInline", s.getClassName());
assertEquals(methodName, s.getMethodName());
@@ -46,7 +45,6 @@
run());
}
- @Ignore("Requires kotlin-specific stepping behavior")
@Test
public void testStepIntoInline() throws Throwable {
String methodName = "singleInline";
@@ -60,7 +58,14 @@
assertEquals(41, s.getLineNumber());
s.checkLocal("this");
}),
- // TODO(shertz) stepping over must take kotlin inline range into account.
+ stepOver(),
+ inspect(s -> {
+ assertEquals("KotlinInline", s.getClassName());
+ assertEquals(methodName, s.getMethodName());
+ assertEquals("KotlinInline.kt", s.getSourceFile());
+ assertEquals(42, s.getLineNumber());
+ s.checkLocal("this");
+ }),
stepInto(),
inspect(s -> {
assertEquals("KotlinInline", s.getClassName());
@@ -76,7 +81,6 @@
run());
}
- @Ignore("Requires kotlin-specific stepping behavior")
@Test
public void testStepOutInline() throws Throwable {
String methodName = "singleInline";
@@ -90,13 +94,20 @@
assertEquals(41, s.getLineNumber());
s.checkLocal("this");
}),
- // TODO(shertz) stepping out must take kotlin inline range into account.
+ stepOver(),
+ inspect(s -> {
+ assertEquals("KotlinInline", s.getClassName());
+ assertEquals(methodName, s.getMethodName());
+ assertEquals("KotlinInline.kt", s.getSourceFile());
+ assertEquals(42, s.getLineNumber());
+ s.checkLocal("this");
+ }),
stepInto(),
inspect(s -> {
assertEquals("KotlinInline", s.getClassName());
assertEquals(methodName, s.getMethodName());
}),
- stepOut(),
+ kotlinStepOut(),
inspect(s -> {
assertEquals("KotlinInline", s.getClassName());
assertEquals(methodName, s.getMethodName());
diff --git a/src/test/java/com/android/tools/r8/debug/KotlinTest.java b/src/test/java/com/android/tools/r8/debug/KotlinTest.java
index a6b57b8..2baa426 100644
--- a/src/test/java/com/android/tools/r8/debug/KotlinTest.java
+++ b/src/test/java/com/android/tools/r8/debug/KotlinTest.java
@@ -8,7 +8,7 @@
import org.apache.harmony.jpda.tests.framework.jdwp.Value;
import org.junit.Test;
-public class KotlinTest extends DebugTestBase {
+public class KotlinTest extends KotlinDebugTestBase {
// TODO(shertz) simplify test
// TODO(shertz) add more variables ?