blob: 2c12fa2d3f4e7969eb2a0d05162a0a23253111f2 [file] [log] [blame]
// Copyright (c) 2016, 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.shaking;
import static com.android.tools.r8.ToolHelper.DEFAULT_PROGUARD_MAP_FILE;
import com.android.tools.r8.OutputMode;
import com.android.tools.r8.R8Command;
import com.android.tools.r8.TestBase.MinifyMode;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
import com.android.tools.r8.naming.MemberNaming.FieldSignature;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.DexInspector;
import com.android.tools.r8.utils.DexInspector.ClassSubject;
import com.android.tools.r8.utils.DexInspector.FieldAccessInstructionSubject;
import com.android.tools.r8.utils.DexInspector.FieldSubject;
import com.android.tools.r8.utils.DexInspector.FoundFieldSubject;
import com.android.tools.r8.utils.DexInspector.FoundMethodSubject;
import com.android.tools.r8.utils.DexInspector.InstructionSubject;
import com.android.tools.r8.utils.DexInspector.MethodSubject;
import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.StringUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
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.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class TreeShakingTest {
private static final Path ANDROID_JAR = ToolHelper.getDefaultAndroidJar();
private static final List<Path> JAR_LIBRARIES = ImmutableList
.of(ANDROID_JAR, Paths.get(ToolHelper.EXAMPLES_BUILD_DIR + "shakinglib.jar"));
private static final String EMPTY_FLAGS = "src/test/proguard/valid/empty.flags";
private static final Set<String> IGNORED_FLAGS = ImmutableSet.of(
"examples/minification:conflict-mapping.txt",
"examples/minification:keep-rules-apply-conflict-mapping.txt"
);
private static final Set<String> IGNORED = ImmutableSet.of(
// there's no point in running those without obfuscation
"examples/shaking1:keep-rules-repackaging.txt:DEX:NONE",
"examples/shaking1:keep-rules-repackaging.txt:JAR:NONE",
"examples/shaking16:keep-rules-1.txt:DEX:NONE",
"examples/shaking16:keep-rules-1.txt:JAR:NONE",
"examples/shaking16:keep-rules-2.txt:DEX:NONE",
"examples/shaking16:keep-rules-2.txt:JAR:NONE",
"examples/shaking15:keep-rules.txt:DEX:NONE",
"examples/shaking15:keep-rules.txt:JAR:NONE",
"examples/minifygeneric:keep-rules.txt:DEX:NONE",
"examples/minifygeneric:keep-rules.txt:JAR:NONE",
"examples/minifygenericwithinner:keep-rules.txt:DEX:NONE",
"examples/minifygenericwithinner:keep-rules.txt:JAR:NONE",
// No prebuild DEX files for AndroidN
"examplesAndroidN/shaking:keep-rules.txt:DEX:NONE",
"examplesAndroidN/shaking:keep-rules.txt:DEX:JAVA",
"examplesAndroidN/shaking:keep-rules.txt:DEX:AGGRESSIVE"
);
private static Set<String> SKIPPED = Collections.emptySet();
private final MinifyMode minify;
private enum Frontend {
DEX, JAR
}
private final Frontend kind;
private final String originalDex;
private final String programFile;
private final String mainClass;
private final List<String> keepRulesFiles;
private final Consumer<DexInspector> inspection;
private final BiConsumer<String, String> outputComparator;
private BiConsumer<DexInspector, DexInspector> dexComparator;
@Rule
public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
public TreeShakingTest(String test, Frontend kind, String mainClass, List<String> keepRulesFiles,
MinifyMode minify, Consumer<DexInspector> inspection,
BiConsumer<String, String> outputComparator,
BiConsumer<DexInspector, DexInspector> dexComparator) {
this.kind = kind;
originalDex = ToolHelper.TESTS_BUILD_DIR + test + "/classes.dex";
if (kind == Frontend.DEX) {
this.programFile = originalDex;
} else {
this.programFile = ToolHelper.TESTS_BUILD_DIR + test + ".jar";
}
this.mainClass = mainClass;
this.keepRulesFiles = keepRulesFiles;
this.inspection = inspection;
this.minify = minify;
this.outputComparator = outputComparator;
this.dexComparator = dexComparator;
}
@Before
public void generateTreeShakedVersion() throws Exception {
// Generate R8 processed version without library option.
Path out = temp.getRoot().toPath();
boolean inline = programFile.contains("inlining");
R8Command.Builder builder =
ToolHelper.addProguardConfigurationConsumer(
R8Command.builder(),
pgConfig -> {
pgConfig.setPrintMapping(true);
pgConfig.setPrintMappingFile(out.resolve(ToolHelper.DEFAULT_PROGUARD_MAP_FILE));
pgConfig.setOverloadAggressively(minify == MinifyMode.AGGRESSIVE);
pgConfig.setObfuscating(minify.isMinify());
})
.setOutput(out, OutputMode.DexIndexed)
.addProguardConfigurationFiles(ListUtils.map(keepRulesFiles, Paths::get))
.addLibraryFiles(JAR_LIBRARIES);
ToolHelper.getAppBuilder(builder).addProgramFiles(Paths.get(programFile));
ToolHelper.runR8(builder.build(), options -> options.inlineAccessors = inline);
}
public static void shaking1HasNoClassUnused(DexInspector inspector) {
Assert.assertFalse(inspector.clazz("shaking1.Unused").isPresent());
ClassSubject used = inspector.clazz("shaking1.Used");
Assert.assertTrue(used.isPresent());
Assert.assertTrue(
used.method("java.lang.String", "aMethodThatIsNotUsedButKept", Collections.emptyList())
.isPresent());
Assert.assertTrue(used.field("int", "aStaticFieldThatIsNotUsedButKept").isPresent());
// Rewriting of <clinit> moves the initialization of aStaticFieldThatIsNotUsedButKept
// from <clinit> code into statics value section of the dex file.
Assert.assertFalse(used.clinit().isPresent());
}
public static void shaking1IsCorrectlyRepackaged(DexInspector inspector) {
inspector.forAllClasses(clazz -> {
String descriptor = clazz.getFinalDescriptor();
Assert.assertTrue(descriptor,
DescriptorUtils.getSimpleClassNameFromDescriptor(descriptor).equals("Shaking")
|| DescriptorUtils.getPackageNameFromDescriptor(descriptor).equals("repackaged"));
});
}
private static void shaking2SuperClassIsAbstract(DexInspector inspector) {
ClassSubject clazz = inspector.clazz("shaking2.SuperClass");
Assert.assertTrue(clazz.isAbstract());
Assert.assertTrue(clazz.method("void", "virtualMethod", Collections.emptyList()).isAbstract());
Assert.assertTrue(clazz.method("void", "virtualMethod2", ImmutableList
.of("int", "int", "int", "int", "int", "int", "int", "int")).isAbstract());
}
public static void shaking3HasNoClassB(DexInspector inspector) {
Assert.assertFalse(inspector.clazz("shaking3.B").isPresent());
ClassSubject classA = inspector.clazz("shaking3.A");
Assert.assertTrue(classA.isPresent());
Assert.assertFalse(classA.method("void", "unused", ImmutableList.of()).isPresent());
}
public static void shaking3HasNoPrivateClass(DexInspector inspector) {
Assert.assertTrue(inspector.clazz("shaking3.B").isPresent());
Assert.assertFalse(inspector.clazz("shaking3.AnAbstractClass").isPresent());
}
private static void shaking5Inspection(DexInspector inspector) {
Assert.assertFalse(inspector.clazz("shaking5.Superclass")
.method("void", "virtualMethod", Collections.emptyList()).isPresent());
}
private static void hasNoPrivateMethods(DexInspector inspector) {
inspector.forAllClasses(clazz -> clazz.forAllMethods(
method -> Assert.assertTrue(!method.getMethod().accessFlags.isPrivate())));
}
private static void hasNoPublicMethodsButPrivate(DexInspector inspector) {
inspector.forAllClasses(clazz -> clazz.forAllMethods(method -> {
if (!method.isStatic() && !method.isFinal()) {
Assert.assertTrue(!method.getMethod().accessFlags.isPublic());
}
}));
Assert.assertTrue(inspector.clazz("shaking6.Superclass")
.method("void", "justAMethod", Collections.emptyList()).isPresent());
}
private static void hasNoPrivateJustAMethod(DexInspector inspector) {
Assert.assertFalse(
inspector.clazz("shaking6.Superclass")
.method("void", "justAMethod", Collections.emptyList())
.isPresent());
ClassSubject subclass = inspector.clazz("shaking6.Subclass");
Assert.assertTrue(subclass.isPresent());
Assert.assertTrue(
subclass.method("void", "justAMethod", Collections.emptyList())
.isPresent());
Assert.assertTrue(
subclass.method("void", "justAMethod", Collections.singletonList("int"))
.isPresent());
Assert.assertTrue(
subclass.method("void", "justAMethod", Collections.singletonList("boolean"))
.isPresent());
Assert.assertFalse(
subclass.method("int", "justAMethod", Collections.singletonList("double"))
.isPresent());
}
private static void hasOnlyIntJustAMethod(DexInspector inspector) {
Assert.assertFalse(
inspector.clazz("shaking6.Superclass")
.method("void", "justAMethod", Collections.emptyList())
.isPresent());
ClassSubject subclass = inspector.clazz("shaking6.Subclass");
Assert.assertTrue(subclass.isPresent());
Assert.assertFalse(
subclass.method("void", "justAMethod", Collections.emptyList())
.isPresent());
Assert.assertTrue(
subclass.method("void", "justAMethod", Collections.singletonList("int"))
.isPresent());
Assert.assertFalse(
subclass.method("void", "justAMethod", Collections.singletonList("boolean"))
.isPresent());
Assert.assertFalse(
subclass.method("int", "justAMethod", Collections.singletonList("double"))
.isPresent());
}
private static void shaking7HasOnlyPublicFields(DexInspector inspector) {
inspector.forAllClasses(clazz -> {
clazz.forAllFields(field -> {
Assert.assertTrue(field.getField().accessFlags.isPublic());
});
});
ClassSubject subclass = inspector.clazz("shaking7.Subclass");
Assert.assertTrue(subclass.field("int", "theIntField").isPresent());
Assert.assertTrue(subclass.field("double", "theDoubleField").isPresent());
Assert.assertTrue(inspector.clazz("shaking7.Superclass")
.field("double", "theDoubleField").isPresent());
Assert.assertTrue(inspector.clazz("shaking7.Liar").field("int", "theDoubleField").isPresent());
}
private static void shaking7HasOnlyDoubleFields(DexInspector inspector) {
inspector.forAllClasses(clazz -> {
clazz.forAllFields(field -> {
Assert.assertTrue(field.type().is("double"));
});
});
Assert.assertTrue(
inspector.clazz("shaking7.Subclass").field("double", "theDoubleField").isPresent());
Assert.assertTrue(
inspector.clazz("shaking7.Superclass").field("double", "theDoubleField").isPresent());
Assert.assertFalse(
inspector.clazz("shaking7.Liar").field("int", "theDoubleField").isPresent());
}
private static void shaking7HasOnlyPublicFieldsNamedTheDoubleField(DexInspector inspector) {
inspector.forAllClasses(clazz -> {
clazz.forAllFields(field -> {
Assert.assertTrue(field.getField().accessFlags.isPublic());
});
});
ClassSubject subclass = inspector.clazz("shaking7.Subclass");
Assert.assertFalse(subclass.field("int", "theIntField").isPresent());
Assert.assertTrue(subclass.field("double", "theDoubleField").isPresent());
Assert.assertTrue(inspector.clazz("shaking7.Superclass")
.field("double", "theDoubleField").isPresent());
Assert.assertTrue(inspector.clazz("shaking7.Liar").field("int", "theDoubleField").isPresent());
}
private static void shaking7HasOnlyPublicFieldsNamedTheIntField(DexInspector inspector) {
inspector.forAllClasses(clazz -> {
clazz.forAllFields(field -> {
Assert.assertTrue(field.getField().accessFlags.isPublic());
});
});
ClassSubject subclass = inspector.clazz("shaking7.Subclass");
Assert.assertTrue(subclass.field("int", "theIntField").isPresent());
Assert.assertFalse(subclass.field("double", "theDoubleField").isPresent());
Assert.assertFalse(inspector.clazz("shaking7.Superclass")
.field("double", "theDoubleField").isPresent());
ClassSubject liar = inspector.clazz("shaking7.Liar");
Assert.assertFalse(liar.field("int", "theDoubleField").isPresent());
Assert.assertTrue(liar.field("double", "theIntField").isPresent());
}
private static void shaking8ThingClassIsAbstractAndEmpty(DexInspector inspector) {
ClassSubject clazz = inspector.clazz("shaking8.Thing");
Assert.assertTrue(clazz.isAbstract());
clazz.forAllMethods((method) -> Assert.fail());
clazz = inspector.clazz("shaking8.YetAnotherThing");
Assert.assertTrue(clazz.isAbstract());
clazz.forAllMethods((method) -> Assert.fail());
}
private static void shaking9OnlySuperMethodsKept(DexInspector inspector) {
ClassSubject superclass = inspector.clazz("shaking9.Superclass");
Assert.assertTrue(superclass.isAbstract());
Assert.assertTrue(superclass.method("void", "aMethod", ImmutableList.of()).isPresent());
ClassSubject subclass = inspector.clazz("shaking9.Subclass");
Assert.assertFalse(subclass.method("void", "aMethod", ImmutableList.of()).isPresent());
}
private static void shaking11OnlyOneClassKept(DexInspector dexInspector) {
Assert.assertFalse(dexInspector.clazz("shaking11.Subclass").isPresent());
Assert.assertTrue(dexInspector.clazz("shaking11.SubclassWithMethod").isPresent());
}
private static void shaking11BothMethodsKept(DexInspector dexInspector) {
Assert.assertFalse(
dexInspector.clazz("shaking11.Subclass").method("void", "aMethod", Collections.emptyList())
.isPresent());
Assert.assertTrue(
dexInspector.clazz("shaking11.SuperClass")
.method("void", "aMethod", Collections.emptyList())
.isPresent());
Assert.assertTrue(dexInspector.clazz("shaking11.SubclassWithMethod")
.method("void", "aMethod", Collections.emptyList()).isPresent());
}
private static void shaking12OnlyInstantiatedClassesHaveConstructors(DexInspector inspector) {
ClassSubject animalClass = inspector.clazz("shaking12.AnimalClass");
Assert.assertTrue(animalClass.isPresent());
Assert.assertFalse(
animalClass.method("void", "<init>", Collections.emptyList()).isPresent());
Assert.assertTrue(inspector.clazz("shaking12.MetaphorClass").isAbstract());
ClassSubject peopleClass = inspector.clazz("shaking12.PeopleClass");
Assert.assertTrue((peopleClass.isPresent() && !peopleClass.isAbstract()));
Assert.assertTrue(
peopleClass.method("void", "<init>", Collections.emptyList()).isPresent());
ClassSubject thingClass = inspector.clazz("shaking12.ThingClass");
Assert.assertTrue((thingClass.isPresent() && !thingClass.isAbstract()));
Assert.assertTrue(
thingClass.method("void", "<init>", Collections.emptyList()).isPresent());
}
private static void shaking13EnsureFieldWritesCorrect(DexInspector inspector) {
ClassSubject mainClass = inspector.clazz("shaking13.Shaking");
MethodSubject testMethod = mainClass.method("void", "fieldTest", Collections.emptyList());
Assert.assertTrue(testMethod.isPresent());
Iterator<FieldAccessInstructionSubject> iterator =
testMethod.iterateInstructions(InstructionSubject::isFieldAccess);
Assert.assertTrue(iterator.hasNext() && iterator.next().holder().is("shakinglib.LibraryClass"));
Assert.assertTrue(iterator.hasNext() && iterator.next().holder().is("shakinglib.LibraryClass"));
Assert.assertFalse(iterator.hasNext());
}
private static void shaking14EnsureRightStaticMethodsLive(DexInspector inspector) {
ClassSubject superclass = inspector.clazz("shaking14.Superclass");
Assert.assertFalse(superclass.method("int", "aMethod", ImmutableList.of("int")).isPresent());
Assert.assertFalse(
superclass.method("double", "anotherMethod", ImmutableList.of("double")).isPresent());
ClassSubject subclass = inspector.clazz("shaking14.Subclass");
Assert.assertTrue(subclass.method("int", "aMethod", ImmutableList.of("int")).isPresent());
Assert.assertTrue(
subclass.method("double", "anotherMethod", ImmutableList.of("double")).isPresent());
}
private static void simpleproto1UnusedFieldIsGone(DexInspector inspector) {
ClassSubject protoClass = inspector.clazz("simpleproto1.GeneratedSimpleProto$Simple");
Assert.assertTrue(protoClass.isPresent());
Assert.assertFalse(protoClass.field("boolean", "other_").isPresent());
}
private static void simpleproto2UnusedFieldsAreGone(DexInspector inspector) {
ClassSubject protoClass = inspector.clazz("simpleproto2.GeneratedSimpleProto$Simple");
Assert.assertTrue(protoClass.isPresent());
Assert.assertFalse(protoClass.field("int", "id_").isPresent());
Assert.assertFalse(protoClass.field("float", "hasMe_").isPresent());
Assert.assertFalse(protoClass.field("int", "other_").isPresent());
}
private static void nestedproto1UnusedFieldsAreGone(DexInspector inspector) {
ClassSubject protoClass = inspector.clazz("nestedproto1.GeneratedNestedProto$Outer");
Assert.assertTrue(protoClass.isPresent());
Assert.assertFalse(protoClass.field("int", "id_").isPresent());
Assert.assertTrue(
protoClass.field("nestedproto1.GeneratedNestedProto$NestedOne", "inner_").isPresent());
Assert.assertFalse(
protoClass.field("nestedproto1.GeneratedNestedProto$NestedTwo", "inner2_").isPresent());
ClassSubject nestedOne = inspector.clazz("nestedproto1.GeneratedNestedProto$NestedOne");
Assert.assertTrue(nestedOne.isPresent());
Assert.assertTrue(nestedOne.field("java.lang.String", "other_").isPresent());
Assert.assertFalse(nestedOne.field("int", "id_").isPresent());
Assert.assertFalse(inspector.clazz("nestedproto1.GeneratedNestedProto$NestedTwo").isPresent());
}
private static void nestedproto2UnusedFieldsAreGone(DexInspector inspector) {
ClassSubject protoClass = inspector.clazz("nestedproto2.GeneratedNestedProto$Outer");
Assert.assertTrue(protoClass.isPresent());
Assert.assertTrue(protoClass.field("int", "id_").isPresent());
Assert.assertFalse(
protoClass.field("nestedproto2.GeneratedNestedProto$NestedOne", "inner_").isPresent());
Assert.assertFalse(
protoClass.field("nestedproto2.GeneratedNestedProto$NestedTwo", "inner2_").isPresent());
Assert.assertFalse(inspector.clazz("nestedproto2.GeneratedNestedProto$NestedOne").isPresent());
Assert.assertFalse(inspector.clazz("nestedproto2.GeneratedNestedProto$NestedTwo").isPresent());
}
private static void enumprotoUnusedFieldsAreGone(DexInspector inspector) {
ClassSubject protoClass = inspector.clazz("enumproto.GeneratedEnumProto$Enum");
Assert.assertTrue(protoClass.isPresent());
Assert.assertFalse(protoClass.field("int", "id_").isPresent());
Assert.assertTrue(protoClass.field("int", "enum_").isPresent());
Assert.assertFalse(protoClass.field("int", "other_").isPresent());
ClassSubject protoThreeClass = inspector.clazz("enumproto.three.GeneratedEnumProto$EnumThree");
Assert.assertTrue(protoThreeClass.isPresent());
Assert.assertFalse(protoThreeClass.field("int", "id_").isPresent());
Assert.assertTrue(protoThreeClass.field("int", "enum_").isPresent());
Assert.assertFalse(protoThreeClass.field("int", "other_").isPresent());
}
private static void repeatedUnusedFieldsAreGone(DexInspector inspector) {
ClassSubject protoClass = inspector.clazz("repeatedproto.GeneratedRepeatedProto$Repeated");
Assert.assertTrue(protoClass.isPresent());
Assert.assertFalse(protoClass.field("int", "id_").isPresent());
Assert.assertTrue(
protoClass.field("com.google.protobuf.Internal$ProtobufList", "repeated_").isPresent());
Assert.assertFalse(
protoClass.field("com.google.protobuf.Internal$ProtobufList", "sub_").isPresent());
Assert.assertFalse(
protoClass.field("com.google.protobuf.Internal$BooleanList", "other_").isPresent());
}
private static void oneofprotoUnusedFieldsAreGone(DexInspector inspector) {
ClassSubject protoClass = inspector.clazz("oneofproto.GeneratedOneOfProto$Oneof");
Assert.assertTrue(protoClass.isPresent());
Assert.assertFalse(protoClass.field("int", "id_").isPresent());
Assert.assertFalse(protoClass.field("Object", "otherfields_").isPresent());
}
private static List<String> names =
ImmutableList.of("pqr", "vw$", "abc", "def", "stu", "ghi", "jkl", "ea", "xyz_", "mno");
private static void checkFieldInDictionary(FieldSubject field) {
if (!names.contains(field.getField().field.name.toSourceString())) {
throw new AssertionError();
}
}
private static void checkMethodInDictionary(MethodSubject method) {
String name = method.getMethod().method.name.toSourceString();
if (!names.contains(name) && !name.equals("<init>") && !name.equals("main")) {
throw new AssertionError();
}
}
private static void checkClassAndMemberInDictionary(ClassSubject clazz) {
String name = clazz.getDexClass().type.getName();
if (!names.contains(name) && !name.equals("Shaking")) {
throw new AssertionError();
}
clazz.forAllMethods(method -> checkMethodInDictionary(method));
clazz.forAllFields(field -> checkFieldInDictionary(field));
}
private static void shaking15testDictionary(DexInspector inspector) {
inspector.forAllClasses((clazz) -> checkClassAndMemberInDictionary(clazz));
}
private static void abstractMethodRemains(DexInspector inspector) {
ClassSubject programClass = inspector.clazz("shaking17.AbstractProgramClass");
Assert.assertTrue(programClass.isPresent());
Assert.assertTrue(
programClass.method("int", "abstractMethod", Collections.emptyList()).isPresent());
}
private static void assumenosideeffects1CheckOutput(String output1, String output2) {
Assert.assertEquals(StringUtils.lines("noSideEffectVoid", "noSideEffectInt"), output1);
Assert.assertEquals("", output2);
}
private static void assumenosideeffects2CheckOutput(String output1, String output2) {
Assert.assertEquals(StringUtils.lines("Hello, world!"), output1);
Assert.assertEquals("", output2);
}
private static void assumenosideeffects3CheckOutput(String output1, String output2) {
Assert.assertEquals(StringUtils.lines("0", "1", "0L", "1L"), output1);
Assert.assertEquals(StringUtils.lines("1", "0", "1L", "0L"), output2);
}
private static void assumenosideeffects4CheckOutput(String output1, String output2) {
Assert.assertEquals(
StringUtils.lines("method0", "0", "method1", "1", "method0L", "0L", "method1L", "1L"),
output1);
Assert.assertEquals(StringUtils.lines("1", "0", "1L", "0L"), output2);
}
private static void assumenosideeffects5CheckOutput(String output1, String output2) {
Assert.assertEquals(StringUtils.lines("methodTrue", "true", "methodFalse", "false"), output1);
Assert.assertEquals(StringUtils.lines("false", "true"), output2);
}
private static void assumevalues1CheckOutput(String output1, String output2) {
Assert.assertEquals(StringUtils.lines("3", "3L"), output1);
Assert.assertEquals(StringUtils.lines("1", "1L"), output2);
}
private static void assumevalues2CheckOutput(String output1, String output2) {
Assert.assertEquals(StringUtils.lines("1", "2", "3", "4", "1L", "2L", "3L", "4L"), output1);
Assert.assertEquals(StringUtils.lines("2", "3", "2L", "3L"), output2);
}
private static void assumevalues3CheckOutput(String output1, String output2) {
Assert.assertEquals(StringUtils.lines("3", "3L"), output1);
Assert.assertEquals(StringUtils.lines("1", "1L"), output2);
}
private static void assumevalues4CheckOutput(String output1, String output2) {
Assert.assertEquals(
StringUtils.lines("method0", "0", "method1", "1", "method0L", "0L", "method1L", "1L"),
output1);
Assert.assertEquals(
StringUtils.lines("method0", "1", "method1", "0", "method0L", "1L", "method1L", "0L"),
output2);
}
private static void assumevalues5CheckOutput(String output1, String output2) {
Assert.assertEquals(StringUtils.lines("methodTrue", "true", "methodFalse", "false"), output1);
Assert.assertEquals(StringUtils.lines("methodTrue", "false", "methodFalse", "true"), output2);
}
private static void annotationRemovalHasAllInnerClassAnnotations(DexInspector inspector) {
ClassSubject outer = inspector.clazz("annotationremoval.OuterClass");
Assert.assertTrue(outer.isPresent());
Assert.assertFalse(outer.getDexClass().getInnerClasses().isEmpty());
ClassSubject inner = inspector.clazz("annotationremoval.OuterClass$InnerClass");
Assert.assertTrue(inner.isPresent());
Assert.assertTrue(inner.isMemberClass());
Assert.assertFalse(inner.isAnonymousClass());
Assert.assertFalse(inner.isLocalClass());
ClassSubject anonymous = inspector.clazz("annotationremoval.OuterClass$1");
Assert.assertTrue(anonymous.isPresent());
Assert.assertTrue(anonymous.isAnonymousClass());
Assert.assertFalse(anonymous.isMemberClass());
Assert.assertFalse(anonymous.isLocalClass());
ClassSubject local = inspector.clazz("annotationremoval.OuterClass$1LocalMagic");
Assert.assertTrue(local.isPresent());
Assert.assertTrue(local.isLocalClass());
Assert.assertFalse(local.isMemberClass());
Assert.assertFalse(local.isAnonymousClass());
}
private static void annotationRemovalHasNoInnerClassAnnotations(DexInspector inspector) {
ClassSubject outer = inspector.clazz("annotationremoval.OuterClass");
Assert.assertTrue(outer.isPresent());
Assert.assertTrue(outer.getDexClass().getInnerClasses().isEmpty());
ClassSubject inner = inspector.clazz("annotationremoval.OuterClass$InnerClass");
Assert.assertTrue(inner.isPresent());
Assert.assertNull(inner.getDexClass().getEnclosingMethod());
Assert.assertTrue(inner.getDexClass().getInnerClasses().isEmpty());
ClassSubject anonymous = inspector.clazz("annotationremoval.OuterClass$1");
Assert.assertTrue(anonymous.isPresent());
Assert.assertNull(anonymous.getDexClass().getEnclosingMethod());
Assert.assertTrue(anonymous.getDexClass().getInnerClasses().isEmpty());
ClassSubject local = inspector.clazz("annotationremoval.OuterClass$1LocalMagic");
Assert.assertTrue(local.isPresent());
Assert.assertNull(local.getDexClass().getEnclosingMethod());
Assert.assertTrue(local.getDexClass().getInnerClasses().isEmpty());
}
private static void checkSameStructure(DexInspector ref, DexInspector inspector) {
ref.forAllClasses(refClazz -> checkSameStructure(refClazz,
inspector.clazz(refClazz.getDexClass().toSourceString())));
}
private static void checkSameStructure(ClassSubject refClazz, ClassSubject clazz) {
Assert.assertTrue(clazz.isPresent());
refClazz.forAllFields(refField -> checkSameStructure(refField, clazz));
refClazz.forAllMethods(refMethod -> checkSameStructure(refMethod, clazz));
}
private static void checkSameStructure(FoundMethodSubject refMethod, ClassSubject clazz) {
MethodSignature signature = refMethod.getOriginalSignature();
// Don't check for existence of class initializers, as the code optimization can remove them.
if (!refMethod.isClassInitializer()) {
Assert.assertTrue("Missing Method: " + clazz.getDexClass().toSourceString() + "."
+ signature.toString(),
clazz.method(signature).isPresent());
}
}
private static void checkSameStructure(FoundFieldSubject refField, ClassSubject clazz) {
FieldSignature signature = refField.getOriginalSignature();
Assert.assertTrue(
"Missing field: " + signature.type + " " + clazz.getOriginalDescriptor()
+ "." + signature.name,
clazz.field(signature.type, signature.name).isPresent());
}
@Parameters(name = "dex: {0} frontend: {1} keep: {3} minify: {4}")
public static Collection<Object[]> data() {
List<String> tests = Arrays
.asList(
"examples/shaking1",
"examples/shaking2",
"examples/shaking3",
"examples/shaking4",
"examples/shaking5",
"examples/shaking6",
"examples/shaking7",
"examples/shaking8",
"examples/shaking9",
"examples/shaking10",
"examples/shaking11",
"examples/shaking12",
"examples/shaking13",
"examples/shaking14",
"examples/shaking15",
"examples/shaking16",
"examples/shaking17",
"examples/minification",
"examples/minifygeneric",
"examples/minifygenericwithinner",
"examples/assumenosideeffects1",
// TODO(b/70550443): Enable once fixed.
//"examples/assumenosideeffects2",
"examples/assumenosideeffects3",
"examples/assumenosideeffects4",
"examples/assumenosideeffects5",
"examples/assumevalues1",
"examples/assumevalues2",
"examples/assumevalues3",
"examples/assumevalues4",
"examples/assumevalues5",
"examples/annotationremoval",
"examples/memberrebinding2",
"examples/memberrebinding3",
"examples/inlining",
"examples/simpleproto1",
"examples/simpleproto2",
"examples/simpleproto3",
"examples/nestedproto1",
"examples/nestedproto2",
"examples/enumproto",
"examples/repeatedproto",
"examples/oneofproto",
"examplesAndroidN/shaking");
// Keys can be the name of the test or the name of the test followed by a colon and the name
// of the keep file.
Map<String, Consumer<DexInspector>> inspections = new HashMap<>();
inspections.put("examples/shaking1:keep-rules.txt", TreeShakingTest::shaking1HasNoClassUnused);
inspections.put("examples/shaking1:keep-rules-repackaging.txt",
TreeShakingTest::shaking1IsCorrectlyRepackaged);
inspections
.put("examples/shaking2:keep-rules.txt", TreeShakingTest::shaking2SuperClassIsAbstract);
inspections.put("examples/shaking3:keep-by-tag.txt", TreeShakingTest::shaking3HasNoClassB);
inspections
.put("examples/shaking3:keep-by-tag-default.txt", TreeShakingTest::shaking3HasNoClassB);
inspections.put("examples/shaking3:keep-by-tag-with-pattern.txt",
TreeShakingTest::shaking3HasNoClassB);
inspections.put("examples/shaking3:keep-by-tag-via-interface.txt",
TreeShakingTest::shaking3HasNoClassB);
inspections
.put("examples/shaking3:keep-by-tag-on-method.txt", TreeShakingTest::shaking3HasNoClassB);
inspections.put("examples/shaking3:keep-no-abstract-classes.txt",
TreeShakingTest::shaking3HasNoPrivateClass);
inspections.put("examples/shaking5", TreeShakingTest::shaking5Inspection);
inspections.put("examples/shaking6:keep-public.txt", TreeShakingTest::hasNoPrivateMethods);
inspections.put("examples/shaking6:keep-non-public.txt",
TreeShakingTest::hasNoPublicMethodsButPrivate);
inspections.put("examples/shaking6:keep-justAMethod-public.txt",
TreeShakingTest::hasNoPrivateJustAMethod);
inspections.put("examples/shaking6:keep-justAMethod-OnInt.txt",
TreeShakingTest::hasOnlyIntJustAMethod);
inspections.put("examples/shaking7:keep-public-fields.txt",
TreeShakingTest::shaking7HasOnlyPublicFields);
inspections.put("examples/shaking7:keep-double-fields.txt",
TreeShakingTest::shaking7HasOnlyDoubleFields);
inspections.put("examples/shaking7:keep-public-theDoubleField-fields.txt",
TreeShakingTest::shaking7HasOnlyPublicFieldsNamedTheDoubleField);
inspections.put("examples/shaking7:keep-public-theIntField-fields.txt",
TreeShakingTest::shaking7HasOnlyPublicFieldsNamedTheIntField);
inspections.put("examples/shaking8:keep-rules.txt",
TreeShakingTest::shaking8ThingClassIsAbstractAndEmpty);
inspections
.put("examples/shaking9:keep-rules.txt", TreeShakingTest::shaking9OnlySuperMethodsKept);
inspections
.put("examples/shaking11:keep-rules.txt", TreeShakingTest::shaking11OnlyOneClassKept);
inspections.put("examples/shaking11:keep-rules-keep-method.txt",
TreeShakingTest::shaking11BothMethodsKept);
inspections.put("examples/shaking12:keep-rules.txt",
TreeShakingTest::shaking12OnlyInstantiatedClassesHaveConstructors);
inspections.put("examples/shaking13:keep-rules.txt",
TreeShakingTest::shaking13EnsureFieldWritesCorrect);
inspections.put("examples/shaking14:keep-rules.txt",
TreeShakingTest::shaking14EnsureRightStaticMethodsLive);
inspections.put("examples/shaking15:keep-rules.txt", TreeShakingTest::shaking15testDictionary);
inspections.put("examples/shaking17:keep-rules.txt", TreeShakingTest::abstractMethodRemains);
inspections.put("examples/annotationremoval:keep-rules.txt",
TreeShakingTest::annotationRemovalHasNoInnerClassAnnotations);
inspections.put("examples/annotationremoval:keep-rules-keep-innerannotation.txt",
TreeShakingTest::annotationRemovalHasAllInnerClassAnnotations);
inspections.put("examples/simpleproto1:keep-rules.txt",
TreeShakingTest::simpleproto1UnusedFieldIsGone);
inspections.put("examples/simpleproto2:keep-rules.txt",
TreeShakingTest::simpleproto2UnusedFieldsAreGone);
inspections.put("examples/nestedproto1:keep-rules.txt",
TreeShakingTest::nestedproto1UnusedFieldsAreGone);
inspections.put("examples/nestedproto2:keep-rules.txt",
TreeShakingTest::nestedproto2UnusedFieldsAreGone);
inspections
.put("examples/enumproto:keep-rules.txt", TreeShakingTest::enumprotoUnusedFieldsAreGone);
inspections
.put("examples/repeatedproto:keep-rules.txt", TreeShakingTest::repeatedUnusedFieldsAreGone);
inspections
.put("examples/oneofproto:keep-rules.txt", TreeShakingTest::oneofprotoUnusedFieldsAreGone);
// Keys can be the name of the test or the name of the test followed by a colon and the name
// of the keep file.
Map<String, Collection<List<String>>> optionalRules = new HashMap<>();
optionalRules.put("examples/shaking1", ImmutableList.of(
Collections.singletonList(EMPTY_FLAGS),
Lists.newArrayList(EMPTY_FLAGS, EMPTY_FLAGS)));
List<Object[]> testCases = new ArrayList<>();
Map<String, BiConsumer<String, String>> outputComparators = new HashMap<>();
outputComparators
.put("examples/assumenosideeffects1",
TreeShakingTest::assumenosideeffects1CheckOutput);
// TODO(b/70550443): Enable once fixed.
// outputComparators
// .put("examples/assumenosideeffects2",
// TreeShakingTest::assumenosideeffects2CheckOutput);
outputComparators
.put("examples/assumenosideeffects3",
TreeShakingTest::assumenosideeffects3CheckOutput);
outputComparators
.put("examples/assumenosideeffects4",
TreeShakingTest::assumenosideeffects4CheckOutput);
outputComparators
.put("examples/assumenosideeffects5",
TreeShakingTest::assumenosideeffects5CheckOutput);
outputComparators
.put("examples/assumevalues1",
TreeShakingTest::assumevalues1CheckOutput);
outputComparators
.put("examples/assumevalues2",
TreeShakingTest::assumevalues2CheckOutput);
outputComparators
.put("examples/assumevalues3",
TreeShakingTest::assumevalues3CheckOutput);
outputComparators
.put("examples/assumevalues4",
TreeShakingTest::assumevalues4CheckOutput);
outputComparators
.put("examples/assumevalues5",
TreeShakingTest::assumevalues5CheckOutput);
Map<String, BiConsumer<DexInspector, DexInspector>> dexComparators = new HashMap<>();
dexComparators
.put("examples/shaking1:keep-rules-dont-shrink.txt", TreeShakingTest::checkSameStructure);
dexComparators
.put("examples/shaking2:keep-rules-dont-shrink.txt", TreeShakingTest::checkSameStructure);
dexComparators
.put("examples/shaking4:keep-rules-dont-shrink.txt", TreeShakingTest::checkSameStructure);
Set<String> usedInspections = new HashSet<>();
Set<String> usedOptionalRules = new HashSet<>();
Set<String> usedOutputComparators = new HashSet<>();
Set<String> usedDexComparators = new HashSet<>();
for (String test : tests) {
String mainClass = deriveMainClass(test);
File[] keepFiles = new File(ToolHelper.TESTS_DIR + test)
.listFiles(file -> file.isFile() && file.getName().endsWith(".txt"));
for (File keepFile : keepFiles) {
String keepName = keepFile.getName();
Consumer<DexInspector> inspection =
getTestOptionalParameter(inspections, usedInspections, test, keepName);
Collection<List<String>> additionalRules =
getTestOptionalParameter(optionalRules, usedOptionalRules, test, keepName);
BiConsumer<String, String> outputComparator =
getTestOptionalParameter(outputComparators, usedOutputComparators, test, keepName);
BiConsumer<DexInspector, DexInspector> dexComparator =
getTestOptionalParameter(dexComparators, usedDexComparators, test, keepName);
addTestCases(testCases, test, mainClass, keepName,
Collections.singletonList(keepFile.getPath()), inspection,
outputComparator, dexComparator);
if (additionalRules != null) {
for (List<String> list : additionalRules) {
List<String> keepList = new ArrayList<>(list.size());
keepList.add(keepFile.getPath());
keepList.addAll(list);
addTestCases(testCases, test, mainClass, keepName, keepList, inspection,
outputComparator, dexComparator);
}
}
}
}
assert usedInspections.size() == inspections.size();
assert usedOptionalRules.size() == optionalRules.size();
assert usedOutputComparators.size() == outputComparators.size();
return testCases;
}
private static void addTestCases(List<Object[]> testCases, String test, String mainClass,
String keepName, List<String> keepList, Consumer<DexInspector> inspection,
BiConsumer<String, String> outputComparator,
BiConsumer<DexInspector, DexInspector> dexComparator) {
if (SKIPPED.contains(test)) {
return;
}
for (MinifyMode mode : MinifyMode.values()) {
addTestCase(testCases, test, Frontend.JAR, mainClass, keepName, keepList, mode, inspection,
outputComparator, dexComparator);
addTestCase(testCases, test, Frontend.DEX, mainClass, keepName, keepList, mode, inspection,
outputComparator, dexComparator);
}
}
private static void addTestCase(List<Object[]> testCases, String test, Frontend kind,
String mainClass, String keepName, List<String> keepList, MinifyMode minify,
Consumer<DexInspector> inspection, BiConsumer<String, String> outputComparator,
BiConsumer<DexInspector, DexInspector> dexComparator) {
if (!IGNORED_FLAGS.contains(test + ":" + keepName)
&& !IGNORED.contains(test + ":" + keepName + ":" + kind + ":" + minify)) {
testCases.add(new Object[]{
test, kind, mainClass, keepList, minify, inspection, outputComparator, dexComparator});
}
}
static <T> T getTestOptionalParameter(
Map<String, T> specifications, Set<String> usedSpecifications, String test,
String keepName) {
T parameter = specifications.get(test);
if (parameter == null) {
parameter = specifications.get(test + ":" + keepName);
if (parameter != null) {
usedSpecifications.add(test + ":" + keepName);
}
} else {
usedSpecifications.add(test);
}
return parameter;
}
private static String deriveMainClass(String testName) {
String testBaseName = testName.substring(testName.lastIndexOf('/') + 1);
StringBuilder mainClass = new StringBuilder(testName.length() * 2 + 1);
mainClass.append(testBaseName);
mainClass.append('.');
mainClass.append(Character.toUpperCase(testBaseName.charAt(0)));
for (int i = 1; i < testBaseName.length(); i++) {
char next = testBaseName.charAt(i);
if (!Character.isAlphabetic(next)) {
break;
}
mainClass.append(next);
}
return mainClass.toString();
}
@Test
public void treeShakingTest() throws IOException, InterruptedException, ExecutionException {
if (!ToolHelper.artSupported()) {
return;
}
String out = temp.getRoot().getCanonicalPath();
Path generated = Paths.get(out, "classes.dex");
Consumer<ArtCommandBuilder> extraArtArgs = builder -> {
builder.appendClasspath(ToolHelper.EXAMPLES_BUILD_DIR + "shakinglib/classes.dex");
};
if (Files.exists(Paths.get(originalDex))) {
if (outputComparator != null) {
String output1 = ToolHelper.runArtNoVerificationErrors(
Collections.singletonList(originalDex), mainClass, extraArtArgs, null);
String output2 = ToolHelper.runArtNoVerificationErrors(
Collections.singletonList(generated.toString()), mainClass, extraArtArgs, null);
outputComparator.accept(output1, output2);
} else {
ToolHelper.checkArtOutputIdentical(Collections.singletonList(originalDex),
Collections.singletonList(generated.toString()), mainClass,
extraArtArgs, null);
}
if (dexComparator != null) {
DexInspector ref = new DexInspector(Paths.get(originalDex));
DexInspector inspector = new DexInspector(generated,
minify.isMinify() ? temp.getRoot().toPath().resolve(DEFAULT_PROGUARD_MAP_FILE)
.toString()
: null);
dexComparator.accept(ref, inspector);
}
} else {
Assert.assertNull(outputComparator);
Assert.assertNull(dexComparator);
ToolHelper.runArtNoVerificationErrors(
Collections.singletonList(generated.toString()), mainClass, extraArtArgs, null);
}
if (inspection != null) {
DexInspector inspector = new DexInspector(generated,
minify.isMinify() ? temp.getRoot().toPath().resolve(DEFAULT_PROGUARD_MAP_FILE).toString()
: null);
inspection.accept(inspector);
}
}
}