Smali test to verify line-change assumption in an Art debugging session.

R=sgjesse, shertz

Change-Id: Ic5dd2a1ee9286888318bd680d36591d4f817d155
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 65fe4aa..1cd5c71 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -152,10 +152,14 @@
         ToolHelper.getMinApiLevelForDexVm(ToolHelper.getDexVm()) >= Constants.ANDROID_N_API;
   }
 
-  protected final boolean isRunningJava() {
+  protected static boolean isRunningJava() {
     return RUNTIME_KIND == RuntimeKind.JAVA;
   }
 
+  protected static boolean isRunningArt() {
+    return RUNTIME_KIND == RuntimeKind.ART;
+  }
+
   protected final void runDebugTest(String debuggeeClass, JUnit3Wrapper.Command... commands)
       throws Throwable {
     runDebugTest(Collections.emptyList(), debuggeeClass, Arrays.asList(commands));
diff --git a/src/test/java/com/android/tools/r8/debug/SmaliDebugTest.java b/src/test/java/com/android/tools/r8/debug/SmaliDebugTest.java
new file mode 100644
index 0000000..9c5a636
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/SmaliDebugTest.java
@@ -0,0 +1,211 @@
+// 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 static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.code.IfEqz;
+import com.android.tools.r8.code.Instruction;
+import com.android.tools.r8.debuginfo.DebugInfoInspector;
+import com.android.tools.r8.graph.DexDebugEntry;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.smali.SmaliTestBase.SmaliBuilder;
+import com.android.tools.r8.utils.AndroidApp;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import org.apache.harmony.jpda.tests.framework.jdwp.Value;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class SmaliDebugTest extends DebugTestBase {
+
+  static final String FILE = "SmaliDebugTestDebuggee.smali";
+  static final String CLASS = "SmaliDebugTestDebuggee";
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    Assume.assumeTrue(DebugTestBase.isRunningArt());
+  }
+
+  @Rule
+  public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+
+  /**
+   * Simple test to check setup works for the Java source, ala:
+   *
+   * static int method(int x) {
+   *   if (x != 0) {
+   *     return x
+   *   } else {
+   *     return 42;
+   *   }
+   * }
+   */
+  @Test
+  public void testSimpleIf() throws Throwable {
+    String methodName = "simpleIf";
+    runDebugTest(buildSimpleIf(methodName), CLASS,
+        breakpoint(CLASS, methodName),
+        run(),
+        // first call x == 0
+        checkLine(FILE, 1),
+        checkLocal("x", Value.createInt(0)),
+        stepOver(),
+        checkLine(FILE, 4),
+        run(),
+        // second call x == 1
+        checkLine(FILE, 1),
+        checkLocal("x", Value.createInt(1)),
+        stepOver(),
+        checkLine(FILE, 2),
+        run());
+  }
+
+  private List<Path> buildSimpleIf(String methodName)
+      throws Throwable {
+    SmaliBuilder builder = new SmaliBuilder();
+    builder.addClass(CLASS);
+    builder.setSourceFile(FILE);
+    builder.addStaticMethod("int", methodName, Collections.singletonList("int"), 0,
+        ".param p0, \"x\"    # I",
+        ".line 1",
+        "    if-eqz p0, :onzero",
+        ".line 2",
+        "    return p0",
+        ":onzero",
+        ".line 4",
+        "    const p0, 42",
+        "    return p0");
+
+    builder.addMainMethod(2,
+        "    const v0, 0",
+        "    invoke-static       { v0 }, L" + CLASS + ";->" + methodName + "(I)I",
+        "    move-result         v1",
+        "    sget-object         v0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
+        "    invoke-virtual      { v0, v1 }, Ljava/io/PrintStream;->print(I)V",
+        "    const v0, 1",
+        "    invoke-static       { v0 }, L" + CLASS + ";->" + methodName + "(I)I",
+        "    move-result         v1",
+        "    sget-object         v0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
+        "    invoke-virtual      { v0, v1 }, Ljava/io/PrintStream;->print(I)V",
+        "    return-void"
+    );
+    return buildAndRun(builder);
+  }
+
+  /**
+   * Test that a jump to a position that happens after a "default" debug event still triggers a
+   * break point due to the line change.
+   */
+  @Test
+  public void testJumpAfterLineChange() throws Throwable {
+    String methodName = "jumpAfterLineChange";
+    List<Path> outs = buildJumpAfterLineChange(methodName);
+
+    // Verify that the PC associated with the line entry 4 is prior to the target of the condition.
+    DebugInfoInspector info = new DebugInfoInspector(AndroidApp.fromProgramFiles(outs), CLASS,
+        new MethodSignature(methodName, "int", new String[]{ "int" }));
+    IfEqz cond = null;
+    for (Instruction instruction : info.getMethod().getCode().asDexCode().instructions) {
+      if (instruction.getOpcode() == IfEqz.OPCODE) {
+        cond = (IfEqz) instruction;
+        break;
+      }
+    }
+    assertNotNull(cond);
+    int target = cond.getTargets()[0] + cond.getOffset();
+    int linePC = -1;
+    for (DexDebugEntry entry : info.getEntries()) {
+      if (entry.line == 4) {
+        linePC = entry.address;
+        break;
+      }
+    }
+    assertTrue(linePC > -1);
+    assertTrue(target > linePC);
+
+    // Run debugger to verify that we step to line 4 and the values of v0 and v1 are unchanged.
+    runDebugTest(outs, CLASS,
+        breakpoint(CLASS, methodName),
+        run(),
+        // first call x == 0
+        checkLine(FILE, 1),
+        checkLocal("x", Value.createInt(0)),
+        checkNoLocal("v0"),
+        checkNoLocal("v1"),
+        stepOver(),
+        checkLine(FILE, 4),
+        checkLocal("v0", Value.createInt(0)),
+        checkLocal("v1", Value.createInt(7)),
+        stepOver(),
+        checkLine(FILE, 5),
+        checkLocal("v0", Value.createInt(42)),
+        checkLocal("v1", Value.createInt(7)),
+        run(),
+        // second call x == 1
+        checkLine(FILE, 1),
+        checkLocal("x", Value.createInt(1)),
+        checkNoLocal("v0"),
+        checkNoLocal("v1"),
+        stepOver(),
+        checkLine(FILE, 2),
+        checkNoLocal("v0"),
+        checkNoLocal("v1"),
+        run());
+  }
+
+  private List<Path> buildJumpAfterLineChange(String methodName)
+      throws Throwable {
+    SmaliBuilder builder = new SmaliBuilder();
+    builder.addClass(CLASS);
+    builder.setSourceFile(FILE);
+    builder.addStaticMethod("int", methodName, Collections.singletonList("int"), 2,
+        ".param p0, \"x\"    # I",
+        ".line 1",
+        "    const v0, 0",
+        "    const v1, 7",
+        "    if-eqz p0, :onzero",
+        ".line 2",
+        "    return p0",
+        ".line 4",
+        ".local v0, \"v0\":I",
+        ".local v1, \"v1\":I",
+        "    const v0, 1234", // odd and unreachable code
+        "    const v1, 5678", // ...
+        ":onzero",
+        "    const v0, 42",
+        ".line 5",
+        "    return v0");
+
+    builder.addMainMethod(2,
+        "    const v0, 0",
+        "    invoke-static       { v0 }, L" + CLASS + ";->" + methodName + "(I)I",
+        "    move-result         v1",
+        "    sget-object         v0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
+        "    invoke-virtual      { v0, v1 }, Ljava/io/PrintStream;->print(I)V",
+        "    const v0, 1",
+        "    invoke-static       { v0 }, L" + CLASS + ";->" + methodName + "(I)I",
+        "    move-result         v1",
+        "    sget-object         v0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
+        "    invoke-virtual      { v0, v1 }, Ljava/io/PrintStream;->print(I)V",
+        "    return-void"
+    );
+    return buildAndRun(builder);
+  }
+
+  private List<Path> buildAndRun(SmaliBuilder builder) throws Throwable {
+    byte[] bytes = builder.compile();
+    Path out = temp.getRoot().toPath().resolve("classes.dex");
+    Files.write(out, bytes);
+    ToolHelper.runArtNoVerificationErrors(out.toString(), CLASS);
+    return Collections.singletonList(out);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoInspector.java b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoInspector.java
index 029b1ad..5d15735 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoInspector.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoInspector.java
@@ -196,4 +196,12 @@
             String.join(",", remaining.stream().map(Object::toString).collect(Collectors.toList())),
         expected, expected + remaining.size());
   }
+
+  public DexEncodedMethod getMethod() {
+    return method;
+  }
+
+  public List<DexDebugEntry> getEntries() {
+    return entries;
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/smali/SmaliBuildTest.java b/src/test/java/com/android/tools/r8/smali/SmaliBuildTest.java
index 470f16e..b3a4060 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliBuildTest.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliBuildTest.java
@@ -13,8 +13,6 @@
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.DexInspector.ClassSubject;
 import com.android.tools.r8.utils.InternalOptions;
-import java.io.IOException;
-import org.antlr.runtime.RecognitionException;
 import org.junit.Test;
 
 public class SmaliBuildTest extends SmaliTestBase {
@@ -47,7 +45,7 @@
   }
 
   @Test
-  public void buildWithLibrary() throws IOException, RecognitionException {
+  public void buildWithLibrary() throws Throwable {
     // Build simple "Hello, world!" application.
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
     builder.addMainMethod(
diff --git a/src/test/java/com/android/tools/r8/smali/SmaliDisassembleTest.java b/src/test/java/com/android/tools/r8/smali/SmaliDisassembleTest.java
index 66d0304..ea0d767 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliDisassembleTest.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliDisassembleTest.java
@@ -7,6 +7,7 @@
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.errors.DexOverflowException;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
@@ -32,7 +33,7 @@
                   new Timing("SmaliTest"))
               .read();
       assertEquals(smali, application.smali(new InternalOptions()));
-    } catch (IOException | RecognitionException | ExecutionException e) {
+    } catch (IOException | RecognitionException | ExecutionException | DexOverflowException e) {
       throw new RuntimeException(e);
     }
   }
diff --git a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
index cecc10e..2e3088a 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
@@ -80,6 +80,7 @@
       String name;
       String superName;
       List<String> implementedInterfaces;
+      String sourceFile = null;
       List<String> source = new ArrayList<>();
 
       Builder(String name, String superName, List<String> implementedInterfaces) {
@@ -124,6 +125,9 @@
         appendSuper(builder);
         appendImplementedInterfaces(builder);
         builder.append("\n");
+        if (sourceFile != null) {
+          builder.append(".source \"").append(sourceFile).append("\"\n");
+        }
         writeSource(builder);
         return builder.toString();
       }
@@ -199,6 +203,10 @@
       classes.put(name, new InterfaceBuilder(name, superName));
     }
 
+    public void setSourceFile(String file) {
+      classes.get(currentClassName).sourceFile = file;
+    }
+
     public void addDefaultConstructor() {
       String superDescriptor =
           DescriptorUtils.javaTypeToDescriptor(classes.get(currentClassName).superName);
@@ -350,7 +358,8 @@
       return result;
     }
 
-    public byte[] compile() throws IOException, RecognitionException {
+    public byte[] compile()
+        throws IOException, RecognitionException, DexOverflowException, ExecutionException {
       return Smali.compile(build());
     }
 
@@ -425,7 +434,7 @@
   protected DexApplication buildApplication(SmaliBuilder builder, InternalOptions options) {
     try {
       return buildApplication(AndroidApp.fromDexProgramData(builder.compile()), options);
-    } catch (IOException | RecognitionException e) {
+    } catch (IOException | RecognitionException | ExecutionException | DexOverflowException e) {
       throw new RuntimeException(e);
     }
   }
@@ -438,7 +447,7 @@
           .addLibraryFiles(FilteredClassPath.unfiltered(ToolHelper.getDefaultAndroidJar()))
           .build();
       return buildApplication(input, options);
-    } catch (IOException | RecognitionException e) {
+    } catch (IOException | RecognitionException | ExecutionException | DexOverflowException e) {
       throw new RuntimeException(e);
     }
   }
diff --git a/src/test/java/com/android/tools/r8/utils/Smali.java b/src/test/java/com/android/tools/r8/utils/Smali.java
index fbc5e16..93edf4d 100644
--- a/src/test/java/com/android/tools/r8/utils/Smali.java
+++ b/src/test/java/com/android/tools/r8/utils/Smali.java
@@ -3,12 +3,20 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.dex.ApplicationWriter;
+import com.android.tools.r8.errors.DexOverflowException;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.naming.NamingLens;
 import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
 import java.io.IOException;
 import java.io.Reader;
 import java.io.StringReader;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 import org.antlr.runtime.CommonTokenStream;
 import org.antlr.runtime.RecognitionException;
 import org.antlr.runtime.TokenSource;
@@ -25,26 +33,28 @@
 // Adapted from org.jf.smali.SmaliTestUtils.
 public class Smali {
 
-  public static byte[] compile(String smaliText) throws RecognitionException, IOException {
+  public static byte[] compile(String smaliText)
+      throws RecognitionException, IOException, DexOverflowException, ExecutionException {
     return compile(smaliText, 15);
   }
 
-  public static byte[] compile(String... smaliText) throws RecognitionException, IOException {
+  public static byte[] compile(String... smaliText)
+      throws RecognitionException, IOException, DexOverflowException, ExecutionException {
     return compile(Arrays.asList(smaliText), 15);
   }
 
   public static byte[] compile(String smaliText, int apiLevel)
-      throws IOException, RecognitionException {
+      throws IOException, RecognitionException, DexOverflowException, ExecutionException {
     return compile(ImmutableList.of(smaliText), apiLevel);
   }
 
   public static byte[] compile(List<String> smaliTexts)
-      throws RecognitionException, IOException {
+      throws RecognitionException, IOException, DexOverflowException, ExecutionException {
     return compile(smaliTexts, 15);
   }
 
   public static byte[] compile(List<String> smaliTexts, int apiLevel)
-      throws RecognitionException, IOException {
+      throws RecognitionException, IOException, ExecutionException, DexOverflowException {
     DexBuilder dexBuilder = new DexBuilder(Opcodes.forApi(apiLevel));
 
     for (String smaliText : smaliTexts) {
@@ -85,8 +95,21 @@
 
     dexBuilder.writeTo(dataStore);
 
-    // TODO(sgjesse): This returns the full backingstore from MemoryDataStore, which by default
-    // is 1024k bytes. Our dex file reader does not complain though.
-    return dataStore.getData();
+    // This returns the full backingstore from MemoryDataStore, which by default is 1024k bytes.
+    // We process it via our reader and writer to trim it to the exact size and update its checksum.
+    byte[] data = dataStore.getData();
+    AndroidApp app = AndroidApp.fromDexProgramData(data);
+    InternalOptions options = new InternalOptions();
+    ExecutorService executor = ThreadUtils.getExecutorService(1);
+    try {
+      DexApplication dexApp = new ApplicationReader(
+          app, options, new Timing("smali")).read(executor);
+      ApplicationWriter writer = new ApplicationWriter(
+          dexApp, null, options, null, null, NamingLens.getIdentityLens(), null);
+      AndroidApp trimmed = writer.write(null, executor);
+      return ByteStreams.toByteArray(trimmed.getDexProgramResources().get(0).getStream());
+    } finally {
+      executor.shutdown();
+    }
   }
 }