Debug tests for vertical class merger

When a class pkg.A is merged with a class pkg.B, and both classes have a member named m, then pkg.A.m will be renamed to pkg.B.m$pkg$A.

This CL extends the existing tests for vertical class merging by running the tests with a debugger and checking that we never encounter a method with the name pkg.B.m$pkg$A. In particular, the tests check that there are no methods containing the string "$classmerging$" (this suffices since all tests are in the package named "classmerging").

Change-Id: Ie200afce849080f1808336c79284738b4dbfd893
diff --git a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
index d9d62c3..6322840 100644
--- a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
@@ -3,9 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.classmerging;
 
+import static com.android.tools.r8.ir.desugar.InterfaceMethodRewriter.COMPANION_CLASS_NAME_SUFFIX;
 import static com.android.tools.r8.smali.SmaliBuilder.buildCode;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThat;
@@ -22,6 +24,10 @@
 import com.android.tools.r8.VmTestRunner.IgnoreForRangeOfVmVersions;
 import com.android.tools.r8.code.Instruction;
 import com.android.tools.r8.code.MoveException;
+import com.android.tools.r8.debug.DebugTestBase;
+import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.Command;
+import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.DebuggeeState;
+import com.android.tools.r8.debug.DexDebugTestConfig;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.jasmin.JasminBuilder;
@@ -166,7 +172,9 @@
               configure(options);
               // Avoid that direct methods in B get inlined.
               options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
-            });
+            },
+            // Disable debug testing since the test has a method with "$classmerging$" in the name.
+            null);
 
     ClassSubject clazzSubject = inspector.clazz("classmerging.ConflictInGeneratedNameTest$B");
     assertThat(clazzSubject, isPresent());
@@ -1008,6 +1016,23 @@
       String proguardConfig,
       Consumer<InternalOptions> optionsConsumer)
       throws Throwable {
+    return runTestOnInput(
+        main,
+        input,
+        preservedClassNames,
+        proguardConfig,
+        optionsConsumer,
+        new VerticalClassMergerDebugTest(main));
+  }
+
+  private CodeInspector runTestOnInput(
+      String main,
+      AndroidApp input,
+      Predicate<String> preservedClassNames,
+      String proguardConfig,
+      Consumer<InternalOptions> optionsConsumer,
+      VerticalClassMergerDebugTest debugTestRunner)
+      throws Throwable {
     Path proguardMapPath = File.createTempFile("mapping", ".txt", temp.getRoot()).toPath();
     R8Command.Builder commandBuilder =
         ToolHelper.prepareR8CommandBuilder(input)
@@ -1034,6 +1059,11 @@
     }
     // Check that the R8-generated code produces the same result as D8-generated code.
     assertEquals(runOnArt(compileWithD8(input), main), runOnArt(output, main));
+    // Check that we never come across a method that has a name with "$classmerging$" in it during
+    // debugging.
+    if (debugTestRunner != null) {
+      debugTestRunner.test(output, proguardMapPath);
+    }
     return outputInspector;
   }
 
@@ -1049,4 +1079,82 @@
     }
     return builder.toString();
   }
+
+  private class VerticalClassMergerDebugTest extends DebugTestBase {
+
+    private final String main;
+    private DebugTestRunner runner = null;
+
+    public VerticalClassMergerDebugTest(String main) {
+      this.main = main;
+    }
+
+    public void test(AndroidApp app, Path proguardMapPath) throws Throwable {
+      Path appPath =
+          File.createTempFile("app", ".zip", ClassMergingTest.this.temp.getRoot()).toPath();
+      app.writeToZip(appPath, OutputMode.DexIndexed);
+
+      DexDebugTestConfig config = new DexDebugTestConfig(appPath);
+      config.allowUnprocessedCommands();
+      config.setProguardMap(proguardMapPath);
+
+      this.runner =
+          getDebugTestRunner(
+              config, main, breakpoint(main, "main"), run(), stepIntoUntilNoLongerInApp());
+      this.runner.runBare();
+    }
+
+    private void checkState(DebuggeeState state) {
+      // If a class pkg.A is merged into pkg.B, and a method pkg.A.m() needs to be renamed, then
+      // it will be renamed to pkg.B.m$pkg$A(). Since all tests are in the package "classmerging",
+      // we check that no methods in the debugging state (i.e., after the Proguard map has been
+      // applied) contain "$classmerging$.
+      String qualifiedMethodSignature =
+          state.getClassSignature() + "->" + state.getMethodName() + state.getMethodSignature();
+      boolean holderIsCompanionClass = state.getClassName().endsWith(COMPANION_CLASS_NAME_SUFFIX);
+      if (!holderIsCompanionClass) {
+        assertThat(qualifiedMethodSignature, not(containsString("$classmerging$")));
+      }
+    }
+
+    // Keeps stepping in until it is no longer in a class from the classmerging package.
+    // Then starts stepping out until it is again in the classmerging package.
+    private Command stepIntoUntilNoLongerInApp() {
+      return stepUntil(
+          StepKind.INTO,
+          StepLevel.INSTRUCTION,
+          state -> {
+            if (state.getClassSignature().contains("classmerging")) {
+              checkState(state);
+
+              // Continue stepping into.
+              return false;
+            }
+
+            // Stop stepping into.
+            runner.enqueueCommandFirst(stepOutUntilInApp());
+            return true;
+          });
+    }
+
+    // Keeps stepping out until it is in a class from the classmerging package.
+    // Then starts stepping in until it is no longer in the classmerging package.
+    private Command stepOutUntilInApp() {
+      return stepUntil(
+          StepKind.OUT,
+          StepLevel.INSTRUCTION,
+          state -> {
+            if (state.getClassSignature().contains("classmerging")) {
+              checkState(state);
+
+              // Stop stepping out.
+              runner.enqueueCommandFirst(stepIntoUntilNoLongerInApp());
+              return true;
+            }
+
+            // Continue stepping out.
+            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 97f6820..df2c3e0 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -137,6 +137,18 @@
   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 {
     // Skip test due to unsupported runtime.
     Assume.assumeTrue("Skipping test " + testName.getMethodName() + " because ART is not supported",
         ToolHelper.artSupported());
@@ -152,7 +164,7 @@
             ? null
             : ClassNameMapper.mapperFromFile(config.getProguardMap());
 
-    new JUnit3Wrapper(config, debuggeeClass, commands, classNameMapper).runBare();
+    return new JUnit3Wrapper(config, debuggeeClass, commands, classNameMapper);
   }
 
   /**
@@ -495,8 +507,14 @@
     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.")
-  static class JUnit3Wrapper extends JDWPTestCase {
+  public static class JUnit3Wrapper extends JDWPTestCase implements DebugTestRunner {
 
     private final String debuggeeClassName;
 
@@ -801,8 +819,10 @@
         // Continue stepping until mainLoopStep exits with false.
       }
 
-      assertTrue(
-          "All commands have NOT been processed for config: " + config, commandsQueue.isEmpty());
+      if (config.mustProcessAllCommands()) {
+        assertTrue(
+            "All commands have NOT been processed for config: " + config, commandsQueue.isEmpty());
+      }
 
       logWriter.println("Finish loop");
     }
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestConfig.java b/src/test/java/com/android/tools/r8/debug/DebugTestConfig.java
index 556e17f..519b580 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestConfig.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestConfig.java
@@ -17,6 +17,7 @@
     DEX
   }
 
+  private boolean mustProcessAllCommands = true;
   private List<Path> paths = new ArrayList<>();
 
   private Path proguardMap = null;
@@ -47,6 +48,14 @@
     return this;
   }
 
+  public boolean mustProcessAllCommands() {
+    return mustProcessAllCommands;
+  }
+
+  public void allowUnprocessedCommands() {
+    mustProcessAllCommands = false;
+  }
+
   /** Proguard map that the debuggee has been translated according to, null if not present. */
   public Path getProguardMap() {
     return proguardMap;