| // 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.lambdas; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| 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.references.MethodReference; |
| import com.android.tools.r8.synthesis.SyntheticItemsTestUtils; |
| 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 LambdaStaticInstanceFieldDuplicationTest extends TestBase { |
| |
| static final String EXPECTED = StringUtils.lines("User1.1", "User1.2", "User2"); |
| |
| static final List<Class<?>> CLASSES = |
| ImmutableList.of(TestClass.class, MyConsumer.class, Accept.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 LambdaStaticInstanceFieldDuplicationTest(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) |
| // Prevent R8 from eliminating the lambdas by keeping the application of them. |
| .addKeepClassAndMembersRules(Accept.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(TestClass.class, MyConsumer.class, Accept.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 -> { |
| assertFalse(SyntheticItemsTestUtils.isInternalLambda(clazz.getFinalReference())); |
| clazz.forAllMethods( |
| method -> |
| assertTrue( |
| "Unexpected invoke dynamic:\n" + method.getMethod().codeToString(), |
| method.isAbstract() |
| || method |
| .streamInstructions() |
| .noneMatch(InstructionSubject::isInvokeDynamic))); |
| }); |
| } |
| |
| 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(m -> !m.isInstanceInitializer() && !m.isClassInitializer()) |
| .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( |
| // User1 has two lambdas. |
| SyntheticItemsTestUtils.syntheticLambdaMethod( |
| User1.class, 0, MyConsumer.class.getMethod("accept", Object.class)), |
| SyntheticItemsTestUtils.syntheticLambdaMethod( |
| User1.class, 1, MyConsumer.class.getMethod("accept", Object.class)), |
| // User2 has one lambda. |
| SyntheticItemsTestUtils.syntheticLambdaMethod( |
| User2.class, 0, MyConsumer.class.getMethod("accept", Object.class))); |
| assertEquals(expectedSynthetics, getSyntheticMethods(inspector)); |
| } |
| |
| interface MyConsumer { |
| void accept(Object o); |
| } |
| |
| static class Accept { |
| public static void accept(Object o, MyConsumer consumer) { |
| consumer.accept(o); |
| } |
| } |
| |
| static class User1 { |
| |
| private static void testSystemPrintln1() { |
| // The lambda will reference a lambda$x method on User1 which is created by javac for each |
| // lambda on the class. Thus there can be no sharing unless R8 inlines the lambda method into |
| // the desugared lambda classes. |
| Accept.accept("1.1", o -> System.out.println("User" + o)); |
| } |
| |
| private static void testSystemPrintln2() { |
| Accept.accept("1.2", o -> System.out.println("User" + o)); |
| } |
| } |
| |
| static class User2 { |
| |
| private static void testSystemPrintln() { |
| Accept.accept("2", o -> System.out.println("User" + o)); |
| } |
| } |
| |
| static class TestClass { |
| |
| public static void main(String[] args) { |
| User1.testSystemPrintln1(); |
| User1.testSystemPrintln2(); |
| User2.testSystemPrintln(); |
| } |
| } |
| } |