[ApiModel] Add test for not outlining indirect call to library method
Bug: b/254510678
Change-Id: I92104355a89dca3d85cac08a8702d40736f034ff
diff --git a/src/test/java/com/android/tools/r8/JvmTestBuilder.java b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
index 8b56639..49ce60e 100644
--- a/src/test/java/com/android/tools/r8/JvmTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
@@ -6,6 +6,7 @@
import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.errors.Unimplemented;
import com.android.tools.r8.testing.AndroidBuildVersion;
+import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.FileUtils;
import com.google.common.collect.ObjectArrays;
@@ -189,7 +190,11 @@
}
public JvmTestBuilder addAndroidBuildVersion() {
- addVmArguments("-D" + AndroidBuildVersion.PROPERTY + "=10000");
+ return addAndroidBuildVersion(AndroidApiLevel.ANDROID_PLATFORM);
+ }
+
+ public JvmTestBuilder addAndroidBuildVersion(AndroidApiLevel apiLevel) {
+ addVmArguments("-D" + AndroidBuildVersion.PROPERTY + "=" + apiLevel.getLevel());
return addProgramClasses(AndroidBuildVersion.class);
}
}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelIndirectTargetWithDifferentApiLevelTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelIndirectTargetWithDifferentApiLevelTest.java
new file mode 100644
index 0000000..f859e30
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelIndirectTargetWithDifferentApiLevelTest.java
@@ -0,0 +1,199 @@
+// 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.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForMethod;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.SingleTestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompileResult;
+import com.android.tools.r8.TestCompilerBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.testing.AndroidBuildVersion;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.Collections;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ApiModelIndirectTargetWithDifferentApiLevelTest extends TestBase {
+
+ private final AndroidApiLevel ifaceApiLevel = AndroidApiLevel.M;
+ private final AndroidApiLevel classApiLevel = AndroidApiLevel.M;
+ private final AndroidApiLevel classMethodApiLevel = AndroidApiLevel.O_MR1;
+
+ @Parameter public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+ }
+
+ private boolean isGreaterOrEqualToIfaceMockLevel() {
+ return parameters.getApiLevel().isGreaterThanOrEqualTo(ifaceApiLevel);
+ }
+
+ private boolean isGreaterOrEqualToClassMethodMockLevel() {
+ return parameters.getApiLevel().isGreaterThanOrEqualTo(classMethodApiLevel);
+ }
+
+ private void setupTestBuilder(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) throws Exception {
+ testBuilder
+ .addProgramClasses(Main.class, ProgramJoiner.class)
+ .addLibraryClasses(LibraryClass.class, LibraryInterface.class)
+ .addDefaultRuntimeLibrary(parameters)
+ .setMinApi(parameters.getApiLevel())
+ .addAndroidBuildVersion(parameters.getApiLevel())
+ .apply(ApiModelingTestHelper::enableStubbingOfClasses)
+ .apply(setMockApiLevelForDefaultInstanceInitializer(LibraryClass.class, classApiLevel))
+ .apply(setMockApiLevelForClass(LibraryClass.class, classApiLevel))
+ .apply(
+ setMockApiLevelForMethod(
+ LibraryClass.class.getDeclaredMethod("foo"), classMethodApiLevel))
+ .apply(setMockApiLevelForClass(LibraryInterface.class, ifaceApiLevel))
+ .apply(
+ setMockApiLevelForMethod(
+ LibraryInterface.class.getDeclaredMethod("foo"), ifaceApiLevel));
+ }
+
+ private void setupRunEnvironment(TestCompileResult<?, ?> compileResult) {
+ compileResult
+ .applyIf(
+ isGreaterOrEqualToClassMethodMockLevel(),
+ b -> b.addRunClasspathClasses(LibraryClass.class, LibraryInterface.class))
+ .applyIf(
+ !isGreaterOrEqualToClassMethodMockLevel() && isGreaterOrEqualToIfaceMockLevel(),
+ b ->
+ b.addRunClasspathClasses(LibraryInterface.class)
+ .addRunClasspathClassFileData(
+ transformer(LibraryClass.class).removeMethodsWithName("foo").transform()));
+ }
+
+ @Test
+ public void testReference() throws Exception {
+ assumeTrue(parameters.isCfRuntime() && parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+ testForJvm()
+ .addProgramClasses(Main.class, ProgramJoiner.class)
+ .addAndroidBuildVersion(parameters.getApiLevel())
+ .addLibraryClasses(LibraryClass.class, LibraryInterface.class)
+ .run(parameters.getRuntime(), Main.class)
+ .apply(result -> checkOutput(result, false));
+ }
+
+ @Test
+ public void testD8Debug() throws Exception {
+ testForD8(parameters.getBackend())
+ .setMode(CompilationMode.DEBUG)
+ .apply(this::setupTestBuilder)
+ .compile()
+ .apply(this::setupRunEnvironment)
+ .inspect(this::inspect)
+ .run(parameters.getRuntime(), Main.class)
+ .apply(result -> checkOutput(result, false));
+ }
+
+ @Test
+ public void testD8Release() throws Exception {
+ testForD8(parameters.getBackend())
+ .setMode(CompilationMode.RELEASE)
+ .apply(this::setupTestBuilder)
+ .compile()
+ .apply(this::setupRunEnvironment)
+ .inspect(this::inspect)
+ .run(parameters.getRuntime(), Main.class)
+ .apply(result -> checkOutput(result, false));
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .apply(this::setupTestBuilder)
+ .addKeepMainRule(Main.class)
+ .addKeepClassAndMembersRules(ProgramJoiner.class)
+ .compile()
+ .inspect(this::inspect)
+ .apply(this::setupRunEnvironment)
+ .run(parameters.getRuntime(), Main.class)
+ .apply(result -> checkOutput(result, true));
+ }
+
+ private void checkOutput(SingleTestRunResult<?> runResult, boolean isR8) {
+ if (isGreaterOrEqualToClassMethodMockLevel()) {
+ runResult.assertSuccessWithOutputLines("LibraryClass::foo");
+ } else if (isGreaterOrEqualToIfaceMockLevel()) {
+ if (isR8) {
+ runResult.assertFailureWithErrorThatThrows(NoSuchMethodError.class);
+ } else {
+ runResult.assertFailureWithErrorThatThrows(AbstractMethodError.class);
+ }
+ } else if (isR8 && parameters.isCfRuntime()) {
+ // TODO(b/254510678): R8 should not rebind to the library method.
+ runResult.assertFailureWithErrorThatThrows(NoClassDefFoundError.class);
+ } else {
+ runResult.assertSuccessWithOutputLines("Hello World");
+ }
+ runResult.applyIf(
+ !isGreaterOrEqualToIfaceMockLevel()
+ && parameters.isDexRuntime()
+ && parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V7_0_0),
+ result -> result.assertStderrMatches(not(containsString("This dex file is invalid"))));
+ }
+
+ private void inspect(CodeInspector inspector) throws Exception {
+ // TODO(b/254510678): We should outline the call to ProgramJoiner.foo.
+ verifyThat(
+ inspector,
+ parameters,
+ Reference.method(
+ Reference.classFromClass(ProgramJoiner.class),
+ "foo",
+ Collections.emptyList(),
+ null))
+ .isNotOutlinedFrom(Main.class.getDeclaredMethod("main", String[].class));
+ }
+
+ // Only present from api level 23.
+ public static class LibraryClass {
+
+ // Only present from api level 27;
+ public void foo() {
+ System.out.println("LibraryClass::foo");
+ }
+ }
+
+ // Present from 23
+ public interface LibraryInterface {
+
+ // Present from 23
+ void foo();
+ }
+
+ public static class ProgramJoiner extends LibraryClass implements LibraryInterface {}
+
+ public static class Main {
+
+ public static void main(String[] args) {
+ if (AndroidBuildVersion.VERSION >= 23) {
+ new ProgramJoiner().foo();
+ } else {
+ System.out.println("Hello World");
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelIndirectTargetWithSameApiLevelTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelIndirectTargetWithSameApiLevelTest.java
new file mode 100644
index 0000000..a0cc571
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelIndirectTargetWithSameApiLevelTest.java
@@ -0,0 +1,181 @@
+// 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.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForClass;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForDefaultInstanceInitializer;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForMethod;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.SingleTestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompileResult;
+import com.android.tools.r8.TestCompilerBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.testing.AndroidBuildVersion;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.Collections;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+// The difference between this test and the ApiModelIndirectTargetWithDifferentApiLevelTest is
+// what we should rebind to. If there is a method definition in the class hierarchy and it has
+// the same api-level as one in an interface we should still pick the class.
+@RunWith(Parameterized.class)
+public class ApiModelIndirectTargetWithSameApiLevelTest extends TestBase {
+
+ private final AndroidApiLevel mockApiLevel = AndroidApiLevel.M;
+
+ @Parameter public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+ }
+
+ private boolean isGreaterOrEqualToMockLevel() {
+ return parameters.getApiLevel().isGreaterThanOrEqualTo(mockApiLevel);
+ }
+
+ private void setupTestBuilder(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) throws Exception {
+ testBuilder
+ .addProgramClasses(Main.class, ProgramJoiner.class)
+ .addLibraryClasses(LibraryClass.class, LibraryInterface.class)
+ .addDefaultRuntimeLibrary(parameters)
+ .setMinApi(parameters.getApiLevel())
+ .addAndroidBuildVersion(parameters.getApiLevel())
+ .apply(ApiModelingTestHelper::enableStubbingOfClasses)
+ .apply(setMockApiLevelForDefaultInstanceInitializer(LibraryClass.class, mockApiLevel))
+ .apply(setMockApiLevelForClass(LibraryClass.class, mockApiLevel))
+ .apply(setMockApiLevelForMethod(LibraryClass.class.getDeclaredMethod("foo"), mockApiLevel))
+ .apply(setMockApiLevelForClass(LibraryInterface.class, mockApiLevel))
+ .apply(
+ setMockApiLevelForMethod(
+ LibraryInterface.class.getDeclaredMethod("foo"), mockApiLevel));
+ }
+
+ private void setupRunEnvironment(TestCompileResult<?, ?> compileResult) {
+ compileResult.applyIf(
+ isGreaterOrEqualToMockLevel(),
+ b -> b.addRunClasspathClasses(LibraryClass.class, LibraryInterface.class));
+ }
+
+ @Test
+ public void testReference() throws Exception {
+ assumeTrue(parameters.isCfRuntime() && parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+ testForJvm()
+ .addProgramClasses(Main.class, ProgramJoiner.class)
+ .addAndroidBuildVersion(parameters.getApiLevel())
+ .addLibraryClasses(LibraryClass.class, LibraryInterface.class)
+ .run(parameters.getRuntime(), Main.class)
+ .apply(result -> checkOutput(result, false));
+ }
+
+ @Test
+ public void testD8Debug() throws Exception {
+ testForD8(parameters.getBackend())
+ .setMode(CompilationMode.DEBUG)
+ .apply(this::setupTestBuilder)
+ .compile()
+ .apply(this::setupRunEnvironment)
+ .run(parameters.getRuntime(), Main.class)
+ .apply(result -> checkOutput(result, false))
+ .inspect(this::inspect);
+ }
+
+ @Test
+ public void testD8Release() throws Exception {
+ testForD8(parameters.getBackend())
+ .setMode(CompilationMode.RELEASE)
+ .apply(this::setupTestBuilder)
+ .compile()
+ .apply(this::setupRunEnvironment)
+ .run(parameters.getRuntime(), Main.class)
+ .apply(result -> checkOutput(result, false))
+ .inspect(this::inspect);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .apply(this::setupTestBuilder)
+ .addKeepMainRule(Main.class)
+ .addKeepClassAndMembersRules(ProgramJoiner.class)
+ .compile()
+ .inspect(this::inspect)
+ .apply(this::setupRunEnvironment)
+ .run(parameters.getRuntime(), Main.class)
+ .apply(result -> checkOutput(result, true));
+ }
+
+ private void checkOutput(SingleTestRunResult<?> runResult, boolean isR8) {
+ if (isGreaterOrEqualToMockLevel()) {
+ runResult.assertSuccessWithOutputLines("LibraryClass::foo");
+ } else if (isR8 && parameters.isCfRuntime()) {
+ // TODO(b/254510678): R8 should not rebind to the library method.
+ runResult.assertFailureWithErrorThatThrows(NoClassDefFoundError.class);
+ } else {
+ runResult.assertSuccessWithOutputLines("Hello World");
+ }
+ runResult.applyIf(
+ !isGreaterOrEqualToMockLevel()
+ && parameters.isDexRuntime()
+ && parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V7_0_0),
+ result -> result.assertStderrMatches(not(containsString("This dex file is invalid"))));
+ }
+
+ private void inspect(CodeInspector inspector) throws Exception {
+ // TODO(b/254510678): We should outline the call to ProgramClass.foo.
+ verifyThat(
+ inspector,
+ parameters,
+ Reference.method(
+ Reference.classFromClass(ProgramJoiner.class),
+ "foo",
+ Collections.emptyList(),
+ null))
+ .isNotOutlinedFrom(Main.class.getDeclaredMethod("main", String[].class));
+ }
+
+ // Only present from api level 23.
+ public static class LibraryClass {
+
+ // Only present from api level 27;
+ public void foo() {
+ System.out.println("LibraryClass::foo");
+ }
+ }
+
+ // Present from 23
+ public interface LibraryInterface {
+
+ // Present from 23
+ void foo();
+ }
+
+ public static class ProgramJoiner extends LibraryClass implements LibraryInterface {}
+
+ public static class Main {
+
+ public static void main(String[] args) {
+ if (AndroidBuildVersion.VERSION >= 23) {
+ new ProgramJoiner().foo();
+ } else {
+ System.out.println("Hello World");
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
index 016a147..20d345a 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelingTestHelper.java
@@ -213,6 +213,11 @@
}
static ApiModelingMethodVerificationHelper verifyThat(
+ CodeInspector inspector, TestParameters parameters, MethodReference method) {
+ return new ApiModelingMethodVerificationHelper(inspector, parameters, method);
+ }
+
+ static ApiModelingMethodVerificationHelper verifyThat(
CodeInspector inspector, TestParameters parameters, Constructor method) {
return new ApiModelingMethodVerificationHelper(
inspector, parameters, Reference.methodFromMethod(method));