Test access to locals on instruction level when debugging
Adds more tests related to local variables which are specifically
meant to verify we correctly see local variables on each instruction,
despite extra DEX move instructions (like for invoke-range or
instructions working on low registers only).
The test LocalsTest.testInvokeRangeLong is expected to fail until
we fix the issue detected by this test.
Change-Id: I0799a4c1bfe604f7d24b98b2f32975244db03dc1
diff --git a/src/test/debugTestResources/Locals.java b/src/test/debugTestResources/Locals.java
index 21d08c1..3ad6596 100644
--- a/src/test/debugTestResources/Locals.java
+++ b/src/test/debugTestResources/Locals.java
@@ -34,12 +34,170 @@
}
}
+ private static void manyLocals() {
+ int i1 = 1;
+ int i2 = 2;
+ int i3 = 3;
+ int i4 = 4;
+ int i5 = 5;
+ int i6 = 6;
+ int i7 = 7;
+ int i8 = 8;
+ int i9 = 9;
+ int i10 = 10;
+ int i11 = 11;
+ int i12 = 12;
+ invokeRange(i6, i5, i4, i3, i2, i1, invokeRange(i12, i11, i10, i9, i8, i7, 0));
+ }
+
+ private static int reverseRange(int a, int b, int c, int d, int e, int f, int g) {
+ return invokeRange(g, f, e, d, c, b, a);
+ }
+
+ private static int invokeRange(int a, int b, int c, int d, int e, int f, int g) {
+ System.out.println(a + b + c + d + e + f + g);
+ return a + b + c + d + e + f + g;
+ }
+
+ private void lotsOfArrayLength() {
+ int lengthOfArray1 = 0;
+ int lengthOfArray2 = 0;
+ int lengthOfArray3 = 0;
+ int lengthOfArray4 = 0;
+ int lengthOfArray5 = 0;
+ int lengthOfArray6 = 0;
+ int lengthOfArray7 = 0;
+ int lengthOfArray8 = 0;
+ int lengthOfArray9 = 0;
+ int lengthOfArray10 = 0;
+ int lengthOfArray11 = 0;
+ int lengthOfArray12 = 0;
+ int lengthOfArray13 = 0;
+ int lengthOfArray14 = 0;
+ int lengthOfArray15 = 0;
+ int lengthOfArray16 = 0;
+
+ // These statements are compiled into new-array in DEX which stores the result in a 4bit
+ // register (0..15).
+ boolean[] array1 = new boolean[1];
+ byte[] array2 = new byte[1];
+ char[] array3 = new char[1];
+ short[] array4 = new short[1];
+ int[] array5 = new int[1];
+ long[] array6 = new long[1];
+ float[] array7 = new float[1];
+ double[] array8 = new double[1];
+ Object[] array9 = new Object[1];
+ String[] array10 = new String[1];
+ String[] array11 = new String[1];
+ String[] array12 = new String[1];
+ String[] array13 = new String[1];
+ String[] array14 = new String[1];
+ String[] array15 = new String[1];
+ String[] array16 = new String[1];
+
+ // 1st breakpoint to capture the IDs of each array.
+ breakpoint();
+
+ // Breakpoint at line below. In DEX, the array-length instruction expects a 4bit register
+ // (0..15). By creating >16 locals, we should cause an intermediate move instruction to
+ // copy/move a high register (>= 16) into a lower register (< 16).
+ // A test should step instruction by instruction and make sure all locals have the correct
+ // value.
+ lengthOfArray1 = array1.length;
+ lengthOfArray2 = array2.length;
+ lengthOfArray3 = array3.length;
+ lengthOfArray4 = array4.length;
+ lengthOfArray5 = array5.length;
+ lengthOfArray6 = array6.length;
+ lengthOfArray7 = array7.length;
+ lengthOfArray8 = array8.length;
+ lengthOfArray9 = array9.length;
+ lengthOfArray10 = array10.length;
+ lengthOfArray11 = array11.length;
+ lengthOfArray12 = array12.length;
+ lengthOfArray13 = array13.length;
+ lengthOfArray14 = array14.length;
+ lengthOfArray15 = array15.length;
+ lengthOfArray16 = array16.length;
+
+ // Use all locals
+ System.out.println(array1);
+ System.out.println(array2);
+ System.out.println(array3);
+ System.out.println(array4);
+ System.out.println(array5);
+ System.out.println(array6);
+ System.out.println(array7);
+ System.out.println(array8);
+ System.out.println(array9);
+ System.out.println(array10);
+ System.out.println(array11);
+ System.out.println(array12);
+ System.out.println(array13);
+ System.out.println(array14);
+ System.out.println(array15);
+ System.out.println(array16);
+
+ System.out.println(lengthOfArray1);
+ System.out.println(lengthOfArray2);
+ System.out.println(lengthOfArray3);
+ System.out.println(lengthOfArray4);
+ System.out.println(lengthOfArray5);
+ System.out.println(lengthOfArray6);
+ System.out.println(lengthOfArray7);
+ System.out.println(lengthOfArray8);
+ System.out.println(lengthOfArray9);
+ System.out.println(lengthOfArray10);
+ System.out.println(lengthOfArray11);
+ System.out.println(lengthOfArray12);
+ System.out.println(lengthOfArray13);
+ System.out.println(lengthOfArray14);
+ System.out.println(lengthOfArray15);
+ System.out.println(lengthOfArray16);
+ }
+
+ // Utility method to set a breakpoint and inspect the stack.
+ private static void breakpoint() {
+ }
+
+ public void foo(int x) {
+ Integer obj = new Integer(x + x);
+ long l = obj.longValue();
+ try {
+ l = obj.longValue();
+ x = (int) l / x;
+ invokerangeLong(l, l, l, l, l, l);
+ sout(x);
+ } catch (ArithmeticException e) {
+ sout(l);
+ } catch (RuntimeException e) {
+ sout(l); // We should not attempt to read the previous definition of 'e' here or below.
+ } catch (Throwable e) {
+ sout(l);
+ }
+ }
+
+ private void sout(long l) {
+ System.out.print(l);
+ }
+
+ private void invokerangeLong(long a, long b, long c, long d, long e, long f) {
+ if (a != d) {
+ throw new RuntimeException("unexpected");
+ }
+ }
+
public static void main(String[] args) {
noLocals();
unusedLocals();
constantLocals(10);
zeroLocals();
noFlowOptimization();
+ manyLocals();
+ reverseRange(1,2,3,4,5,6,7);
+ new Locals().lotsOfArrayLength();
+ new Locals().foo(21);
}
}
diff --git a/src/test/java/com/android/tools/r8/debug/LocalsTest.java b/src/test/java/com/android/tools/r8/debug/LocalsTest.java
index 6132ace..6af29c9 100644
--- a/src/test/java/com/android/tools/r8/debug/LocalsTest.java
+++ b/src/test/java/com/android/tools/r8/debug/LocalsTest.java
@@ -3,7 +3,13 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.debug;
+import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.FrameInspector;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.harmony.jpda.tests.framework.TestErrorException;
+import org.apache.harmony.jpda.tests.framework.jdwp.JDWPConstants.Tag;
import org.apache.harmony.jpda.tests.framework.jdwp.Value;
+import org.junit.Assert;
import org.junit.Test;
/**
@@ -147,4 +153,170 @@
run());
}
+ @Test
+ public void testInvokeRange() throws Throwable {
+ runDebugTest("Locals",
+ breakpoint("Locals", "invokeRange"),
+ run(),
+ inspect(state -> {
+ // 1st breakpoint
+ Assert.assertEquals("invokeRange", state.getMethodName());
+ Assert.assertEquals(58, state.getLineNumber());
+ state.checkLocal("a", Value.createInt(12));
+ state.checkLocal("b", Value.createInt(11));
+ state.checkLocal("c", Value.createInt(10));
+ state.checkLocal("d", Value.createInt(9));
+ state.checkLocal("e", Value.createInt(8));
+ state.checkLocal("f", Value.createInt(7));
+ state.checkLocal("g", Value.createInt(0));
+
+ FrameInspector outerFrame = state.getFrame(1);
+ for (int i = 1; i < 12; ++i) {
+ outerFrame.checkLocal("i" + i, Value.createInt(i));
+ }
+ }),
+ run(),
+ inspect(state -> {
+ // 2nd breakpoint
+ Assert.assertEquals("invokeRange", state.getMethodName());
+ Assert.assertEquals(58, state.getLineNumber());
+ state.checkLocal("a", Value.createInt(6));
+ state.checkLocal("b", Value.createInt(5));
+ state.checkLocal("c", Value.createInt(4));
+ state.checkLocal("d", Value.createInt(3));
+ state.checkLocal("e", Value.createInt(2));
+ state.checkLocal("f", Value.createInt(1));
+ state.checkLocal("g", Value.createInt(57));
+
+ FrameInspector outerFrame = state.getFrame(1);
+ for (int i = 1; i < 12; ++i) {
+ outerFrame.checkLocal("i" + i, Value.createInt(i));
+ }
+ }),
+ run(),
+ // TODO(shertz) maybe we should duplicate invokeRange to avoid this extra 'skip'.
+ // Skip last breakpoint
+ run());
+ }
+
+ @Test
+ public void testInvokeRange2() throws Throwable {
+ runDebugTest("Locals",
+ breakpoint("Locals", "reverseRange"),
+ run(),
+ inspect(state -> {
+ Assert.assertEquals("reverseRange", state.getMethodName());
+ Assert.assertEquals(54, state.getLineNumber());
+ state.checkLocal("a", Value.createInt(1));
+ state.checkLocal("b", Value.createInt(2));
+ state.checkLocal("c", Value.createInt(3));
+ state.checkLocal("d", Value.createInt(4));
+ state.checkLocal("e", Value.createInt(5));
+ state.checkLocal("f", Value.createInt(6));
+ state.checkLocal("g", Value.createInt(7));
+ }),
+ stepInto(),
+ inspect(state -> {
+ Assert.assertEquals("invokeRange", state.getMethodName());
+ Assert.assertEquals(58, state.getLineNumber());
+ state.checkLocal("a", Value.createInt(7));
+ state.checkLocal("b", Value.createInt(6));
+ state.checkLocal("c", Value.createInt(5));
+ state.checkLocal("d", Value.createInt(4));
+ state.checkLocal("e", Value.createInt(3));
+ state.checkLocal("f", Value.createInt(2));
+ state.checkLocal("g", Value.createInt(1));
+ }),
+ inspect(state -> {
+ FrameInspector outerFrame = state.getFrame(1);
+ outerFrame.checkLocal("a", Value.createInt(1));
+ outerFrame.checkLocal("b", Value.createInt(2));
+ outerFrame.checkLocal("c", Value.createInt(3));
+ outerFrame.checkLocal("d", Value.createInt(4));
+ outerFrame.checkLocal("e", Value.createInt(5));
+ outerFrame.checkLocal("f", Value.createInt(6));
+ outerFrame.checkLocal("g", Value.createInt(7));
+ }),
+ run());
+ }
+
+ @Test
+ public void testLocals_MoreThan16() throws Throwable {
+ final int minIndex = 1;
+ final int maxIndex = 16;
+ Map<String, Value> arrayLocals = new HashMap<>();
+ runDebugTest("Locals",
+ breakpoint("Locals", "breakpoint"),
+ run(),
+ inspect(state -> {
+ // 1st breakpoint: all lengthOfArray[N] are set to 0
+ FrameInspector outerFrame = state.getFrame(1);
+
+ Map<String, Value> localValues = outerFrame.getLocalValues();
+
+ for (int i = minIndex; i <= maxIndex; ++i) {
+ String varName = "lengthOfArray" + i;
+ Assert.assertTrue(localValues.containsKey(varName));
+ Assert.assertEquals(Value.createInt(0), localValues.get(varName));
+ }
+
+ // Capture IDs of arrays.
+ for (int i = minIndex; i <= maxIndex; ++i) {
+ String varName = "array" + i;
+ Assert.assertTrue(localValues.containsKey(varName));
+ arrayLocals.put(varName, localValues.get(varName));
+ }
+ }),
+ // Step out to reach next instructions in the tested method
+ stepOut(),
+ inspect(state -> {
+ Assert.assertEquals("Locals.java", state.getSourceFile());
+ Assert.assertEquals(107, state.getLineNumber());
+ // Verify that all arrays have the same value.
+ arrayLocals.forEach((name, value) -> state.checkLocal(name, value));
+ }),
+ // Step instruction by instruction to ensure all locals previously declared are safe.
+ stepUntil(StepKind.OVER, StepLevel.INSTRUCTION, state -> {
+ final String sourceFile = state.getSourceFile();
+ final int lineNumber = state.getLineNumber();
+ arrayLocals.forEach((name, value) -> state.checkLocal(name, value));
+ // Stop when we reach the expected line.
+ return lineNumber == 125 && sourceFile.equals("Locals.java");
+ }),
+ run());
+ }
+
+ @Test(expected = TestErrorException.class)
+ public void testInvokeRangeLong() throws Throwable {
+ final int initialValueOfX = 21;
+ final long expectedValueOfL = (long) initialValueOfX * 2;
+ final int expectedValueOfX = (int) expectedValueOfL / initialValueOfX;
+ runDebugTest("Locals",
+ breakpoint("Locals", "invokerangeLong"),
+ run(),
+ inspect(state -> {
+ FrameInspector outerFrame = state.getFrame(1);
+ Map<String, Value> values = outerFrame.getLocalValues();
+ Assert.assertTrue("No variable 'x'", values.containsKey("x"));
+ Assert.assertTrue("No variable 'obj'", values.containsKey("obj"));
+ Assert.assertTrue("No variable 'l'", values.containsKey("l"));
+
+ // 'x' is an int
+ Value valueOfX = values.get("x");
+ Assert.assertEquals(Tag.INT_TAG, valueOfX.getTag());
+ Assert.assertEquals(Value.createInt(expectedValueOfX), valueOfX);
+
+
+ // 'obj' is an Object (Integer).
+ Value valueOfObj = values.get("obj");
+ Assert.assertEquals(Tag.OBJECT_TAG, valueOfObj.getTag());
+
+ // 'l' is a long.
+ Value valueOfL = values.get("l");
+ Assert.assertEquals(Tag.LONG_TAG, valueOfL.getTag());
+ Assert.assertEquals(Value.createLong(expectedValueOfL), valueOfL);
+ }),
+ run());
+ }
+
}