|  | // Copyright (c) 2023, 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.setMockApiLevelForMethod; | 
|  | import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; | 
|  | import static org.hamcrest.MatcherAssert.assertThat; | 
|  | import static org.junit.Assert.assertEquals; | 
|  | import static org.junit.Assert.assertFalse; | 
|  | import static org.junit.Assert.assertTrue; | 
|  | import static org.junit.Assume.assumeFalse; | 
|  | import static org.junit.Assume.assumeTrue; | 
|  |  | 
|  | import com.android.tools.r8.CompilationMode; | 
|  | import com.android.tools.r8.D8TestCompileResult; | 
|  | import com.android.tools.r8.SingleTestRunResult; | 
|  | import com.android.tools.r8.TestBase; | 
|  | import com.android.tools.r8.TestCompilerBuilder; | 
|  | import com.android.tools.r8.TestParameters; | 
|  | import com.android.tools.r8.TestParametersCollection; | 
|  | import com.android.tools.r8.ThrowableConsumer; | 
|  | import com.android.tools.r8.synthesis.globals.GlobalSyntheticsTestingConsumer; | 
|  | import com.android.tools.r8.testing.AndroidBuildVersion; | 
|  | import com.android.tools.r8.utils.AndroidApiLevel; | 
|  | import com.android.tools.r8.utils.BooleanUtils; | 
|  | import com.android.tools.r8.utils.ThrowingConsumer; | 
|  | import com.android.tools.r8.utils.codeinspector.CodeInspector; | 
|  | import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector; | 
|  | import java.util.ArrayList; | 
|  | import java.util.List; | 
|  | 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; | 
|  |  | 
|  | /*** | 
|  | * This is a replication of b/268596049. | 
|  | */ | 
|  | @RunWith(Parameterized.class) | 
|  | public class ApiModelD8GradleSetupTest extends TestBase { | 
|  |  | 
|  | private static final AndroidApiLevel mockApiLevelOne = AndroidApiLevel.M; | 
|  | private static final AndroidApiLevel mockApiLevelTwo = AndroidApiLevel.O; | 
|  | private static final AndroidApiLevel mockApiLevelThree = AndroidApiLevel.R; | 
|  |  | 
|  | @Parameter() public TestParameters parameters; | 
|  |  | 
|  | @Parameters(name = "{0}") | 
|  | public static TestParametersCollection data() { | 
|  | return getTestParameters().withAllRuntimesAndApiLevels().build(); | 
|  | } | 
|  |  | 
|  | private void setupTestBuilder(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) throws Exception { | 
|  | testBuilder | 
|  | .addLibraryClasses(LibraryClassOne.class, LibraryClassTwo.class, LibraryClassThree.class) | 
|  | .addDefaultRuntimeLibrary(parameters) | 
|  | .setMinApi(parameters) | 
|  | .apply(setMockApiLevelForClass(LibraryClassOne.class, mockApiLevelOne)) | 
|  | .apply( | 
|  | setMockApiLevelForMethod( | 
|  | LibraryClassOne.class.getDeclaredMethod("foo"), mockApiLevelOne)) | 
|  | .apply(setMockApiLevelForClass(LibraryClassTwo.class, mockApiLevelTwo)) | 
|  | .apply( | 
|  | setMockApiLevelForMethod( | 
|  | LibraryClassTwo.class.getDeclaredMethod("bar"), mockApiLevelTwo)) | 
|  | .apply(setMockApiLevelForClass(LibraryClassThree.class, mockApiLevelThree)) | 
|  | .apply( | 
|  | setMockApiLevelForMethod( | 
|  | LibraryClassThree.class.getDeclaredMethod("baz"), mockApiLevelThree)) | 
|  | .apply(ApiModelingTestHelper::enableApiCallerIdentification) | 
|  | .apply(ApiModelingTestHelper::enableOutliningOfMethods) | 
|  | .apply(ApiModelingTestHelper::enableStubbingOfClasses); | 
|  | } | 
|  |  | 
|  | private boolean willHorizontallyMergeOutlines() { | 
|  | // After api level mockApiLevelTwo we only have a single outline and therefore will not merge. | 
|  | return parameters.getApiLevel().isLessThan(mockApiLevelTwo); | 
|  | } | 
|  |  | 
|  | private boolean willStubLibraryClassThree() { | 
|  | return parameters.getApiLevel().isLessThan(mockApiLevelThree); | 
|  | } | 
|  |  | 
|  | public AndroidApiLevel getApiLevelForRuntime() { | 
|  | return parameters.isCfRuntime() | 
|  | ? AndroidApiLevel.B | 
|  | : parameters.getRuntime().asDex().maxSupportedApiLevel(); | 
|  | } | 
|  |  | 
|  | public boolean addToBootClasspath(Class<?> clazz) { | 
|  | if (clazz == LibraryClassOne.class) { | 
|  | return getApiLevelForRuntime().isGreaterThanOrEqualTo(mockApiLevelOne); | 
|  | } | 
|  | if (clazz == LibraryClassTwo.class) { | 
|  | return getApiLevelForRuntime().isGreaterThanOrEqualTo(mockApiLevelTwo); | 
|  | } | 
|  | assert clazz == LibraryClassThree.class; | 
|  | return getApiLevelForRuntime().isGreaterThanOrEqualTo(mockApiLevelThree); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void testReference() throws Exception { | 
|  | assumeTrue(parameters.isCfRuntime()); | 
|  | testForJvm() | 
|  | .addProgramClasses(Main.class, ProgramClassOne.class, ProgramClassTwo.class) | 
|  | .addAndroidBuildVersion(AndroidApiLevel.B) | 
|  | .addLibraryClasses(LibraryClassOne.class, LibraryClassTwo.class, LibraryClassThree.class) | 
|  | .run(parameters.getRuntime(), Main.class) | 
|  | .apply(this::checkOutput); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void testD8DebugWithMerge() throws Exception { | 
|  | assumeTrue(parameters.isDexRuntime()); | 
|  | testD8( | 
|  | CompilationMode.DEBUG, | 
|  | this::inspectNumberOfClassesFromOutput, | 
|  | HorizontallyMergedClassesInspector::assertNoClassesMerged); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void testD8ReleaseForApiLevelWithOutlining() throws Exception { | 
|  | assumeTrue(parameters.isDexRuntime()); | 
|  | assumeTrue(willHorizontallyMergeOutlines()); | 
|  | testD8( | 
|  | CompilationMode.RELEASE, | 
|  | this::inspectNumberOfClassesFromOutput, | 
|  | HorizontallyMergedClassesInspector::assertNoClassesMerged); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void testD8ReleaseForApiLevelWithNoOutlining() throws Exception { | 
|  | assumeTrue(parameters.isDexRuntime()); | 
|  | assumeFalse(willHorizontallyMergeOutlines()); | 
|  | testD8( | 
|  | CompilationMode.RELEASE, | 
|  | this::inspectNumberOfClassesFromOutput, | 
|  | HorizontallyMergedClassesInspector::assertNoClassesMerged); | 
|  | } | 
|  |  | 
|  | private void testD8( | 
|  | CompilationMode mode, | 
|  | ThrowingConsumer<CodeInspector, Exception> inspect, | 
|  | ThrowableConsumer<HorizontallyMergedClassesInspector> horizontallyMergingConsumer) | 
|  | throws Exception { | 
|  | GlobalSyntheticsTestingConsumer globals = new GlobalSyntheticsTestingConsumer(); | 
|  | D8TestCompileResult compileResultProgramClass = | 
|  | compileIntermediate(mode, globals, ProgramClassOne.class); | 
|  | D8TestCompileResult compileResultProgramClassTwo = | 
|  | compileIntermediate(mode, globals, ProgramClassTwo.class); | 
|  | D8TestCompileResult compileResultMain = | 
|  | compileIntermediate( | 
|  | mode, globals, Main.class, ProgramClassOne.class, ProgramClassTwo.class); | 
|  |  | 
|  | if (willStubLibraryClassThree()) { | 
|  | assertTrue(globals.isSingleGlobal()); | 
|  | } else { | 
|  | assertFalse(globals.hasGlobals()); | 
|  | } | 
|  |  | 
|  | List<Class<?>> bootClassPath = new ArrayList<>(); | 
|  | if (addToBootClasspath(LibraryClassOne.class)) { | 
|  | bootClassPath.add(LibraryClassOne.class); | 
|  | } | 
|  | if (addToBootClasspath(LibraryClassTwo.class)) { | 
|  | bootClassPath.add(LibraryClassTwo.class); | 
|  | } | 
|  | if (addToBootClasspath(LibraryClassThree.class)) { | 
|  | bootClassPath.add(LibraryClassThree.class); | 
|  | } | 
|  |  | 
|  | testForD8() | 
|  | .setMode(mode) | 
|  | .setUseDefaultRuntimeLibrary(false) | 
|  | .apply(b -> b.getBuilder().addGlobalSyntheticsResourceProviders(globals.getProviders())) | 
|  | .addProgramFiles( | 
|  | compileResultProgramClass.writeToZip(), | 
|  | compileResultProgramClassTwo.writeToZip(), | 
|  | compileResultMain.writeToZip()) | 
|  | .setMinApi(parameters) | 
|  | .addAndroidBuildVersion(getApiLevelForRuntime()) | 
|  | .addHorizontallyMergedClassesInspector(horizontallyMergingConsumer) | 
|  | .compile() | 
|  | .inspect(inspect) | 
|  | .addBootClasspathFiles( | 
|  | buildOnDexRuntime(parameters, bootClassPath.toArray(new Class<?>[0]))) | 
|  | .run(parameters.getRuntime(), Main.class) | 
|  | .apply(this::checkOutput); | 
|  | } | 
|  |  | 
|  | private D8TestCompileResult compileIntermediate( | 
|  | CompilationMode mode, | 
|  | GlobalSyntheticsTestingConsumer globals, | 
|  | Class<?> programClass, | 
|  | Class<?>... classpathClass) | 
|  | throws Exception { | 
|  | return testForD8(parameters.getBackend()) | 
|  | .setMode(mode) | 
|  | .setIntermediate(true) | 
|  | .addProgramClasses(programClass) | 
|  | .addClasspathClasses(classpathClass) | 
|  | .apply(this::setupTestBuilder) | 
|  | .apply(b -> b.getBuilder().setGlobalSyntheticsConsumer(globals)) | 
|  | .compile(); | 
|  | } | 
|  |  | 
|  | private void checkOutput(SingleTestRunResult<?> runResult) { | 
|  | runResult.assertSuccessWithOutputLines( | 
|  | addToBootClasspath(LibraryClassOne.class) ? "LibraryClassOne::foo" : "Not calling foo()", | 
|  | addToBootClasspath(LibraryClassTwo.class) ? "LibraryClassTwo::bar" : "Not calling bar()", | 
|  | addToBootClasspath(LibraryClassThree.class) | 
|  | ? "LibraryClassThree::baz" | 
|  | : "Not calling baz()"); | 
|  | } | 
|  |  | 
|  | private void inspectNumberOfClassesFromOutput(CodeInspector inspector) { | 
|  | // We always have Main, ProgramClassOne, ProgramClassTwo and AndroidBuildVersion as program | 
|  | // classes. Depending on the api a number of synthetic classes. | 
|  | int numberOfClasses = | 
|  | 4 | 
|  | + (willStubLibraryClassThree() ? 2 : 0) | 
|  | + BooleanUtils.intValue(parameters.getApiLevel().isLessThan(mockApiLevelTwo)) | 
|  | + BooleanUtils.intValue(parameters.getApiLevel().isLessThan(mockApiLevelOne)); | 
|  | assertEquals(numberOfClasses, inspector.allClasses().size()); | 
|  | assertThat(inspector.clazz(Main.class), isPresent()); | 
|  | assertThat(inspector.clazz(ProgramClassOne.class), isPresent()); | 
|  | } | 
|  |  | 
|  | // Will be present from api level 23 | 
|  | public static class LibraryClassOne { | 
|  |  | 
|  | public static void foo() { | 
|  | System.out.println("LibraryClassOne::foo"); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Will be present from api level 26 | 
|  | public static class LibraryClassTwo { | 
|  |  | 
|  | public static void bar() { | 
|  | System.out.println("LibraryClassTwo::bar"); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Will be present form api level 30 | 
|  | public static class LibraryClassThree { | 
|  |  | 
|  | public void baz() { | 
|  | System.out.println("LibraryClassThree::baz"); | 
|  | } | 
|  | } | 
|  |  | 
|  | public static class ProgramClassOne { | 
|  |  | 
|  | public static void callOneAndTwo() { | 
|  | if (AndroidBuildVersion.VERSION >= 23) { | 
|  | LibraryClassOne.foo(); | 
|  | } else { | 
|  | System.out.println("Not calling foo()"); | 
|  | } | 
|  | if (AndroidBuildVersion.VERSION >= 26) { | 
|  | LibraryClassTwo.bar(); | 
|  | } else { | 
|  | System.out.println("Not calling bar()"); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | public static class ProgramClassTwo extends LibraryClassThree {} | 
|  |  | 
|  | public static class Main { | 
|  |  | 
|  | public static void main(String[] args) { | 
|  | ProgramClassOne.callOneAndTwo(); | 
|  | if (AndroidBuildVersion.VERSION >= 30) { | 
|  | new ProgramClassTwo().baz(); | 
|  | } else { | 
|  | System.out.println("Not calling baz()"); | 
|  | } | 
|  | } | 
|  | } | 
|  | } |