|  | // 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); | 
|  | } | 
|  | } | 
|  | } |