Reproduce CONSTANT_Dynamic desugaring issue with getDeclaredMethods

The first argument of a CONSTANT_Dynamic bootstrap method has
parameter type java.lang.invoke.MethodHandles.Lookup. That type
is not defined on Android with API levels below 26. Desugaring
of CONSTANT_Dynamic leaves the bootstrap method in the program
causing getDeclaredMethods on a class which had a CONSTANT_Dynamic
bootstrap method to fail with ClassNotFoundException.

Bug: 210485236
Change-Id: I0527ed10d1255a3f23da99f847f75bc14313b7ba
diff --git a/src/test/java/com/android/tools/r8/D8TestCompileResult.java b/src/test/java/com/android/tools/r8/D8TestCompileResult.java
index 3908d64..6d7634c 100644
--- a/src/test/java/com/android/tools/r8/D8TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/D8TestCompileResult.java
@@ -5,10 +5,7 @@
 
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.utils.AndroidApp;
-import java.io.IOException;
-import java.nio.file.Path;
 import java.util.Set;
-import java.util.concurrent.ExecutionException;
 
 public class D8TestCompileResult extends TestCompileResult<D8TestCompileResult, D8TestRunResult> {
   private final String proguardMap;
@@ -57,13 +54,4 @@
   public D8TestRunResult createRunResult(TestRuntime runtime, ProcessResult result) {
     return new D8TestRunResult(app, runtime, result, proguardMap);
   }
-
-  public D8TestRunResult runWithJaCoCo(
-      Path output, TestRuntime runtime, String mainClass, String... args)
-      throws IOException, ExecutionException {
-    setSystemProperty("jacoco-agent.destfile", output.toString());
-    setSystemProperty("jacoco-agent.dumponexit", "true");
-    setSystemProperty("jacoco-agent.output", "file");
-    return run(runtime, mainClass, args);
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java
index ebf30b6..5b6996d 100644
--- a/src/test/java/com/android/tools/r8/TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -217,6 +217,14 @@
         ObjectArrays.concat(mainClassSubject.getFinalName(), args));
   }
 
+  public RR runWithJaCoCo(Path output, TestRuntime runtime, String mainClass, String... args)
+      throws IOException, ExecutionException {
+    setSystemProperty("jacoco-agent.destfile", output.toString());
+    setSystemProperty("jacoco-agent.dumponexit", "true");
+    setSystemProperty("jacoco-agent.output", "file");
+    return run(runtime, mainClass, args);
+  }
+
   public CR addRunClasspathFiles(Path... classpath) {
     return addRunClasspathFiles(Arrays.asList(classpath));
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicGetDeclaredMethodsTest.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicGetDeclaredMethodsTest.java
new file mode 100644
index 0000000..de9685e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/ConstantDynamicGetDeclaredMethodsTest.java
@@ -0,0 +1,154 @@
+// Copyright (c) 2021, 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.desugar.constantdynamic;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.DesugarTestConfiguration;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Method;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ConstantDynamicGetDeclaredMethodsTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build());
+  }
+
+  private static final String EXPECTED_OUTPUT =
+      StringUtils.lines(
+          "Hello, world!",
+          "myConstant",
+          "3",
+          "java.lang.invoke.MethodHandles$Lookup",
+          "java.lang.String",
+          "java.lang.Class");
+  private static final String EXPECTED_OUTPUT_R8 =
+      StringUtils.lines("Hello, world!", "No myConstant method");
+
+  private static final Class<?> MAIN_CLASS = A.class;
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    assumeTrue(parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11));
+    assumeTrue(parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+
+    testForJvm()
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testDesugaring() throws Exception {
+    testForDesugaring(parameters)
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        // TODO(b/210485236): This should never fail.
+        .applyIf(
+            // When not desugaring the CF code requires JDK 11.
+            DesugarTestConfiguration::isNotDesugared,
+            r -> {
+              if (parameters.isCfRuntime()
+                  && parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11)) {
+                r.assertSuccessWithOutput(EXPECTED_OUTPUT);
+              } else {
+                r.assertFailureWithErrorThatThrows(UnsupportedClassVersionError.class);
+              }
+            },
+            c ->
+                DesugarTestConfiguration.isDesugared(c)
+                    && parameters.isDexRuntime()
+                    && parameters.asDexRuntime().getVersion().isOlderThan(Version.V8_1_0),
+            r -> r.assertFailureWithErrorThatThrows(ClassNotFoundException.class),
+            r -> r.assertSuccessWithOutput(EXPECTED_OUTPUT));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getTransformedClasses())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(MAIN_CLASS)
+        .applyIf(
+            parameters.getApiLevel().isLessThan(AndroidApiLevel.O),
+            b -> b.addDontWarn(MethodHandles.Lookup.class))
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT_R8);
+  }
+
+  @Test
+  public void testR8KeepBootstrapMethod() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getTransformedClasses())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(MAIN_CLASS)
+        .addKeepMethodRules(MAIN_CLASS, "myConstant(...)")
+        .applyIf(
+            parameters.getApiLevel().isLessThan(AndroidApiLevel.O),
+            b -> b.addDontWarn(MethodHandles.Lookup.class))
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        // TODO(b/210485236): This should never fail.
+        .applyIf(
+            parameters.getDexRuntimeVersion().isOlderThan(Version.V8_1_0),
+            b -> b.assertFailureWithErrorThatThrows(ClassNotFoundException.class),
+            b -> b.assertSuccessWithOutput(EXPECTED_OUTPUT));
+  }
+
+  private byte[] getTransformedClasses() throws IOException {
+    return transformer(A.class)
+        .setVersion(CfVersion.V11)
+        .transformConstStringToConstantDynamic(
+            "condy", A.class, "myConstant", "constantName", Object.class)
+        .transform();
+  }
+
+  public static class A {
+
+    public static Object f() {
+      return "condy"; // Will be transformed to Constant_DYNAMIC.
+    }
+
+    public static void main(String[] args) {
+      System.out.println(f());
+      Method[] methods = A.class.getDeclaredMethods();
+      for (Method method : methods) {
+        if (method.getName().equals("myConstant")) {
+          System.out.println(method.getName());
+          System.out.println(method.getParameterTypes().length);
+          for (int j = 0; j < method.getParameterTypes().length; j++) {
+            System.out.println(method.getParameterTypes()[j].getName());
+          }
+          return;
+        }
+      }
+      System.out.println("No myConstant method");
+    }
+
+    private static Object myConstant(MethodHandles.Lookup lookup, String name, Class<?> type) {
+      return "Hello, world!";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/JacocoConstantDynamicGetDeclaredMethods.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/JacocoConstantDynamicGetDeclaredMethods.java
new file mode 100644
index 0000000..79fc834
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/JacocoConstantDynamicGetDeclaredMethods.java
@@ -0,0 +1,193 @@
+// Copyright (c) 2021, 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.desugar.constantdynamic;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.jacoco.JacocoClasses;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Method;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class JacocoConstantDynamicGetDeclaredMethods extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
+  }
+
+  public static final String jacocoBootstrapMethodName = "$jacocoInit";
+
+  public static JacocoClasses testClasses;
+
+  private static final String MAIN_CLASS = TestRunner.class.getTypeName();
+  private static final String EXPECTED_OUTPUT =
+      StringUtils.lines(
+          jacocoBootstrapMethodName,
+          "3",
+          "java.lang.invoke.MethodHandles$Lookup",
+          "java.lang.String",
+          "java.lang.Class");
+
+  @BeforeClass
+  public static void setUpInput() throws IOException {
+    testClasses = testClasses(getStaticTemp());
+  }
+
+  private void checkJacocoReport(Path agentOutput) throws IOException {
+    // TODO(sgjesse): Need to figure out why there is no instrumentation output for newer VMs.
+    if (parameters.isCfRuntime()
+        || parameters.getRuntime().asDex().getVm().isOlderThanOrEqual(DexVm.ART_4_4_4_HOST)) {
+      assertEquals(2, testClasses.generateReport(agentOutput).size());
+    } else {
+      assertFalse(Files.exists(agentOutput));
+    }
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(
+        parameters.isCfRuntime()
+            && (parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11)));
+
+    // Run non-instrumented code with an agent causing on the fly instrumentation on the JVM.
+    Path output = temp.newFolder().toPath();
+    Path agentOutputOnTheFly = output.resolve("on-the-fly");
+    testForJvm()
+        .addProgramFiles(testClasses.getOriginal())
+        .enableJaCoCoAgent(ToolHelper.JACOCO_AGENT, agentOutputOnTheFly)
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+    checkJacocoReport(agentOutputOnTheFly);
+
+    // Run the instrumented code.
+    Path agentOutputOffline = output.resolve("offline");
+    testForJvm()
+        .addProgramFiles(testClasses.getInstrumented())
+        .configureJaCoCoAgentForOfflineInstrumentedCode(ToolHelper.JACOCO_AGENT, agentOutputOffline)
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+    checkJacocoReport(agentOutputOffline);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.getRuntime().isDex());
+    Path agentOutput = temp.newFolder().toPath().resolve("jacoco.exec");
+    testForD8(parameters.getBackend())
+        .addProgramFiles(testClasses.getInstrumented())
+        .addProgramFiles(ToolHelper.JACOCO_AGENT)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .runWithJaCoCo(agentOutput, parameters.getRuntime(), MAIN_CLASS)
+        // TODO(b/210485236): This should never fail.
+        .applyIf(
+            parameters.getDexRuntimeVersion().isOlderThan(Version.V8_1_0),
+            b -> b.assertFailureWithErrorThatThrows(ClassNotFoundException.class),
+            b -> b.assertSuccessWithOutput(EXPECTED_OUTPUT));
+    checkJacocoReport(agentOutput);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeTrue(parameters.getRuntime().isDex());
+    Path agentOutput = temp.newFolder().toPath().resolve("jacoco.exec");
+    testForR8(parameters.getBackend())
+        .addProgramFiles(testClasses.getInstrumented())
+        .addProgramFiles(ToolHelper.JACOCO_AGENT)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRules(TestRunner.class)
+        .applyIf(
+            parameters.getApiLevel().isLessThan(AndroidApiLevel.O),
+            b -> b.addDontWarn(MethodHandles.Lookup.class))
+        .addDontWarn("java.lang.management.ManagementFactory", "javax.management.**")
+        .compile()
+        .runWithJaCoCo(agentOutput, parameters.getRuntime(), MAIN_CLASS)
+        .inspect(
+            inspector -> {
+              assertThat(
+                  inspector.clazz(TestRunner.class).uniqueMethodWithName(jacocoBootstrapMethodName),
+                  isAbsent());
+            })
+        .assertSuccessWithOutputLines("No " + jacocoBootstrapMethodName + " method");
+    checkJacocoReport(agentOutput);
+  }
+
+  @Test
+  public void testR8KeepingJacocoInit() throws Exception {
+    assumeTrue(parameters.getRuntime().isDex());
+    Path agentOutput = temp.newFolder().toPath().resolve("jacoco.exec");
+    testForR8(parameters.getBackend())
+        .addProgramFiles(testClasses.getInstrumented())
+        .addProgramFiles(ToolHelper.JACOCO_AGENT)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRules(TestRunner.class)
+        .addKeepRules("-keep class ** { *** " + jacocoBootstrapMethodName + "(...); }")
+        .applyIf(
+            parameters.getApiLevel().isLessThan(AndroidApiLevel.O),
+            b -> b.addDontWarn(MethodHandles.Lookup.class))
+        .addDontWarn(
+            "java.lang.instrument.ClassFileTransformer",
+            "java.lang.management.ManagementFactory",
+            "javax.management.**")
+        .compile()
+        .runWithJaCoCo(agentOutput, parameters.getRuntime(), MAIN_CLASS)
+        // TODO(b/210485236): This should never fail.
+        .applyIf(
+            parameters.getDexRuntimeVersion().isOlderThan(Version.V8_1_0),
+            b -> b.assertFailureWithErrorThatThrows(ClassNotFoundException.class),
+            b -> b.assertSuccessWithOutput(EXPECTED_OUTPUT));
+    checkJacocoReport(agentOutput);
+  }
+
+  private static JacocoClasses testClasses(TemporaryFolder temp) throws IOException {
+    return new JacocoClasses(
+        transformer(TestRunner.class).setVersion(CfVersion.V11).transform(), temp);
+  }
+
+  static class TestRunner {
+
+    public static void main(String[] args) {
+      Method[] methods = TestRunner.class.getDeclaredMethods();
+      for (Method method : methods) {
+        if (method.getName().equals(jacocoBootstrapMethodName)) {
+          System.out.println(method.getName());
+          System.out.println(method.getParameterTypes().length);
+          for (int j = 0; j < method.getParameterTypes().length; j++) {
+            System.out.println(method.getParameterTypes()[j].getName());
+          }
+          return;
+        }
+      }
+      System.out.println("No " + jacocoBootstrapMethodName + " method");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/jacoco/JacocoClasses.java b/src/test/java/com/android/tools/r8/jacoco/JacocoClasses.java
index 0149669..b98cc63 100644
--- a/src/test/java/com/android/tools/r8/jacoco/JacocoClasses.java
+++ b/src/test/java/com/android/tools/r8/jacoco/JacocoClasses.java
@@ -22,6 +22,7 @@
 // Two sets of class files with and without JaCoCo offline instrumentation.
 public class JacocoClasses {
 
+  private final TemporaryFolder temp;
   private final Path dir;
 
   private final Path originalJar;
@@ -29,6 +30,7 @@
 
   // Create JacocoClasses with just one class provided as bytes.
   public JacocoClasses(byte[] clazz, TemporaryFolder temp) throws IOException {
+    this.temp = temp;
     dir = temp.newFolder().toPath();
 
     // Write the class to a .class file with package sub-directories.
@@ -62,7 +64,7 @@
   }
 
   public List<String> generateReport(Path jacocoExec) throws IOException {
-    Path report = dir.resolve("report.scv");
+    Path report = temp.newFolder().toPath().resolve("report.scv");
     ProcessResult result = ToolHelper.runJaCoCoReport(originalJar, jacocoExec, report);
     assertEquals(result.toString(), 0, result.exitCode);
     return Files.readAllLines(report);