Add a test for JaCoCo using CONSTANT_Dynamic for class file version 55

Bug: 178172809
Change-Id: I914f3b4edf49f196fc53a761a37f004e6ff55c7a
diff --git a/src/test/java/com/android/tools/r8/D8TestCompileResult.java b/src/test/java/com/android/tools/r8/D8TestCompileResult.java
index 6d7634c..3908d64 100644
--- a/src/test/java/com/android/tools/r8/D8TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/D8TestCompileResult.java
@@ -5,7 +5,10 @@
 
 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;
@@ -54,4 +57,13 @@
   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/JvmTestBuilder.java b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
index 9951a04..f92a490 100644
--- a/src/test/java/com/android/tools/r8/JvmTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
@@ -158,6 +158,23 @@
     return self();
   }
 
+  public JvmTestBuilder enableJaCoCoAgentForOfflineInstrumentedCode(Path jacocoAgent, Path output) {
+    addProgramFiles(jacocoAgent);
+    addVmArguments(
+        "-Djacoco-agent.destfile=" + output.toString(),
+        "-Djacoco-agent.dumponexit=true",
+        "-Djacoco-agent.output=file");
+    return self();
+  }
+
+  public JvmTestBuilder enableJaCoCoAgent(Path jacocoAgent, Path output) {
+    addProgramFiles(jacocoAgent);
+    addVmArguments(
+        String.format(
+            "-javaagent:%s=destfile=%s,dumponexit=true,output=file", jacocoAgent, output));
+    return self();
+  }
+
   public JvmTestBuilder addVmArguments(Collection<String> arguments) {
     vmArguments.addAll(arguments);
     return self();
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 54e3139..a1bdeb8 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -151,8 +151,9 @@
   private static final String PROGUARD5_2_1 = "third_party/proguard/proguard5.2.1/bin/proguard";
   private static final String PROGUARD6_0_1 = "third_party/proguard/proguard6.0.1/bin/proguard";
   private static final String PROGUARD = PROGUARD5_2_1;
-  public static final String JACOCO_AGENT = "third_party/jacoco/0.8.6/lib/jacocoagent.jar";
-  public static final String JACOCO_CLI = "third_party/jacoco/0.8.6/lib/jacococli.jar";
+  public static final Path JACOCO_ROOT = Paths.get("third_party", "jacoco", "0.8.6");
+  public static final Path JACOCO_AGENT = JACOCO_ROOT.resolve(Paths.get("lib", "jacocoagent.jar"));
+  public static final Path JACOCO_CLI = JACOCO_ROOT.resolve(Paths.get("lib", "jacococli.jar"));
   public static final String PROGUARD_SETTINGS_FOR_INTERNAL_APPS = "third_party/proguardsettings/";
 
   private static final String RETRACE6_0_1 = "third_party/proguard/proguard6.0.1/bin/retrace";
@@ -1592,6 +1593,36 @@
     return processResult;
   }
 
+  public static ProcessResult runJaCoCoInstrument(Path sourceClassFiles, Path outputDirectory)
+      throws IOException {
+    List<String> cmdline = new ArrayList<>();
+    cmdline.add(TestRuntime.getSystemRuntime().asCf().getJavaExecutable().toString());
+    cmdline.add("-jar");
+    cmdline.add(ToolHelper.JACOCO_CLI.toString());
+    cmdline.add("instrument");
+    cmdline.add(sourceClassFiles.toString());
+    cmdline.add("--dest");
+    cmdline.add(outputDirectory.toString());
+    ProcessBuilder builder = new ProcessBuilder(cmdline);
+    return ToolHelper.runProcess(builder);
+  }
+
+  public static ProcessResult runJaCoCoReport(Path classfiles, Path jacocoExec, Path reportFile)
+      throws IOException {
+    List<String> cmdline = new ArrayList<>();
+    cmdline.add(TestRuntime.getSystemRuntime().asCf().getJavaExecutable().toString());
+    cmdline.add("-jar");
+    cmdline.add(ToolHelper.JACOCO_CLI.toString());
+    cmdline.add("report");
+    cmdline.add(jacocoExec.toString());
+    cmdline.add("--classfiles");
+    cmdline.add(classfiles.toString());
+    cmdline.add("--csv");
+    cmdline.add(reportFile.toString());
+    ProcessBuilder builder = new ProcessBuilder(cmdline);
+    return ToolHelper.runProcess(builder);
+  }
+
   private static Path findNonConflictingDestinationFilePath(Path testOutputPath) {
     int index = 0;
     Path destFilePath;
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/JacocoConstantDynamicTest.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/JacocoConstantDynamicTest.java
new file mode 100644
index 0000000..ee5142f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/JacocoConstantDynamicTest.java
@@ -0,0 +1,238 @@
+// 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.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticOrigin;
+import static com.android.tools.r8.utils.DescriptorUtils.JAVA_PACKAGE_SEPARATOR;
+import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
+import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+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;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ZipUtils;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import org.junit.Before;
+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 JacocoConstantDynamicTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public boolean useConstantDynamic;
+
+  @Parameters(name = "{0}, useConstantDynamic: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build(),
+        BooleanUtils.values());
+  }
+
+  public static JacocoClasses testClassesNoConstantDynamic;
+  public static JacocoClasses testClassesConstantDynamic;
+
+  public JacocoClasses testClasses;
+
+  private static final String MAIN_CLASS = TestRunner.class.getTypeName();
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("Hello, world!");
+
+  @BeforeClass
+  public static void setUpInput() throws IOException {
+    testClassesNoConstantDynamic = testClasses(getStaticTemp(), CfVersion.V1_8);
+    testClassesConstantDynamic = testClasses(getStaticTemp(), CfVersion.V11);
+  }
+
+  @Before
+  public void setUp() throws IOException {
+    testClasses = useConstantDynamic ? testClassesConstantDynamic : testClassesNoConstantDynamic;
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(
+        parameters.isCfRuntime()
+            && (parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11)
+                || !useConstantDynamic));
+
+    // Run non-instrumented code.
+    testForRuntime(parameters)
+        .addProgramFiles(testClasses.getOriginal())
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+
+    // Run instrumented code without an agent.
+    testForRuntime(parameters)
+        .addProgramFiles(testClasses.getInstrumented())
+        .addProgramFiles(ToolHelper.JACOCO_AGENT)
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+
+    // 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);
+    List<String> onTheFlyReport = testClasses.generateReport(agentOutputOnTheFly);
+    assertEquals(2, onTheFlyReport.size());
+
+    // Run the instrumented code with offline instrumentation turned on.
+    Path agentOutputOffline = output.resolve("offline");
+    testForJvm()
+        .addProgramFiles(testClasses.getInstrumented())
+        .enableJaCoCoAgentForOfflineInstrumentedCode(ToolHelper.JACOCO_AGENT, agentOutputOffline)
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+    List<String> offlineReport = testClasses.generateReport(agentOutputOffline);
+    assertEquals(onTheFlyReport, offlineReport);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.getRuntime().isDex());
+    if (!useConstantDynamic) {
+      Path output = temp.newFolder().toPath();
+      Path agentOutput = output.resolve("jacoco.exec");
+      testForD8(parameters.getBackend())
+          .addProgramFiles(testClasses.getInstrumented())
+          .addProgramFiles(ToolHelper.JACOCO_AGENT)
+          .setMinApi(parameters.getApiLevel())
+          .compile()
+          .runWithJaCoCo(agentOutput, parameters.getRuntime(), MAIN_CLASS)
+          .assertSuccessWithOutput(EXPECTED_OUTPUT);
+      // TODO(sgjesse): Need to figure out why there is no instrumentation output for newer VMs.
+      if (parameters.getRuntime().asDex().getVm().isOlderThanOrEqual(DexVm.ART_4_4_4_HOST)) {
+        List<String> report = testClasses.generateReport(agentOutput);
+        assertEquals(2, report.size());
+      } else {
+        assertFalse(Files.exists(agentOutput));
+      }
+    } else {
+      assertThrows(
+          CompilationFailedException.class,
+          () ->
+              testForD8(parameters.getBackend())
+                  .addProgramFiles(testClasses.getInstrumented())
+                  .addProgramFiles(ToolHelper.JACOCO_AGENT)
+                  .setMinApi(parameters.getApiLevel())
+                  .compileWithExpectedDiagnostics(
+                      diagnostics -> {
+                        // Check that the error is reported as an error to the diagnostics handler.
+                        diagnostics.assertErrorsCount(1);
+                        diagnostics.assertAllErrorsMatch(
+                            allOf(
+                                diagnosticMessage(containsString("Unsupported dynamic constant")),
+                                // The fatal error is not given an origin, so it can't provide it.
+                                // Note: This could be fixed by delaying reporting and associate the
+                                // info
+                                //  at the top-level handler. It would require mangling of the
+                                // diagnostic,
+                                //  so maybe not that elegant.
+                                diagnosticOrigin(Origin.unknown())));
+                        diagnostics.assertWarningsCount(0);
+                        diagnostics.assertInfosCount(0);
+                      }));
+    }
+  }
+
+  private static JacocoClasses testClasses(TemporaryFolder temp, CfVersion version)
+      throws IOException {
+    return new JacocoClasses(
+        transformer(TestRunner.class)
+            .setVersion(version) /*.setClassDescriptor("LTestRunner;")*/
+            .transform(),
+        temp);
+  }
+
+  // Two sets of class files with and without JaCoCo off line instrumentation.
+  private static class JacocoClasses {
+    private final Path dir;
+
+    private final Path originalJar;
+    private final Path instrumentedJar;
+
+    // Create JacocoClasses with just one class provided as bytes.
+    private JacocoClasses(byte[] clazz, TemporaryFolder temp) throws IOException {
+      dir = temp.newFolder().toPath();
+
+      // Write the class to a .class file with package sub-directories.
+      String typeName = extractClassName(clazz);
+      int lastDotIndex = typeName.lastIndexOf('.');
+      String pkg = typeName.substring(0, lastDotIndex);
+      String baseFileName = typeName.substring(lastDotIndex + 1) + CLASS_EXTENSION;
+      Path original = dir.resolve("original");
+      Files.createDirectories(original);
+      Path packageDir = original.resolve(pkg.replace(JAVA_PACKAGE_SEPARATOR, File.separatorChar));
+      Files.createDirectories(packageDir);
+      Path classFile = packageDir.resolve(baseFileName);
+      Files.write(classFile, clazz);
+
+      // Run offline instrumentation.
+      Path instrumented = dir.resolve("instrumented");
+      Files.createDirectories(instrumented);
+      runJacocoInstrumentation(original, instrumented);
+      originalJar = dir.resolve("original" + JAR_EXTENSION);
+      ZipUtils.zip(originalJar, original);
+      instrumentedJar = dir.resolve("instrumented" + JAR_EXTENSION);
+      ZipUtils.zip(instrumentedJar, instrumented);
+    }
+
+    public Path getOriginal() {
+      return originalJar;
+    }
+
+    public Path getInstrumented() {
+      return instrumentedJar;
+    }
+
+    public List<String> generateReport(Path jacocoExec) throws IOException {
+      Path report = dir.resolve("report.scv");
+      ProcessResult result = ToolHelper.runJaCoCoReport(originalJar, jacocoExec, report);
+      assertEquals(result.toString(), 0, result.exitCode);
+      return Files.readAllLines(report);
+    }
+
+    private void runJacocoInstrumentation(Path input, Path outdir) throws IOException {
+      ProcessResult result = ToolHelper.runJaCoCoInstrument(input, outdir);
+      assertEquals(result.toString(), 0, result.exitCode);
+    }
+  }
+
+  static class TestRunner {
+
+    public static void main(String[] args) {
+      System.out.println("Hello, world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/assertions/AssertionsConfigurationJacocoTest.java b/src/test/java/com/android/tools/r8/rewrite/assertions/AssertionsConfigurationJacocoTest.java
index 11829f6..f860ca2 100644
--- a/src/test/java/com/android/tools/r8/rewrite/assertions/AssertionsConfigurationJacocoTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/assertions/AssertionsConfigurationJacocoTest.java
@@ -100,7 +100,7 @@
     List<String> cmdline = new ArrayList<>();
     cmdline.add(TestRuntime.getSystemRuntime().asCf().getJavaExecutable().toString());
     cmdline.add("-jar");
-    cmdline.add(ToolHelper.JACOCO_CLI);
+    cmdline.add(ToolHelper.JACOCO_CLI.toString());
     cmdline.add("instrument");
     cmdline.add(input.toString());
     cmdline.add("--dest");