Add tests about duplicate entries in annotation set.

Bug: 129241209
Change-Id: I21af225f37689e50b58aaf7cdf913b4b4ed17e09
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index c9b8ffc..a0cb023 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -522,7 +522,8 @@
   }
 
   private void writeAnnotationSet(DexAnnotationSet set) {
-    assert PresortedComparable.isSorted(set.annotations, (item) -> item.annotation.type);
+    assert PresortedComparable.isSorted(set.annotations, (item) -> item.annotation.type)
+        : "Unsorted annotation set: " + set.toSourceString();
     mixedSectionOffsets.setOffsetFor(set, dest.align(4));
     if (Log.ENABLED) {
       Log.verbose(getClass(), "Writing AnnotationSet @ 0x%08x.", dest.position());
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java b/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java
index 44b09b0..a0c7006 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java
@@ -14,7 +14,7 @@
 
   private static final int UNSORTED = 0;
   private static final DexAnnotationSet THE_EMPTY_ANNOTATIONS_SET =
-      new DexAnnotationSet(new DexAnnotation[0]);
+      new DexAnnotationSet(DexAnnotation.EMPTY_ARRAY);
 
   public final DexAnnotation[] annotations;
   private int sorted = UNSORTED;
@@ -136,4 +136,9 @@
     }
     return new DexAnnotationSet(rewritten);
   }
+
+  @Override
+  public String toString() {
+    return Arrays.toString(annotations);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/d8/DuplicateAnnotationTest.java b/src/test/java/com/android/tools/r8/d8/DuplicateAnnotationTest.java
new file mode 100644
index 0000000..ca67633
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/d8/DuplicateAnnotationTest.java
@@ -0,0 +1,140 @@
+// Copyright (c) 2019, 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.d8;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DexFilePerClassFileConsumer.ArchiveConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.AnnotationSubject;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FieldSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.nio.file.Path;
+import org.junit.Test;
+
+@Retention(RetentionPolicy.CLASS)
+@Target({ElementType.METHOD, ElementType.FIELD})
+@interface TestKeep {
+}
+
+class TestA {
+  @TestKeep
+  void foo() {
+    System.out.println("TestA::foo");
+  }
+}
+
+class TestB extends TestA {
+  @TestKeep
+  static TestA instance;
+
+  @TestKeep
+  void foo() {
+    super.foo();
+    System.out.println("TestB::foo");
+  }
+
+  @TestKeep
+  static void bar() {
+    instance = new TestB();
+  }
+
+  public static void main(String... args) {
+    bar();
+    instance.foo();
+  }
+}
+
+public class DuplicateAnnotationTest extends TestBase {
+
+  @Test
+  public void testMergingViaD8() throws Exception {
+    Path dex1 = temp.newFile("classes1.zip").toPath().toAbsolutePath();
+    CodeInspector inspector =
+        testForD8()
+            .addProgramClasses(TestA.class)
+            .setIntermediate(true)
+            .setProgramConsumer(new ArchiveConsumer(dex1))
+            .compile()
+            .inspector();
+
+    ClassSubject testA = inspector.clazz(TestA.class);
+    assertThat(testA, isPresent());
+
+    MethodSubject foo = testA.uniqueMethodWithName("foo");
+    assertThat(foo, isPresent());
+    AnnotationSubject annotation = foo.annotation(TestKeep.class.getName());
+    assertThat(annotation, isPresent());
+
+    Path dex2 = temp.newFile("classes2.zip").toPath().toAbsolutePath();
+    inspector =
+        testForD8()
+            .addProgramClasses(TestB.class)
+            .setIntermediate(true)
+            .setProgramConsumer(new ArchiveConsumer(dex2))
+            .compile()
+            .inspector();
+    ClassSubject testB = inspector.clazz(TestB.class);
+    assertThat(testB, isPresent());
+
+    FieldSubject instance = testB.uniqueFieldWithName("instance");
+    assertThat(instance, isPresent());
+    annotation = instance.annotation(TestKeep.class.getName());
+    assertThat(annotation, isPresent());
+
+    foo = testB.uniqueMethodWithName("foo");
+    assertThat(foo, isPresent());
+    annotation = foo.annotation(TestKeep.class.getName());
+    assertThat(annotation, isPresent());
+
+    MethodSubject bar = testB.uniqueMethodWithName("bar");
+    assertThat(bar, isPresent());
+    annotation = bar.annotation(TestKeep.class.getName());
+    assertThat(annotation, isPresent());
+
+    Path merged = temp.newFile("merge.zip").toPath().toAbsolutePath();
+    testForD8()
+        .addProgramFiles(dex1, dex2)
+        .setProgramConsumer(new ArchiveConsumer(merged))
+        .compile();
+  }
+
+  @Test
+  public void testDuplicationInInput() throws Exception {
+    Path dex1 = temp.newFile("classes1.zip").toPath().toAbsolutePath();
+    try {
+      testForD8()
+          .addProgramClassFileData(TestADump.dump())
+          .setIntermediate(true)
+          .setProgramConsumer(new ArchiveConsumer(dex1))
+          .compile();
+    } catch (CompilationFailedException e) {
+      assertThat(e.getCause().getMessage(), containsString("Unsorted annotation set"));
+      assertThat(e.getCause().getMessage(), containsString(TestKeep.class.getName()));
+    }
+  }
+
+  @Test
+  public void testJVMOutput() throws Exception {
+    testForJvm()
+        .addProgramClassFileData(
+            TestADump.dump(),
+            ToolHelper.getClassAsBytes(TestB.class),
+            ToolHelper.getClassAsBytes(TestKeep.class))
+        .run(TestB.class.getTypeName())
+        .assertSuccessWithOutput(StringUtils.lines("TestA::foo", "TestB::foo"));
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/d8/TestADump.java b/src/test/java/com/android/tools/r8/d8/TestADump.java
new file mode 100644
index 0000000..610d194
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/d8/TestADump.java
@@ -0,0 +1,80 @@
+// Copyright (c) 2019, 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.d8;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+class TestADump implements Opcodes {
+
+  public static byte[] dump () throws Exception {
+
+    ClassWriter classWriter = new ClassWriter(0);
+    MethodVisitor methodVisitor;
+    AnnotationVisitor annotationVisitor0;
+
+    classWriter.visit(
+        V1_8, ACC_SUPER,
+        "com/android/tools/r8/d8/TestA",
+        null,
+        "java/lang/Object",
+        null);
+
+    classWriter.visitSource("DuplicateAnnotationTest.java", null);
+
+    {
+      methodVisitor = classWriter.visitMethod(0, "<init>", "()V", null, null);
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(28, label0);
+      methodVisitor.visitVarInsn(ALOAD, 0);
+      methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      methodVisitor.visitInsn(RETURN);
+      Label label1 = new Label();
+      methodVisitor.visitLabel(label1);
+      methodVisitor.visitLocalVariable(
+          "this", "Lcom/android/tools/r8/d8/TestA;", null, label0, label1, 0);
+      methodVisitor.visitMaxs(1, 1);
+      methodVisitor.visitEnd();
+    }
+    {
+      methodVisitor = classWriter.visitMethod(0, "foo", "()V", null, null);
+      {
+        annotationVisitor0 =
+            methodVisitor.visitAnnotation("Lcom/android/tools/r8/d8/TestKeep;", false);
+        annotationVisitor0.visitEnd();
+        // Intentionally introduce a duplication.
+        annotationVisitor0 =
+            methodVisitor.visitAnnotation("Lcom/android/tools/r8/d8/TestKeep;", false);
+        annotationVisitor0.visitEnd();
+      }
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(31, label0);
+      methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+      methodVisitor.visitLdcInsn("TestA::foo");
+      methodVisitor.visitMethodInsn(
+          INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
+      Label label1 = new Label();
+      methodVisitor.visitLabel(label1);
+      methodVisitor.visitLineNumber(32, label1);
+      methodVisitor.visitInsn(RETURN);
+      Label label2 = new Label();
+      methodVisitor.visitLabel(label2);
+      methodVisitor.visitLocalVariable(
+          "this", "Lcom/android/tools/r8/d8/TestA;", null, label0, label2, 0);
+      methodVisitor.visitMaxs(2, 1);
+      methodVisitor.visitEnd();
+    }
+    classWriter.visitEnd();
+
+    return classWriter.toByteArray();
+  }
+}
+
diff --git a/src/test/java/com/android/tools/r8/dexfilemerger/DexFileMergerTests.java b/src/test/java/com/android/tools/r8/dexfilemerger/DexFileMergerTests.java
index 83673a7..91364d4 100644
--- a/src/test/java/com/android/tools/r8/dexfilemerger/DexFileMergerTests.java
+++ b/src/test/java/com/android/tools/r8/dexfilemerger/DexFileMergerTests.java
@@ -4,7 +4,10 @@
 
 package com.android.tools.r8.dexfilemerger;
 
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.D8;
@@ -13,6 +16,7 @@
 import com.android.tools.r8.ExtractMarker;
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
 import com.android.tools.r8.dex.Constants;
@@ -26,19 +30,15 @@
 import java.nio.file.Paths;
 import java.util.Collection;
 import java.util.concurrent.ExecutionException;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
 
-public class DexFileMergerTests {
+public class DexFileMergerTests extends TestBase {
 
   private static final String CLASS_DIR = ToolHelper.EXAMPLES_BUILD_DIR + "classes/dexmergesample";
   private static final String CLASS1_CLASS = CLASS_DIR + "/Class1.class";
   private static final String CLASS2_CLASS = CLASS_DIR + "/Class2.class";
   private static final int MAX_METHOD_COUNT = Constants.U16BIT_MAX;
 
-  @Rule public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
-
   private Path createMergerInputWithTwoClasses(OutputMode outputMode, boolean addMarker)
       throws CompilationFailedException, IOException {
     // Compile Class1 and Class2
@@ -128,11 +128,16 @@
         });
   }
 
-  @Test(expected = CompilationFailedException.class)
-  public void failIfTooBig() throws IOException, ExecutionException, CompilationFailedException {
+  @Test
+  public void failIfTooBig() throws IOException, ExecutionException {
     // Generates an application with two classes, each with the number of methods just enough not to
     // fit into a single dex file.
-    generateClassesAndTest(1, 2);
+    try {
+      generateClassesAndTest(1, 2);
+      fail("Expect to fail");
+    } catch (CompilationFailedException e) {
+      assertThat(e.getCause().getMessage(), containsString("does not fit into a single dex file"));
+    }
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinDuplicateAnnotationTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinDuplicateAnnotationTest.java
new file mode 100644
index 0000000..3dca26b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinDuplicateAnnotationTest.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2019, 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.kotlin;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.function.Consumer;
+import org.junit.Test;
+
+public class KotlinDuplicateAnnotationTest extends AbstractR8KotlinTestBase {
+  private Consumer<InternalOptions> optionsModifier =
+    o -> {
+      o.enableTreeShaking = true;
+      o.enableMinification = false;
+      o.enableInlining = false;
+    };
+
+  public KotlinDuplicateAnnotationTest(
+      KotlinTargetVersion targetVersion, boolean allowAccessModification) {
+    super(targetVersion, allowAccessModification);
+  }
+
+  @Test
+  public void testDuplicateAnnotation() throws Exception {
+    final String mainClassName = "duplicate_annotation.MainKt";
+    try {
+      runTest(
+          "duplicate_annotation",
+          mainClassName,
+          StringUtils.lines(
+              "-keep,allowobfuscation @interface **.TestAnnotation",
+              "-keepattributes *Annotations*"
+          ),
+          optionsModifier,
+          null);
+    } catch (CompilationFailedException e) {
+      assertThat(e.getCause().getMessage(), containsString("Unsorted annotation set"));
+      assertThat(e.getCause().getMessage(), containsString("TestAnnotation"));
+    }
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
index 11e92e1..33a8bf3 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
@@ -4,8 +4,11 @@
 
 package com.android.tools.r8.maindexlist;
 
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.DexIndexedConsumer;
@@ -25,9 +28,7 @@
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.stream.Collectors;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
 public class MainDexListOutputTest extends TestBase {
 
@@ -70,10 +71,7 @@
     }
   }
 
-  @Rule
-  public ExpectedException thrown = ExpectedException.none();
-
-  @Test(expected = CompilationFailedException.class)
+  @Test
   public void testNoMainDex() throws Exception {
     Reporter reporter = new Reporter();
     try {
@@ -84,9 +82,12 @@
               ToolHelper.getClassAsBytes(HelloWorldMain.class), Origin.unknown())
           .setMainDexListOutputPath(mainDexListOutput)
           .build();
+      fail("Expect to fail");
     } catch (CompilationFailedException e) {
       assertEquals(1, reporter.errorCount);
-      throw e;
+      assertThat(
+          e.getCause().getMessage(),
+          containsString("--main-dex-list-output require --main-dex-rules and/or --main-dex-list"));
     }
   }
 
diff --git a/src/test/kotlinR8TestResources/duplicate_annotation/main.kt b/src/test/kotlinR8TestResources/duplicate_annotation/main.kt
new file mode 100644
index 0000000..99d2273
--- /dev/null
+++ b/src/test/kotlinR8TestResources/duplicate_annotation/main.kt
@@ -0,0 +1,17 @@
+// Copyright (c) 2019, 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 duplicate_annotation
+
+annotation class TestAnnotation
+
+class Foo {
+  @get:TestAnnotation
+  val test: Int
+    @TestAnnotation
+    get() = 0
+}
+
+fun main() {
+  println(Foo().test)
+}
\ No newline at end of file