blob: 81f0674a2ebf4750c0547a80ff6fffee7a45664f [file] [log] [blame]
// 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);
}
}
}