| // Copyright (c) 2019, 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.internal.proto; |
| |
| import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; |
| import static org.hamcrest.CoreMatchers.anyOf; |
| import static org.hamcrest.CoreMatchers.containsString; |
| import static org.hamcrest.CoreMatchers.equalTo; |
| import static org.hamcrest.CoreMatchers.not; |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.junit.Assert.assertEquals; |
| |
| import com.android.tools.r8.R8TestRunResult; |
| import com.android.tools.r8.TestParameters; |
| import com.android.tools.r8.ToolHelper; |
| import com.android.tools.r8.graph.DexItemFactory; |
| import com.android.tools.r8.utils.BooleanUtils; |
| import com.android.tools.r8.utils.codeinspector.ClassSubject; |
| import com.android.tools.r8.utils.codeinspector.CodeInspector; |
| import com.android.tools.r8.utils.codeinspector.analysis.ProtoApplicationStats; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import java.nio.file.Path; |
| import java.util.List; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| |
| @RunWith(Parameterized.class) |
| public class Proto2ShrinkingTest extends ProtoShrinkingTestBase { |
| |
| private static final String CONTAINS_FLAGGED_OFF_FIELD = |
| "com.android.tools.r8.proto2.Shrinking$ContainsFlaggedOffField"; |
| private static final String EXT_B = |
| "com.android.tools.r8.proto2.Shrinking$PartiallyUsedWithExtension$ExtB"; |
| private static final String EXT_C = |
| "com.android.tools.r8.proto2.Shrinking$PartiallyUsedWithExtension$ExtC"; |
| private static final String FLAGGED_OFF_EXTENSION = |
| "com.android.tools.r8.proto2.Shrinking$HasFlaggedOffExtension$Ext"; |
| private static final String HAS_NO_USED_EXTENSIONS = |
| "com.android.tools.r8.proto2.Shrinking$HasNoUsedExtensions"; |
| private static final String HAS_REQUIRED_FIELD = |
| "com.android.tools.r8.proto2.Graph$HasRequiredField"; |
| private static final String PARTIALLY_USED = |
| "com.android.tools.r8.proto2.Shrinking$PartiallyUsed"; |
| private static final String USED_ROOT = "com.android.tools.r8.proto2.Graph$UsedRoot"; |
| private static final String USED_VIA_HAZZER = |
| "com.android.tools.r8.proto2.Shrinking$UsedViaHazzer"; |
| private static final String USES_ONLY_REPEATED_FIELDS = |
| "com.android.tools.r8.proto2.Shrinking$UsesOnlyRepeatedFields"; |
| |
| private static List<Path> PROGRAM_FILES = |
| ImmutableList.of(PROTO2_EXAMPLES_JAR, PROTO2_PROTO_JAR, PROTOBUF_LITE_JAR); |
| |
| private final boolean allowAccessModification; |
| private final boolean enableMinification; |
| private final TestParameters parameters; |
| |
| @Parameterized.Parameters(name = "{2}, allow access modification: {0}, enable minification: {1}") |
| public static List<Object[]> data() { |
| return buildParameters( |
| BooleanUtils.values(), |
| BooleanUtils.values(), |
| getTestParameters().withAllRuntimesAndApiLevels().build()); |
| } |
| |
| public Proto2ShrinkingTest( |
| boolean allowAccessModification, boolean enableMinification, TestParameters parameters) { |
| this.allowAccessModification = allowAccessModification; |
| this.enableMinification = enableMinification; |
| this.parameters = parameters; |
| } |
| |
| @Test |
| public void test() throws Exception { |
| CodeInspector inputInspector = new CodeInspector(PROGRAM_FILES); |
| R8TestRunResult result = |
| testForR8(parameters.getBackend()) |
| .addProgramFiles(PROGRAM_FILES) |
| .addKeepMainRule("proto2.TestClass") |
| .addKeepRuleFiles(PROTOBUF_LITE_PROGUARD_RULES) |
| .addKeepRules(allGeneratedMessageLiteSubtypesAreInstantiatedRule()) |
| .allowAccessModification(allowAccessModification) |
| .allowDiagnosticMessages() |
| .allowUnusedProguardConfigurationRules() |
| .enableProtoShrinking() |
| .minification(enableMinification) |
| .setMinApi(parameters.getApiLevel()) |
| .compile() |
| .assertAllInfoMessagesMatch( |
| containsString("Proguard configuration rule does not match anything")) |
| .assertAllWarningMessagesMatch( |
| equalTo("Resource 'META-INF/MANIFEST.MF' already exists.")) |
| .inspect( |
| outputInspector -> { |
| verifyMapAndRequiredFieldsAreKept(inputInspector, outputInspector); |
| verifyUnusedExtensionsAreRemoved(inputInspector, outputInspector); |
| verifyUnusedFieldsAreRemoved(inputInspector, outputInspector); |
| verifyUnusedHazzerBitFieldsAreRemoved(inputInspector, outputInspector); |
| verifyUnusedTypesAreRemoved(inputInspector, outputInspector); |
| }) |
| .run(parameters.getRuntime(), "proto2.TestClass") |
| .assertSuccessWithOutputLines( |
| "--- roundtrip ---", |
| "true", |
| "123", |
| "asdf", |
| "9223372036854775807", |
| "qwerty", |
| "--- partiallyUsed_proto2 ---", |
| "true", |
| "42", |
| "--- usedViaHazzer ---", |
| "true", |
| "--- usedViaOneofCase ---", |
| "true", |
| "--- usesOnlyRepeatedFields ---", |
| "1", |
| "--- containsFlaggedOffField ---", |
| "0", |
| "--- hasFlaggedOffExtension ---", |
| "4", |
| "--- useOneExtension ---", |
| "42", |
| "--- keepMapAndRequiredFields ---", |
| "true", |
| "10", |
| "10", |
| "10"); |
| |
| DexItemFactory dexItemFactory = new DexItemFactory(); |
| ProtoApplicationStats original = new ProtoApplicationStats(dexItemFactory, inputInspector); |
| ProtoApplicationStats actual = |
| new ProtoApplicationStats(dexItemFactory, result.inspector(), original); |
| |
| assertEquals( |
| ImmutableSet.of(), |
| actual.getGeneratedExtensionRegistryStats().getSpuriouslyRetainedExtensionFields()); |
| |
| if (ToolHelper.isLocalDevelopment()) { |
| System.out.println(actual.getStats()); |
| } |
| } |
| |
| private void verifyMapAndRequiredFieldsAreKept( |
| CodeInspector inputInspector, CodeInspector outputInspector) { |
| // Verify the existence of various fields in the input. |
| { |
| ClassSubject usedRootClassSubject = inputInspector.clazz(USED_ROOT); |
| assertThat(usedRootClassSubject, isPresent()); |
| assertThat(usedRootClassSubject.uniqueFieldWithName("hasRequiredFieldA_"), isPresent()); |
| assertThat(usedRootClassSubject.uniqueFieldWithName("hasRequiredFieldB_"), isPresent()); |
| assertThat(usedRootClassSubject.uniqueFieldWithName("myOneof_"), isPresent()); |
| assertThat( |
| usedRootClassSubject.uniqueFieldWithName("recursiveWithRequiredField_"), isPresent()); |
| assertThat(usedRootClassSubject.uniqueFieldWithName("isExtendedWithOptional_"), isPresent()); |
| assertThat(usedRootClassSubject.uniqueFieldWithName("isExtendedWithScalars_"), isPresent()); |
| assertThat( |
| usedRootClassSubject.uniqueFieldWithName("isExtendedWithRequiredField_"), isPresent()); |
| assertThat( |
| usedRootClassSubject.uniqueFieldWithName("isRepeatedlyExtendedWithRequiredField_"), |
| isPresent()); |
| assertThat(usedRootClassSubject.uniqueFieldWithName("hasMapField_"), isPresent()); |
| |
| ClassSubject hasRequiredFieldClassSubject = inputInspector.clazz(HAS_REQUIRED_FIELD); |
| assertThat(hasRequiredFieldClassSubject, isPresent()); |
| assertThat(hasRequiredFieldClassSubject.uniqueFieldWithName("value_"), isPresent()); |
| } |
| |
| // Verify the existence of various fields in the output. |
| { |
| ClassSubject usedRootClassSubject = outputInspector.clazz(USED_ROOT); |
| assertThat(usedRootClassSubject, isPresent()); |
| assertThat(usedRootClassSubject.uniqueFieldWithName("hasRequiredFieldA_"), isPresent()); |
| assertThat(usedRootClassSubject.uniqueFieldWithName("hasRequiredFieldB_"), isPresent()); |
| assertThat(usedRootClassSubject.uniqueFieldWithName("myOneof_"), isPresent()); |
| assertThat( |
| usedRootClassSubject.uniqueFieldWithName("recursiveWithRequiredField_"), isPresent()); |
| assertThat(usedRootClassSubject.uniqueFieldWithName("hasMapField_"), isPresent()); |
| |
| assertThat( |
| usedRootClassSubject.uniqueFieldWithName("isExtendedWithRequiredField_"), isPresent()); |
| assertThat( |
| usedRootClassSubject.uniqueFieldWithName("isRepeatedlyExtendedWithRequiredField_"), |
| isPresent()); |
| |
| ClassSubject hasRequiredFieldClassSubject = outputInspector.clazz(HAS_REQUIRED_FIELD); |
| assertThat(hasRequiredFieldClassSubject, isPresent()); |
| assertThat(hasRequiredFieldClassSubject.uniqueFieldWithName("value_"), isPresent()); |
| } |
| |
| // Verify the absence of various fields in the output. |
| { |
| ClassSubject usedRootClassSubject = outputInspector.clazz(USED_ROOT); |
| assertThat(usedRootClassSubject, isPresent()); |
| assertThat( |
| usedRootClassSubject.uniqueFieldWithName("isExtendedWithOptional_"), not(isPresent())); |
| assertThat( |
| usedRootClassSubject.uniqueFieldWithName("isExtendedWithScalars_"), not(isPresent())); |
| } |
| } |
| |
| private void verifyUnusedExtensionsAreRemoved( |
| CodeInspector inputInspector, CodeInspector outputInspector) { |
| // Verify that the registry was split across multiple methods in the input. |
| { |
| ClassSubject generatedExtensionRegistryLoader = |
| inputInspector.clazz("com.google.protobuf.proto2_registryGeneratedExtensionRegistryLite"); |
| assertThat(generatedExtensionRegistryLoader, isPresent()); |
| assertThat( |
| generatedExtensionRegistryLoader.uniqueMethodWithName("findLiteExtensionByNumber"), |
| isPresent()); |
| assertThat( |
| generatedExtensionRegistryLoader.uniqueMethodWithName("findLiteExtensionByNumber1"), |
| isPresent()); |
| assertThat( |
| generatedExtensionRegistryLoader.uniqueMethodWithName("findLiteExtensionByNumber2"), |
| isPresent()); |
| } |
| |
| // Verify that the registry methods are still present in the output. |
| // TODO(b/112437944): Should they be optimized into a single findLiteExtensionByNumber() method? |
| { |
| ClassSubject generatedExtensionRegistryLoader = |
| outputInspector.clazz( |
| "com.google.protobuf.proto2_registryGeneratedExtensionRegistryLite"); |
| assertThat(generatedExtensionRegistryLoader, isPresent()); |
| assertThat( |
| generatedExtensionRegistryLoader.uniqueMethodWithName("findLiteExtensionByNumber"), |
| isPresent()); |
| assertThat( |
| generatedExtensionRegistryLoader.uniqueMethodWithName("findLiteExtensionByNumber1"), |
| isPresent()); |
| assertThat( |
| generatedExtensionRegistryLoader.uniqueMethodWithName("findLiteExtensionByNumber2"), |
| isPresent()); |
| } |
| |
| // Verify that unused extensions have been removed with -allowaccessmodification. |
| if (allowAccessModification) { |
| List<String> unusedExtensionNames = |
| ImmutableList.of(FLAGGED_OFF_EXTENSION, HAS_NO_USED_EXTENSIONS, EXT_B, EXT_C); |
| for (String unusedExtensionName : unusedExtensionNames) { |
| assertThat(inputInspector.clazz(unusedExtensionName), isPresent()); |
| assertThat( |
| unusedExtensionName, outputInspector.clazz(unusedExtensionName), not(isPresent())); |
| } |
| } |
| } |
| |
| private void verifyUnusedFieldsAreRemoved( |
| CodeInspector inputInspector, CodeInspector outputInspector) { |
| // Verify that various proto fields are present the input. |
| { |
| ClassSubject cfofClassSubject = inputInspector.clazz(CONTAINS_FLAGGED_OFF_FIELD); |
| assertThat(cfofClassSubject, isPresent()); |
| assertThat(cfofClassSubject.uniqueFieldWithName("conditionallyUsed_"), isPresent()); |
| |
| ClassSubject puClassSubject = inputInspector.clazz(PARTIALLY_USED); |
| assertThat(puClassSubject, isPresent()); |
| assertEquals(7, puClassSubject.allInstanceFields().size()); |
| assertThat(puClassSubject.uniqueFieldWithName("bitField0_"), isPresent()); |
| assertThat(puClassSubject.uniqueFieldWithName("used_"), isPresent()); |
| assertThat(puClassSubject.uniqueFieldWithName("completelyUnused_"), isPresent()); |
| assertThat(puClassSubject.uniqueFieldWithName("unusedEnum_"), isPresent()); |
| assertThat(puClassSubject.uniqueFieldWithName("unusedRepeatedEnum_"), isPresent()); |
| assertThat(puClassSubject.uniqueFieldWithName("unusedMessage_"), isPresent()); |
| assertThat(puClassSubject.uniqueFieldWithName("unusedRepeatedMessage_"), isPresent()); |
| |
| ClassSubject uvhClassSubject = inputInspector.clazz(USED_VIA_HAZZER); |
| assertThat(uvhClassSubject, isPresent()); |
| assertThat(uvhClassSubject.uniqueFieldWithName("used_"), isPresent()); |
| assertThat(uvhClassSubject.uniqueFieldWithName("unused_"), isPresent()); |
| } |
| |
| // Verify that various proto fields have been removed in the output. |
| { |
| ClassSubject cfofClassSubject = outputInspector.clazz(CONTAINS_FLAGGED_OFF_FIELD); |
| assertThat(cfofClassSubject, isPresent()); |
| assertThat(cfofClassSubject.uniqueFieldWithName("conditionallyUsed_"), not(isPresent())); |
| |
| ClassSubject puClassSubject = outputInspector.clazz(PARTIALLY_USED); |
| assertThat(puClassSubject, isPresent()); |
| assertThat(puClassSubject.uniqueFieldWithName("bitField0_"), isPresent()); |
| assertThat(puClassSubject.uniqueFieldWithName("used_"), isPresent()); |
| assertThat(puClassSubject.uniqueFieldWithName("completelyUnused_"), not(isPresent())); |
| assertThat(puClassSubject.uniqueFieldWithName("unusedEnum_"), not(isPresent())); |
| assertThat(puClassSubject.uniqueFieldWithName("unusedRepeatedEnum_"), not(isPresent())); |
| assertThat(puClassSubject.uniqueFieldWithName("unusedMessage_"), not(isPresent())); |
| assertThat(puClassSubject.uniqueFieldWithName("unusedRepeatedMessage_"), not(isPresent())); |
| |
| ClassSubject uvhClassSubject = outputInspector.clazz(USED_VIA_HAZZER); |
| assertThat(uvhClassSubject, isPresent()); |
| assertThat(uvhClassSubject.uniqueFieldWithName("used_"), isPresent()); |
| assertThat(uvhClassSubject.uniqueFieldWithName("unused_"), not(isPresent())); |
| } |
| } |
| |
| private void verifyUnusedHazzerBitFieldsAreRemoved( |
| CodeInspector inputInspector, CodeInspector outputInspector) { |
| // Verify that various proto fields are present the input. |
| { |
| ClassSubject classSubject = inputInspector.clazz(USES_ONLY_REPEATED_FIELDS); |
| assertThat(classSubject, isPresent()); |
| assertThat(classSubject.uniqueFieldWithName("bitField0_"), isPresent()); |
| assertThat(classSubject.uniqueFieldWithName("myoneof_"), isPresent()); |
| assertThat(classSubject.uniqueFieldWithName("myoneofCase_"), isPresent()); |
| } |
| |
| // Verify that various proto fields have been removed in the output. |
| { |
| ClassSubject classSubject = outputInspector.clazz(USES_ONLY_REPEATED_FIELDS); |
| assertThat(classSubject, isPresent()); |
| assertThat(classSubject.uniqueFieldWithName("bitField0_"), not(isPresent())); |
| assertThat(classSubject.uniqueFieldWithName("myoneof_"), not(isPresent())); |
| assertThat(classSubject.uniqueFieldWithName("myoneofCase_"), not(isPresent())); |
| } |
| } |
| |
| private void verifyUnusedTypesAreRemoved( |
| CodeInspector inputInspector, CodeInspector outputInspector) { |
| // Verify that various types are present the input. |
| { |
| ClassSubject enumClassSubject = inputInspector.clazz(PARTIALLY_USED + "$Enum"); |
| assertThat(enumClassSubject, isPresent()); |
| |
| ClassSubject nestedClassSubject = inputInspector.clazz(PARTIALLY_USED + "$Nested"); |
| assertThat(nestedClassSubject, isPresent()); |
| } |
| |
| // Verify that various types have been removed in the output. |
| { |
| ClassSubject enumClassSubject = outputInspector.clazz(PARTIALLY_USED + "$Enum"); |
| assertThat(enumClassSubject, not(isPresent())); |
| |
| ClassSubject nestedClassSubject = outputInspector.clazz(PARTIALLY_USED + "$Nested"); |
| assertThat(nestedClassSubject, not(isPresent())); |
| } |
| } |
| |
| @Test |
| public void testNoRewriting() throws Exception { |
| testForR8(parameters.getBackend()) |
| .addProgramFiles(PROGRAM_FILES) |
| .addKeepMainRule("proto2.TestClass") |
| .addKeepRuleFiles(PROTOBUF_LITE_PROGUARD_RULES) |
| // Retain all protos. |
| .addKeepRules(keepAllProtosRule()) |
| // Retain the signature of dynamicMethod() and newMessageInfo(). |
| .addKeepRules(keepDynamicMethodSignatureRule(), keepNewMessageInfoSignatureRule()) |
| .allowAccessModification(allowAccessModification) |
| .allowDiagnosticMessages() |
| .allowUnusedProguardConfigurationRules() |
| .enableProtoShrinking() |
| .minification(enableMinification) |
| .setMinApi(parameters.getApiLevel()) |
| .compile() |
| .assertAllInfoMessagesMatch( |
| containsString("Proguard configuration rule does not match anything")) |
| .assertAllWarningMessagesMatch( |
| anyOf( |
| equalTo("Resource 'META-INF/MANIFEST.MF' already exists."), |
| containsString("required for default or static interface methods desugaring"))) |
| .inspect( |
| inspector -> |
| assertRewrittenProtoSchemasMatch(new CodeInspector(PROGRAM_FILES), inspector)); |
| } |
| } |