Add support for building java 15 examples

This also adds the first JDK 15 example of using records.

Bug: 169692487
Bug: 141587937
Change-Id: I3ed7800dc16b9f08510d5bd77b658d5891b98707
diff --git a/build.gradle b/build.gradle
index be4d51e..197a5e5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -135,6 +135,11 @@
             srcDirs = ['src/test/examplesJava11']
         }
     }
+    examplesJava15 {
+        java {
+            srcDirs = ['src/test/examplesJava15']
+        }
+    }
     jdk11TimeTests {
         java {
             srcDirs = [
@@ -287,7 +292,7 @@
 def r8DesugaredPath = "$buildDir/libs/r8desugared.jar"
 def r8LibGeneratedKeepRulesPath = "$buildDir/generated/keep.txt"
 def r8LibTestPath = "$buildDir/classes/r8libtest"
-def java11ClassFiles = "build/classes/java/mainJava11"
+def java11ClassFiles = "$buildDir/classes/java/mainJava11"
 
 def osString = OperatingSystem.current().isLinux() ? "linux" :
         OperatingSystem.current().isMacOsX() ? "mac" : "windows"
@@ -558,21 +563,37 @@
     }
 }
 
-tasks.named(sourceSets.examplesJava9.compileJavaTaskName).get().configure {
-    def jdkDir = 'third_party/openjdk/openjdk-9.0.4/'
-    options.fork = true
-    options.forkOptions.jvmArgs = []
-    if (OperatingSystem.current().isLinux()) {
-        options.forkOptions.javaHome = file(jdkDir + 'linux')
-    } else if (OperatingSystem.current().isMacOsX()) {
-        options.forkOptions.javaHome = file(jdkDir + 'osx')
-    } else {
-        options.forkOptions.javaHome = file(jdkDir + 'windows')
+def setJdkCompilationWithCompatibility(String sourceSet, String javaHome, JavaVersion compatibility, boolean enablePreview) {
+    tasks.named(sourceSet).get().configure {
+        def jdkDir = "third_party/openjdk/${javaHome}/"
+        options.fork = true
+        options.forkOptions.jvmArgs = []
+        if (enablePreview) {
+            options.compilerArgs.add('--enable-preview')
+        }
+        if (OperatingSystem.current().isLinux()) {
+            options.forkOptions.javaHome = file(jdkDir + 'linux')
+        } else if (OperatingSystem.current().isMacOsX()) {
+            options.forkOptions.javaHome = file(jdkDir + 'osx')
+        } else {
+            options.forkOptions.javaHome = file(jdkDir + 'windows')
+        }
+        sourceCompatibility = compatibility
+        targetCompatibility = compatibility
     }
-    sourceCompatibility = JavaVersion.VERSION_1_9
-    targetCompatibility = JavaVersion.VERSION_1_9
 }
 
+setJdkCompilationWithCompatibility(
+        sourceSets.examplesJava9.compileJavaTaskName,
+        'openjdk-9.0.4',
+        JavaVersion.VERSION_1_9,
+        false)
+setJdkCompilationWithCompatibility(
+        sourceSets.examplesJava15.compileJavaTaskName,
+        'jdk-15',
+        JavaVersion.VERSION_15,
+        true)
+
 def setJdk11CompilationWithCompatibility(String sourceSet, JavaVersion compatibility) {
     tasks.named(sourceSet).get().configure {
         def jdkDir = 'third_party/openjdk/jdk-11/'
@@ -717,6 +738,8 @@
 }
 
 task repackageSourcesNew(type: ShadowJar) {
+    // If this fails then remove all generated folders from
+    // build/classes/java/test that is not {com,dalvik}
     from sourceSets.main.output
     mergeServiceFiles(it)
     baseName 'sources_main'
@@ -733,6 +756,7 @@
     return tasks.create("r8Create${name}", ShadowJar) {
         from consolidatedLicense.outputs.files
         from sources
+        exclude "$buildDir/classes/**"
         baseName baseNameName
         classifier = null
         version = null
@@ -1522,53 +1546,28 @@
     }
 }
 
-task buildExampleJava9Jars {
-    def examplesDir = file("src/test/examplesJava9")
-    examplesDir.eachDir { dir ->
-        def name = dir.getName();
-        def exampleOutputDir = file("build/test/examplesJava9");
-        def jarName = "${name}.jar"
-        dependsOn "jar_examplesJava9_${name}"
-        task "jar_examplesJava9_${name}"(type: Jar) {
-            archiveName = jarName
-            destinationDir = exampleOutputDir
-            from sourceSets.examplesJava9.output
-            include "**/" + name + "/**/*.class"
+def buildExampleJarsCreateTask(javaVersion, sourceSet) {
+    return tasks.create("buildExample${javaVersion}Jars") {
+        def examplesDir = file("src/test/examples${javaVersion}")
+        examplesDir.eachDir { dir ->
+            def name = dir.getName();
+            def exampleOutputDir = file("build/test/examples${javaVersion}");
+            def jarName = "${name}.jar"
+            dependsOn "jar_examples${javaVersion}_${name}"
+            task "jar_examples${javaVersion}_${name}"(type: Jar) {
+                archiveName = jarName
+                destinationDir = exampleOutputDir
+                from sourceSet.output
+                include "**/" + name + "/**/*.class"
+            }
         }
     }
 }
 
-task buildExampleJava10Jars {
-    def examplesDir = file("src/test/examplesJava10")
-    examplesDir.eachDir { dir ->
-        def name = dir.getName();
-        def exampleOutputDir = file("build/test/examplesJava10");
-        def jarName = "${name}.jar"
-        dependsOn "jar_examplesJava10_${name}"
-        task "jar_examplesJava10_${name}"(type: Jar) {
-            archiveName = jarName
-            destinationDir = exampleOutputDir
-            from sourceSets.examplesJava10.output
-            include "**/" + name + "/**/*.class"
-        }
-    }
-}
-
-task buildExampleJava11Jars {
-    def examplesDir = file("src/test/examplesJava11")
-    examplesDir.eachDir { dir ->
-        def name = dir.getName();
-        def exampleOutputDir = file("build/test/examplesJava11");
-        def jarName = "${name}.jar"
-        dependsOn "jar_examplesJava11_${name}"
-        task "jar_examplesJava11_${name}"(type: Jar) {
-            archiveName = jarName
-            destinationDir = exampleOutputDir
-            from sourceSets.examplesJava11.output
-            include "**/" + name + "/**/*.class"
-        }
-    }
-}
+buildExampleJarsCreateTask("Java9", sourceSets.examplesJava9)
+buildExampleJarsCreateTask("Java10", sourceSets.examplesJava10)
+buildExampleJarsCreateTask("Java11", sourceSets.examplesJava11)
+buildExampleJarsCreateTask("Java15", sourceSets.examplesJava15)
 
 task provideArtFrameworksDependencies {
     cloudDependencies.tools.forEach({ art ->
@@ -1688,6 +1687,7 @@
     dependsOn buildExampleJava9Jars
     dependsOn buildExampleJava10Jars
     dependsOn buildExampleJava11Jars
+    dependsOn buildExampleJava15Jars
     dependsOn buildExampleAndroidApi
     def examplesDir = file("src/test/examples")
     def noDexTests = [
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..41fabcc
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,5 @@
+# Copyright (c) 2020, 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.
+
+org.gradle.jvmargs=-Xmx2048M
\ No newline at end of file
diff --git a/src/main/java/com/android/tools/r8/utils/StringUtils.java b/src/main/java/com/android/tools/r8/utils/StringUtils.java
index 902924a..a90d2bc 100644
--- a/src/main/java/com/android/tools/r8/utils/StringUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/StringUtils.java
@@ -341,4 +341,11 @@
     throwable.printStackTrace(new PrintWriter(sw));
     return sw.toString();
   }
+
+  public static String capitalize(String stringToCapitalize) {
+    if (stringToCapitalize == null || stringToCapitalize.isEmpty()) {
+      return stringToCapitalize;
+    }
+    return stringToCapitalize.substring(0, 1).toUpperCase() + stringToCapitalize.substring(1);
+  }
 }
diff --git a/src/test/examplesJava15/records/Main.java b/src/test/examplesJava15/records/Main.java
new file mode 100644
index 0000000..7be696d
--- /dev/null
+++ b/src/test/examplesJava15/records/Main.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2020, 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 records;
+
+public class Main {
+
+  record Person(String name, int age) {}
+
+  public static void main(String[] args) {
+    Person janeDoe = new Person("Jane Doe", 42);
+    System.out.println(janeDoe.name);
+    System.out.println(janeDoe.age);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestRuntime.java b/src/test/java/com/android/tools/r8/TestRuntime.java
index db1ca7f..ac2fce4 100644
--- a/src/test/java/com/android/tools/r8/TestRuntime.java
+++ b/src/test/java/com/android/tools/r8/TestRuntime.java
@@ -27,6 +27,7 @@
     JDK9("jdk9", 53),
     JDK10("jdk10", 54),
     JDK11("jdk11", 55),
+    JDK15("jdk15", 59),
     ;
 
     private final String name;
@@ -67,6 +68,7 @@
   private static final Path JDK9_PATH =
       Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "openjdk-9.0.4");
   private static final Path JDK11_PATH = Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "jdk-11");
+  private static final Path JDK15_PATH = Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "jdk-15");
 
   public static CfRuntime getCheckedInJdk8() {
     Path home;
@@ -107,6 +109,20 @@
     return new CfRuntime(CfVm.JDK11, home);
   }
 
+  // TODO(b/169692487): Add this to 'getCheckedInCfRuntimes' when we start having support for JDK15.
+  public static CfRuntime getCheckedInJdk15() {
+    Path home;
+    if (ToolHelper.isLinux()) {
+      home = JDK15_PATH.resolve("linux");
+    } else if (ToolHelper.isMac()) {
+      home = JDK15_PATH.resolve("osx");
+    } else {
+      assert ToolHelper.isWindows();
+      home = JDK15_PATH.resolve("windows");
+    }
+    return new CfRuntime(CfVm.JDK15, home);
+  }
+
   public static List<CfRuntime> getCheckedInCfRuntimes() {
     CfRuntime[] jdks =
         new CfRuntime[] {getCheckedInJdk8(), getCheckedInJdk9(), getCheckedInJdk11()};
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordsAttributeTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordsAttributeTest.java
new file mode 100644
index 0000000..723dfe6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordsAttributeTest.java
@@ -0,0 +1,80 @@
+// Copyright (c) 2020, 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.records;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static org.hamcrest.CoreMatchers.containsString;
+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;
+import com.android.tools.r8.examples.jdk15.Records;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class RecordsAttributeTest extends TestBase {
+
+  private final Backend backend;
+
+  @Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    return buildParameters(getTestParameters().withNoneRuntime().build(), Backend.values());
+  }
+
+  public RecordsAttributeTest(TestParameters parameters, Backend backend) {
+    this.backend = backend;
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(backend == Backend.CF);
+    testForJvm()
+        .addRunClasspathFiles(Records.jar())
+        .addVmArguments("--enable-preview")
+        .run(TestRuntime.getCheckedInJdk15(), Records.Main.typeName())
+        .assertSuccessWithOutputLines("Jane Doe", "42");
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assertThrows(
+        CompilationFailedException.class,
+        () -> {
+          testForD8(backend)
+              .addProgramClassFileData(Records.Main.bytes(), Records.Main$Person.bytes())
+              .setMinApi(AndroidApiLevel.B)
+              .compileWithExpectedDiagnostics(
+                  diagnostics -> {
+                    diagnostics.assertErrorThatMatches(
+                        diagnosticMessage(containsString("Unsupported class file version: 59")));
+                  });
+        });
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assertThrows(
+        CompilationFailedException.class,
+        () -> {
+          testForR8(backend)
+              .addProgramClassFileData(Records.Main.bytes(), Records.Main$Person.bytes())
+              .setMinApi(AndroidApiLevel.B)
+              .addKeepMainRule(Records.Main.typeName())
+              .compileWithExpectedDiagnostics(
+                  diagnostics -> {
+                    diagnostics.assertErrorThatMatches(
+                        diagnosticMessage(containsString("Unsupported class file version: 59")));
+                  });
+        });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/examples/JavaExampleClassProxy.java b/src/test/java/com/android/tools/r8/examples/JavaExampleClassProxy.java
new file mode 100644
index 0000000..6bbbfd1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/examples/JavaExampleClassProxy.java
@@ -0,0 +1,53 @@
+// Copyright (c) 2020, 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.examples;
+
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.zip.ZipFile;
+
+public class JavaExampleClassProxy {
+
+  private final String examplesFolder;
+  private final String binaryName;
+
+  public JavaExampleClassProxy(String examples, String binaryName) {
+    this.examplesFolder = examples;
+    this.binaryName = binaryName;
+  }
+
+  public static Path examplesJar(String examplesFolder) {
+    return Paths.get(ToolHelper.BUILD_DIR, "test", examplesFolder + ".jar");
+  }
+
+  public byte[] bytes() {
+    Path examplePath = examplesJar(examplesFolder);
+    if (!Files.exists(examplePath)) {
+      throw new RuntimeException(
+          "Could not find path "
+              + examplePath
+              + ". Build "
+              + examplesFolder
+              + " by running tools/gradle.py build"
+              + StringUtils.capitalize(examplesFolder));
+    }
+    try (ZipFile zipFile = new ZipFile(examplePath.toFile())) {
+      return ByteStreams.toByteArray(
+          zipFile.getInputStream(zipFile.getEntry(binaryName + ".class")));
+    } catch (IOException e) {
+      throw new RuntimeException("Could not read zip-entry from " + examplePath.toString(), e);
+    }
+  }
+
+  public String typeName() {
+    return DescriptorUtils.getJavaTypeFromBinaryName(binaryName);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/examples/jdk15/Records.java b/src/test/java/com/android/tools/r8/examples/jdk15/Records.java
new file mode 100644
index 0000000..9eafc56
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/examples/jdk15/Records.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2020, 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.examples.jdk15;
+
+import com.android.tools.r8.examples.JavaExampleClassProxy;
+import java.nio.file.Path;
+
+public class Records {
+
+  private static final String EXAMPLE_FILE = "examplesJava15/records";
+
+  public static final JavaExampleClassProxy Main =
+      new JavaExampleClassProxy(EXAMPLE_FILE, "records/Main");
+  public static final JavaExampleClassProxy Main$Person =
+      new JavaExampleClassProxy(EXAMPLE_FILE, "records/Main$Person");
+
+  public static Path jar() {
+    return JavaExampleClassProxy.examplesJar(EXAMPLE_FILE);
+  }
+}