blob: eb6273468e3a91dbd7b04413e7a2690df81409e4 [file] [log] [blame]
// 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.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())
.addOptionsModification(
options -> {
options.enableFieldBitAccessAnalysis = true;
options.protoShrinking().enableGeneratedExtensionRegistryShrinking = true;
options.protoShrinking().enableGeneratedMessageLiteShrinking = true;
options.protoShrinking().enableGeneratedMessageLiteBuilderShrinking = true;
options.enableStringSwitchConversion = true;
})
.allowAccessModification(allowAccessModification)
.allowUnusedProguardConfigurationRules()
.minification(enableMinification)
.setMinApi(parameters.getApiLevel())
.compile()
.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());
// TODO(b/112437944): Should be present.
assertThat(
usedRootClassSubject.uniqueFieldWithName("isExtendedWithRequiredField_"),
not(isPresent()));
// TODO(b/112437944): Should be present.
assertThat(
usedRootClassSubject.uniqueFieldWithName("isRepeatedlyExtendedWithRequiredField_"),
not(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())
// Enable the dynamicMethod() rewritings.
.addOptionsModification(
options -> {
assert !options.protoShrinking().enableGeneratedMessageLiteShrinking;
options.protoShrinking().enableGeneratedMessageLiteShrinking = true;
})
.allowAccessModification(allowAccessModification)
.allowUnusedProguardConfigurationRules()
.minification(enableMinification)
.setMinApi(parameters.getApiLevel())
.compile()
.inspect(
inspector ->
assertRewrittenProtoSchemasMatch(new CodeInspector(PROGRAM_FILES), inspector));
}
}