Test for setup with classpath extending program.

Bug: 149201158

Change-Id: I1af3172ce256434a5db2e190f5457844bdc406aa
diff --git a/src/test/java/com/android/tools/r8/JvmTestBuilder.java b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
index 0e510a9..8bc4820 100644
--- a/src/test/java/com/android/tools/r8/JvmTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
@@ -30,6 +30,16 @@
     super(state);
   }
 
+  private Path writeClassesToJar(Collection<Class<?>> classes) {
+    try {
+      Path archive = getState().getNewTempFolder().resolve("out.jar");
+      TestBase.writeClassesToJar(archive, classes);
+      return archive;
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   public static JvmTestBuilder create(TestState state) {
     return new JvmTestBuilder(state);
   }
@@ -77,14 +87,7 @@
 
   @Override
   public JvmTestBuilder addProgramClasses(Collection<Class<?>> classes) {
-    try {
-      Path out = getState().getNewTempFolder().resolve("out.jar");
-      TestBase.writeClassesToJar(out, classes);
-      addProgramFiles(out);
-      return self();
-    } catch (IOException e) {
-      throw new RuntimeException(e);
-    }
+    return addProgramFiles(writeClassesToJar(classes));
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/classlookup/ClasspathClassExtendsProgramClassTest.java b/src/test/java/com/android/tools/r8/classlookup/ClasspathClassExtendsProgramClassTest.java
new file mode 100644
index 0000000..b409e5a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classlookup/ClasspathClassExtendsProgramClassTest.java
@@ -0,0 +1,105 @@
+// 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.classlookup;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.Reference;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+// This test runs an R8 setup where a classpath class extends a program class and the program in
+// turn uses the classpath class. Having such a cyclic compilation setup is very odd, but with
+// sufficient keep rules it can work.
+@RunWith(Parameterized.class)
+public class ClasspathClassExtendsProgramClassTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ClasspathClassExtendsProgramClassTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  public static class ProgramClass {
+
+    public void foo() {
+      System.out.println("ProgramClass::foo");
+    }
+  }
+
+  static class Main {
+    public static void main(String[] args) {
+      ProgramClass object = args.length == 42 ? new ProgramClass() : new ClasspathClass();
+      object.foo();
+    }
+  }
+
+  public static class ClasspathIndirection extends ProgramClass {
+    // Intentionally empty.
+  }
+
+  public static class ClasspathClass extends ClasspathIndirection {
+
+    @Override
+    public void foo() {
+      System.out.println("ClasspathClass::foo");
+    }
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Main.class, ProgramClass.class)
+        .addRunClasspathFiles(compileClasspath())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("ClasspathClass::foo");
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters.getApiLevel())
+        .addProgramClasses(Main.class, ProgramClass.class)
+        .addClasspathClasses(ClasspathClass.class, ClasspathIndirection.class)
+        .addKeepMainRule(Main.class)
+        // Keep the method that is overridden by the classpath class.
+        .addKeepMethodRules(Reference.methodFromMethod(ProgramClass.class.getDeclaredMethod("foo")))
+        .addRunClasspathFiles(compileClasspath())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("ClasspathClass::foo");
+  }
+
+  private Path compileClasspath() throws java.io.IOException, CompilationFailedException {
+    return compile(
+        ImmutableList.of(ClasspathClass.class, ClasspathIndirection.class),
+        ImmutableList.of(ProgramClass.class));
+  }
+
+  private Path compile(Collection<Class<?>> compilationUnit, Collection<Class<?>> classpath)
+      throws java.io.IOException, CompilationFailedException {
+    if (parameters.isCfRuntime()) {
+      Path out = temp.newFolder().toPath().resolve("out.jar");
+      writeClassesToJar(out, compilationUnit);
+      return out;
+    }
+    return testForD8()
+        .setMinApi(parameters.getApiLevel())
+        .addProgramClasses(compilationUnit)
+        .addClasspathClasses(classpath)
+        .compile()
+        .writeToZip();
+  }
+}