Add tests for definitions of default methods on program and library
Bug: b/214382176
Change-Id: Ia4743f269b0110894fce9c95dbe01ae944af5f17
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
index ae8a263..c4d8683 100644
--- a/src/test/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -16,6 +16,7 @@
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.function.BiFunction;
+import java.util.function.Function;
public abstract class TestBuilder<RR extends TestRunResult<RR>, T extends TestBuilder<RR, T>> {
@@ -154,6 +155,14 @@
return addLibraryFiles(Arrays.asList(files));
}
+ public T addLibraryClassFileData(byte[]... classes) {
+ return addLibraryClassFileData(Arrays.asList(classes));
+ }
+
+ public T addLibraryClassFileData(Collection<byte[]> classes) {
+ return addByteCollectionToJar("library.jar", classes, this::addLibraryFiles);
+ }
+
public T addDefaultRuntimeLibrary(TestParameters parameters) {
if (parameters.getBackend() == Backend.DEX) {
addLibraryFiles(ToolHelper.getFirstSupportedAndroidJar(parameters.getApiLevel()));
@@ -181,9 +190,14 @@
}
public T addClasspathClassFileData(Collection<byte[]> classes) {
+ return addByteCollectionToJar("cp.jar", classes, this::addClasspathFiles);
+ }
+
+ private T addByteCollectionToJar(
+ String name, Collection<byte[]> classes, Function<Path, T> outputConsumer) {
Path out;
try {
- out = getState().getNewTempFolder().resolve("cp.jar");
+ out = getState().getNewTempFolder().resolve(name);
} catch (IOException e) {
throw new RuntimeException(e);
}
@@ -192,7 +206,7 @@
consumer.accept(ByteDataView.of(bytes), TestBase.extractClassDescriptor(bytes), null);
}
consumer.finished(null);
- return addClasspathFiles(out);
+ return outputConsumer.apply(out);
}
public final T addTestingAnnotationsAsProgramClasses() {
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java
index a476d13..719dbd8 100644
--- a/src/test/java/com/android/tools/r8/TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -36,6 +36,7 @@
import com.android.tools.r8.utils.codeinspector.ClassSubject;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ObjectArrays;
import java.io.File;
import java.io.IOException;
@@ -286,7 +287,8 @@
try {
AndroidApp.Builder appBuilder = AndroidApp.builder();
for (byte[] clazz : classes) {
- appBuilder.addClassProgramData(clazz, Origin.unknown());
+ appBuilder.addClassProgramData(
+ clazz, Origin.unknown(), ImmutableSet.of(TestBase.extractClassDescriptor(clazz)));
}
Path path = state.getNewTempFolder().resolve("runtime-classes.jar");
appBuilder.build().writeToZip(path, OutputMode.ClassFile);
diff --git a/src/test/java/com/android/tools/r8/TestParametersBuilder.java b/src/test/java/com/android/tools/r8/TestParametersBuilder.java
index cb24356..48e94f0 100644
--- a/src/test/java/com/android/tools/r8/TestParametersBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestParametersBuilder.java
@@ -232,6 +232,10 @@
return withApiLevelsEndingAtExcluding(AndroidApiLevel.L);
}
+ public TestParametersBuilder apiLevelWithDefaultMethodsSupport() {
+ return withApiLevelsStartingAtIncluding(TestBase.apiLevelWithDefaultInterfaceMethodsSupport());
+ }
+
public TestParametersBuilder withCustomRuntime(TestRuntime runtime) {
assert getUnfilteredAvailableRuntimes().noneMatch(r -> r == runtime);
customRuntimes.add(runtime);
diff --git a/src/test/java/com/android/tools/r8/resolution/interfacetargets/ProgramAndLibraryDefinitionTest.java b/src/test/java/com/android/tools/r8/resolution/interfacetargets/ProgramAndLibraryDefinitionTest.java
new file mode 100644
index 0000000..d0c2f5c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/resolution/interfacetargets/ProgramAndLibraryDefinitionTest.java
@@ -0,0 +1,231 @@
+// Copyright (c) 2022, 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.resolution.interfacetargets;
+
+import static com.android.tools.r8.resolution.interfacetargets.ProgramAndLibraryDefinitionTest.ClassTestParam.DEFINED_WITH_METHOD;
+import static com.android.tools.r8.resolution.interfacetargets.ProgramAndLibraryDefinitionTest.ClassTestParam.NOT_DEFINED;
+
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import java.util.List;
+import java.util.function.Consumer;
+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 ProgramAndLibraryDefinitionTest extends TestBase {
+
+ public enum ClassTestParam {
+ NOT_DEFINED,
+ DEFINED_NO_METHOD,
+ DEFINED_WITH_METHOD
+ }
+
+ private final TestParameters parameters;
+ private final ClassTestParam aInLibrary;
+ private final ClassTestParam bInLibrary;
+ private final ClassTestParam aInProgram;
+ private final ClassTestParam bInProgram;
+
+ @Parameters(name = "{0}, aInLibrary: {1}, bInLibrary: {2}, aInProgram: {3}, bInProgram: {4}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ getTestParameters()
+ .withDefaultDexRuntime()
+ .withDefaultCfRuntime()
+ .apiLevelWithDefaultMethodsSupport()
+ .build(),
+ ClassTestParam.values(),
+ ClassTestParam.values(),
+ ClassTestParam.values(),
+ ClassTestParam.values());
+ }
+
+ public ProgramAndLibraryDefinitionTest(
+ TestParameters parameters,
+ ClassTestParam aInLibrary,
+ ClassTestParam bInLibrary,
+ ClassTestParam aInProgram,
+ ClassTestParam bInProgram) {
+ this.parameters = parameters;
+ this.aInLibrary = aInLibrary;
+ this.bInLibrary = bInLibrary;
+ this.aInProgram = aInProgram;
+ this.bInProgram = bInProgram;
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ R8FullTestBuilder testBuilder =
+ testForR8(parameters.getBackend())
+ .addDefaultRuntimeLibrary(parameters)
+ .addProgramClasses(Main.class, Implementer.class, I.class)
+ .setMinApi(parameters.getApiLevel())
+ .addDontWarn(A.class, B.class)
+ .addKeepMainRule(Main.class)
+ .noMinification()
+ .allowUnusedDontWarnPatterns();
+ byte[] libraryA = getAFromClassTestParam(this.aInLibrary);
+ addIfNotNull(libraryA, testBuilder::addLibraryClassFileData);
+ byte[] libraryB = getBFromClassTestParam(bInLibrary);
+ addIfNotNull(libraryB, testBuilder::addLibraryClassFileData);
+ addIfNotNull(getAFromClassTestParam(aInProgram), testBuilder::addProgramClassFileData);
+ addIfNotNull(getBFromClassTestParam(bInProgram), testBuilder::addProgramClassFileData);
+ R8TestCompileResult compileResult = testBuilder.compile();
+ if (libraryA != null) {
+ compileResult.addRunClasspathClassFileData(libraryA);
+ }
+ if (libraryB != null) {
+ compileResult.addRunClasspathClassFileData(libraryB);
+ }
+ R8TestRunResult runResult = compileResult.run(parameters.getRuntime(), Main.class);
+ if (isExpectedToFailWithNoClassDefError()) {
+ runResult.assertFailureWithErrorThatThrows(NoClassDefFoundError.class);
+ } else if (isExpectedToFailWithNoSuchMethodError()) {
+ runResult.assertFailureWithErrorThatThrows(NoSuchMethodError.class);
+ } else if (isExpectedToFailWithICCE()) {
+ runResult.assertFailureWithErrorThatThrows(IncompatibleClassChangeError.class);
+ } else if (isDefinedOnAProgram() || (isDefinedOnALibrary() && !isAInProgram())) {
+ runResult.assertSuccessWithOutputLines("A::foo");
+ } else {
+ assert isDefinedOnBProgram() || (isDefinedOnBLibrary() && !isBInProgram());
+ runResult.assertSuccessWithOutputLines("B::foo");
+ }
+ }
+
+ private boolean isAInProgram() {
+ return aInProgram != NOT_DEFINED;
+ }
+
+ private boolean isBInProgram() {
+ return bInProgram != NOT_DEFINED;
+ }
+
+ private boolean isAInLibrary() {
+ return aInLibrary != NOT_DEFINED;
+ }
+
+ private boolean isBInLibrary() {
+ return bInLibrary != NOT_DEFINED;
+ }
+
+ private boolean isDefinedOnAProgram() {
+ return aInProgram == DEFINED_WITH_METHOD;
+ }
+
+ private boolean isDefinedOnBProgram() {
+ return bInProgram == DEFINED_WITH_METHOD;
+ }
+
+ private boolean isDefinedOnALibrary() {
+ return aInLibrary == DEFINED_WITH_METHOD;
+ }
+
+ private boolean isDefinedOnBLibrary() {
+ return bInLibrary == DEFINED_WITH_METHOD;
+ }
+
+ private boolean isExpectedToFailWithNoClassDefError() {
+ return (!isAInLibrary() && !isAInProgram()) || (!isBInLibrary() && !isBInProgram());
+ }
+
+ private boolean isExpectedToFailWithNoSuchMethodError() {
+ boolean notDefinedInProgram = !isDefinedOnAProgram() && !isDefinedOnBProgram();
+ boolean notDefinedInLibrary = !isDefinedOnALibrary() && !isDefinedOnBLibrary();
+ if (notDefinedInLibrary && notDefinedInProgram) {
+ return true;
+ }
+ if (notDefinedInProgram) {
+ // TODO(b/214382176): Currently, a program definition will shadow the library definition and
+ // R8 will optimize the interfaces away.
+ if (isDefinedOnALibrary() && isDefinedOnBLibrary()) {
+ return isAInProgram() && isBInProgram();
+ } else if (isDefinedOnALibrary()) {
+ return isAInProgram();
+ } else {
+ assert isDefinedOnBLibrary();
+ return isBInProgram();
+ }
+ }
+ return false;
+ }
+
+ private boolean isExpectedToFailWithICCE() {
+ if (isDefinedOnAProgram() && isDefinedOnBProgram()) {
+ return true;
+ }
+ if (!isAInProgram() && !isBInProgram()) {
+ return isDefinedOnALibrary() && isDefinedOnBLibrary();
+ }
+ if (isDefinedOnALibrary() && !isAInProgram() && isDefinedOnBProgram()) {
+ return true;
+ }
+ if (isDefinedOnBLibrary() && !isBInProgram() && isDefinedOnAProgram()) {
+ return true;
+ }
+ return false;
+ }
+
+ private void addIfNotNull(byte[] clazz, Consumer<byte[]> consumer) {
+ if (clazz != null) {
+ consumer.accept(clazz);
+ }
+ }
+
+ private byte[] getAFromClassTestParam(ClassTestParam param) throws Exception {
+ switch (param) {
+ case NOT_DEFINED:
+ return null;
+ case DEFINED_NO_METHOD:
+ return transformer(A.class).removeMethods(MethodPredicate.onName("foo")).transform();
+ default:
+ assert param == DEFINED_WITH_METHOD;
+ return transformer(A.class).transform();
+ }
+ }
+
+ private byte[] getBFromClassTestParam(ClassTestParam param) throws Exception {
+ switch (param) {
+ case NOT_DEFINED:
+ return null;
+ case DEFINED_NO_METHOD:
+ return transformer(B.class).removeMethods(MethodPredicate.onName("bar")).transform();
+ default:
+ assert param == DEFINED_WITH_METHOD;
+ return transformer(B.class).renameMethod(MethodPredicate.onName("bar"), "foo").transform();
+ }
+ }
+
+ public interface A {
+
+ default void foo() {
+ System.out.println("A::foo");
+ }
+ }
+
+ public interface B {
+
+ default void /* foo */ bar() { // Will be renamed to foo.
+ System.out.println("B::foo");
+ }
+ }
+
+ public interface I extends A {}
+
+ public static class Implementer implements I, B {}
+
+ public static class Main {
+
+ public static void main(String[] args) {
+ new Implementer().foo();
+ }
+ }
+}