Records: initial tests

- Tests for simple record cases
- Warning for empty records
- Support ASM ACC_RECORD flag for empty record support

Bug: 169645628
Change-Id: I83c7a091893c40241ad6d3b90b7ae5fefaa18e2f
diff --git a/src/main/java/com/android/tools/r8/dex/Constants.java b/src/main/java/com/android/tools/r8/dex/Constants.java
index dfa8667..8ea7319 100644
--- a/src/main/java/com/android/tools/r8/dex/Constants.java
+++ b/src/main/java/com/android/tools/r8/dex/Constants.java
@@ -135,6 +135,7 @@
   public static final int ACC_ANNOTATION = 0x2000;
   public static final int ACC_ENUM = 0x4000;
   public static final int ACC_CONSTRUCTOR = 0x10000;
+  public static final int ACC_RECORD = 0x10000;
   public static final int ACC_DECLARED_SYNCHRONIZED = 0x20000;
 
   public static final String JAVA_LANG_OBJECT_NAME = "java/lang/Object";
diff --git a/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java b/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
index 7998171..abeb404 100644
--- a/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
@@ -22,9 +22,7 @@
   private static final int DEX_FLAGS
       = SHARED_FLAGS;
 
-  private static final int CF_FLAGS
-      = SHARED_FLAGS
-      | Constants.ACC_SUPER;
+  private static final int CF_FLAGS = SHARED_FLAGS | Constants.ACC_SUPER | Constants.ACC_RECORD;
 
   @Override
   protected List<String> getNames() {
@@ -35,6 +33,7 @@
         .add("annotation")
         .add("enum")
         .add("super")
+        .add("record")
         .build();
   }
 
@@ -47,6 +46,7 @@
         .add(this::isAnnotation)
         .add(this::isEnum)
         .add(this::isSuper)
+        .add(this::isRecord)
         .build();
   }
 
@@ -178,6 +178,14 @@
     set(Constants.ACC_ENUM);
   }
 
+  public boolean isRecord() {
+    return isSet(Constants.ACC_RECORD);
+  }
+
+  public void setRecord() {
+    set(Constants.ACC_RECORD);
+  }
+
   public boolean isSuper() {
     return isSet(Constants.ACC_SUPER);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 4cb631b..18b0434 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -213,6 +213,7 @@
   public final DexString stringDescriptor = createString("Ljava/lang/String;");
   public final DexString stringArrayDescriptor = createString("[Ljava/lang/String;");
   public final DexString objectDescriptor = createString("Ljava/lang/Object;");
+  public final DexString recordDescriptor = createString("Ljava/lang/Record;");
   public final DexString objectArrayDescriptor = createString("[Ljava/lang/Object;");
   public final DexString classDescriptor = createString("Ljava/lang/Class;");
   public final DexString classLoaderDescriptor = createString("Ljava/lang/ClassLoader;");
@@ -342,6 +343,7 @@
   public final DexType stringType = createStaticallyKnownType(stringDescriptor);
   public final DexType stringArrayType = createStaticallyKnownType(stringArrayDescriptor);
   public final DexType objectType = createStaticallyKnownType(objectDescriptor);
+  public final DexType recordType = createStaticallyKnownType(recordDescriptor);
   public final DexType objectArrayType = createStaticallyKnownType(objectArrayDescriptor);
   public final DexType classArrayType = createStaticallyKnownType(classArrayDescriptor);
   public final DexType enumType = createStaticallyKnownType(enumDescriptor);
diff --git a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
index 40b6445..1f67c4d 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -301,7 +301,8 @@
     @Override
     public RecordComponentVisitor visitRecordComponent(
         String name, String descriptor, String signature) {
-      throw new CompilationError("Records are not supported", origin);
+      // TODO(b/169645628): Support Records.
+      return super.visitRecordComponent(name, descriptor, signature);
     }
 
     @Override
@@ -326,6 +327,12 @@
       }
       this.deprecated = AsmUtils.isDeprecated(access);
       accessFlags = ClassAccessFlags.fromCfAccessFlags(cleanAccessFlags(access));
+      if (accessFlags.isRecord()) {
+        // TODO(b/169645628): Support records in all compilation.
+        if (!application.options.canUseRecords()) {
+          throw new CompilationError("Records are not supported", origin);
+        }
+      }
       type = application.getTypeFromName(name);
       // Check if constraints from
       // https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.1 are met.
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 240d3b9..07bdb1b 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -485,6 +485,11 @@
     return !canUseNestBasedAccess();
   }
 
+  public boolean canUseRecords() {
+    // TODO(b/169645628): Replace by true when records are supported.
+    return testing.canUseRecords;
+  }
+
   public Set<String> extensiveLoggingFilter = getExtensiveLoggingFilter();
   public Set<String> extensiveInterfaceMethodMinifierLoggingFilter =
       getExtensiveInterfaceMethodMinifierLoggingFilter();
@@ -1281,6 +1286,7 @@
     public boolean enableSwitchToIfRewriting = true;
     public boolean enableEnumUnboxingDebugLogs = false;
     public boolean forceRedundantConstNumberRemoval = false;
+    public boolean canUseRecords = false;
     public boolean invertConditionals = false;
     public boolean placeExceptionalBlocksLast = false;
     public boolean dontCreateMarkerInD8 = false;
diff --git a/src/test/examplesJava15/records/Main.java b/src/test/examplesJava15/records/EmptyRecord.java
similarity index 60%
copy from src/test/examplesJava15/records/Main.java
copy to src/test/examplesJava15/records/EmptyRecord.java
index 7be696d..5d16e44 100644
--- a/src/test/examplesJava15/records/Main.java
+++ b/src/test/examplesJava15/records/EmptyRecord.java
@@ -4,13 +4,11 @@
 
 package records;
 
-public class Main {
+public class EmptyRecord {
 
-  record Person(String name, int age) {}
+  record Empty() {}
 
   public static void main(String[] args) {
-    Person janeDoe = new Person("Jane Doe", 42);
-    System.out.println(janeDoe.name);
-    System.out.println(janeDoe.age);
+    System.out.println(new Empty());
   }
 }
diff --git a/src/test/examplesJava15/records/RecordInstanceOf.java b/src/test/examplesJava15/records/RecordInstanceOf.java
new file mode 100644
index 0000000..591192c
--- /dev/null
+++ b/src/test/examplesJava15/records/RecordInstanceOf.java
@@ -0,0 +1,21 @@
+// 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 RecordInstanceOf {
+
+  record Empty() {}
+
+  record Person(String name, int age) {}
+
+  public static void main(String[] args) {
+    Empty empty = new Empty();
+    Person janeDoe = new Person("Jane Doe", 42);
+    Object o = new Object();
+    System.out.println(janeDoe instanceof java.lang.Record);
+    System.out.println(empty instanceof java.lang.Record);
+    System.out.println(o instanceof java.lang.Record);
+  }
+}
diff --git a/src/test/examplesJava15/records/RecordInvokeCustom.java b/src/test/examplesJava15/records/RecordInvokeCustom.java
new file mode 100644
index 0000000..c460f56
--- /dev/null
+++ b/src/test/examplesJava15/records/RecordInvokeCustom.java
@@ -0,0 +1,48 @@
+// 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 RecordInvokeCustom {
+
+  record Empty() {}
+
+  record Person(String name, int age) {}
+
+  public static void main(String[] args) {
+    emptyTest();
+    equalityTest();
+    toStringTest();
+  }
+
+  private static void emptyTest() {
+    Empty empty1 = new Empty();
+    Empty empty2 = new Empty();
+    System.out.println(empty1.toString());
+    System.out.println(empty1.equals(empty2));
+    System.out.println(empty1.hashCode() == empty2.hashCode());
+    System.out.println(empty1.toString().equals(empty2.toString()));
+  }
+
+  private static void toStringTest() {
+    Person janeDoe = new Person("Jane Doe", 42);
+    System.out.println(janeDoe.toString());
+  }
+
+  private static void equalityTest() {
+    Person jane1 = new Person("Jane Doe", 42);
+    Person jane2 = new Person("Jane Doe", 42);
+    String nonIdenticalString = "Jane " + (System.currentTimeMillis() > 0 ? "Doe" : "Zan");
+    Person jane3 = new Person(nonIdenticalString, 42);
+    Person bob = new Person("Bob", 42);
+    Person youngJane = new Person("Jane Doe", 22);
+    System.out.println(jane1.equals(jane2));
+    System.out.println(jane1.toString().equals(jane2.toString()));
+    System.out.println(nonIdenticalString == "Jane Doe"); // false.
+    System.out.println(nonIdenticalString.equals("Jane Doe")); // true.
+    System.out.println(jane1.equals(jane3));
+    System.out.println(jane1.equals(bob));
+    System.out.println(jane1.equals(youngJane));
+  }
+}
diff --git a/src/test/examplesJava15/records/RecordReflection.java b/src/test/examplesJava15/records/RecordReflection.java
new file mode 100644
index 0000000..1b94a12
--- /dev/null
+++ b/src/test/examplesJava15/records/RecordReflection.java
@@ -0,0 +1,29 @@
+// 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;
+
+import java.util.Arrays;
+
+public class RecordReflection {
+
+  record Empty(){}
+
+  record Person(String name, int age) {}
+
+  record PersonGeneric <S extends CharSequence>(S name, int age) {}
+
+  public static void main(String[] args) {
+    System.out.println(Empty.class.isRecord());
+    System.out.println(Arrays.toString(Empty.class.getRecordComponents()));
+    System.out.println(Person.class.isRecord());
+    System.out.println(Arrays.toString(Person.class.getRecordComponents()));
+    System.out.println(PersonGeneric.class.isRecord());
+    System.out.println(Arrays.toString(PersonGeneric.class.getRecordComponents()));
+    System.out.println(Arrays.toString(PersonGeneric.class.getTypeParameters()));
+    System.out.println(Object.class.isRecord());
+    System.out.println(Arrays.toString(Object.class.getRecordComponents()));
+  }
+
+}
diff --git a/src/test/examplesJava15/records/RecordWithMembers.java b/src/test/examplesJava15/records/RecordWithMembers.java
new file mode 100644
index 0000000..6de2b99
--- /dev/null
+++ b/src/test/examplesJava15/records/RecordWithMembers.java
@@ -0,0 +1,69 @@
+// 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 RecordWithMembers {
+
+
+  record PersonWithConstructors(String name, int age) {
+
+    public PersonWithConstructors(String name, int age) {
+      this.name = name + "X";
+      this.age = age;
+    }
+
+    public PersonWithConstructors(String name) {
+      this(name, -1);
+    }
+  }
+
+  record PersonWithMethods(String name, int age) {
+    public static void staticPrint() {
+      System.out.println("print");
+    }
+
+    @Override
+    public String toString() {
+      return name + age;
+    }
+  }
+
+  record PersonWithFields(String name, int age) {
+
+    // Extra instance fields are not allowed on records.
+    public static String globalName;
+
+  }
+
+  public static void main(String[] args) {
+    personWithConstructorTest();
+    personWithMethodsTest();
+    personWithFieldsTest();
+  }
+
+  private static void personWithConstructorTest() {
+    PersonWithConstructors bob = new PersonWithConstructors("Bob", 43);
+    System.out.println(bob.name);
+    System.out.println(bob.age);
+    System.out.println(bob.name());
+    System.out.println(bob.age());
+    PersonWithConstructors felix = new PersonWithConstructors("Felix");
+    System.out.println(felix.name);
+    System.out.println(felix.age);
+    System.out.println(felix.name());
+    System.out.println(felix.age());
+  }
+
+  private static void personWithMethodsTest() {
+    PersonWithMethods.staticPrint();
+    PersonWithMethods bob = new PersonWithMethods("Bob", 43);
+    System.out.println(bob.toString());
+  }
+
+  private static void personWithFieldsTest() {
+    PersonWithFields.globalName = "extra";
+    System.out.println(PersonWithFields.globalName);
+  }
+}
diff --git a/src/test/examplesJava15/records/Main.java b/src/test/examplesJava15/records/SimpleRecord.java
similarity index 80%
rename from src/test/examplesJava15/records/Main.java
rename to src/test/examplesJava15/records/SimpleRecord.java
index 7be696d..1f8fca4 100644
--- a/src/test/examplesJava15/records/Main.java
+++ b/src/test/examplesJava15/records/SimpleRecord.java
@@ -4,7 +4,7 @@
 
 package records;
 
-public class Main {
+public class SimpleRecord {
 
   record Person(String name, int age) {}
 
@@ -12,5 +12,7 @@
     Person janeDoe = new Person("Jane Doe", 42);
     System.out.println(janeDoe.name);
     System.out.println(janeDoe.age);
+    System.out.println(janeDoe.name());
+    System.out.println(janeDoe.age());
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java b/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java
new file mode 100644
index 0000000..92183a7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java
@@ -0,0 +1,45 @@
+// 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.records;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class EmptyRecordTest extends TestBase {
+
+  private static final String RECORD_NAME = "EmptyRecord";
+  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
+  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
+  private static final String EXPECTED_RESULT = StringUtils.lines("Empty[]");
+
+  private final TestParameters parameters;
+
+  public EmptyRecordTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk15).
+    return buildParameters(
+        getTestParameters().withCustomRuntime(CfRuntime.getCheckedInJdk15()).build());
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    testForJvm()
+        .addProgramClassFileData(PROGRAM_DATA)
+        .addVmArguments("--enable-preview")
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/InvalidRecordAttributeTest.java b/src/test/java/com/android/tools/r8/desugar/records/InvalidRecordAttributeTest.java
new file mode 100644
index 0000000..e936a33
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/InvalidRecordAttributeTest.java
@@ -0,0 +1,111 @@
+// 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.records;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThrows;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.util.List;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Remove this test when Records are supported by default. */
+@RunWith(Parameterized.class)
+public class InvalidRecordAttributeTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final Backend backend;
+
+  private static final String EMPTY_RECORD = "EmptyRecord";
+  private static final byte[][] EMPTY_RECORD_PROGRAM_DATA =
+      RecordTestUtils.getProgramData(EMPTY_RECORD);
+  private static final String SIMPLE_RECORD = "SimpleRecord";
+  private static final byte[][] SIMPLE_RECORD_PROGRAM_DATA =
+      RecordTestUtils.getProgramData(SIMPLE_RECORD);
+
+  @Parameters(name = "{0} back: {1}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk15).
+    return buildParameters(
+        getTestParameters().withCustomRuntime(CfRuntime.getCheckedInJdk15()).build(),
+        Backend.values());
+  }
+
+  public InvalidRecordAttributeTest(TestParameters parameters, Backend backend) {
+    this.parameters = parameters;
+    this.backend = backend;
+  }
+
+  @Test
+  public void testD8EmptyRecord() throws Exception {
+    Assume.assumeTrue(backend.isDex());
+    assertThrows(
+        CompilationFailedException.class,
+        () -> {
+          testForD8(backend)
+              .addProgramClassFileData(EMPTY_RECORD_PROGRAM_DATA)
+              .setMinApi(AndroidApiLevel.B)
+              .compileWithExpectedDiagnostics(
+                  InvalidRecordAttributeTest::assertUnsupportedRecordError);
+        });
+  }
+
+  @Test
+  public void testD8SimpleRecord() throws Exception {
+    Assume.assumeTrue(backend.isDex());
+    assertThrows(
+        CompilationFailedException.class,
+        () -> {
+          testForD8(backend)
+              .addProgramClassFileData(RecordTestUtils.getProgramData(SIMPLE_RECORD))
+              .setMinApi(AndroidApiLevel.B)
+              .compileWithExpectedDiagnostics(
+                  InvalidRecordAttributeTest::assertUnsupportedRecordError);
+        });
+  }
+
+  @Test
+  public void testR8EmptyRecord() throws Exception {
+    assertThrows(
+        CompilationFailedException.class,
+        () -> {
+          testForR8(backend)
+              .addProgramClassFileData(EMPTY_RECORD_PROGRAM_DATA)
+              .setMinApi(AndroidApiLevel.B)
+              .addKeepMainRule(RecordTestUtils.getMainType(EMPTY_RECORD))
+              .compileWithExpectedDiagnostics(
+                  InvalidRecordAttributeTest::assertUnsupportedRecordError);
+        });
+  }
+
+  @Test
+  public void testR8SimpleRecord() throws Exception {
+    assertThrows(
+        CompilationFailedException.class,
+        () -> {
+          testForR8(backend)
+              .addProgramClassFileData(SIMPLE_RECORD_PROGRAM_DATA)
+              .setMinApi(AndroidApiLevel.B)
+              .addKeepMainRule(RecordTestUtils.getMainType(SIMPLE_RECORD))
+              .compileWithExpectedDiagnostics(
+                  InvalidRecordAttributeTest::assertUnsupportedRecordError);
+        });
+  }
+
+  private static void assertUnsupportedRecordError(TestDiagnosticMessages diagnostics) {
+    diagnostics.assertErrorThatMatches(
+        diagnosticMessage(containsString("Records are not supported")));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java
new file mode 100644
index 0000000..33a3b57
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java
@@ -0,0 +1,45 @@
+// 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.records;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RecordInstanceOfTest extends TestBase {
+
+  private static final String RECORD_NAME = "RecordInstanceOf";
+  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
+  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
+  private static final String EXPECTED_RESULT = StringUtils.lines("true", "true", "false");
+
+  private final TestParameters parameters;
+
+  public RecordInstanceOfTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk15).
+    return buildParameters(
+        getTestParameters().withCustomRuntime(CfRuntime.getCheckedInJdk15()).build());
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    testForJvm()
+        .addProgramClassFileData(PROGRAM_DATA)
+        .addVmArguments("--enable-preview")
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomTest.java
new file mode 100644
index 0000000..1d1e785
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomTest.java
@@ -0,0 +1,58 @@
+// 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.records;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RecordInvokeCustomTest extends TestBase {
+
+  private static final String RECORD_NAME = "RecordInvokeCustom";
+  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
+  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
+  private static final String EXPECTED_RESULT =
+      StringUtils.lines(
+          "Empty[]",
+          "true",
+          "true",
+          "true",
+          "true",
+          "true",
+          "false",
+          "true",
+          "true",
+          "false",
+          "false",
+          "Person[name=Jane Doe, age=42]");
+
+  private final TestParameters parameters;
+
+  public RecordInvokeCustomTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk15).
+    return buildParameters(
+        getTestParameters().withCustomRuntime(CfRuntime.getCheckedInJdk15()).build());
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    testForJvm()
+        .addProgramClassFileData(PROGRAM_DATA)
+        .addVmArguments("--enable-preview")
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordReflectionTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordReflectionTest.java
new file mode 100644
index 0000000..1bb0a1a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordReflectionTest.java
@@ -0,0 +1,55 @@
+// 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.records;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RecordReflectionTest extends TestBase {
+
+  private static final String RECORD_NAME = "RecordReflection";
+  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
+  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
+  private static final String EXPECTED_RESULT =
+      StringUtils.lines(
+          "true",
+          "[]",
+          "true",
+          "[java.lang.String name, int age]",
+          "true",
+          "[java.lang.CharSequence name, int age]",
+          "[S]",
+          "false",
+          "null");
+
+  private final TestParameters parameters;
+
+  public RecordReflectionTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk15).
+    return buildParameters(
+        getTestParameters().withCustomRuntime(CfRuntime.getCheckedInJdk15()).build());
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    testForJvm()
+        .addProgramClassFileData(PROGRAM_DATA)
+        .addVmArguments("--enable-preview")
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordTestUtils.java b/src/test/java/com/android/tools/r8/desugar/records/RecordTestUtils.java
new file mode 100644
index 0000000..d4a804b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordTestUtils.java
@@ -0,0 +1,71 @@
+// 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.records;
+
+import com.android.tools.r8.ToolHelper;
+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.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * Records are compiled using: third_party/openjdk/jdk-15/linux/bin/javac --release 15
+ * --enable-preview path/to/file.java
+ */
+public class RecordTestUtils {
+
+  private static final String EXAMPLE_FOLDER = "examplesJava15";
+  private static final String RECORD_FOLDER = "records";
+
+  public static Path jar() {
+    return Paths.get(ToolHelper.TESTS_BUILD_DIR, EXAMPLE_FOLDER, RECORD_FOLDER + ".jar");
+  }
+
+  public static byte[][] getProgramData(String mainClassSimpleName) {
+    byte[][] bytes = classDataFromPrefix(RECORD_FOLDER + "/" + mainClassSimpleName);
+    assert bytes.length > 0 : "Did not find any program data for " + mainClassSimpleName;
+    return bytes;
+  }
+
+  public static String getMainType(String mainClassSimpleName) {
+    return RECORD_FOLDER + "." + mainClassSimpleName;
+  }
+
+  private static byte[][] classDataFromPrefix(String prefix) {
+    Path examplePath = jar();
+    if (!Files.exists(examplePath)) {
+      throw new RuntimeException(
+          "Could not find path "
+              + examplePath
+              + ". Build "
+              + EXAMPLE_FOLDER
+              + " by running tools/gradle.py build"
+              + StringUtils.capitalize(EXAMPLE_FOLDER));
+    }
+    List<byte[]> result = new ArrayList<>();
+    try (ZipFile zipFile = new ZipFile(examplePath.toFile())) {
+      Enumeration<? extends ZipEntry> entries = zipFile.entries();
+      while (entries.hasMoreElements()) {
+        ZipEntry zipEntry = entries.nextElement();
+        if (zipEntry.getName().startsWith(prefix)) {
+          result.add(ByteStreams.toByteArray(zipFile.getInputStream(zipEntry)));
+        }
+      }
+    } catch (IOException e) {
+      throw new RuntimeException("Could not read zip-entry from " + examplePath.toString(), e);
+    }
+    if (result.isEmpty()) {
+      throw new RuntimeException("Did not find any class with prefix " + prefix);
+    }
+    return result.toArray(new byte[0][0]);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordWithMembersTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordWithMembersTest.java
new file mode 100644
index 0000000..c0ca829
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordWithMembersTest.java
@@ -0,0 +1,47 @@
+// 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.records;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RecordWithMembersTest extends TestBase {
+
+  private static final String RECORD_NAME = "RecordWithMembers";
+  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
+  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
+  private static final String EXPECTED_RESULT =
+      StringUtils.lines(
+          "BobX", "43", "BobX", "43", "FelixX", "-1", "FelixX", "-1", "print", "Bob43", "extra");
+
+  private final TestParameters parameters;
+
+  public RecordWithMembersTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk15).
+    return buildParameters(
+        getTestParameters().withCustomRuntime(CfRuntime.getCheckedInJdk15()).build());
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    testForJvm()
+        .addProgramClassFileData(PROGRAM_DATA)
+        .addVmArguments("--enable-preview")
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+}
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
deleted file mode 100644
index 831c426..0000000
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordsAttributeTest.java
+++ /dev/null
@@ -1,87 +0,0 @@
-// 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.assumeFalse;
-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.CfRuntime;
-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;
-  private final TestParameters parameters;
-
-  @Parameters(name = "{0}")
-  public static List<Object[]> data() {
-    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk15).
-    return buildParameters(
-        getTestParameters().withCustomRuntime(CfRuntime.getCheckedInJdk15()).build(),
-        Backend.values());
-  }
-
-  public RecordsAttributeTest(TestParameters parameters, Backend backend) {
-    this.parameters = parameters;
-    this.backend = backend;
-  }
-
-  @Test
-  public void testJvm() throws Exception {
-    assumeFalse(parameters.isNoneRuntime());
-    assumeTrue(backend == Backend.CF);
-    testForJvm()
-        .addRunClasspathFiles(Records.jar())
-        .addVmArguments("--enable-preview")
-        .run(parameters.getRuntime(), 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("Records are not supported")));
-                  });
-        });
-  }
-
-  @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("Records are not supported")));
-                  });
-        });
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java b/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
new file mode 100644
index 0000000..35ce924
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
@@ -0,0 +1,46 @@
+// 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.records;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class SimpleRecordTest extends TestBase {
+
+  private static final String RECORD_NAME = "SimpleRecord";
+  private static final byte[][] PROGRAM_DATA = RecordTestUtils.getProgramData(RECORD_NAME);
+  private static final String MAIN_TYPE = RecordTestUtils.getMainType(RECORD_NAME);
+  private static final String EXPECTED_RESULT =
+      StringUtils.lines("Jane Doe", "42", "Jane Doe", "42");
+
+  private final TestParameters parameters;
+
+  public SimpleRecordTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk15).
+    return buildParameters(
+        getTestParameters().withCustomRuntime(CfRuntime.getCheckedInJdk15()).build());
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    testForJvm()
+        .addProgramClassFileData(PROGRAM_DATA)
+        .addVmArguments("--enable-preview")
+        .run(parameters.getRuntime(), MAIN_TYPE)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+}