Add a test for incorrect join of class types in presence of missing classes

Bug: 130211035
Change-Id: I6b56c82be6cbd78d2227094c6fe04e0f363a48d0
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/MissingClassesJoinTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/MissingClassesJoinTest.java
new file mode 100644
index 0000000..60ab870
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/MissingClassesJoinTest.java
@@ -0,0 +1,135 @@
+// 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.ir.analysis.type;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import java.nio.file.Path;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Test for b/130211035. */
+@RunWith(Parameterized.class)
+public class MissingClassesJoinTest extends TestBase {
+
+  private static final String expectedOutput = StringUtils.lines("Hello world!");
+
+  private final boolean allowTypeErrors;
+  private final TestParameters parameters;
+
+  @Parameters(name = "{1}, allow type errors: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(BooleanUtils.values(), getTestParameters().withAllRuntimes().build());
+  }
+
+  public MissingClassesJoinTest(boolean allowTypeErrors, TestParameters parameters) {
+    this.allowTypeErrors = allowTypeErrors;
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    Path classpathFile =
+        testForD8()
+            .addProgramClasses(ASub2.class)
+            .setMinApi(parameters.getRuntime())
+            .compile()
+            .writeToZip();
+
+    if (parameters.getBackend() == Backend.DEX && !allowTypeErrors) {
+      testForD8()
+          // Intentionally not adding ASub2 as a program class.
+          .addProgramClasses(A.class, ASub1.class, Box.class, TestClass.class)
+          .setMinApi(parameters.getRuntime())
+          .compile()
+          .addRunClasspathFiles(classpathFile)
+          .run(parameters.getRuntime(), TestClass.class)
+          .assertSuccessWithOutput(expectedOutput);
+    }
+
+    try {
+      R8TestRunResult result =
+          testForR8(parameters.getBackend())
+              // Intentionally not adding ASub2 as a program class.
+              .addProgramClasses(A.class, ASub1.class, Box.class, TestClass.class)
+              .addKeepAllClassesRule()
+              .addOptionsModification(options -> options.testing.allowTypeErrors = allowTypeErrors)
+              .enableInliningAnnotations()
+              .enableMergeAnnotations()
+              .setMinApi(parameters.getRuntime())
+              .compile()
+              .addRunClasspathFiles(classpathFile)
+              .run(parameters.getRuntime(), TestClass.class);
+
+      // Compilation fails unless type errors are allowed.
+      assertTrue(allowTypeErrors);
+
+      // TestClass.main() does not type check, so it should have been replaced by `throw null`.
+      // Note that, even if we do not replace the body of main() with `throw null`, the code would
+      // still not work for the CF backend:
+      //
+      //     java.lang.VerifyError: Bad type on operand stack
+      //     Exception Details:
+      //       Location:
+      //         MissingClassesJoinTest$TestClass.main([Ljava/lang/String;)V @28: putstatic
+      //       Reason:
+      //         Type 'java/lang/Object' (current frame, stack[0]) is not assignable to
+      //         'com/android/tools/r8/ir/analysis/type/MissingClassesJoinTest$A'
+      //       Current Frame:
+      //         bci: @28
+      //         flags: { }
+      //         locals: { 'java/lang/Object' }
+      //         stack: { 'java/lang/Object' }
+      result.assertFailureWithErrorThatMatches(containsString("NullPointerException"));
+    } catch (Exception e) {
+      // Compilation should only fail when type errors are not allowed.
+      assertFalse(allowTypeErrors);
+
+      // Verify that we fail with an assertion error.
+      assertThat(e.getCause().getMessage(), containsString("AssertionError"));
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      A a;
+      if (System.currentTimeMillis() < 0) {
+        a = new ASub1();
+      } else {
+        a = new ASub2();
+      }
+      Box.field = a;
+    }
+  }
+
+  @NeverMerge
+  abstract static class A {}
+
+  static class ASub1 extends A {}
+
+  static class ASub2 extends A {}
+
+  static class Box {
+
+    static {
+      System.out.println("Hello world!");
+    }
+
+    static A field;
+  }
+}