blob: acfc021d174da9c83ed81787140894098558531c [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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
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.desugar.backports.AbstractBackportTest.MiniAssert;
import com.android.tools.r8.references.MethodReference;
import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
import com.android.tools.r8.synthesis.SyntheticNaming;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.android.tools.r8.utils.codeinspector.InstructionSubject;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import java.nio.file.Path;
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 BackportDuplicationTest 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<String> CLASS_TYPE_NAMES =
CLASSES.stream().map(Class::getTypeName).collect(Collectors.toList());
private final TestParameters parameters;
@Parameterized.Parameters(name = "{0}")
public static TestParametersCollection data() {
return getTestParameters()
.withAllRuntimes()
.withApiLevel(AndroidApiLevel.J)
.enableApiLevelsForCf()
.build();
}
public BackportDuplicationTest(TestParameters parameters) {
this.parameters = parameters;
}
@Test
public void testR8() throws Exception {
// R8 does not support desugaring with class file output so this test is only valid for DEX.
assumeTrue(parameters.isDexRuntime());
runR8(false);
runR8(true);
}
private void runR8(boolean minify) throws Exception {
testForR8(parameters.getBackend())
.addProgramClasses(CLASSES)
.addKeepMainRule(TestClass.class)
.addKeepClassAndMembersRules(MiniAssert.class)
.setMinApi(parameters.getApiLevel())
.minification(minify)
.run(parameters.getRuntime(), TestClass.class)
.assertSuccessWithOutput(EXPECTED)
.inspect(this::checkNoOriginalsAndNoInternalSynthetics);
}
@Test
public void testD8() throws Exception {
testForD8(parameters.getBackend())
.addProgramClasses(CLASSES)
.setMinApi(parameters.getApiLevel())
.run(parameters.getRuntime(), TestClass.class)
.assertSuccessWithOutput(EXPECTED)
.inspect(this::checkNoOriginalsAndNoInternalSynthetics)
.inspect(this::checkExpectedSynthetics);
}
@Test
public void testD8Merging() throws Exception {
assumeTrue(
"b/147485959: Merging does not happen for CF due to lack of synthetic annotations",
parameters.isDexRuntime());
boolean intermediate = true;
runD8Merging(intermediate);
}
@Test
public void testD8MergingNonIntermediate() throws Exception {
boolean intermediate = false;
runD8Merging(intermediate);
}
private void runD8Merging(boolean intermediate) throws Exception {
// Compile part 1 of the input (maybe intermediate)
Path out1 =
testForD8(parameters.getBackend())
.addProgramClasses(User1.class)
.addClasspathClasses(CLASSES)
.setMinApi(parameters.getApiLevel())
.setIntermediate(intermediate)
.compile()
.writeToZip();
// Compile part 2 of the input (maybe intermediate)
Path out2 =
testForD8(parameters.getBackend())
.addProgramClasses(User2.class)
.addClasspathClasses(CLASSES)
.setMinApi(parameters.getApiLevel())
.setIntermediate(intermediate)
.compile()
.writeToZip();
SetView<MethodReference> syntheticsInParts =
Sets.union(
getSyntheticMethods(new CodeInspector(out1)),
getSyntheticMethods(new CodeInspector(out2)));
// Merge parts as an intermediate artifact.
// This will not merge synthetics regardless of the setting of intermediate.
Path out3 = temp.newFolder().toPath().resolve("out3.zip");
testForD8(parameters.getBackend())
.addProgramClasses(MiniAssert.class, TestClass.class)
.addProgramFiles(out1, out2)
.setMinApi(parameters.getApiLevel())
.setIntermediate(true)
.compile()
.writeToZip(out3)
.run(parameters.getRuntime(), TestClass.class)
.assertSuccessWithOutput(EXPECTED)
.inspect(this::checkNoOriginalsAndNoInternalSynthetics)
.inspect(inspector -> assertEquals(syntheticsInParts, getSyntheticMethods(inspector)));
// Finally do a non-intermediate merge.
testForD8(parameters.getBackend())
.addProgramFiles(out3)
.setMinApi(parameters.getApiLevel())
.run(parameters.getRuntime(), TestClass.class)
.assertSuccessWithOutput(EXPECTED)
.inspect(this::checkNoOriginalsAndNoInternalSynthetics)
.inspect(
inspector -> {
if (intermediate) {
// If all previous builds where intermediate then synthetics are merged.
checkExpectedSynthetics(inspector);
} else {
// Otherwise merging non-intermediate artifacts, synthetics will not be identified.
// Check that they are exactly as in the part inputs.
assertEquals(syntheticsInParts, getSyntheticMethods(inspector));
}
});
}
@Test
public void testD8FilePerClassFile() throws Exception {
assumeTrue(parameters.isDexRuntime());
runD8FilePerMode(OutputMode.DexFilePerClassFile);
}
@Test
public void testD8FilePerClass() throws Exception {
assumeTrue(parameters.isDexRuntime());
runD8FilePerMode(OutputMode.DexFilePerClass);
}
public void runD8FilePerMode(OutputMode outputMode) throws Exception {
Path perClassOutput =
testForD8(parameters.getBackend())
.setOutputMode(outputMode)
.addProgramClasses(CLASSES)
.setMinApi(parameters.getApiLevel())
.compile()
.writeToZip();
testForD8()
.addProgramFiles(perClassOutput)
.setMinApi(parameters.getApiLevel())
.run(parameters.getRuntime(), TestClass.class)
.assertSuccessWithOutput(EXPECTED)
.inspect(this::checkNoOriginalsAndNoInternalSynthetics)
.inspect(this::checkExpectedSynthetics);
}
private void checkNoOriginalsAndNoInternalSynthetics(CodeInspector inspector) {
inspector.forAllClasses(
clazz -> {
SyntheticNaming.verifyNotInternalSynthetic(clazz.getFinalReference());
if (!clazz.getOriginalName().equals(MiniAssert.class.getTypeName())) {
clazz.forAllMethods(
method ->
assertTrue(
"Unexpected static invoke to java.lang method:\n"
+ method.getMethod().codeToString(),
method
.streamInstructions()
.filter(InstructionSubject::isInvokeStatic)
.noneMatch(
i -> i.getMethod().qualifiedName().startsWith("java.lang"))));
}
});
}
private Set<MethodReference> getSyntheticMethods(CodeInspector inspector) {
Set<MethodReference> methods = new HashSet<>();
inspector.allClasses().stream()
.filter(c -> !CLASS_TYPE_NAMES.contains(c.getFinalName()))
.forEach(c -> c.allMethods().forEach(m -> methods.add(m.asMethodReference())));
return methods;
}
private void checkExpectedSynthetics(CodeInspector inspector) throws Exception {
// 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.
Set<MethodReference> expectedSynthetics =
ImmutableSet.of(
SyntheticItemsTestUtils.syntheticBackportMethod(
User1.class, 0, Character.class.getMethod("compare", char.class, char.class)),
SyntheticItemsTestUtils.syntheticBackportMethod(
User1.class, 1, Boolean.class.getMethod("compare", boolean.class, boolean.class)),
SyntheticItemsTestUtils.syntheticBackportMethod(
User2.class, 0, Integer.class.getMethod("compare", int.class, int.class)));
assertEquals(expectedSynthetics, getSyntheticMethods(inspector));
}
static class User1 {
private 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);
}
private 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 {
private 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);
}
private 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) {
User1.testBooleanCompare();
User1.testCharacterCompare();
User2.testCharacterCompare();
User2.testIntegerCompare();
System.out.println("Hello, world");
}
}
}