| // 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.desugar.backports; |
| |
| 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.assertNotNull; |
| import static org.junit.Assert.assertNull; |
| import static org.junit.Assume.assumeTrue; |
| |
| import com.android.tools.r8.ByteDataView; |
| import com.android.tools.r8.DexIndexedConsumer; |
| import com.android.tools.r8.DiagnosticsHandler; |
| import com.android.tools.r8.GenerateMainDexListRunResult; |
| import com.android.tools.r8.OutputMode; |
| import com.android.tools.r8.TestBase; |
| import com.android.tools.r8.TestParameters; |
| import com.android.tools.r8.TestParametersCollection; |
| import com.android.tools.r8.ToolHelper; |
| import com.android.tools.r8.desugar.backports.AbstractBackportTest.MiniAssert; |
| import com.android.tools.r8.origin.Origin; |
| import com.android.tools.r8.references.ClassReference; |
| import com.android.tools.r8.references.MethodReference; |
| import com.android.tools.r8.references.Reference; |
| import com.android.tools.r8.synthesis.SyntheticItemsTestUtils; |
| import com.android.tools.r8.utils.AndroidApiLevel; |
| import com.android.tools.r8.utils.AndroidApp; |
| import com.android.tools.r8.utils.ListUtils; |
| import com.android.tools.r8.utils.StringUtils; |
| 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.Sets; |
| import com.google.common.collect.Streams; |
| import java.nio.file.Path; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| |
| @RunWith(Parameterized.class) |
| public class BackportMainDexTest extends TestBase { |
| |
| static final String EXPECTED = StringUtils.lines("Hello, world"); |
| |
| static final List<Class<?>> CLASSES = |
| ImmutableList.of(MiniAssert.class, TestClass.class, User1.class, User2.class); |
| |
| static final List<Class<?>> MAIN_DEX_LIST_CLASSES = |
| ImmutableList.of(MiniAssert.class, TestClass.class, User2.class); |
| |
| private final TestParameters parameters; |
| |
| @Parameterized.Parameters(name = "{0}") |
| public static TestParametersCollection data() { |
| return getTestParameters().withAllRuntimes().withApiLevel(AndroidApiLevel.J).build(); |
| } |
| |
| public BackportMainDexTest(TestParameters parameters) { |
| this.parameters = parameters; |
| } |
| |
| private String[] getRunArgs() { |
| // Only call User1 methods on runtimes with native multidex. |
| if (parameters.isCfRuntime() |
| || parameters |
| .getRuntime() |
| .asDex() |
| .getMinApiLevel() |
| .isGreaterThanOrEqualTo(apiLevelWithNativeMultiDexSupport())) { |
| return new String[] {User1.class.getTypeName()}; |
| } |
| return new String[0]; |
| } |
| |
| @Test |
| public void testJvm() throws Exception { |
| assumeTrue(parameters.isCfRuntime()); |
| testForJvm() |
| .addProgramClasses(CLASSES) |
| .run(parameters.getRuntime(), TestClass.class, getRunArgs()) |
| .assertSuccessWithOutput(EXPECTED); |
| } |
| |
| private GenerateMainDexListRunResult traceMainDex( |
| Collection<Class<?>> classes, Collection<Path> files) throws Exception { |
| return testForMainDexListGenerator() |
| .addProgramClasses(classes) |
| .addProgramFiles(files) |
| .addLibraryFiles(ToolHelper.getFirstSupportedAndroidJar(parameters.getApiLevel())) |
| .addMainDexRules(keepMainProguardConfiguration(TestClass.class)) |
| .run(); |
| } |
| |
| @Test |
| public void testMainDexTracingCf() throws Exception { |
| assumeTrue(parameters.isDexRuntime()); |
| GenerateMainDexListRunResult mainDexListFromCf = traceMainDex(CLASSES, Collections.emptyList()); |
| assertEquals( |
| ListUtils.map(MAIN_DEX_LIST_CLASSES, Reference::classFromClass), |
| mainDexListFromCf.getMainDexList()); |
| } |
| |
| @Test |
| public void testMainDexTracingDex() throws Exception { |
| assumeTrue(parameters.isDexRuntime()); |
| Path out = |
| testForD8() |
| .addProgramClasses(CLASSES) |
| .setMinApi(parameters.getApiLevel()) |
| .compile() |
| .writeToZip(); |
| GenerateMainDexListRunResult mainDexListFromDex = |
| traceMainDex(Collections.emptyList(), Collections.singleton(out)); |
| assertEquals( |
| Streams.concat( |
| MAIN_DEX_LIST_CLASSES.stream().map(Reference::classFromClass), |
| getMainDexExpectedSynthetics().stream().map(MethodReference::getHolderClass)) |
| .collect(Collectors.toSet()), |
| ImmutableSet.copyOf(mainDexListFromDex.getMainDexList())); |
| } |
| |
| @Test |
| public void testMainDexTracingDexIntermediates() throws Exception { |
| assumeTrue(parameters.isDexRuntime()); |
| Path out = |
| testForD8() |
| .addProgramClasses(CLASSES) |
| // Setting intermediate will annotate synthetics, which should not cause types in those |
| // to become main-dex included. |
| .setIntermediate(true) |
| .setMinApi(parameters.getApiLevel()) |
| .compile() |
| .writeToZip(); |
| GenerateMainDexListRunResult mainDexListFromDex = |
| traceMainDex(Collections.emptyList(), Collections.singleton(out)); |
| // Compiling in intermediate will share the synthetics within the context types so there is one |
| // synthetic class per backport in User2: Character.compare and Integer.compare. |
| assertEquals(MAIN_DEX_LIST_CLASSES.size() + 2, mainDexListFromDex.getMainDexList().size()); |
| } |
| |
| @Test |
| public void testD8() throws Exception { |
| assumeTrue(parameters.isDexRuntime()); |
| MainDexConsumer mainDexConsumer = new MainDexConsumer(); |
| testForD8(parameters.getBackend()) |
| .addProgramClasses(CLASSES) |
| .setMinApi(parameters.getApiLevel()) |
| .addMainDexRules(keepMainProguardConfiguration(TestClass.class)) |
| .setProgramConsumer(mainDexConsumer) |
| .compile() |
| .inspect(this::checkExpectedSynthetics) |
| .run(parameters.getRuntime(), TestClass.class, getRunArgs()) |
| .assertSuccessWithOutput(EXPECTED); |
| checkMainDex(mainDexConsumer); |
| } |
| |
| @Test |
| public void testD8FilePerClassFile() throws Exception { |
| runD8FilePerMode(OutputMode.DexFilePerClassFile); |
| } |
| |
| @Test |
| public void testD8FilePerClass() throws Exception { |
| runD8FilePerMode(OutputMode.DexFilePerClass); |
| } |
| |
| private void runD8FilePerMode(OutputMode outputMode) throws Exception { |
| assumeTrue(parameters.isDexRuntime()); |
| Path perClassOutput = |
| testForD8(parameters.getBackend()) |
| .setOutputMode(outputMode) |
| .addProgramClasses(CLASSES) |
| .setMinApi(parameters.getApiLevel()) |
| .compile() |
| .writeToZip(); |
| MainDexConsumer mainDexConsumer = new MainDexConsumer(); |
| testForD8() |
| .addProgramFiles(perClassOutput) |
| .setMinApi(parameters.getApiLevel()) |
| // Trace the classes run by main which will pick up their dependencies. |
| .addMainDexRules(keepMainProguardConfiguration(TestClass.class)) |
| .setProgramConsumer(mainDexConsumer) |
| .compile() |
| .inspect(this::checkExpectedSynthetics) |
| .run(parameters.getRuntime(), TestClass.class, getRunArgs()) |
| .assertSuccessWithOutput(EXPECTED); |
| checkMainDex(mainDexConsumer); |
| } |
| |
| @Test |
| public void testD8MergingWithTraceDex() throws Exception { |
| assumeTrue(parameters.isDexRuntime()); |
| Path out1 = |
| testForD8() |
| .addProgramClasses(User1.class) |
| .addClasspathClasses(CLASSES) |
| .setIntermediate(true) |
| .setMinApi(parameters.getApiLevel()) |
| .compile() |
| .writeToZip(); |
| |
| Path out2 = |
| testForD8() |
| .addProgramClasses(User2.class) |
| .addClasspathClasses(CLASSES) |
| .setIntermediate(true) |
| .setMinApi(parameters.getApiLevel()) |
| .compile() |
| .writeToZip(); |
| |
| MainDexConsumer mainDexConsumer = new MainDexConsumer(); |
| List<Class<?>> classes = ImmutableList.of(TestClass.class, MiniAssert.class); |
| List<Path> files = ImmutableList.of(out1, out2); |
| GenerateMainDexListRunResult traceResult = traceMainDex(classes, files); |
| testForD8(parameters.getBackend()) |
| .addProgramClasses(classes) |
| .addProgramFiles(files) |
| .setMinApi(parameters.getApiLevel()) |
| .addMainDexListClassReferences(traceResult.getMainDexList()) |
| .setProgramConsumer(mainDexConsumer) |
| .compile() |
| .inspect(this::checkExpectedSynthetics) |
| .run(parameters.getRuntime(), TestClass.class, getRunArgs()) |
| .assertSuccessWithOutput(EXPECTED); |
| checkMainDex(mainDexConsumer); |
| } |
| |
| @Test |
| public void testR8() throws Exception { |
| MainDexConsumer mainDexConsumer = parameters.isDexRuntime() ? new MainDexConsumer() : null; |
| testForR8(parameters.getBackend()) |
| .debug() // Use debug mode to force a minimal main dex. |
| .noMinification() // Disable minification so we can inspect the synthetic names. |
| .applyIf(mainDexConsumer != null, b -> b.setProgramConsumer(mainDexConsumer)) |
| .addProgramClasses(CLASSES) |
| .addKeepMainRule(TestClass.class) |
| .addKeepClassAndMembersRules(MiniAssert.class) |
| .addKeepMethodRules( |
| Reference.methodFromMethod(User1.class.getMethod("testBooleanCompare")), |
| Reference.methodFromMethod(User1.class.getMethod("testCharacterCompare"))) |
| .addMainDexRules(keepMainProguardConfiguration(TestClass.class)) |
| .setMinApi(parameters.getApiLevel()) |
| .run(parameters.getRuntime(), TestClass.class, getRunArgs()) |
| .assertSuccessWithOutput(EXPECTED) |
| .inspect(this::checkExpectedSynthetics); |
| if (mainDexConsumer != null) { |
| checkMainDex(mainDexConsumer); |
| } |
| } |
| |
| private void checkMainDex(MainDexConsumer mainDexConsumer) throws Exception { |
| AndroidApp mainDexApp = |
| AndroidApp.builder() |
| .addDexProgramData(mainDexConsumer.mainDexBytes, Origin.unknown()) |
| .build(); |
| CodeInspector mainDexInspector = new CodeInspector(mainDexApp); |
| |
| // The program classes in the main-dex list must be in main-dex. |
| assertThat(mainDexInspector.clazz(MiniAssert.class), isPresent()); |
| assertThat(mainDexInspector.clazz(TestClass.class), isPresent()); |
| assertThat(mainDexInspector.clazz(User2.class), isPresent()); |
| assertEquals(getMainDexExpectedSynthetics(), getSyntheticMethods(mainDexInspector)); |
| } |
| |
| private Set<MethodReference> getSyntheticMethods(CodeInspector inspector) { |
| Set<ClassReference> nonSyntheticCLasses = |
| CLASSES.stream().map(Reference::classFromClass).collect(Collectors.toSet()); |
| Set<MethodReference> methods = new HashSet<>(); |
| inspector.allClasses().stream() |
| .filter(c -> !nonSyntheticCLasses.contains(c.getFinalReference())) |
| .forEach(c -> c.allMethods().forEach(m -> methods.add(m.asMethodReference()))); |
| return methods; |
| } |
| |
| private void checkExpectedSynthetics(CodeInspector inspector) throws Exception { |
| if (parameters.getApiLevel() == null) { |
| assertEquals(Collections.emptySet(), getSyntheticMethods(inspector)); |
| } else { |
| assertEquals( |
| Sets.union(getMainDexExpectedSynthetics(), getNonMainDexExpectedSynthetics()), |
| getSyntheticMethods(inspector)); |
| } |
| } |
| |
| // Hardcoded set of expected synthetics in a "final" build. This set could change if the |
| // compiler makes any changes to the naming, sorting or grouping of synthetics. It is hard-coded |
| // here to |
| // check that the compiler generates this deterministically for any single run or merge of |
| // intermediates. |
| |
| private ImmutableSet<MethodReference> getNonMainDexExpectedSynthetics() |
| throws NoSuchMethodException { |
| return ImmutableSet.of( |
| SyntheticItemsTestUtils.syntheticBackportMethod( |
| User1.class, 1, Boolean.class.getMethod("compare", boolean.class, boolean.class))); |
| } |
| |
| private ImmutableSet<MethodReference> getMainDexExpectedSynthetics() |
| throws NoSuchMethodException { |
| return ImmutableSet.of( |
| SyntheticItemsTestUtils.syntheticBackportMethod( |
| User1.class, 0, Character.class.getMethod("compare", char.class, char.class)), |
| SyntheticItemsTestUtils.syntheticBackportMethod( |
| User2.class, 0, Integer.class.getMethod("compare", int.class, int.class))); |
| } |
| |
| static class User1 { |
| |
| public static void testBooleanCompare() { |
| // These 4 calls should share the same synthetic method. |
| MiniAssert.assertTrue(Boolean.compare(true, false) > 0); |
| MiniAssert.assertTrue(Boolean.compare(true, true) == 0); |
| MiniAssert.assertTrue(Boolean.compare(false, false) == 0); |
| MiniAssert.assertTrue(Boolean.compare(false, true) < 0); |
| } |
| |
| public static void testCharacterCompare() { |
| // All 6 (User1 and User2) calls should share the same synthetic method. |
| MiniAssert.assertTrue(Character.compare('b', 'a') > 0); |
| MiniAssert.assertTrue(Character.compare('a', 'a') == 0); |
| MiniAssert.assertTrue(Character.compare('a', 'b') < 0); |
| } |
| } |
| |
| static class User2 { |
| |
| public static void testCharacterCompare() { |
| // All 6 (User1 and User2) calls should share the same synthetic method. |
| MiniAssert.assertTrue(Character.compare('y', 'x') > 0); |
| MiniAssert.assertTrue(Character.compare('x', 'x') == 0); |
| MiniAssert.assertTrue(Character.compare('x', 'y') < 0); |
| } |
| |
| public static void testIntegerCompare() { |
| // These 3 calls should share the same synthetic method. |
| MiniAssert.assertTrue(Integer.compare(2, 0) > 0); |
| MiniAssert.assertTrue(Integer.compare(0, 0) == 0); |
| MiniAssert.assertTrue(Integer.compare(0, 2) < 0); |
| } |
| } |
| |
| static class TestClass { |
| |
| public static void main(String[] args) throws Exception { |
| if (args.length == 1) { |
| // Reflectively call the backports on User1 which is not in the main-dex list. |
| Class<?> user1 = Class.forName(args[0]); |
| user1.getMethod("testBooleanCompare").invoke(user1); |
| user1.getMethod("testCharacterCompare").invoke(user1); |
| } |
| User2.testCharacterCompare(); |
| User2.testIntegerCompare(); |
| System.out.println("Hello, world"); |
| } |
| } |
| |
| private static class MainDexConsumer implements DexIndexedConsumer { |
| |
| byte[] mainDexBytes; |
| Set<String> mainDexDescriptors; |
| |
| @Override |
| public void accept( |
| int fileIndex, ByteDataView data, Set<String> descriptors, DiagnosticsHandler handler) { |
| if (fileIndex == 0) { |
| assertNull(mainDexBytes); |
| assertNull(mainDexDescriptors); |
| mainDexBytes = data.copyByteData(); |
| mainDexDescriptors = descriptors; |
| } |
| } |
| |
| @Override |
| public void finished(DiagnosticsHandler handler) { |
| assertNotNull(mainDexBytes); |
| assertNotNull(mainDexDescriptors); |
| } |
| } |
| } |