1 package com.android.tools.rmtypedefs;
2 
3 import com.google.common.base.Charsets;
4 import com.google.common.collect.Lists;
5 import com.google.common.io.Files;
6 
7 import junit.framework.TestCase;
8 
9 import org.eclipse.jdt.core.compiler.batch.BatchCompiler;
10 
11 import java.io.File;
12 import java.io.IOException;
13 import java.io.PrintWriter;
14 import java.security.Permission;
15 import java.util.Collections;
16 import java.util.Comparator;
17 import java.util.List;
18 
19 import static com.google.common.base.Charsets.UTF_8;
20 import static java.io.File.separatorChar;
21 
22 @SuppressWarnings("SpellCheckingInspection")
23 public class RmTypeDefsTest extends TestCase {
test()24     public void test() throws IOException {
25         // Creates a test class containing various typedefs, as well as the @IntDef annotation
26         // itself (to make the test case independent of the SDK), and compiles this using
27         // ECJ. It then runs the RmTypeDefs tool on the resulting output directory, and
28         // finally verifies that the tool exits with a 0 exit code.
29 
30         File dir = Files.createTempDir();
31         String testClass = ""
32             + "package test.pkg;\n"
33             + "\n"
34             + "import android.annotation.IntDef;\n"
35             + "\n"
36             + "import java.lang.annotation.Retention;\n"
37             + "import java.lang.annotation.RetentionPolicy;\n"
38             + "\n"
39             + "@SuppressWarnings({\"UnusedDeclaration\",\"JavaDoc\"})\n"
40             + "public class TestClass {\n"
41             + "    /** @hide */\n"
42             + "    @Retention(RetentionPolicy.SOURCE)\n"
43             + "    @IntDef(flag = true,\n"
44             + "            value = {\n"
45             + "                    DISPLAY_USE_LOGO,\n"
46             + "                    DISPLAY_SHOW_HOME,\n"
47             + "                    DISPLAY_HOME_AS_UP,\n"
48             + "                    DISPLAY_SHOW_TITLE,\n"
49             + "                    DISPLAY_SHOW_CUSTOM,\n"
50             + "                    DISPLAY_TITLE_MULTIPLE_LINES\n"
51             + "            })\n"
52             + "    public @interface DisplayOptions {}\n"
53             + "\n"
54             + "    public static final int DISPLAY_USE_LOGO = 0x1;\n"
55             + "    public static final int DISPLAY_SHOW_HOME = 0x2;\n"
56             + "    public static final int DISPLAY_HOME_AS_UP = 0x4;\n"
57             + "    public static final int DISPLAY_SHOW_TITLE = 0x8;\n"
58             + "    public static final int DISPLAY_SHOW_CUSTOM = 0x10;\n"
59             + "    public static final int DISPLAY_TITLE_MULTIPLE_LINES = 0x20;\n"
60             + "\n"
61             + "    public void setDisplayOptions(@DisplayOptions int options) {\n"
62             + "        System.out.println(\"setDisplayOptions \" + options);\n"
63             + "    }\n"
64             + "    public void setDisplayOptions(@DisplayOptions int options, @DisplayOptions int mask) {\n"
65             + "        System.out.println(\"setDisplayOptions \" + options + \", mask=\" + mask);\n"
66             + "    }\n"
67             + "\n"
68             + "    public static class StaticInnerClass {\n"
69             + "        int mViewFlags = 0;\n"
70             + "        static final int VISIBILITY_MASK = 0x0000000C;\n"
71             + "\n"
72             + "        /** @hide */\n"
73             + "        @IntDef({VISIBLE, INVISIBLE, GONE})\n"
74             + "        @Retention(RetentionPolicy.SOURCE)\n"
75             + "        public @interface Visibility {}\n"
76             + "\n"
77             + "        public static final int VISIBLE = 0x00000000;\n"
78             + "        public static final int INVISIBLE = 0x00000004;\n"
79             + "        public static final int GONE = 0x00000008;\n"
80             + "\n"
81             + "        @Visibility\n"
82             + "        public int getVisibility() {\n"
83             + "            return mViewFlags & VISIBILITY_MASK;\n"
84             + "        }\n"
85             + "    }\n"
86             + "\n"
87             + "    public static class Inherits extends StaticInnerClass {\n"
88             + "        @Override\n"
89             + "        @Visibility\n"
90             + "        public int getVisibility() {\n"
91             + "            return 0;\n"
92             + "        }\n"
93             + "    }\n"
94             + "}\n";
95         String intdef = ""
96             + "package android.annotation;\n"
97             + "@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS)\n"
98             + "@java.lang.annotation.Target({java.lang.annotation.ElementType.ANNOTATION_TYPE})\n"
99             + "public @interface IntDef {\n"
100             + "    long[] value() default {};\n"
101             + "    boolean flag() default false;\n"
102             + "}";
103 
104         File srcDir = new File(dir, "test" + File.separator + "pkg");
105         boolean mkdirs = srcDir.mkdirs();
106         assertTrue(mkdirs);
107         File srcFile1 = new File(srcDir, "TestClass.java");
108         Files.write(testClass, srcFile1, Charsets.UTF_8);
109 
110         srcDir = new File(dir, "android" + File.separator + "annotation");
111         mkdirs = srcDir.mkdirs();
112         assertTrue(mkdirs);
113         File srcFile2 = new File(srcDir, "IntDef.java");
114         Files.write(intdef, srcFile2, Charsets.UTF_8);
115 
116         boolean compileSuccessful = BatchCompiler.compile(srcFile1 + " " + srcFile2 +
117                         " -source 1.6 -target 1.6 -nowarn",
118                 new PrintWriter(System.out),
119                 new PrintWriter(System.err), null);
120         assertTrue(compileSuccessful);
121 
122         assertEquals(""
123             + "testDir/\n"
124             + "    testDir/android/\n"
125             + "        testDir/android/annotation/\n"
126             + "            testDir/android/annotation/IntDef.class\n"
127             + "            testDir/android/annotation/IntDef.java\n"
128             + "    testDir/test/\n"
129             + "        testDir/test/pkg/\n"
130             + "            testDir/test/pkg/TestClass$DisplayOptions.class\n"
131             + "            testDir/test/pkg/TestClass$Inherits.class\n"
132             + "            testDir/test/pkg/TestClass$StaticInnerClass$Visibility.class\n"
133             + "            testDir/test/pkg/TestClass$StaticInnerClass.class\n"
134             + "            testDir/test/pkg/TestClass.class\n"
135             + "            testDir/test/pkg/TestClass.java\n",
136             getDirectoryContents(dir));
137 
138         // Trap System.exit calls:
139         System.setSecurityManager(new SecurityManager() {
140             @Override
141             public void checkPermission(Permission perm) {
142             }
143             @Override
144             public void checkPermission(Permission perm, Object context) {
145             }
146             @Override
147             public void checkExit(int status) {
148                 throw new ExitException(status);
149             }
150         });
151         try {
152             RmTypeDefs.main(new String[]{"--verbose", dir.getPath()});
153         } catch (ExitException e) {
154             assertEquals(0, e.getStatus());
155         }
156         System.setSecurityManager(null);
157 
158         // TODO: check that the classes are identical
159         // BEFORE removal
160 
161         assertEquals(""
162                 + "testDir/\n"
163                 + "    testDir/android/\n"
164                 + "        testDir/android/annotation/\n"
165                 + "            testDir/android/annotation/IntDef.class\n"
166                 + "            testDir/android/annotation/IntDef.java\n"
167                 + "    testDir/test/\n"
168                 + "        testDir/test/pkg/\n"
169                 + "            testDir/test/pkg/TestClass$Inherits.class\n"
170                 + "            testDir/test/pkg/TestClass$StaticInnerClass.class\n"
171                 + "            testDir/test/pkg/TestClass.class\n"
172                 + "            testDir/test/pkg/TestClass.java\n",
173                 getDirectoryContents(dir));
174 
175         // Make sure the Visibility symbol is completely gone from the outer class
176         assertDoesNotContainBytes(new File(dir,
177                 "test/pkg/TestClass$StaticInnerClass.class".replace('/', separatorChar)),
178                 "Visibility");
179 
180         deleteDir(dir);
181     }
182 
assertDoesNotContainBytes(File file, String sub)183     private void assertDoesNotContainBytes(File file, String sub) throws IOException {
184         byte[] contents = Files.toByteArray(file);
185         // Like the strings command, look for 4 or more consecutive printable characters
186         for (int i = 0, n = contents.length; i < n; i++) {
187             if (Character.isJavaIdentifierStart(contents[i])) {
188                 for (int j = i + 1; j < n; j++) {
189                     if (!Character.isJavaIdentifierPart(contents[j])) {
190                         if (j > i + 4) {
191                             int length = j - i - 1;
192                             if (length == sub.length()) {
193                                 String symbol = new String(contents, i, length, UTF_8);
194                                 assertFalse("Found " + sub + " in class file " + file,
195                                         sub.equals(symbol));
196                             }
197                         }
198                         i = j;
199                         break;
200                     }
201                 }
202             }
203         }
204     }
205 
getDirectoryContents(File root)206     String getDirectoryContents(File root) {
207         StringBuilder sb = new StringBuilder();
208         list(sb, root, "", 0, "testDir");
209         return sb.toString();
210     }
211 
list(StringBuilder sb, File file, String prefix, int depth, String rootName)212     private void list(StringBuilder sb, File file, String prefix, int depth, String rootName) {
213         for (int i = 0; i < depth; i++) {
214             sb.append("    ");
215         }
216 
217         if (!prefix.isEmpty()) {
218             sb.append(prefix);
219         }
220         String fileName = file.getName();
221         if (depth == 0 && rootName != null) { // avoid temp-name
222             fileName = rootName;
223         }
224         sb.append(fileName);
225         if (file.isDirectory()) {
226             sb.append('/');
227             sb.append('\n');
228             File[] files = file.listFiles();
229             if (files != null) {
230                 List<File> children = Lists.newArrayList();
231                 Collections.addAll(children, files);
232                 Collections.sort(children, new Comparator<File>() {
233                     @Override
234                     public int compare(File o1, File o2) {
235                         return o1.getName().compareTo(o2.getName());
236                     }
237                 });
238                 prefix = prefix + fileName + "/";
239                 for (File child : children) {
240                     list(sb, child, prefix, depth + 1, rootName);
241                 }
242             }
243         } else {
244             sb.append('\n');
245         }
246     }
247 
248     /**
249      * Recursive delete directory. Mostly for fake SDKs.
250      *
251      * @param root directory to delete
252      */
253     @SuppressWarnings("ResultOfMethodCallIgnored")
deleteDir(File root)254     private static void deleteDir(File root) {
255         if (root.exists()) {
256             File[] files = root.listFiles();
257             if (files != null) {
258                 for (File file : files) {
259                     if (file.isDirectory()) {
260                         deleteDir(file);
261                     } else {
262                         file.delete();
263                     }
264                 }
265             }
266             root.delete();
267         }
268     }
269 
270     private static class ExitException extends SecurityException {
271         private static final long serialVersionUID = 1L;
272 
273         private final int mStatus;
274 
ExitException(int status)275         public ExitException(int status) {
276             super("Unit test");
277             mStatus = status;
278         }
279 
getStatus()280         public int getStatus() {
281             return mStatus;
282         }
283     }
284 }
285