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);