blob: e9181de6d307e275adf996942a97344ef69316c7 [file] [log] [blame]
// Copyright (c) 2024, 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.isAbsentIf;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assume.assumeTrue;
import com.android.tools.r8.R8TestRunResult;
import com.android.tools.r8.SingleTestRunResult;
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.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
// TODO(b/339100248): Avoid creating edition2023 messages in proto2 package.
// Instead move proto2 and proto edition2023 messages to com.android.tools.r8.proto.
@RunWith(Parameterized.class)
public class ProtoShrinkingTest 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 final String MAIN = "proto2.TestClass";
@Parameter(0)
public boolean allowAccessModification;
@Parameter(1)
public boolean enableMinification;
@Parameter(2)
public TestParameters parameters;
@Parameter(3)
public ProtoRuntime protoRuntime;
@Parameter(4)
public ProtoTestSources protoTestSources;
@Parameters(name = "{2}, {3}, {4}, allow access modification: {0}, enable minification: {1}")
public static List<Object[]> data() {
return buildParameters(
BooleanUtils.values(),
BooleanUtils.values(),
getTestParameters().withDefaultDexRuntime().withAllApiLevels().build(),
ProtoRuntime.values(),
ProtoTestSources.getEdition2023AndProto2());
}
@Test
public void testD8() throws Exception {
protoRuntime.assumeIsNewerThanOrEqualToMinimumRequiredRuntime(protoTestSources);
testForD8()
.addProgramFiles(protoRuntime.getProgramFiles())
.addProgramFiles(protoTestSources.getProgramFiles())
.setMinApi(parameters)
.run(parameters.getRuntime(), MAIN)
.apply(this::checkRunResult);
}
@Test
public void testR8() throws Exception {
protoRuntime.assumeIsNewerThanOrEqualToMinimumRequiredRuntime(protoTestSources);
R8TestRunResult result =
testForR8(parameters.getBackend())
.apply(protoRuntime::addRuntime)
.apply(protoRuntime::workaroundProtoMessageRemoval)
.addProgramFiles(protoTestSources.getProgramFiles())
.addKeepMainRule(MAIN)
// TODO(b/173340579): This rule should not be needed to allow shrinking of
// PartiallyUsed$Enum.
.addNoHorizontalClassMergingRule(PARTIALLY_USED + "$Enum$1")
.allowAccessModification(allowAccessModification)
.allowDiagnosticMessages()
.allowUnusedDontWarnPatterns()
.allowUnusedProguardConfigurationRules()
.enableProguardTestOptions()
.enableProtoShrinking()
.minification(enableMinification)
.setMinApi(parameters)
.compile()
.assertAllInfoMessagesMatch(
containsString("Proguard configuration rule does not match anything"))
.apply(this::inspectWarningMessages)
.inspect(
outputInspector -> {
CodeInspector inputInspector = protoTestSources.getInspector();
verifyMapAndRequiredFieldsAreKept(inputInspector, outputInspector);
verifyUnusedExtensionsAreRemoved(inputInspector, outputInspector);
verifyUnusedFieldsAreRemoved(inputInspector, outputInspector);
verifyUnusedHazzerBitFieldsAreRemoved(inputInspector, outputInspector);
verifyUnusedTypesAreRemoved(inputInspector, outputInspector);
})
.run(parameters.getRuntime(), MAIN)
.apply(this::checkRunResult);
if (protoTestSources.getCorrespondingRuntime() != protoRuntime) {
result.assertFailure();
return;
}
DexItemFactory dexItemFactory = new DexItemFactory();
ProtoApplicationStats original =
new ProtoApplicationStats(dexItemFactory, protoTestSources.getInspector());
ProtoApplicationStats actual =
new ProtoApplicationStats(dexItemFactory, result.inspector(), original);
assertEquals(
ImmutableSet.of(),
actual.getGeneratedExtensionRegistryStats().getSpuriouslyRetainedExtensionFields());
if (ToolHelper.isLocalDevelopment()) {
System.out.println(actual.getStats());
}
}
private void checkRunResult(SingleTestRunResult<?> runResult) {
runResult.applyIf(
protoTestSources.getCorrespondingRuntime() == protoRuntime,
rr ->
rr.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"),
rr -> rr.assertFailureWithErrorThatThrows(ArrayIndexOutOfBoundsException.class));
}
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.uniqueFieldWithOriginalName("hasRequiredFieldA_"), isPresent());
assertThat(
usedRootClassSubject.uniqueFieldWithOriginalName("hasRequiredFieldB_"), isPresent());
assertThat(usedRootClassSubject.uniqueFieldWithOriginalName("myOneof_"), isPresent());
assertThat(
usedRootClassSubject.uniqueFieldWithOriginalName("recursiveWithRequiredField_"),
isPresent());
assertThat(
usedRootClassSubject.uniqueFieldWithOriginalName("isExtendedWithOptional_"), isPresent());
assertThat(
usedRootClassSubject.uniqueFieldWithOriginalName("isExtendedWithScalars_"), isPresent());
assertThat(
usedRootClassSubject.uniqueFieldWithOriginalName("isExtendedWithRequiredField_"),
isPresent());
assertThat(
usedRootClassSubject.uniqueFieldWithOriginalName(
"isRepeatedlyExtendedWithRequiredField_"),
isPresent());
assertThat(usedRootClassSubject.uniqueFieldWithOriginalName("hasMapField_"), isPresent());
ClassSubject hasRequiredFieldClassSubject = inputInspector.clazz(HAS_REQUIRED_FIELD);
assertThat(hasRequiredFieldClassSubject, isPresent());
assertThat(hasRequiredFieldClassSubject.uniqueFieldWithOriginalName("value_"), isPresent());
}
// Verify the existence of various fields in the output.
{
ClassSubject usedRootClassSubject = outputInspector.clazz(USED_ROOT);
assertThat(usedRootClassSubject, isPresent());
assertThat(
usedRootClassSubject.uniqueFieldWithOriginalName("hasRequiredFieldA_"), isPresent());
assertThat(
usedRootClassSubject.uniqueFieldWithOriginalName("hasRequiredFieldB_"), isPresent());
assertThat(usedRootClassSubject.uniqueFieldWithOriginalName("myOneof_"), isPresent());
assertThat(
usedRootClassSubject.uniqueFieldWithOriginalName("recursiveWithRequiredField_"),
isPresent());
assertThat(usedRootClassSubject.uniqueFieldWithOriginalName("hasMapField_"), isPresent());
assertThat(
usedRootClassSubject.uniqueFieldWithOriginalName("isExtendedWithRequiredField_"),
isPresent());
assertThat(
usedRootClassSubject.uniqueFieldWithOriginalName(
"isRepeatedlyExtendedWithRequiredField_"),
isPresent());
ClassSubject hasRequiredFieldClassSubject = outputInspector.clazz(HAS_REQUIRED_FIELD);
assertThat(hasRequiredFieldClassSubject, isPresent());
assertThat(hasRequiredFieldClassSubject.uniqueFieldWithOriginalName("value_"), isPresent());
}
// Verify the absence of various fields in the output.
{
ClassSubject usedRootClassSubject = outputInspector.clazz(USED_ROOT);
assertThat(usedRootClassSubject, isPresent());
assertThat(
usedRootClassSubject.uniqueFieldWithOriginalName("isExtendedWithOptional_"),
not(isPresent()));
assertThat(
usedRootClassSubject.uniqueFieldWithOriginalName("isExtendedWithScalars_"),
not(isPresent()));
}
}
private void verifyUnusedExtensionsAreRemoved(
CodeInspector inputInspector, CodeInspector outputInspector) {
verifyUnusedExtensionsAreRemoved(
inputInspector,
outputInspector,
"com.google.protobuf.proto2_registryGeneratedExtensionRegistryLite");
}
private void verifyUnusedExtensionsAreRemoved(
CodeInspector inputInspector, CodeInspector outputInspector, String extensionRegistryName) {
// Verify that the registry was split across multiple methods in the input.
{
ClassSubject generatedExtensionRegistryLoader = inputInspector.clazz(extensionRegistryName);
assertThat(generatedExtensionRegistryLoader, isPresent());
assertThat(
generatedExtensionRegistryLoader.uniqueMethodWithOriginalName(
"findLiteExtensionByNumber"),
isPresent());
assertThat(
generatedExtensionRegistryLoader.uniqueMethodWithOriginalName(
"findLiteExtensionByNumber1"),
isPresent());
assertThat(
generatedExtensionRegistryLoader.uniqueMethodWithOriginalName(
"findLiteExtensionByNumber2"),
isPresent());
}
// Verify that the registry methods are still present in the output.
//
// We expect findLiteExtensionByNumber2() to be inlined into findLiteExtensionByNumber1() and
// findLiteExtensionByNumber1() to be inlined into findLiteExtensionByNumber().
{
ClassSubject generatedExtensionRegistryLoader = outputInspector.clazz(extensionRegistryName);
assertThat(generatedExtensionRegistryLoader, isPresent());
assertThat(
generatedExtensionRegistryLoader.uniqueMethodWithOriginalName(
"findLiteExtensionByNumber"),
isPresent());
assertThat(
generatedExtensionRegistryLoader.uniqueMethodWithOriginalName(
"findLiteExtensionByNumber1"),
isAbsentIf(enableMinification));
assertThat(
generatedExtensionRegistryLoader.uniqueMethodWithOriginalName(
"findLiteExtensionByNumber2"),
isAbsentIf(enableMinification));
}
// 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.uniqueFieldWithOriginalName("conditionallyUsed_"), isPresent());
ClassSubject puClassSubject = inputInspector.clazz(PARTIALLY_USED);
assertThat(puClassSubject, isPresent());
assertEquals(7, puClassSubject.allInstanceFields().size());
assertThat(puClassSubject.uniqueFieldWithOriginalName("bitField0_"), isPresent());
assertThat(puClassSubject.uniqueFieldWithOriginalName("used_"), isPresent());
assertThat(puClassSubject.uniqueFieldWithOriginalName("completelyUnused_"), isPresent());
assertThat(puClassSubject.uniqueFieldWithOriginalName("unusedEnum_"), isPresent());
assertThat(puClassSubject.uniqueFieldWithOriginalName("unusedRepeatedEnum_"), isPresent());
assertThat(puClassSubject.uniqueFieldWithOriginalName("unusedMessage_"), isPresent());
assertThat(puClassSubject.uniqueFieldWithOriginalName("unusedRepeatedMessage_"), isPresent());
ClassSubject uvhClassSubject = inputInspector.clazz(USED_VIA_HAZZER);
assertThat(uvhClassSubject, isPresent());
assertThat(uvhClassSubject.uniqueFieldWithOriginalName("used_"), isPresent());
assertThat(uvhClassSubject.uniqueFieldWithOriginalName("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.uniqueFieldWithOriginalName("conditionallyUsed_"), not(isPresent()));
ClassSubject puClassSubject = outputInspector.clazz(PARTIALLY_USED);
assertThat(puClassSubject, isPresent());
assertThat(puClassSubject.uniqueFieldWithOriginalName("bitField0_"), isPresent());
assertThat(puClassSubject.uniqueFieldWithOriginalName("used_"), isPresent());
assertThat(puClassSubject.uniqueFieldWithOriginalName("completelyUnused_"), not(isPresent()));
assertThat(puClassSubject.uniqueFieldWithOriginalName("unusedEnum_"), not(isPresent()));
assertThat(
puClassSubject.uniqueFieldWithOriginalName("unusedRepeatedEnum_"), not(isPresent()));
assertThat(puClassSubject.uniqueFieldWithOriginalName("unusedMessage_"), not(isPresent()));
assertThat(
puClassSubject.uniqueFieldWithOriginalName("unusedRepeatedMessage_"), not(isPresent()));
ClassSubject uvhClassSubject = outputInspector.clazz(USED_VIA_HAZZER);
assertThat(uvhClassSubject, isPresent());
assertThat(uvhClassSubject.uniqueFieldWithOriginalName("used_"), isPresent());
assertThat(uvhClassSubject.uniqueFieldWithOriginalName("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.uniqueFieldWithOriginalName("bitField0_"), isPresent());
assertThat(classSubject.uniqueFieldWithOriginalName("myoneof_"), isPresent());
assertThat(classSubject.uniqueFieldWithOriginalName("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.uniqueFieldWithOriginalName("bitField0_"), not(isPresent()));
assertThat(classSubject.uniqueFieldWithOriginalName("myoneof_"), not(isPresent()));
assertThat(classSubject.uniqueFieldWithOriginalName("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 testR8NoRewriting() throws Exception {
protoRuntime.assumeIsNewerThanOrEqualToMinimumRequiredRuntime(protoTestSources);
testForR8(parameters.getBackend())
.apply(protoRuntime::addRuntime)
.apply(protoRuntime::workaroundProtoMessageRemoval)
.addProgramFiles(protoTestSources.getProgramFiles())
.addKeepMainRule(MAIN)
// Retain all protos.
.addKeepRules(keepAllProtosRule())
// Retain the signature of dynamicMethod() and newMessageInfo().
.addKeepRules(keepDynamicMethodSignatureRule(), keepNewMessageInfoSignatureRule())
.allowAccessModification(allowAccessModification)
.allowDiagnosticMessages()
.allowUnusedDontWarnPatterns()
.allowUnusedProguardConfigurationRules()
.enableProtoShrinking()
.minification(enableMinification)
.setMinApi(parameters)
.compile()
.assertAllInfoMessagesMatch(
containsString("Proguard configuration rule does not match anything"))
.apply(this::inspectWarningMessages)
.inspect(
inspector ->
assertRewrittenProtoSchemasMatch(protoTestSources.getInspector(), inspector));
}
@Test
public void testR8TwoExtensionRegistries() throws Exception {
protoRuntime.assumeIsNewerThanOrEqualToMinimumRequiredRuntime(protoTestSources);
assumeTrue("Only proto2 has two extension registries", protoTestSources.isProto2());
testForR8(parameters.getBackend())
.apply(protoRuntime::addRuntime)
.apply(protoRuntime::workaroundProtoMessageRemoval)
.addProgramFiles(protoTestSources.getProgramFiles())
.addKeepMainRule(MAIN)
.addKeepRules(findLiteExtensionByNumberInDuplicateCalledRule())
// TODO(b/173340579): This rule should not be needed to allow shrinking of
// PartiallyUsed$Enum.
.addNoHorizontalClassMergingRule(PARTIALLY_USED + "$Enum$1")
.allowAccessModification(allowAccessModification)
.allowDiagnosticMessages()
.allowUnusedDontWarnPatterns()
.allowUnusedProguardConfigurationRules()
.enableProtoShrinking()
.minification(enableMinification)
.setMinApi(parameters)
.compile()
.assertAllInfoMessagesMatch(
containsString("Proguard configuration rule does not match anything"))
.apply(this::inspectWarningMessages)
.inspect(
outputInspector -> {
CodeInspector inputInspector = protoTestSources.getInspector();
verifyUnusedExtensionsAreRemoved(inputInspector, outputInspector);
verifyUnusedExtensionsAreRemoved(
inputInspector,
outputInspector,
"com.google.protobuf.proto2_registryGeneratedExtensionRegistryLiteDuplicate");
});
}
}