| // 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.classmerging.vertical; |
| |
| import static com.android.tools.r8.smali.SmaliBuilder.buildCode; |
| import static com.android.tools.r8.utils.AndroidApiLevel.K; |
| import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; |
| import static org.hamcrest.CoreMatchers.containsString; |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assume.assumeFalse; |
| import static org.junit.Assume.assumeTrue; |
| |
| import com.android.tools.r8.CompilationFailedException; |
| import com.android.tools.r8.KeepUnusedReturnValue; |
| import com.android.tools.r8.ProgramResourceProvider; |
| import com.android.tools.r8.R8FullTestBuilder; |
| import com.android.tools.r8.R8TestCompileResult; |
| 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.ToolHelper.DexVm.Version; |
| import com.android.tools.r8.apimodel.ApiModelingTestHelper; |
| import com.android.tools.r8.ir.optimize.Inliner.Reason; |
| import com.android.tools.r8.jasmin.JasminBuilder; |
| import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder; |
| import com.android.tools.r8.naming.ClassNameMapper; |
| import com.android.tools.r8.naming.ClassNamingForNameMapper; |
| import com.android.tools.r8.origin.Origin; |
| import com.android.tools.r8.smali.SmaliBuilder; |
| import com.android.tools.r8.utils.AndroidApiLevel; |
| import com.android.tools.r8.utils.AndroidApp; |
| import com.android.tools.r8.utils.AndroidApp.Builder; |
| import com.android.tools.r8.utils.FileUtils; |
| import com.android.tools.r8.utils.InternalOptions; |
| import com.android.tools.r8.utils.SetUtils; |
| import com.android.tools.r8.utils.StringUtils; |
| import com.android.tools.r8.utils.codeinspector.ClassSubject; |
| import com.android.tools.r8.utils.codeinspector.CodeInspector; |
| import com.android.tools.r8.utils.codeinspector.FoundClassSubject; |
| import com.android.tools.r8.utils.codeinspector.MethodSubject; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.function.Consumer; |
| import java.util.function.Predicate; |
| import org.junit.Assume; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| import org.junit.runners.Parameterized.Parameters; |
| |
| // TODO(christofferqa): Add tests to check that statically typed invocations on method handles |
| // continue to work after class merging. |
| @RunWith(Parameterized.class) |
| public class VerticalClassMergerTest extends TestBase { |
| |
| private static final Path CF_DIR = |
| Paths.get(ToolHelper.BUILD_DIR).resolve("test/examples/classes/classmerging"); |
| private static final Path JAVA8_CF_DIR = |
| Paths.get(ToolHelper.BUILD_DIR).resolve("test/examplesAndroidO/classes/classmerging"); |
| private static final Path EXAMPLE_JAR = Paths.get(ToolHelper.EXAMPLES_BUILD_DIR) |
| .resolve("classmerging.jar"); |
| private static final Path EXAMPLE_KEEP = Paths.get(ToolHelper.EXAMPLES_DIR) |
| .resolve("classmerging").resolve("keep-rules.txt"); |
| private static final Path JAVA8_EXAMPLE_KEEP = Paths.get(ToolHelper.EXAMPLES_ANDROID_O_DIR) |
| .resolve("classmerging").resolve("keep-rules.txt"); |
| private static final Path DONT_OPTIMIZE = Paths.get(ToolHelper.EXAMPLES_DIR) |
| .resolve("classmerging").resolve("keep-rules-dontoptimize.txt"); |
| |
| private final TestParameters parameters; |
| |
| @Parameters(name = "{0}") |
| public static TestParametersCollection params() { |
| return getTestParameters().withAllRuntimesAndApiLevels().build(); |
| } |
| |
| public VerticalClassMergerTest(TestParameters parameters) { |
| this.parameters = parameters; |
| } |
| |
| private void configure(InternalOptions options) { |
| options.enableSideEffectAnalysis = false; |
| options.enableUnusedInterfaceRemoval = false; |
| } |
| |
| private void runR8(Path proguardConfig, Consumer<InternalOptions> optionsConsumer) |
| throws IOException, CompilationFailedException { |
| inspector = |
| testForR8(parameters.getBackend()) |
| .addProgramFiles(EXAMPLE_JAR) |
| .addKeepRuleFiles(proguardConfig) |
| .enableProguardTestOptions() |
| .addDontObfuscate() |
| .addOptionsModification(optionsConsumer) |
| .setMinApi(parameters) |
| .compile() |
| .inspector(); |
| } |
| |
| private CodeInspector inspector; |
| |
| private final List<String> CAN_BE_MERGED = ImmutableList.of( |
| "classmerging.GenericInterface", |
| "classmerging.GenericAbstractClass", |
| "classmerging.Outer$SuperClass", |
| "classmerging.SuperClass" |
| ); |
| |
| @Test |
| public void testClassesHaveNotBeenMerged() throws Throwable { |
| runR8(DONT_OPTIMIZE, null); |
| for (String candidate : CAN_BE_MERGED) { |
| assertThat(inspector.clazz(candidate), isPresent()); |
| } |
| } |
| |
| @Test |
| public void testArrayTypeCollision() throws Throwable { |
| String main = "classmerging.ArrayTypeCollisionTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("ArrayTypeCollisionTest.class"), |
| CF_DIR.resolve("ArrayTypeCollisionTest$A.class"), |
| CF_DIR.resolve("ArrayTypeCollisionTest$B.class") |
| }; |
| Set<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.ArrayTypeCollisionTest", |
| "classmerging.ArrayTypeCollisionTest$A", |
| "classmerging.ArrayTypeCollisionTest$B"); |
| runTest( |
| testForR8(parameters.getBackend()) |
| .addKeepRules( |
| getProguardConfig( |
| EXAMPLE_KEEP, |
| "-neverinline public class classmerging.ArrayTypeCollisionTest {", |
| " static void method(...);", |
| "}")) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| programFiles, |
| preservedClassNames::contains); |
| } |
| |
| /** |
| * Tests that two classes A and B are not merged when there are two methods in the same class that |
| * returns an array type and would become conflicting due to the renaming of their return types, |
| * as in the following class. |
| * |
| * <pre> |
| * class ArrayReturnTypeCollisionTest { |
| * public static A[] method() { ... } |
| * public static B[] method() { ... } |
| * } |
| * </pre> |
| */ |
| @Test |
| public void testArrayReturnTypeCollision() throws Throwable { |
| String main = "classmerging.ArrayReturnTypeCollisionTest"; |
| Set<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.ArrayReturnTypeCollisionTest", "classmerging.A", "classmerging.B"); |
| |
| JasminBuilder jasminBuilder = new JasminBuilder(); |
| |
| ClassBuilder classBuilder = jasminBuilder.addClass(main); |
| classBuilder.addMainMethod( |
| ".limit locals 1", |
| ".limit stack 2", |
| "invokestatic classmerging/ArrayReturnTypeCollisionTest/method()[Lclassmerging/A;", |
| "pop", |
| "invokestatic classmerging/ArrayReturnTypeCollisionTest/method()[Lclassmerging/B;", |
| "pop", |
| "return"); |
| |
| // Add two methods with the same name that have return types A[] and B[], respectively. |
| classBuilder.addRuntimeInvisibleAnnotation(KeepUnusedReturnValue.class.getTypeName()); |
| classBuilder.addStaticMethod( |
| "method", ImmutableList.of(), "[Lclassmerging/A;", |
| ".limit stack 1", ".limit locals 1", "iconst_0", "anewarray classmerging/A", "areturn"); |
| |
| classBuilder.addRuntimeInvisibleAnnotation(KeepUnusedReturnValue.class.getTypeName()); |
| classBuilder.addStaticMethod( |
| "method", ImmutableList.of(), "[Lclassmerging/B;", |
| ".limit stack 1", ".limit locals 1", "iconst_0", "anewarray classmerging/B", "areturn"); |
| |
| // Class A is empty so that it can easily be merged into B. |
| classBuilder = jasminBuilder.addClass("classmerging.A"); |
| classBuilder.addDefaultConstructor(); |
| |
| // Class B inherits from A and is also empty. |
| classBuilder = jasminBuilder.addClass("classmerging.B", "classmerging.A"); |
| classBuilder.addDefaultConstructor(); |
| |
| // Run test. |
| runTestOnInput( |
| testForR8(parameters.getBackend()) |
| .addKeepRules( |
| "-keep class " + main + " {", |
| " public static void main(...);", |
| "}", |
| "-neverinline class " + main + " {", |
| " static classmerging.A[] method(...);", |
| " static classmerging.B[] method(...);", |
| "}") |
| .enableKeepUnusedReturnValueAnnotations(), |
| main, |
| jasminBuilder.build(), |
| preservedClassNames::contains); |
| } |
| |
| // This test has a cycle in the call graph consisting of the methods A.<init> and B.<init>. |
| @Test |
| public void testCallGraphCycle() throws Throwable { |
| String main = "classmerging.CallGraphCycleTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("CallGraphCycleTest.class"), |
| CF_DIR.resolve("CallGraphCycleTest$A.class"), |
| CF_DIR.resolve("CallGraphCycleTest$B.class") |
| }; |
| Set<String> preservedClassNames = |
| ImmutableSet.of("classmerging.CallGraphCycleTest", "classmerging.CallGraphCycleTest$B"); |
| for (int i = 0; i < 5; i++) { |
| runTest( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(EXAMPLE_KEEP)) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| programFiles, |
| preservedClassNames::contains); |
| } |
| } |
| |
| @Test |
| public void testConflictInGeneratedName() throws Throwable { |
| String main = "classmerging.ConflictInGeneratedNameTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("NeverPropagateValue.class"), |
| CF_DIR.resolve("ConflictInGeneratedNameTest.class"), |
| CF_DIR.resolve("ConflictInGeneratedNameTest$A.class"), |
| CF_DIR.resolve("ConflictInGeneratedNameTest$B.class") |
| }; |
| Set<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.ConflictInGeneratedNameTest", |
| "classmerging.ConflictInGeneratedNameTest$B"); |
| CodeInspector inspector = |
| runTestOnInput( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(EXAMPLE_KEEP)) |
| .addOptionsModification(this::configure) |
| .addOptionsModification( |
| options -> |
| options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE)) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| readProgramFiles(programFiles), |
| preservedClassNames::contains, |
| // Disable debug testing since the test has a method with "$classmerging$" in the |
| // name. |
| null) |
| .inspector(); |
| |
| ClassSubject clazzSubject = inspector.clazz("classmerging.ConflictInGeneratedNameTest$B"); |
| assertThat(clazzSubject, isPresent()); |
| |
| String suffix = "$classmerging$ConflictInGeneratedNameTest$A"; |
| List<String> EMPTY = ImmutableList.of(); |
| |
| // There should be three fields. |
| assertThat(clazzSubject.field("java.lang.String", "name"), isPresent()); |
| assertThat(clazzSubject.field("java.lang.String", "name" + suffix), isPresent()); |
| assertThat(clazzSubject.field("java.lang.String", "name" + suffix + "2"), isPresent()); |
| |
| // The direct method "$r8$constructor$classmerging$ConflictInGeneratedNameTest$A" is processed |
| // after the method "<init>" is renamed to exactly that name. Therefore the conflict should have |
| // been resolved by appending [suffix] to it. |
| assertThat( |
| clazzSubject.method("void", "$r8$constructor" + suffix + suffix, EMPTY), isPresent()); |
| |
| // There should be two foo's. |
| assertThat(clazzSubject.method("void", "foo", EMPTY), isPresent()); |
| assertThat(clazzSubject.method("void", "foo" + suffix, EMPTY), isPresent()); |
| |
| // There should be two bar's. |
| assertThat(clazzSubject.method("void", "bar", EMPTY), isPresent()); |
| assertThat(clazzSubject.method("void", "bar" + suffix, EMPTY), isPresent()); |
| |
| // There should be three baz's. |
| assertThat(clazzSubject.method("void", "baz", EMPTY), isPresent()); |
| assertThat(clazzSubject.method("void", "baz" + suffix, EMPTY), isPresent()); |
| assertThat(clazzSubject.method("void", "baz" + suffix + "2", EMPTY), isPresent()); |
| |
| // There should be three boo's. |
| assertThat(clazzSubject.method("void", "boo", EMPTY), isPresent()); |
| assertThat(clazzSubject.method("void", "boo" + suffix, EMPTY), isPresent()); |
| assertThat(clazzSubject.method("void", "boo" + suffix + "2", EMPTY), isPresent()); |
| |
| // There should be two getName's. |
| assertThat(clazzSubject.method("java.lang.String", "getName", EMPTY), isPresent()); |
| assertThat(clazzSubject.method("java.lang.String", "getName" + suffix, EMPTY), isPresent()); |
| } |
| |
| @Test |
| public void testFieldCollision() throws Throwable { |
| String main = "classmerging.FieldCollisionTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("FieldCollisionTest.class"), |
| CF_DIR.resolve("FieldCollisionTest$A.class"), |
| CF_DIR.resolve("FieldCollisionTest$B.class") |
| }; |
| ImmutableSet.Builder<String> preservedNamesBuilder = ImmutableSet.builder(); |
| preservedNamesBuilder.add("classmerging.FieldCollisionTest"); |
| preservedNamesBuilder.add("classmerging.FieldCollisionTest$B"); |
| if (parameters.isCfRuntime() || parameters.getApiLevel().isLessThan(K)) { |
| preservedNamesBuilder.add("classmerging.FieldCollisionTest$A"); |
| } |
| Set<String> preservedClassNames = preservedNamesBuilder.build(); |
| runTest( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(EXAMPLE_KEEP)) |
| .allowUnusedProguardConfigurationRules() |
| .apply(ApiModelingTestHelper::enableApiCallerIdentification) |
| .apply(ApiModelingTestHelper::disableOutlining), |
| main, |
| programFiles, |
| preservedClassNames::contains); |
| } |
| |
| @Test |
| public void testMergeInterfaceWithoutInlining() throws Throwable { |
| String main = "classmerging.ConflictingInterfaceSignaturesTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("ConflictingInterfaceSignaturesTest.class"), |
| CF_DIR.resolve("ConflictingInterfaceSignaturesTest$A.class"), |
| CF_DIR.resolve("ConflictingInterfaceSignaturesTest$B.class"), |
| CF_DIR.resolve("ConflictingInterfaceSignaturesTest$InterfaceImpl.class"), |
| CF_DIR.resolve("NeverInline.class") |
| }; |
| Set<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.ConflictingInterfaceSignaturesTest", |
| "classmerging.ConflictingInterfaceSignaturesTest$InterfaceImpl"); |
| runTestOnInput( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(EXAMPLE_KEEP)) |
| .addOptionsModification(this::configure) |
| .addOptionsModification(options -> options.inlinerOptions().enableInlining = false) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| readProgramFiles(programFiles), |
| preservedClassNames::contains); |
| } |
| |
| @Test |
| public void testNestedDefaultInterfaceMethodsTest() throws Throwable { |
| Assume.assumeTrue("b/197494749", parameters.canUseDefaultAndStaticInterfaceMethods()); |
| String main = "classmerging.NestedDefaultInterfaceMethodsTest"; |
| Path[] programFiles = |
| new Path[] { |
| JAVA8_CF_DIR.resolve("NestedDefaultInterfaceMethodsTest.class"), |
| JAVA8_CF_DIR.resolve("NestedDefaultInterfaceMethodsTest$A.class"), |
| JAVA8_CF_DIR.resolve("NestedDefaultInterfaceMethodsTest$B.class"), |
| JAVA8_CF_DIR.resolve("NestedDefaultInterfaceMethodsTest$C.class"), |
| JAVA8_CF_DIR.resolve("NeverInline.class") |
| }; |
| Set<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.NestedDefaultInterfaceMethodsTest", |
| "classmerging.NestedDefaultInterfaceMethodsTest$B", |
| "classmerging.NestedDefaultInterfaceMethodsTest$C"); |
| runTest( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(JAVA8_EXAMPLE_KEEP)) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| programFiles, |
| preservedClassNames::contains); |
| } |
| |
| @Test |
| public void testNestedDefaultInterfaceMethodsWithCDumpTest() throws Throwable { |
| String main = "classmerging.NestedDefaultInterfaceMethodsTest"; |
| Path[] programFiles = |
| new Path[] { |
| JAVA8_CF_DIR.resolve("NestedDefaultInterfaceMethodsTest.class"), |
| JAVA8_CF_DIR.resolve("NestedDefaultInterfaceMethodsTest$A.class"), |
| JAVA8_CF_DIR.resolve("NestedDefaultInterfaceMethodsTest$B.class"), |
| JAVA8_CF_DIR.resolve("NeverInline.class") |
| }; |
| Set<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.NestedDefaultInterfaceMethodsTest", |
| "classmerging.NestedDefaultInterfaceMethodsTest$B", |
| "classmerging.NestedDefaultInterfaceMethodsTest$C"); |
| runTestOnInput( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(JAVA8_EXAMPLE_KEEP)) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| AndroidApp.builder() |
| .addProgramFiles(programFiles) |
| .addClassProgramData( |
| NestedDefaultInterfaceMethodsTestDump.CDump.dump(), Origin.unknown()) |
| .build(), |
| preservedClassNames::contains); |
| } |
| |
| @Test |
| public void testPinnedParameterTypes() throws Throwable { |
| String main = "classmerging.PinnedParameterTypesTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("PinnedParameterTypesTest.class"), |
| CF_DIR.resolve("PinnedParameterTypesTest$Interface.class"), |
| CF_DIR.resolve("PinnedParameterTypesTest$InterfaceImpl.class"), |
| CF_DIR.resolve("PinnedParameterTypesTest$TestClass.class") |
| }; |
| Set<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.PinnedParameterTypesTest", |
| "classmerging.PinnedParameterTypesTest$Interface", |
| "classmerging.PinnedParameterTypesTest$InterfaceImpl", |
| "classmerging.PinnedParameterTypesTest$TestClass"); |
| runTest( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(EXAMPLE_KEEP, "-keepparameternames")) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| programFiles, |
| preservedClassNames::contains); |
| } |
| |
| @Test |
| public void testPinnedArrayParameterTypes() throws Throwable { |
| String main = "classmerging.PinnedArrayParameterTypesTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("PinnedArrayParameterTypesTest.class"), |
| CF_DIR.resolve("PinnedArrayParameterTypesTest$Interface.class"), |
| CF_DIR.resolve("PinnedArrayParameterTypesTest$InterfaceImpl.class"), |
| CF_DIR.resolve("PinnedArrayParameterTypesTest$TestClass.class") |
| }; |
| Set<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.PinnedArrayParameterTypesTest", |
| "classmerging.PinnedArrayParameterTypesTest$Interface", |
| "classmerging.PinnedArrayParameterTypesTest$InterfaceImpl", |
| "classmerging.PinnedArrayParameterTypesTest$TestClass"); |
| runTest( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(EXAMPLE_KEEP, "-keepparameternames")) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| programFiles, |
| preservedClassNames::contains); |
| } |
| |
| @Test |
| public void testProguardFieldMap() throws Throwable { |
| String main = "classmerging.ProguardFieldMapTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("ProguardFieldMapTest.class"), |
| CF_DIR.resolve("ProguardFieldMapTest$A.class"), |
| CF_DIR.resolve("ProguardFieldMapTest$B.class"), |
| CF_DIR.resolve("NeverClassInline.class") |
| }; |
| Set<String> preservedClassNamesWithoutClassMerging = |
| ImmutableSet.of( |
| "classmerging.ProguardFieldMapTest", |
| "classmerging.ProguardFieldMapTest$A", |
| "classmerging.ProguardFieldMapTest$B"); |
| |
| // Try without vertical class merging, to check that output is as expected. |
| R8TestCompileResult compileResultWithoutClassMerging = |
| runTestOnInput( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(EXAMPLE_KEEP)) |
| .addOptionsModification(this::configure) |
| .addOptionsModification( |
| options -> { |
| options.enableVerticalClassMerging = false; |
| options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE); |
| }) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| readProgramFiles(programFiles), |
| preservedClassNamesWithoutClassMerging::contains); |
| |
| ClassNameMapper mappingWithoutClassMerging = |
| ClassNameMapper.mapperFromString(compileResultWithoutClassMerging.getProguardMap()); |
| ClassNamingForNameMapper mappingsForClassAWithoutClassMerging = |
| mappingWithoutClassMerging.getClassNaming("classmerging.ProguardFieldMapTest$A"); |
| assertTrue(mappingsForClassAWithoutClassMerging.allFieldNamings().isEmpty()); |
| |
| // Try with vertical class merging. |
| Set<String> preservedClassNames = |
| ImmutableSet.of("classmerging.ProguardFieldMapTest", "classmerging.ProguardFieldMapTest$B"); |
| R8TestCompileResult compileResultWithClassMerging = |
| runTestOnInput( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(EXAMPLE_KEEP)) |
| .addOptionsModification(this::configure) |
| .addOptionsModification( |
| options -> options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE)) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| readProgramFiles(programFiles), |
| preservedClassNames::contains); |
| |
| ClassNameMapper mappingWithClassMerging = |
| ClassNameMapper.mapperFromString(compileResultWithClassMerging.getProguardMap()); |
| assertNull(mappingWithClassMerging.getClassNaming("classmerging.ProguardFieldMapTest$A")); |
| |
| ClassNamingForNameMapper mappingsForClassBWithClassMerging = |
| mappingWithClassMerging.getClassNaming("classmerging.ProguardFieldMapTest$B"); |
| assertTrue( |
| mappingsForClassBWithClassMerging.allFieldNamings().stream() |
| .anyMatch( |
| fieldNaming -> |
| fieldNaming |
| .getOriginalSignature() |
| .toString() |
| .equals("java.lang.String classmerging.ProguardFieldMapTest$A.f") |
| && fieldNaming |
| .getResidualSignature() |
| .toString() |
| .equals("java.lang.String f"))); |
| } |
| |
| @Test |
| public void testProguardMethodMap() throws Throwable { |
| String main = "classmerging.ProguardMethodMapTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("ProguardMethodMapTest.class"), |
| CF_DIR.resolve("ProguardMethodMapTest$A.class"), |
| CF_DIR.resolve("ProguardMethodMapTest$B.class"), |
| CF_DIR.resolve("NeverClassInline.class") |
| }; |
| Set<String> preservedClassNamesWithoutClassMerging = |
| ImmutableSet.of( |
| "classmerging.ProguardMethodMapTest", |
| "classmerging.ProguardMethodMapTest$A", |
| "classmerging.ProguardMethodMapTest$B"); |
| |
| // Try without vertical class merging, to check that output is as expected. |
| R8TestCompileResult compileResultWithoutClassMerging = |
| runTestOnInput( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(EXAMPLE_KEEP)) |
| .addOptionsModification(this::configure) |
| .addOptionsModification( |
| options -> { |
| options.enableVerticalClassMerging = false; |
| options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE); |
| }) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| readProgramFiles(programFiles), |
| preservedClassNamesWithoutClassMerging::contains); |
| |
| ClassNameMapper mappingWithoutClassMerging = |
| ClassNameMapper.mapperFromString(compileResultWithoutClassMerging.getProguardMap()); |
| ClassNamingForNameMapper mappingsForClassAWithoutClassMerging = |
| mappingWithoutClassMerging.getClassNaming("classmerging.ProguardMethodMapTest$A"); |
| assertTrue( |
| mappingsForClassAWithoutClassMerging.allMethodNamings().stream() |
| .allMatch( |
| methodNaming -> |
| methodNaming |
| .getOriginalSignature() |
| .toString() |
| .equals(methodNaming.getResidualSignature().toString()))); |
| |
| // Try with vertical class merging. |
| Set<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.ProguardMethodMapTest", "classmerging.ProguardMethodMapTest$B"); |
| R8TestCompileResult compileResultWithClassMerging = |
| runTestOnInput( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(EXAMPLE_KEEP)) |
| .addOptionsModification(this::configure) |
| .addOptionsModification( |
| options -> options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE)) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| readProgramFiles(programFiles), |
| preservedClassNames::contains); |
| |
| ClassNameMapper mappingWithClassMerging = |
| ClassNameMapper.mapperFromString(compileResultWithClassMerging.getProguardMap()); |
| assertNull(mappingWithClassMerging.getClassNaming("classmerging.ProguardMethodMapTest$A")); |
| |
| ClassNamingForNameMapper mappingsForClassBWithClassMerging = |
| mappingWithClassMerging.getClassNaming("classmerging.ProguardMethodMapTest$B"); |
| assertTrue( |
| mappingsForClassBWithClassMerging.allMethodNamings().stream() |
| .anyMatch( |
| methodNaming -> |
| methodNaming |
| .getOriginalSignature() |
| .toString() |
| .equals("void classmerging.ProguardMethodMapTest$A.method()") |
| && methodNaming |
| .getResidualSignature() |
| .toString() |
| .equals("void method$classmerging$ProguardMethodMapTest$A()"))); |
| } |
| |
| @Test |
| public void testProguardMethodMapAfterInlining() throws Throwable { |
| String main = "classmerging.ProguardMethodMapTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("ProguardMethodMapTest.class"), |
| CF_DIR.resolve("ProguardMethodMapTest$A.class"), |
| CF_DIR.resolve("ProguardMethodMapTest$B.class"), |
| CF_DIR.resolve("NeverClassInline.class") |
| }; |
| Set<String> preservedClassNamesWithoutClassMerging = |
| ImmutableSet.of( |
| "classmerging.ProguardMethodMapTest", |
| "classmerging.ProguardMethodMapTest$A", |
| "classmerging.ProguardMethodMapTest$B"); |
| |
| // Try without vertical class merging, to check that output is as expected. |
| R8TestCompileResult compileResultWithoutClassMerging = |
| runTestOnInput( |
| testForR8(parameters.getBackend()) |
| .addKeepRules( |
| getProguardConfig( |
| EXAMPLE_KEEP, |
| "-alwaysinline class classmerging.ProguardMethodMapTest$A { public void" |
| + " method(); }")) |
| .addOptionsModification(this::configure) |
| .addOptionsModification( |
| options -> { |
| options.enableVerticalClassMerging = false; |
| options.testing.validInliningReasons = |
| ImmutableSet.of(Reason.ALWAYS, Reason.FORCE); |
| }) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| readProgramFiles(programFiles), |
| preservedClassNamesWithoutClassMerging::contains); |
| |
| ClassNameMapper mappingWithoutClassMerging = |
| ClassNameMapper.mapperFromString(compileResultWithoutClassMerging.getProguardMap()); |
| ClassNamingForNameMapper mappingsForClassBWithoutClassMerging = |
| mappingWithoutClassMerging.getClassNaming("classmerging.ProguardMethodMapTest$B"); |
| assertTrue( |
| mappingsForClassBWithoutClassMerging |
| .getMappedRangesForRenamedName("method") |
| .getMappedRanges() |
| .stream() |
| .anyMatch( |
| mappedRange -> |
| mappedRange |
| .signature |
| .toString() |
| .equals("void classmerging.ProguardMethodMapTest$A.method()") |
| && mappedRange.getRenamedName().equals("method"))); |
| |
| // Try with vertical class merging. |
| Set<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.ProguardMethodMapTest", "classmerging.ProguardMethodMapTest$B"); |
| R8TestCompileResult compileResultWithClassMerging = |
| runTestOnInput( |
| testForR8(parameters.getBackend()) |
| .addKeepRules( |
| getProguardConfig( |
| EXAMPLE_KEEP, |
| "-alwaysinline class classmerging.ProguardMethodMapTest$A { public void" |
| + " method(); }")) |
| .addOptionsModification(this::configure) |
| .addOptionsModification( |
| options -> { |
| options.testing.validInliningReasons = |
| ImmutableSet.of(Reason.ALWAYS, Reason.FORCE); |
| }) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| readProgramFiles(programFiles), |
| preservedClassNames::contains); |
| |
| ClassNameMapper mappingWithClassMerging = |
| ClassNameMapper.mapperFromString(compileResultWithClassMerging.getProguardMap()); |
| ClassNamingForNameMapper mappingsForClassBWithClassMerging = |
| mappingWithClassMerging.getClassNaming("classmerging.ProguardMethodMapTest$B"); |
| assertTrue( |
| mappingsForClassBWithClassMerging |
| .getMappedRangesForRenamedName("method") |
| .getMappedRanges() |
| .stream() |
| .anyMatch( |
| mappedRange -> |
| mappedRange |
| .signature |
| .toString() |
| .equals("void classmerging.ProguardMethodMapTest$A.method()") |
| && mappedRange.getRenamedName().equals("method"))); |
| } |
| |
| @Test |
| public void testSuperCallWasDetected() throws Throwable { |
| String main = "classmerging.SuperCallRewritingTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("SubClassThatReferencesSuperMethod.class"), |
| CF_DIR.resolve("SuperClassWithReferencedMethod.class"), |
| CF_DIR.resolve("SuperCallRewritingTest.class"), |
| CF_DIR.resolve("NeverInline.class") |
| }; |
| Set<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.SubClassThatReferencesSuperMethod", |
| "classmerging.SuperCallRewritingTest"); |
| runTest( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(EXAMPLE_KEEP)) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| programFiles, |
| preservedClassNames::contains); |
| } |
| |
| // When a subclass A has been merged into its subclass B, we rewrite invoke-super calls that hit |
| // methods in A to invoke-direct calls. However, we should be careful not to transform invoke- |
| // super instructions into invoke-direct instructions simply because the static target is a method |
| // in the enclosing class. |
| // |
| // This test hand-crafts an invoke-super instruction in SubClassThatReferencesSuperMethod that |
| // targets SubClassThatReferencesSuperMethod.referencedMethod. When running without class |
| // merging, R8 should not rewrite the invoke-super instruction into invoke-direct. |
| @Test |
| public void testSuperCallNotRewrittenToDirect() throws Throwable { |
| assumeTrue(parameters.isDexRuntime()); // Due to smali input. |
| |
| String main = "classmerging.SuperCallRewritingTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("SuperClassWithReferencedMethod.class"), |
| CF_DIR.resolve("SuperCallRewritingTest.class"), |
| CF_DIR.resolve("NeverInline.class") |
| }; |
| |
| // Build SubClassThatReferencesMethod. |
| SmaliBuilder smaliBuilder = |
| new SmaliBuilder( |
| "classmerging.SubClassThatReferencesSuperMethod", |
| "classmerging.SuperClassWithReferencedMethod"); |
| smaliBuilder.addInitializer( |
| 0, |
| "invoke-direct {p0}, Lclassmerging/SuperClassWithReferencedMethod;-><init>()V", |
| "return-void"); |
| smaliBuilder.addInstanceMethod( |
| "java.lang.String", |
| "referencedMethod", |
| 2, |
| "sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;", |
| "const-string v1, \"In referencedMethod on SubClassThatReferencesSuperMethod\"", |
| "invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V", |
| "invoke-super {p0}, " |
| + "Lclassmerging/SubClassThatReferencesSuperMethod;->referencedMethod()" |
| + "Ljava/lang/String;", |
| "move-result-object v1", |
| "return-object v1"); |
| |
| String expectedOutput = |
| StringUtils.lines( |
| "Calling referencedMethod on SubClassThatReferencesSuperMethod", |
| "In referencedMethod on SubClassThatReferencesSuperMethod", |
| "In referencedMethod on SuperClassWithReferencedMethod", |
| "SuperClassWithReferencedMethod.referencedMethod()"); |
| |
| testForD8() |
| .addProgramFiles(programFiles) |
| .addProgramDexFileData(smaliBuilder.compile()) |
| .run(main) |
| .assertSuccessWithOutput(expectedOutput); |
| |
| testForR8(parameters.getBackend()) |
| .addOptionsModification(this::configure) |
| .addKeepMainRule(main) |
| // Keep the classes to avoid merge, but don't keep methods which allows inlining. |
| .addKeepRules("-keep class *") |
| .addProgramFiles(programFiles) |
| .addProgramDexFileData(smaliBuilder.compile()) |
| .run(main) |
| .assertSuccessWithOutput(expectedOutput); |
| } |
| |
| // The following test checks that our rewriting of invoke-super instructions in a class F works |
| // if a super type of F has previously been merged into its sub class. |
| // |
| // class A {} <- A is kept to preserve the error |
| // class B extends A { in F.invokeMethodOnA(). |
| // public void m() {} |
| // } |
| // class C extends B { <- C will be merged into D. |
| // @Override |
| // public void m() { "invoke-super B.m()" } |
| // } |
| // class D extends C { |
| // @Override |
| // public void m() { "invoke-super C.m()" } |
| // } |
| // class E extends D { <- E will be merged into F. |
| // @Override |
| // public void m() { "invoke-super D.m()" } |
| // } |
| // class F extends E { |
| // @Override |
| // public void m() { throw new Exception() } <- Should be dead code. |
| // |
| // public void invokeMethodOnA() { "invoke-super A.m()" } <- Should yield NoSuchMethodError. |
| // public void invokeMethodOnB() { "invoke-super B.m()" } |
| // public void invokeMethodOnC() { "invoke-super C.m()" } |
| // public void invokeMethodOnD() { "invoke-super D.m()" } |
| // public void invokeMethodOnE() { "invoke-super E.m()" } |
| // public void invokeMethodOnF() { "invoke-super F.m()" } |
| // } |
| @Test |
| public void testSuperCallToMergedClassIsRewritten() throws Throwable { |
| assumeTrue(parameters.isDexRuntime()); // Due to smali input. |
| assumeFalse(parameters.getRuntime().asDex().getVm().getVersion() == Version.V5_1_1); |
| assumeFalse(parameters.getRuntime().asDex().getVm().getVersion() == Version.V6_0_1); |
| |
| String main = "classmerging.SuperCallToMergedClassIsRewrittenTest"; |
| Set<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.SuperCallToMergedClassIsRewrittenTest", |
| "classmerging.A", |
| "classmerging.B", |
| "classmerging.D", |
| "classmerging.F"); |
| |
| JasminBuilder jasminBuilder = new JasminBuilder(); |
| |
| ClassBuilder classBuilder = jasminBuilder.addClass(main); |
| classBuilder.addMainMethod( |
| ".limit locals 1", |
| ".limit stack 3", |
| // Instantiate B so that it is not merged into C. |
| "new classmerging/B", |
| "dup", |
| "invokespecial classmerging/B/<init>()V", |
| "invokevirtual classmerging/B/m()V", |
| // Instantiate D so that it is not merged into E. |
| "new classmerging/D", |
| "dup", |
| "invokespecial classmerging/D/<init>()V", |
| "invokevirtual classmerging/D/m()V", |
| // Start the actual testing. |
| "new classmerging/F", |
| "dup", |
| "invokespecial classmerging/F/<init>()V", |
| "dup", |
| "invokevirtual classmerging/F/invokeMethodOnB()V", |
| "dup", |
| "invokevirtual classmerging/F/invokeMethodOnC()V", |
| "dup", |
| "invokevirtual classmerging/F/invokeMethodOnD()V", |
| "dup", |
| "invokevirtual classmerging/F/invokeMethodOnE()V", |
| "dup", |
| "invokevirtual classmerging/F/invokeMethodOnF()V", |
| "dup", |
| // The method invokeMethodOnA() should yield a NoSuchMethodError. |
| "try_start:", |
| "invokevirtual classmerging/F/invokeMethodOnA()V", |
| "try_end:", |
| "return", |
| "catch:", |
| jasminCodeForPrinting("NoSuchMethodError"), |
| "return", |
| ".catch java/lang/NoSuchMethodError from try_start to try_end using catch"); |
| |
| // Class A deliberately has no method m. We need to make sure that the "invoke-super A.m()" |
| // instruction in class F is not rewritten into something that does not throw. |
| classBuilder = jasminBuilder.addClass("classmerging.A"); |
| classBuilder.addDefaultConstructor(); |
| |
| // Class B declares a virtual method m() that prints "In B.m()". |
| classBuilder = jasminBuilder.addClass("classmerging.B", "classmerging.A"); |
| classBuilder.addDefaultConstructor(); |
| classBuilder.addVirtualMethod( |
| "m", "V", ".limit locals 1", ".limit stack 2", jasminCodeForPrinting("In B.m()"), "return"); |
| |
| // Class C, D, and E declare a virtual method m() that prints "In C.m()", "In D.m()", and |
| // "In E.m()", respectively. |
| String[][] pairs = |
| new String[][] {new String[] {"C", "B"}, new String[] {"D", "C"}, new String[] {"E", "D"}}; |
| for (String[] pair : pairs) { |
| String name = pair[0], superName = pair[1]; |
| classBuilder = jasminBuilder.addClass("classmerging." + name, "classmerging." + superName); |
| classBuilder.addDefaultConstructor(); |
| classBuilder.addVirtualMethod( |
| "m", |
| "V", |
| ".limit locals 1", |
| ".limit stack 2", |
| jasminCodeForPrinting("In " + name + ".m()"), |
| "aload_0", |
| "invokespecial classmerging/" + superName + "/m()V", |
| "return"); |
| } |
| |
| // Class F declares a virtual method m that throws an exception (it is expected to be dead). |
| // |
| // Note that F is generated from smali since it is the only way to generate an instruction on |
| // the form "invoke-super F.m()". |
| SmaliBuilder smaliBuilder = new SmaliBuilder(); |
| smaliBuilder.addClass("classmerging.F", "classmerging.E"); |
| smaliBuilder.addDefaultConstructor(); |
| smaliBuilder.addInstanceMethod( |
| "void", |
| "m", |
| 1, |
| "new-instance v1, Ljava/lang/Exception;", |
| "invoke-direct {v1}, Ljava/lang/Exception;-><init>()V", |
| "throw v1"); |
| // Add methods to F with an "invoke-super X.m()" instruction for X in {A, B, C, D, E, F}. |
| for (String type : ImmutableList.of("A", "B", "C", "D", "E", "F")) { |
| String code = |
| buildCode("invoke-super {p0}, Lclassmerging/" + type + ";->m()V", "return-void"); |
| smaliBuilder.addInstanceMethod("void", "invokeMethodOn" + type, 0, code); |
| } |
| |
| // Build app. |
| AndroidApp.Builder appBuilder = AndroidApp.builder(); |
| appBuilder.addClassProgramData(jasminBuilder.buildClasses()); |
| appBuilder.addDexProgramData(smaliBuilder.compile(), Origin.unknown()); |
| |
| // Run test. |
| runTestOnInput( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(String.format("-keep class %s { public static void main(...); }", main)), |
| main, |
| appBuilder.build(), |
| preservedClassNames::contains); |
| } |
| |
| private static String jasminCodeForPrinting(String message) { |
| return buildCode( |
| "getstatic java/lang/System/out Ljava/io/PrintStream;", |
| String.format("ldc \"%s\"", message), |
| "invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V"); |
| } |
| |
| @Test |
| public void testConflictingInterfaceSignatures() throws Throwable { |
| String main = "classmerging.ConflictingInterfaceSignaturesTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("ConflictingInterfaceSignaturesTest.class"), |
| CF_DIR.resolve("ConflictingInterfaceSignaturesTest$A.class"), |
| CF_DIR.resolve("ConflictingInterfaceSignaturesTest$B.class"), |
| CF_DIR.resolve("ConflictingInterfaceSignaturesTest$InterfaceImpl.class"), |
| CF_DIR.resolve("NeverInline.class") |
| }; |
| Set<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.ConflictingInterfaceSignaturesTest", |
| "classmerging.ConflictingInterfaceSignaturesTest$InterfaceImpl"); |
| runTest( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(EXAMPLE_KEEP)) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| programFiles, |
| preservedClassNames::contains); |
| } |
| |
| @Test |
| public void testMergeDefaultMethodIntoClass() throws Throwable { |
| String main = "classmerging.MergeDefaultMethodIntoClassTest"; |
| Path[] programFiles = |
| new Path[] { |
| JAVA8_CF_DIR.resolve("MergeDefaultMethodIntoClassTest.class"), |
| JAVA8_CF_DIR.resolve("MergeDefaultMethodIntoClassTest$A.class"), |
| JAVA8_CF_DIR.resolve("MergeDefaultMethodIntoClassTest$B.class"), |
| JAVA8_CF_DIR.resolve("NeverInline.class") |
| }; |
| ImmutableSet<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.MergeDefaultMethodIntoClassTest", |
| "classmerging.MergeDefaultMethodIntoClassTest$B"); |
| AndroidApp app = |
| AndroidApp.builder() |
| .addProgramFiles(programFiles) |
| .addLibraryFile(ToolHelper.getAndroidJar(AndroidApiLevel.O)) |
| .build(); |
| |
| // Sanity check that there is actually an invoke-interface instruction in the input. We need |
| // to make sure that this invoke-interface instruction is translated to invoke-virtual after |
| // the classes A and B are merged. |
| CodeInspector inputInspector = new CodeInspector(app); |
| ClassSubject clazz = inputInspector.clazz("classmerging.MergeDefaultMethodIntoClassTest"); |
| assertThat(clazz, isPresent()); |
| MethodSubject method = clazz.method("void", "main", ImmutableList.of("java.lang.String[]")); |
| assertThat(method, isPresent()); |
| assertThat( |
| method.getMethod().getCode().asCfCode().toString(), |
| containsString("invokeinterface Lclassmerging/MergeDefaultMethodIntoClassTest$A;f()V")); |
| |
| runTestOnInput( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(JAVA8_EXAMPLE_KEEP)) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| app, |
| preservedClassNames::contains); |
| } |
| |
| @Test |
| public void testNativeMethod() throws Throwable { |
| String main = "classmerging.ClassWithNativeMethodTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("ClassWithNativeMethodTest.class"), |
| CF_DIR.resolve("ClassWithNativeMethodTest$A.class"), |
| CF_DIR.resolve("ClassWithNativeMethodTest$B.class") |
| }; |
| // Ensures that the class A with a native method has not been merged into its subclass B. |
| ImmutableSet<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.ClassWithNativeMethodTest", |
| "classmerging.ClassWithNativeMethodTest$A", |
| "classmerging.ClassWithNativeMethodTest$B"); |
| runTest( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(EXAMPLE_KEEP)) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| programFiles, |
| preservedClassNames::contains); |
| } |
| |
| @Test |
| public void testNoIllegalClassAccess() throws Throwable { |
| String main = "classmerging.SimpleInterfaceAccessTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("SimpleInterfaceAccessTest.class"), |
| CF_DIR.resolve("SimpleInterfaceAccessTest$1.class"), |
| CF_DIR.resolve("SimpleInterfaceAccessTest$SimpleInterface.class"), |
| CF_DIR.resolve("SimpleInterfaceAccessTest$OtherSimpleInterface.class"), |
| CF_DIR.resolve("SimpleInterfaceAccessTest$OtherSimpleInterfaceImpl.class"), |
| CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever.class"), |
| CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever$SimpleInterfaceImpl.class"), |
| CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever$1.class"), |
| CF_DIR.resolve("NeverInline.class"), |
| CF_DIR.resolve("NoHorizontalClassMerging.class") |
| }; |
| // SimpleInterface cannot be merged into SimpleInterfaceImpl because SimpleInterfaceImpl |
| // is in a different package and is not public. |
| Set<String> preservedClassNames = |
| SetUtils.newHashSet( |
| "classmerging.SimpleInterfaceAccessTest", |
| "classmerging.SimpleInterfaceAccessTest$SimpleInterface", |
| "classmerging.SimpleInterfaceAccessTest$OtherSimpleInterface", |
| "classmerging.SimpleInterfaceAccessTest$OtherSimpleInterfaceImpl", |
| "classmerging.pkg.SimpleInterfaceImplRetriever", |
| "classmerging.pkg.SimpleInterfaceImplRetriever$SimpleInterfaceImpl"); |
| runTest( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(EXAMPLE_KEEP)) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| programFiles, |
| preservedClassNames::contains); |
| } |
| |
| // TODO(christofferqa): This test checks that the invoke-super instruction in B is not rewritten |
| // into an invoke-direct instruction after it gets merged into class C. We should add a test that |
| // checks that this works with and without inlining of the method B.m(). |
| @Test |
| public void testRewritePinnedMethod() throws Throwable { |
| String main = "classmerging.RewritePinnedMethodTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("RewritePinnedMethodTest.class"), |
| CF_DIR.resolve("RewritePinnedMethodTest$A.class"), |
| CF_DIR.resolve("RewritePinnedMethodTest$B.class"), |
| CF_DIR.resolve("RewritePinnedMethodTest$C.class") |
| }; |
| Set<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.RewritePinnedMethodTest", |
| "classmerging.RewritePinnedMethodTest$A", |
| "classmerging.RewritePinnedMethodTest$C"); |
| runTest( |
| testForR8(parameters.getBackend()) |
| .addKeepRules( |
| getProguardConfig( |
| EXAMPLE_KEEP, "-keep class classmerging.RewritePinnedMethodTest$A { *; }")) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| programFiles, |
| preservedClassNames::contains); |
| } |
| |
| @Test |
| public void testTemplateMethodPattern() throws Throwable { |
| String main = "classmerging.TemplateMethodTest"; |
| Path[] programFiles = |
| new Path[] { |
| CF_DIR.resolve("TemplateMethodTest.class"), |
| CF_DIR.resolve("TemplateMethodTest$1.class"), |
| CF_DIR.resolve("TemplateMethodTest$AbstractClass.class"), |
| CF_DIR.resolve("TemplateMethodTest$AbstractClassImpl.class"), |
| CF_DIR.resolve("NeverInline.class") |
| }; |
| Set<String> preservedClassNames = |
| ImmutableSet.of( |
| "classmerging.TemplateMethodTest", "classmerging.TemplateMethodTest$AbstractClassImpl"); |
| runTest( |
| testForR8(parameters.getBackend()) |
| .addKeepRules(getProguardConfig(EXAMPLE_KEEP)) |
| .allowUnusedProguardConfigurationRules(), |
| main, |
| programFiles, |
| preservedClassNames::contains); |
| } |
| |
| private R8TestCompileResult runTest( |
| R8FullTestBuilder builder, |
| String main, |
| Path[] programFiles, |
| Predicate<String> preservedClassNames) |
| throws Throwable { |
| return runTestOnInput(builder, main, readProgramFiles(programFiles), preservedClassNames); |
| } |
| |
| private R8TestCompileResult runTestOnInput( |
| R8FullTestBuilder builder, |
| String main, |
| AndroidApp input, |
| Predicate<String> preservedClassNames) |
| throws Throwable { |
| return runTestOnInput( |
| builder.addOptionsModification(this::configure), |
| main, |
| input, |
| preservedClassNames, |
| new VerticalClassMergerDebugTestRunner(main, temp)); |
| } |
| |
| private R8TestCompileResult runTestOnInput( |
| R8FullTestBuilder builder, |
| String main, |
| AndroidApp input, |
| Predicate<String> preservedClassNames, |
| VerticalClassMergerDebugTestRunner debugTestRunner) |
| throws Throwable { |
| R8TestCompileResult compileResult = |
| builder |
| .apply( |
| b -> { |
| // Some tests add DEX inputs, so circumvent the check by adding directly to the |
| // app. |
| Builder appBuilder = ToolHelper.getAppBuilder(b.getBuilder()); |
| for (ProgramResourceProvider provider : input.getProgramResourceProviders()) { |
| appBuilder.addProgramResourceProvider(provider); |
| } |
| }) |
| .addDontObfuscate() |
| .enableProguardTestOptions() |
| .setMinApi(parameters) |
| .compile(); |
| |
| Path proguardMapPath = File.createTempFile("mapping", ".txt", temp.getRoot()).toPath(); |
| FileUtils.writeTextFile(proguardMapPath, compileResult.getProguardMap()); |
| |
| CodeInspector inputInspector = new CodeInspector(input); |
| CodeInspector outputInspector = compileResult.inspector(); |
| |
| // Check that all classes in [preservedClassNames] are in fact preserved. |
| for (FoundClassSubject classSubject : inputInspector.allClasses()) { |
| String className = classSubject.getOriginalName(); |
| boolean shouldBePresent = preservedClassNames.test(className); |
| assertEquals( |
| "Class " + className + " should be " + (shouldBePresent ? "present" : "absent"), |
| shouldBePresent, |
| outputInspector.clazz(className).isPresent()); |
| } |
| |
| // Check that the R8-generated code produces the same result as D8-generated code. |
| String d8Result = |
| testForD8() |
| .addProgramResourceProviders(input.getProgramResourceProviders()) |
| .run(main) |
| .assertSuccess() |
| .getStdOut(); |
| |
| compileResult.run(parameters.getRuntime(), main).assertSuccessWithOutput(d8Result); |
| // Check that we never come across a method that has a name with "$classmerging$" in it during |
| // debugging. |
| if (debugTestRunner != null && parameters.isDexRuntime()) { |
| debugTestRunner.run(parameters.getRuntime(), compileResult.app, proguardMapPath); |
| } |
| return compileResult; |
| } |
| |
| private String getProguardConfig(Path path, String... additionalRules) throws IOException { |
| StringBuilder builder = new StringBuilder(); |
| for (String line : Files.readAllLines(path)) { |
| builder.append(line); |
| builder.append(System.lineSeparator()); |
| } |
| for (String rule : additionalRules) { |
| builder.append(rule); |
| builder.append(System.lineSeparator()); |
| } |
| return builder.toString(); |
| } |
| |
| } |