/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.build.gradle.tasks.annotations; import static com.android.utils.SdkUtils.fileToUrlString; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.testutils.SdkTestCase; import com.google.common.base.Charsets; import com.google.common.io.ByteStreams; import com.google.common.io.Closeables; import com.google.common.io.Files; import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.List; public class ExtractAnnotationsDriverTest extends SdkTestCase { private static boolean isValidEcj() { // When this test is run from within Gradle's testing infrastructure, e.g. // via "./gradlew :base:gradle-core:test", Gradle's own *internal* dependencies // somehow end up on the classpath, and in particular, an ancient version of // (ECJ (3.x)) take priority over our explicit lint dependency which should // bring in ECJ 4. // // This causes the test to fail. For now, make the test only run when a valid // ECJ is present. try { CompilerOptions.class.getField("originalComplianceLevel"); return true; } catch (Throwable t) { return false; } } public void testProGuard() throws Exception { if (!isValidEcj()) { return; } File androidJar = findAndroidJar(false); if (androidJar == null) { System.err.println("Skipping test " + this.getClass() + "#" + getName() + ": No android.jar found"); return; } File project = createProject(mKeepTest, mKeepAnnotation); File output = File.createTempFile("proguard", ".cfg"); output.deleteOnExit(); List<String> list = java.util.Arrays.asList( "--sources", new File(project, "src").getPath(), "--classpath", androidJar.getPath(), "--quiet", "--language-level", "1.6", "--proguard", output.getPath() ); String[] args = list.toArray(new String[list.size()]); assertNotNull(args); new ExtractAnnotationsDriver().run(args); assertEquals("" + "-keep class test.pkg.KeepTest {\n" + " java.lang.Object myField\n" + "}\n" + "\n" + "-keep class test.pkg.KeepTest {\n" + " void foo()\n" + "}\n" + "\n" + "-keep class test.pkg.KeepTest.MyClass\n" + "\n" + "-keep enum test.pkg.KeepTest.MyEnum\n" + "\n" + "-keep interface test.pkg.KeepTest.MyInterface\n" + "\n" + "-keep interface test.pkg.KeepTest.MyInterface2 {\n" + " void paint2()\n" + "}\n" + "\n", Files.toString(output, Charsets.UTF_8)); deleteFile(project); } public void testIncludeClassRetention() throws Exception { if (!isValidEcj()) { return; } File androidJar = findAndroidJar(false); if (androidJar == null) { System.err.println("Skipping test " + this.getClass() + "#" + getName() + ": No android.jar found"); return; } File project = createProject( mIntDefTest, mPermissionsTest, mManifest, mKeepAnnotation, mIntDefAnnotation, mIntRangeAnnotation, mPermissionAnnotation); File output = File.createTempFile("annotations", ".zip"); File proguard = File.createTempFile("proguard", ".cfg"); output.deleteOnExit(); proguard.deleteOnExit(); List<String> list = java.util.Arrays.asList( "--sources", new File(project, "src").getPath(), "--classpath", androidJar.getPath(), "--quiet", "--language-level", "1.6", "--output", output.getPath(), "--proguard", proguard.getPath() ); String[] args = list.toArray(new String[list.size()]); assertNotNull(args); new ExtractAnnotationsDriver().run(args); // Check proguard rules assertEquals("" + "-keep class test.pkg.IntDefTest {\n" + " void testIntDef(int)\n" + "}\n" + "\n", Files.toString(proguard, Charsets.UTF_8)); // Check extracted annotations checkPackageXml("test.pkg", output, "" + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<root>\n" + " <item name=\"test.pkg.IntDefTest void setFlags(java.lang.Object, int) 1\">\n" + " <annotation name=\"android.support.annotation.IntDef\">\n" + " <val name=\"value\" val=\"{test.pkg.IntDefTest.STYLE_NORMAL, test.pkg.IntDefTest.STYLE_NO_TITLE, test.pkg.IntDefTest.STYLE_NO_FRAME, test.pkg.IntDefTest.STYLE_NO_INPUT}\" />\n" + " <val name=\"flag\" val=\"true\" />\n" + " </annotation>\n" + " </item>\n" + " <item name=\"test.pkg.IntDefTest void setStyle(int, int) 0\">\n" + " <annotation name=\"android.support.annotation.IntDef\">\n" + " <val name=\"value\" val=\"{test.pkg.IntDefTest.STYLE_NORMAL, test.pkg.IntDefTest.STYLE_NO_TITLE, test.pkg.IntDefTest.STYLE_NO_FRAME, test.pkg.IntDefTest.STYLE_NO_INPUT}\" />\n" + " </annotation>\n" + " <annotation name=\"android.support.annotation.IntRange\">\n" + " <val name=\"from\" val=\"20\" />\n" + " </annotation>\n" + " </item>\n" + " <item name=\"test.pkg.PermissionsTest CONTENT_URI\">\n" + " <annotation name=\"android.support.annotation.RequiresPermission.Read\">\n" + " <val name=\"value\" val=\""android.permission.MY_READ_PERMISSION_STRING"\" />\n" + " </annotation>\n" + " <annotation name=\"android.support.annotation.RequiresPermission.Write\">\n" + " <val name=\"value\" val=\""android.permission.MY_WRITE_PERMISSION_STRING"\" />\n" + " </annotation>\n" + " </item>\n" + " <item name=\"test.pkg.PermissionsTest void myMethod()\">\n" + " <annotation name=\"android.support.annotation.RequiresPermission\">\n" + " <val name=\"value\" val=\""android.permission.MY_PERMISSION_STRING"\" />\n" + " </annotation>\n" + " </item>\n" + "</root>\n" + "\n"); deleteFile(project); } public void testSkipClassRetention() throws Exception { if (!isValidEcj()) { return; } File androidJar = findAndroidJar(false); if (androidJar == null) { System.err.println("Skipping test " + this.getClass() + "#" + getName() + ": No android.jar found"); return; } File project = createProject( mIntDefTest, mPermissionsTest, mManifest, mKeepAnnotation, mIntDefAnnotation, mIntRangeAnnotation, mPermissionAnnotation); File output = File.createTempFile("annotations", ".zip"); File proguard = File.createTempFile("proguard", ".cfg"); output.deleteOnExit(); proguard.deleteOnExit(); List<String> list = java.util.Arrays.asList( "--sources", new File(project, "src").getPath(), "--classpath", androidJar.getPath(), "--quiet", "--skip-class-retention", "--language-level", "1.6", "--output", output.getPath(), "--proguard", proguard.getPath() ); String[] args = list.toArray(new String[list.size()]); assertNotNull(args); new ExtractAnnotationsDriver().run(args); // Check external annotations checkPackageXml("test.pkg", output, "" + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<root>\n" + " <item name=\"test.pkg.IntDefTest void setFlags(java.lang.Object, int) 1\">\n" + " <annotation name=\"android.support.annotation.IntDef\">\n" + " <val name=\"value\" val=\"{test.pkg.IntDefTest.STYLE_NORMAL, test.pkg.IntDefTest.STYLE_NO_TITLE, test.pkg.IntDefTest.STYLE_NO_FRAME, test.pkg.IntDefTest.STYLE_NO_INPUT}\" />\n" + " <val name=\"flag\" val=\"true\" />\n" + " </annotation>\n" + " </item>\n" + " <item name=\"test.pkg.IntDefTest void setStyle(int, int) 0\">\n" + " <annotation name=\"android.support.annotation.IntDef\">\n" + " <val name=\"value\" val=\"{test.pkg.IntDefTest.STYLE_NORMAL, test.pkg.IntDefTest.STYLE_NO_TITLE, test.pkg.IntDefTest.STYLE_NO_FRAME, test.pkg.IntDefTest.STYLE_NO_INPUT}\" />\n" + " </annotation>\n" + " <annotation name=\"android.support.annotation.IntRange\">\n" + " <val name=\"from\" val=\"20\" />\n" + " </annotation>\n" + " </item>\n" + "</root>\n" + "\n"); deleteFile(project); } @SuppressWarnings("SpellCheckingInspection") @NonNull private TestFile mksrc(@NonNull String to, @NonNull String source) { return new TestFile().to(to).withSource(source); } private File createProject(TestFile... files) throws IOException { File dir = Files.createTempDir(); for (TestFile fp : files) { File file = fp.createFile(dir); assertNotNull(file); } return dir; } private final TestFile mKeepAnnotation = mksrc("src/android/support/annotation/Keep.java", "" + "package android.support.annotation;\n" + "import java.lang.annotation.Retention;\n" + "import java.lang.annotation.Target;\n" + "import static java.lang.annotation.ElementType.*;\n" + "import static java.lang.annotation.RetentionPolicy.*;\n" + "@Retention(CLASS)\n" + "@Target({PACKAGE,TYPE,ANNOTATION_TYPE,CONSTRUCTOR,METHOD,FIELD})\n" + "public @interface Keep {\n" + "}\n"); private final TestFile mIntDefAnnotation = mksrc("src/android/support/annotation/IntDef.java", "" + "package android.support.annotation;\n" + "import java.lang.annotation.Retention;\n" + "import java.lang.annotation.RetentionPolicy;\n" + "import java.lang.annotation.Target;\n" + "import static java.lang.annotation.ElementType.*;\n" + "import static java.lang.annotation.RetentionPolicy.SOURCE;\n" + "@Retention(SOURCE)\n" + "@Target({ANNOTATION_TYPE})\n" + "public @interface IntDef {\n" + " long[] value() default {};\n" + " boolean flag() default false;\n" + "}\n"); private final TestFile mIntRangeAnnotation = mksrc("src/android/support/annotation/IntRange.java", "" + "package android.support.annotation;\n" + "\n" + "import java.lang.annotation.Retention;\n" + "import java.lang.annotation.Target;\n" + "\n" + "import static java.lang.annotation.ElementType.*;\n" + "import static java.lang.annotation.RetentionPolicy.CLASS;\n" + "\n" + "@Retention(CLASS)\n" + "@Target({CONSTRUCTOR,METHOD,PARAMETER,FIELD,LOCAL_VARIABLE,ANNOTATION_TYPE})\n" + "public @interface IntRange {\n" + " long from() default Long.MIN_VALUE;\n" + " long to() default Long.MAX_VALUE;\n" + "}\n"); private final TestFile mPermissionAnnotation = mksrc( "src/android/support/annotation/RequiresPermission.java", "" + "package android.support.annotation;\n" + "import java.lang.annotation.Retention;\n" + "import java.lang.annotation.RetentionPolicy;\n" + "import java.lang.annotation.Target;\n" + "import static java.lang.annotation.ElementType.*;\n" + "import static java.lang.annotation.RetentionPolicy.*;\n" + "@Retention(CLASS)\n" + "@Target({ANNOTATION_TYPE,METHOD,CONSTRUCTOR,FIELD})\n" + "public @interface RequiresPermission {\n" + " String value() default \"\";\n" + " String[] allOf() default {};\n" + " String[] anyOf() default {};\n" + " boolean conditional() default false;\n" + " @Target(FIELD)\n" + " @interface Read {\n" + " RequiresPermission value();\n" + " }\n" + " @Target(FIELD)\n" + " @interface Write {\n" + " RequiresPermission value();\n" + " }\n" + "}"); private final TestFile mIntDefTest = mksrc("src/test/pkg/IntDefTest.java", "" + "package test.pkg;\n" + "\n" + "import android.content.Context;\n" + "import android.support.annotation.IntDef;\n" + "import android.support.annotation.IntRange;\n" + "import android.support.annotation.Keep;\n" + "import android.view.View;\n" + "\n" + "import java.lang.annotation.Retention;\n" + "import java.lang.annotation.RetentionPolicy;\n" + "\n" + "@SuppressWarnings(\"UnusedDeclaration\")\n" + "public class IntDefTest {\n" + " @IntDef({STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT})\n" + " @IntRange(from = 20)\n" + " @Retention(RetentionPolicy.SOURCE)\n" + " private @interface DialogStyle {}\n" + "\n" + " public static final int STYLE_NORMAL = 0;\n" + " public static final int STYLE_NO_TITLE = 1;\n" + " public static final int STYLE_NO_FRAME = 2;\n" + " public static final int STYLE_NO_INPUT = 3;\n" + " public static final int UNRELATED = 3;\n" + "\n" + " public void setStyle(@DialogStyle int style, int theme) {\n" + " }\n" + "\n" + " @Keep" + " public void testIntDef(int arg) {\n" + " }\n" + " @IntDef(value = {STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT}, flag=true)\n" + " @Retention(RetentionPolicy.SOURCE)\n" + " private @interface DialogFlags {}\n" + "\n" + " public void setFlags(Object first, @DialogFlags int flags) {\n" + " }\n" + "\n" + " public static final String TYPE_1 = \"type1\";\n" + " public static final String TYPE_2 = \"type2\";\n" + " public static final String UNRELATED_TYPE = \"other\";\n" + "}"); private final TestFile mPermissionsTest = mksrc("src/test/pkg/PermissionsTest.java", "" + "package test.pkg;\n" + "\n" + "import android.support.annotation.RequiresPermission;\n" + "\n" + "public class PermissionsTest {\n" + " @RequiresPermission(Manifest.permission.MY_PERMISSION)\n" + " public void myMethod() {\n" + " }\n" + "\n" + " @RequiresPermission.Read(@RequiresPermission(Manifest.permission.MY_READ_PERMISSION))\n" + " @RequiresPermission.Write(@RequiresPermission(Manifest.permission.MY_WRITE_PERMISSION))\n" + " public static final String CONTENT_URI = \"\";\n" + "}\n"); private final TestFile mManifest = mksrc("src/test/pkg/Manifest.java", "" + "package test.pkg;\n" + "\n" + "public class Manifest {\n" + " public static final class permission {\n" + " public static final String MY_PERMISSION = \"android.permission.MY_PERMISSION_STRING\";\n" + " public static final String MY_READ_PERMISSION = \"android.permission.MY_READ_PERMISSION_STRING\";\n" + " public static final String MY_WRITE_PERMISSION = \"android.permission.MY_WRITE_PERMISSION_STRING\";\n" + " }\n" + "}\n"); private final TestFile mKeepTest = mksrc("src/test/pkg/KeepTest.java", "" + "package test.pkg;\n" + "import android.support.annotation.Keep;\n" + "\n" + "public class KeepTest {\n" + " @Keep\n" + " public void foo() {\n" + " }\n" + "\n" + " @Keep\n" + " public static class MyClass {\n" + " public void paint() {\n" + " }\n" + " }\n" + "\n" + " @Keep\n" + " public static interface MyInterface {\n" + " public void paint();\n" + " }\n" + "\n" + " public static interface MyInterface2 {\n" + " @Keep\n" + " public void paint2();\n" + " }\n" + "\n" + " @Keep\n" + " public static enum MyEnum {\n" + " TYPE1, TYPE2;\n" + " }\n" + "\n" + " @Keep\n" + " public static @interface MyAnnotation {\n" + " }\n" + "\n" + " @Keep\n" + " public Object myField = null;" + "}\n"); private static void checkPackageXml(String pkg, File output, String expected) throws IOException { assertNotNull(output); assertTrue(output.exists()); URL url = new URL("jar:" + fileToUrlString(output) + "!/" + pkg.replace('.','/') + "/annotations.xml"); InputStream stream = url.openStream(); try { byte[] bytes = ByteStreams.toByteArray(stream); assertNotNull(bytes); String xml = new String(bytes, Charsets.UTF_8); assertEquals(expected, xml); } finally { Closeables.closeQuietly(stream); } } @Nullable private static File findAndroidJar(boolean requireExists) { String androidHomePath = System.getenv("ANDROID_HOME"); assertNotNull("Must set $ANDROID_HOME to run this test", androidHomePath); File androidHome = new File(androidHomePath); if (!androidHome.exists()) { if (!requireExists) { return null; } fail(androidHomePath + " does not exist"); } File androidJar = new File(androidHome, "platforms/android-22/android.jar"); assertTrue( androidJar + " does not exist: make sure you have Lollipop installed in your SDK", androidJar.exists()); return androidJar; } public void testGetRaw() throws Exception { assertEquals("", ApiDatabase.getRawClass("")); assertEquals("Foo", ApiDatabase.getRawClass("Foo")); assertEquals("Foo", ApiDatabase.getRawClass("Foo<T>")); assertEquals("Foo", ApiDatabase.getRawMethod("Foo<T>")); assertEquals("Foo", ApiDatabase.getRawClass("Foo<A,B>")); assertEquals("Foo", ApiDatabase.getRawParameterList("Foo<? extends java.util.List>")); assertEquals("Object,java.util.List,List,int[],Object[]", ApiDatabase.getRawParameterList("Object<? extends java.util.List>,java.util.List<String>," + "List<? super Number>,int[],Object...")); } }