| // Copyright (c) 2017, 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; | |
| import static org.hamcrest.CoreMatchers.hasItems; | |
| import static org.hamcrest.CoreMatchers.is; | |
| import static org.hamcrest.MatcherAssert.assertThat; | |
| import static org.junit.Assert.assertEquals; | |
| import com.android.tools.r8.origin.Origin; | |
| import com.android.tools.r8.references.ClassReference; | |
| import com.android.tools.r8.references.MethodReference; | |
| import com.android.tools.r8.smali.SmaliBuilder; | |
| import com.android.tools.r8.utils.AndroidApp; | |
| import com.google.common.collect.Lists; | |
| import com.google.common.collect.Sets; | |
| import java.io.IOException; | |
| import java.lang.annotation.Retention; | |
| import java.lang.annotation.RetentionPolicy; | |
| import java.nio.file.Path; | |
| import java.util.List; | |
| import java.util.Objects; | |
| import java.util.Set; | |
| import java.util.concurrent.ExecutionException; | |
| import java.util.stream.Collectors; | |
| import org.junit.Rule; | |
| import org.junit.Test; | |
| import org.junit.rules.TemporaryFolder; | |
| /** | |
| * Tests for resource shrinker analyzer. This is checking that dex files are processed correctly. | |
| */ | |
| public class ResourceShrinkerTest extends TestBase { | |
| @Rule | |
| public TemporaryFolder tmp = new TemporaryFolder(); | |
| private static class TrackAll implements ResourceShrinker.ReferenceChecker { | |
| Set<Integer> refIntegers = Sets.newHashSet(); | |
| Set<String> refStrings = Sets.newHashSet(); | |
| List<List<String>> refFields = Lists.newArrayList(); | |
| List<List<String>> refMethods = Lists.newArrayList(); | |
| List<MethodReference> methodsVisited = Lists.newArrayList(); | |
| List<ClassReference> classesVisited = Lists.newArrayList(); | |
| @Override | |
| public boolean shouldProcess(String internalName) { | |
| return !internalName.equals(ResourceClassToSkip.class.getName()); | |
| } | |
| @Override | |
| public void referencedInt(int value) { | |
| refIntegers.add(value); | |
| } | |
| @Override | |
| public void referencedString(String value) { | |
| refStrings.add(value); | |
| } | |
| @Override | |
| public void referencedStaticField(String internalName, String fieldName) { | |
| refFields.add(Lists.newArrayList(internalName, fieldName)); | |
| } | |
| @Override | |
| public void referencedMethod(String internalName, String methodName, String methodDescriptor) { | |
| if (Objects.equals(internalName, "java/lang/Object") | |
| && Objects.equals(methodName, "<init>")) { | |
| return; | |
| } | |
| refMethods.add(Lists.newArrayList(internalName, methodName, methodDescriptor)); | |
| } | |
| @Override | |
| public void startMethodVisit(MethodReference methodReference) { | |
| methodsVisited.add(methodReference); | |
| } | |
| @Override | |
| public void endMethodVisit(MethodReference methodReference) { | |
| assertEquals(methodsVisited.get(methodsVisited.size() - 1), methodReference); | |
| } | |
| @Override | |
| public void startClassVisit(ClassReference classReference) { | |
| classesVisited.add(classReference); | |
| } | |
| @Override | |
| public void endClassVisit(ClassReference classReference) { | |
| assertEquals(classesVisited.get(classesVisited.size() - 1), classReference); | |
| } | |
| } | |
| private static class EmptyClass { | |
| } | |
| @Test | |
| public void testEmptyClass() throws CompilationFailedException, IOException, ExecutionException { | |
| TrackAll analysis = runAnalysis(EmptyClass.class); | |
| assertThat(analysis.refIntegers, is(Sets.newHashSet())); | |
| assertThat(analysis.refStrings, is(Sets.newHashSet())); | |
| assertThat(analysis.refFields, is(Lists.newArrayList())); | |
| assertThat(analysis.refMethods, is(Lists.newArrayList())); | |
| } | |
| private static class ConstInCode { | |
| public void foo() { | |
| int i = 10; | |
| System.out.print(i); | |
| System.out.print(11); | |
| String s = "my_layout"; | |
| System.out.print("another_layout"); | |
| } | |
| } | |
| @Test | |
| public void testConstsAndFieldAndMethods() | |
| throws CompilationFailedException, IOException, ExecutionException { | |
| TrackAll analysis = runAnalysis(ConstInCode.class); | |
| assertThat(analysis.refIntegers, is(Sets.newHashSet(10, 11))); | |
| assertThat(analysis.refStrings, is(Sets.newHashSet("my_layout", "another_layout"))); | |
| assertEquals(3, analysis.refFields.size()); | |
| assertThat(analysis.refFields.get(0), is(Lists.newArrayList("java/lang/System", "out"))); | |
| assertThat(analysis.refFields.get(1), is(Lists.newArrayList("java/lang/System", "out"))); | |
| assertThat(analysis.refFields.get(2), is(Lists.newArrayList("java/lang/System", "out"))); | |
| assertEquals(3, analysis.refMethods.size()); | |
| assertThat( | |
| analysis.refMethods.get(0), is(Lists.newArrayList("java/io/PrintStream", "print", "(I)V"))); | |
| assertThat( | |
| analysis.refMethods.get(1), is(Lists.newArrayList("java/io/PrintStream", "print", "(I)V"))); | |
| assertThat( | |
| analysis.refMethods.get(2), | |
| is(Lists.newArrayList("java/io/PrintStream", "print", "(Ljava/lang/String;)V"))); | |
| } | |
| @SuppressWarnings("unused") | |
| private static class StaticFields { | |
| static final String sStringValue = "staticValue"; | |
| static final int sIntValue = 10; | |
| static final int[] sIntArrayValue = {11, 12, 13}; | |
| static final String[] sStringArrayValue = {"a", "b", "c"}; | |
| } | |
| @Test | |
| public void testStaticValues() | |
| throws CompilationFailedException, IOException, ExecutionException { | |
| TrackAll analysis = runAnalysis(StaticFields.class); | |
| assertThat(analysis.refIntegers, hasItems(10, 11, 12, 13)); | |
| assertThat(analysis.refStrings, hasItems("staticValue", "a", "b", "c")); | |
| assertThat(analysis.refFields, is(Lists.newArrayList())); | |
| assertThat(analysis.refMethods, is(Lists.newArrayList())); | |
| } | |
| @SuppressWarnings("unused") | |
| private static class ClassesAndMethodsVisited { | |
| final int value = getValue(); | |
| static final int staticValue; | |
| static { | |
| staticValue = 42; | |
| } | |
| int getValue() { | |
| return true ? 0 : 1; | |
| } | |
| } | |
| @Test | |
| public void testNumberOfMethodsAndClassesVisited() | |
| throws CompilationFailedException, IOException, ExecutionException { | |
| TrackAll analysis = runAnalysis(ClassesAndMethodsVisited.class); | |
| List<String> methodNames = | |
| analysis.methodsVisited.stream() | |
| .map(MethodReference::getMethodName) | |
| .collect(Collectors.toList()); | |
| List<String> classNames = | |
| analysis.classesVisited.stream() | |
| .map(ClassReference::getBinaryName) | |
| .collect(Collectors.toList()); | |
| assertThat(methodNames, hasItems("<init>", "<clinit>", "getValue")); | |
| assertThat( | |
| classNames, hasItems("com/android/tools/r8/ResourceShrinkerTest$ClassesAndMethodsVisited")); | |
| } | |
| @Retention(RetentionPolicy.RUNTIME) | |
| private @interface IntAnnotation { | |
| int value() default 10; | |
| } | |
| @Retention(RetentionPolicy.RUNTIME) | |
| private @interface OuterAnnotation { | |
| IntAnnotation inner() default @IntAnnotation(11); | |
| } | |
| @IntAnnotation(42) | |
| private static class Annotated { | |
| @OuterAnnotation | |
| Object defaultAnnotated = new Object(); | |
| @IntAnnotation(12) | |
| Object withValueAnnotated = new Object(); | |
| @IntAnnotation(13) | |
| static Object staticValueAnnotated = new Object(); | |
| @IntAnnotation(14) | |
| public void annotatedPublic() { | |
| } | |
| @IntAnnotation(15) | |
| public void annotatedPrivate() { | |
| } | |
| } | |
| @Test | |
| public void testAnnotations() throws CompilationFailedException, IOException, ExecutionException { | |
| TrackAll analysis = runAnalysis(IntAnnotation.class, OuterAnnotation.class, Annotated.class); | |
| assertThat(analysis.refIntegers, hasItems(10, 11, 12, 13, 14, 15, 42)); | |
| assertThat(analysis.refStrings, is(Sets.newHashSet())); | |
| assertThat(analysis.refFields, is(Lists.newArrayList())); | |
| assertThat(analysis.refMethods, is(Lists.newArrayList())); | |
| } | |
| private static class ResourceClassToSkip { | |
| int[] i = {100, 101, 102}; | |
| } | |
| private static class ToProcess { | |
| int[] i = {10, 11, 12}; | |
| String[] s = {"10", "11", "12"}; | |
| } | |
| @Test | |
| public void testWithSkippingSome() | |
| throws ExecutionException, CompilationFailedException, IOException { | |
| TrackAll analysis = runAnalysis(ResourceClassToSkip.class, ToProcess.class); | |
| assertThat(analysis.refIntegers, hasItems(10, 11, 12)); | |
| assertThat(analysis.refStrings, is(Sets.newHashSet("10", "11", "12"))); | |
| assertThat(analysis.refFields, is(Lists.newArrayList())); | |
| assertThat(analysis.refMethods, is(Lists.newArrayList())); | |
| } | |
| @Test | |
| public void testPayloadBeforeFillArrayData() throws Exception { | |
| SmaliBuilder builder = new SmaliBuilder("Test"); | |
| builder.addMainMethod( | |
| 2, | |
| "goto :start", | |
| "", | |
| ":array_data", | |
| ".array-data 4", | |
| " 4 5 6", | |
| ".end array-data", | |
| "", | |
| ":start", | |
| "const/4 v1, 3", | |
| "new-array v0, v1, [I", | |
| "fill-array-data v0, :array_data", | |
| "return-object v0" | |
| ); | |
| AndroidApp app = | |
| AndroidApp.builder().addDexProgramData(builder.compile(), Origin.unknown()).build(); | |
| TrackAll analysis = runOnApp(app); | |
| assertThat(analysis.refIntegers, hasItems(4, 5, 6)); | |
| assertThat(analysis.refStrings, is(Sets.newHashSet())); | |
| assertThat(analysis.refFields, is(Lists.newArrayList())); | |
| assertThat(analysis.refMethods, is(Lists.newArrayList())); | |
| } | |
| private TrackAll runAnalysis(Class<?>... classes) | |
| throws IOException, ExecutionException, CompilationFailedException { | |
| AndroidApp app = readClasses(classes); | |
| return runOnApp(app); | |
| } | |
| private TrackAll runOnApp(AndroidApp app) | |
| throws IOException, ExecutionException, CompilationFailedException { | |
| AndroidApp outputApp = compileWithD8(app); | |
| Path outputDex = tmp.newFolder().toPath().resolve("classes.dex"); | |
| outputApp.writeToDirectory(outputDex.getParent(), OutputMode.DexIndexed); | |
| ProgramResourceProvider provider = | |
| () -> Lists.newArrayList(ProgramResource.fromFile(ProgramResource.Kind.DEX, outputDex)); | |
| ResourceShrinker.Command command = | |
| new ResourceShrinker.Builder().addProgramResourceProvider(provider).build(); | |
| TrackAll analysis = new TrackAll(); | |
| ResourceShrinker.run(command, analysis); | |
| return analysis; | |
| } | |
| } |