1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.processor.compat.changeid;
18 
19 import static java.nio.charset.StandardCharsets.UTF_8;
20 
21 import static javax.tools.StandardLocation.CLASS_OUTPUT;
22 
23 import com.google.common.collect.ObjectArrays;
24 import com.google.testing.compile.Compilation;
25 import com.google.testing.compile.CompilationSubject;
26 import com.google.testing.compile.Compiler;
27 import com.google.testing.compile.JavaFileObjects;
28 
29 import org.junit.Test;
30 
31 import javax.tools.JavaFileObject;
32 
33 
34 public class ChangeIdProcessorTest {
35 
36     // Hard coding the annotation definitions to avoid dependency on libcore, where they're defined.
37     private static final JavaFileObject[] mAnnotations = {
38             JavaFileObjects.forSourceLines("android.compat.annotation.ChangeId",
39                     "package android.compat.annotation;",
40                     "import static java.lang.annotation.ElementType.FIELD;",
41                     "import static java.lang.annotation.ElementType.PARAMETER;",
42                     "import static java.lang.annotation.RetentionPolicy.SOURCE;",
43                     "import java.lang.annotation.Retention;",
44                     "import java.lang.annotation.Target;",
45                     "@Retention(SOURCE)",
46                     "@Target({FIELD, PARAMETER})",
47                     "public @interface ChangeId {",
48                     "}"),
49             JavaFileObjects.forSourceLines("android.compat.annotation.Disabled",
50                     "package android.compat.annotation;",
51                     "import static java.lang.annotation.ElementType.FIELD;",
52                     "import static java.lang.annotation.RetentionPolicy.SOURCE;",
53                     "import java.lang.annotation.Retention;",
54                     "import java.lang.annotation.Target;",
55                     "@Retention(SOURCE)",
56                     "@Target({FIELD})",
57                     "public @interface Disabled {",
58                     "}"),
59             JavaFileObjects.forSourceLines("android.compat.annotation.LoggingOnly",
60                     "package android.compat.annotation;",
61                     "import static java.lang.annotation.ElementType.FIELD;",
62                     "import static java.lang.annotation.RetentionPolicy.SOURCE;",
63                     "import java.lang.annotation.Retention;",
64                     "import java.lang.annotation.Target;",
65                     "@Retention(SOURCE)",
66                     "@Target({FIELD})",
67                     "public @interface LoggingOnly {",
68                     "}"),
69             JavaFileObjects.forSourceLines("android.compat.annotation.EnabledAfter",
70                     "package android.compat.annotation;",
71                     "import static java.lang.annotation.ElementType.FIELD;",
72                     "import static java.lang.annotation.RetentionPolicy.SOURCE;",
73                     "import java.lang.annotation.Retention;",
74                     "import java.lang.annotation.Target;",
75                     "@Retention(SOURCE)",
76                     "@Target({FIELD})",
77                     "public @interface EnabledAfter {",
78                     "int targetSdkVersion();",
79                     "}")
80 
81     };
82 
83     private static final String HEADER =
84             "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>";
85 
86     @Test
testCompatConfigXmlOutput()87     public void testCompatConfigXmlOutput() {
88         JavaFileObject[] source = {
89                 JavaFileObjects.forSourceLines(
90                         "libcore.util.Compat",
91                         "package libcore.util;",
92                         "import android.compat.annotation.ChangeId;",
93                         "import android.compat.annotation.EnabledAfter;",
94                         "import android.compat.annotation.Disabled;",
95                         "public class Compat {",
96                         "    /**",
97                         "    * description of",
98                         "    * MY_CHANGE_ID",
99                         "    */",
100                         "    @EnabledAfter(targetSdkVersion=29)",
101                         "    @ChangeId",
102                         "    static final long MY_CHANGE_ID = 123456789l;",
103                         "    /** description of ANOTHER_CHANGE **/",
104                         "    @ChangeId",
105                         "    @Disabled",
106                         "    public static final long ANOTHER_CHANGE = 23456700l;",
107                         "}")
108         };
109         String expectedFile = HEADER + "<config>" +
110                 "<compat-change description=\"description of MY_CHANGE_ID\" "
111                 + "enableAfterTargetSdk=\"29\" id=\"123456789\" name=\"MY_CHANGE_ID\">"
112                 + "<meta-data definedIn=\"libcore.util.Compat\" "
113                 + "sourcePosition=\"libcore/util/Compat.java:11\"/></compat-change>"
114                 + "<compat-change description=\"description of ANOTHER_CHANGE\" disabled=\"true\" "
115                 + "id=\"23456700\" name=\"ANOTHER_CHANGE\"><meta-data definedIn=\"libcore.util"
116                 + ".Compat\" sourcePosition=\"libcore/util/Compat.java:14\"/></compat-change>"
117                 + "</config>";
118         Compilation compilation =
119                 Compiler.javac()
120                         .withProcessors(new ChangeIdProcessor())
121                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
122         CompilationSubject.assertThat(compilation).succeeded();
123         CompilationSubject.assertThat(compilation).generatedFile(CLASS_OUTPUT, "libcore.util",
124                 "Compat_compat_config.xml").contentsAsString(UTF_8).isEqualTo(expectedFile);
125     }
126 
127     @Test
testCompatConfigXmlOutput_multiplePackages()128     public void testCompatConfigXmlOutput_multiplePackages() {
129         JavaFileObject[] source = {
130                 JavaFileObjects.forSourceLines(
131                         "libcore.util.Compat",
132                         "package libcore.util;",
133                         "import android.compat.annotation.ChangeId;",
134                         "import android.compat.annotation.EnabledAfter;",
135                         "import android.compat.annotation.Disabled;",
136                         "public class Compat {",
137                         "    /**",
138                         "    * description of",
139                         "    * MY_CHANGE_ID",
140                         "    */",
141                         "    @ChangeId",
142                         "    static final long MY_CHANGE_ID = 123456789l;",
143                         "}"),
144                 JavaFileObjects.forSourceLines("android.util.SomeClass",
145                         "package android.util;",
146                         "import android.compat.annotation.ChangeId;",
147                         "import android.compat.annotation.EnabledAfter;",
148                         "import android.compat.annotation.Disabled;",
149                         "public class SomeClass {",
150                         "    /** description of ANOTHER_CHANGE **/",
151                         "    @ChangeId",
152                         "    public static final long ANOTHER_CHANGE = 23456700l;",
153                         "}")
154         };
155         String libcoreExpectedFile = HEADER + "<config>" +
156                 "<compat-change description=\"description of MY_CHANGE_ID\" "
157                 + "id=\"123456789\" name=\"MY_CHANGE_ID\">"
158                 + "<meta-data definedIn=\"libcore.util.Compat\" "
159                 + "sourcePosition=\"libcore/util/Compat.java:10\"/></compat-change>"
160                 + "</config>";
161         String androidExpectedFile = HEADER + "<config>" +
162                 "<compat-change description=\"description of ANOTHER_CHANGE\" "
163                 + "id=\"23456700\" name=\"ANOTHER_CHANGE\"><meta-data definedIn=\"android.util"
164                 + ".SomeClass\" sourcePosition=\"android/util/SomeClass.java:7\"/></compat-change>"
165                 + "</config>";
166         Compilation compilation =
167                 Compiler.javac()
168                         .withProcessors(new ChangeIdProcessor())
169                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
170         CompilationSubject.assertThat(compilation).succeeded();
171         CompilationSubject.assertThat(compilation).generatedFile(CLASS_OUTPUT, "libcore.util",
172                 "Compat_compat_config.xml").contentsAsString(UTF_8).isEqualTo(libcoreExpectedFile);
173         CompilationSubject.assertThat(compilation).generatedFile(CLASS_OUTPUT, "android.util",
174                 "SomeClass_compat_config.xml").contentsAsString(UTF_8).isEqualTo(
175                 androidExpectedFile);
176     }
177 
178     @Test
testCompatConfigXmlOutput_innerClass()179     public void testCompatConfigXmlOutput_innerClass() {
180         JavaFileObject[] source = {
181                 JavaFileObjects.forSourceLines(
182                         "libcore.util.Compat",
183                         "package libcore.util;",
184                         "import android.compat.annotation.ChangeId;",
185                         "import android.compat.annotation.EnabledAfter;",
186                         "import android.compat.annotation.Disabled;",
187                         "public class Compat {",
188                         "    public class Inner {",
189                         "        /**",
190                         "        * description of",
191                         "        * MY_CHANGE_ID",
192                         "        */",
193                         "        @ChangeId",
194                         "        static final long MY_CHANGE_ID = 123456789l;",
195                         "    }",
196                         "}"),
197         };
198         String expectedFile = HEADER + "<config>" +
199                 "<compat-change description=\"description of MY_CHANGE_ID\" "
200                 + "id=\"123456789\" name=\"MY_CHANGE_ID\"><meta-data definedIn=\"libcore.util"
201                 + ".Compat.Inner\" sourcePosition=\"libcore/util/Compat.java:11\"/>"
202                 + "</compat-change></config>";
203         Compilation compilation =
204                 Compiler.javac()
205                         .withProcessors(new ChangeIdProcessor())
206                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
207         CompilationSubject.assertThat(compilation).succeeded();
208         CompilationSubject.assertThat(compilation).generatedFile(CLASS_OUTPUT, "libcore.util",
209                 "Compat.Inner_compat_config.xml").contentsAsString(UTF_8).isEqualTo(expectedFile);
210     }
211 
212     @Test
testCompatConfigXmlOutput_interface()213     public void testCompatConfigXmlOutput_interface() {
214         JavaFileObject[] source = {
215                 JavaFileObjects.forSourceLines(
216                         "libcore.util.Compat",
217                         "package libcore.util;",
218                         "import android.compat.annotation.ChangeId;",
219                         "import android.compat.annotation.EnabledAfter;",
220                         "import android.compat.annotation.Disabled;",
221                         "public interface Compat {",
222                         "    /**",
223                         "     * description of",
224                         "     * MY_CHANGE_ID",
225                         "     */",
226                         "    @ChangeId",
227                         "    static final long MY_CHANGE_ID = 123456789l;",
228                         "}"),
229         };
230         String expectedFile = HEADER + "<config>" +
231                 "<compat-change description=\"description of MY_CHANGE_ID\" "
232                 + "id=\"123456789\" name=\"MY_CHANGE_ID\"><meta-data definedIn=\"libcore.util"
233                 + ".Compat\" sourcePosition=\"libcore/util/Compat.java:10\"/>"
234                 + "</compat-change></config>";
235         Compilation compilation =
236                 Compiler.javac()
237                         .withProcessors(new ChangeIdProcessor())
238                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
239         CompilationSubject.assertThat(compilation).succeeded();
240         CompilationSubject.assertThat(compilation).generatedFile(CLASS_OUTPUT, "libcore.util",
241                 "Compat_compat_config.xml").contentsAsString(UTF_8).isEqualTo(expectedFile);
242     }
243 
244     @Test
testCompatConfigXmlOutput_enum()245     public void testCompatConfigXmlOutput_enum() {
246         JavaFileObject[] source = {
247                 JavaFileObjects.forSourceLines(
248                         "libcore.util.Compat",
249                         "package libcore.util;",
250                         "import android.compat.annotation.ChangeId;",
251                         "import android.compat.annotation.EnabledAfter;",
252                         "import android.compat.annotation.Disabled;",
253                         "public enum Compat {",
254                         "    ENUM_CONSTANT;",
255                         "    /**",
256                         "     * description of",
257                         "     * MY_CHANGE_ID",
258                         "     */",
259                         "    @ChangeId",
260                         "    private static final long MY_CHANGE_ID = 123456789l;",
261                         "}"),
262         };
263         String expectedFile = HEADER + "<config>" +
264                 "<compat-change description=\"description of MY_CHANGE_ID\" "
265                 + "id=\"123456789\" name=\"MY_CHANGE_ID\"><meta-data definedIn=\"libcore.util"
266                 + ".Compat\" sourcePosition=\"libcore/util/Compat.java:11\"/>"
267                 + "</compat-change></config>";
268         Compilation compilation =
269                 Compiler.javac()
270                         .withProcessors(new ChangeIdProcessor())
271                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
272         CompilationSubject.assertThat(compilation).succeeded();
273         CompilationSubject.assertThat(compilation).generatedFile(CLASS_OUTPUT, "libcore.util",
274                 "Compat_compat_config.xml").contentsAsString(UTF_8).isEqualTo(expectedFile);
275     }
276 
277     @Test
testBothDisabledAndEnabledAfter()278     public void testBothDisabledAndEnabledAfter() {
279         JavaFileObject[] source = {
280                 JavaFileObjects.forSourceLines(
281                         "libcore.util.Compat",
282                         "package libcore.util;",
283                         "import android.compat.annotation.ChangeId;",
284                         "import android.compat.annotation.EnabledAfter;",
285                         "import android.compat.annotation.Disabled;",
286                         "public class Compat {",
287                         "    @EnabledAfter(targetSdkVersion=29)",
288                         "    @Disabled",
289                         "    @ChangeId",
290                         "    static final long MY_CHANGE_ID = 123456789l;",
291                         "}")
292         };
293         Compilation compilation =
294                 Compiler.javac()
295                         .withProcessors(new ChangeIdProcessor())
296                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
297         CompilationSubject.assertThat(compilation).hadErrorContaining(
298                 "ChangeId cannot be annotated with both @Disabled and @EnabledAfter.");
299     }
300 
301 
302     @Test
testBothLoggingOnlyAndEnabledAfter()303     public void testBothLoggingOnlyAndEnabledAfter() {
304         JavaFileObject[] source = {
305                 JavaFileObjects.forSourceLines(
306                         "libcore.util.Compat",
307                         "package libcore.util;",
308                         "import android.compat.annotation.ChangeId;",
309                         "import android.compat.annotation.EnabledAfter;",
310                         "import android.compat.annotation.LoggingOnly;",
311                         "public class Compat {",
312                         "    @EnabledAfter(targetSdkVersion=29)",
313                         "    @LoggingOnly",
314                         "    @ChangeId",
315                         "    static final long MY_CHANGE_ID = 123456789l;",
316                         "}")
317         };
318         Compilation compilation =
319                 Compiler.javac()
320                         .withProcessors(new ChangeIdProcessor())
321                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
322         CompilationSubject.assertThat(compilation).hadErrorContaining(
323                 "ChangeId cannot be annotated with both @LoggingOnly and "
324                         + "(@EnabledAfter | @Disabled).");
325     }
326 
327     @Test
testBothLoggingOnlyAndDisabled()328     public void testBothLoggingOnlyAndDisabled() {
329         JavaFileObject[] source = {
330                 JavaFileObjects.forSourceLines(
331                         "libcore.util.Compat",
332                         "package libcore.util;",
333                         "import android.compat.annotation.ChangeId;",
334                         "import android.compat.annotation.LoggingOnly;",
335                         "import android.compat.annotation.Disabled;",
336                         "public class Compat {",
337                         "    @LoggingOnly",
338                         "    @Disabled",
339                         "    @ChangeId",
340                         "    static final long MY_CHANGE_ID = 123456789l;",
341                         "}")
342         };
343         Compilation compilation =
344                 Compiler.javac()
345                         .withProcessors(new ChangeIdProcessor())
346                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
347         CompilationSubject.assertThat(compilation).hadErrorContaining(
348                 "ChangeId cannot be annotated with both @LoggingOnly and "
349                         + "(@EnabledAfter | @Disabled).");
350     }
351 
352     @Test
testLoggingOnly()353     public void testLoggingOnly() {
354         JavaFileObject[] source = {
355                 JavaFileObjects.forSourceLines(
356                         "libcore.util.Compat",
357                         "package libcore.util;",
358                         "import android.compat.annotation.ChangeId;",
359                         "import android.compat.annotation.LoggingOnly;",
360                         "public class Compat {",
361                         "    @LoggingOnly",
362                         "    @ChangeId",
363                         "    static final long MY_CHANGE_ID = 123456789l;",
364                         "}")
365         };
366         String expectedFile = HEADER + "<config>" +
367                 "<compat-change id=\"123456789\" loggingOnly=\"true\" name=\"MY_CHANGE_ID\">" +
368                 "<meta-data definedIn=\"libcore.util.Compat\" " +
369                 "sourcePosition=\"libcore/util/Compat.java:6\"/>" +
370                 "</compat-change></config>";
371         Compilation compilation =
372                 Compiler.javac()
373                         .withProcessors(new ChangeIdProcessor())
374                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
375         CompilationSubject.assertThat(compilation).succeeded();
376         CompilationSubject.assertThat(compilation).generatedFile(CLASS_OUTPUT, "libcore.util",
377                 "Compat_compat_config.xml").contentsAsString(UTF_8).isEqualTo(expectedFile);
378     }
379 
380     @Test
testIgnoredParams()381     public void testIgnoredParams() {
382         JavaFileObject[] source = {
383                 JavaFileObjects.forSourceLines(
384                         "android.compat.Compatibility",
385                         "package android.compat;",
386                         "import android.compat.annotation.ChangeId;",
387                         "public final class Compatibility {",
388                         "   public static void reportChange(@ChangeId long changeId) {}",
389                         "   public static boolean isChangeEnabled(@ChangeId long changeId) {",
390                         "       return true;",
391                         "   }",
392                         "}")
393         };
394         Compilation compilation =
395                 Compiler.javac()
396                         .withProcessors(new ChangeIdProcessor())
397                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
398         CompilationSubject.assertThat(compilation).succeeded();
399     }
400 
401     @Test
testOtherClassParams()402     public void testOtherClassParams() {
403         JavaFileObject[] source = {
404                 JavaFileObjects.forSourceLines(
405                         "android.compat.OtherClass",
406                         "package android.compat;",
407                         "import android.compat.annotation.ChangeId;",
408                         "public final class OtherClass {",
409                         "   public static void reportChange(@ChangeId long changeId) {}",
410                         "}")
411         };
412         Compilation compilation =
413                 Compiler.javac()
414                         .withProcessors(new ChangeIdProcessor())
415                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
416         CompilationSubject.assertThat(compilation).hadErrorContaining(
417                 "Non FIELD element annotated with @ChangeId.");
418     }
419 
420     @Test
testOtherMethodParams()421     public void testOtherMethodParams() {
422         JavaFileObject[] source = {
423                 JavaFileObjects.forSourceLines(
424                         "android.compat.Compatibility",
425                         "package android.compat;",
426                         "import android.compat.annotation.ChangeId;",
427                         "public final class Compatibility {",
428                         "   public static void otherMethod(@ChangeId long changeId) {}",
429                         "}")
430         };
431         Compilation compilation =
432                 Compiler.javac()
433                         .withProcessors(new ChangeIdProcessor())
434                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
435         CompilationSubject.assertThat(compilation).hadErrorContaining(
436                 "Non FIELD element annotated with @ChangeId.");
437     }
438 
439     @Test
testNonFinal()440     public void testNonFinal() {
441         JavaFileObject[] source = {
442                 JavaFileObjects.forSourceLines(
443                         "libcore.util.Compat",
444                         "package libcore.util;",
445                         "import android.compat.annotation.ChangeId;",
446                         "import android.compat.annotation.EnabledAfter;",
447                         "public class Compat {",
448                         "    @EnabledAfter(targetSdkVersion=29)",
449                         "    @ChangeId",
450                         "    static long MY_CHANGE_ID = 123456789l;",
451                         "}")
452         };
453         Compilation compilation =
454                 Compiler.javac()
455                         .withProcessors(new ChangeIdProcessor())
456                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
457         CompilationSubject.assertThat(compilation).hadErrorContaining(
458                 "Non constant/final variable annotated with @ChangeId.");
459     }
460 
461     @Test
testNonLong()462     public void testNonLong() {
463         JavaFileObject[] source = {
464                 JavaFileObjects.forSourceLines(
465                         "libcore.util.Compat",
466                         "package libcore.util;",
467                         "import android.compat.annotation.ChangeId;",
468                         "import android.compat.annotation.EnabledAfter;",
469                         "public class Compat {",
470                         "    @EnabledAfter(targetSdkVersion=29)",
471                         "    @ChangeId",
472                         "    static final int MY_CHANGE_ID = 12345;",
473                         "}")
474         };
475         Compilation compilation =
476                 Compiler.javac()
477                         .withProcessors(new ChangeIdProcessor())
478                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
479         CompilationSubject.assertThat(compilation).hadErrorContaining(
480                 "Variables annotated with @ChangeId must be of type long.");
481     }
482 
483     @Test
testNonStatic()484     public void testNonStatic() {
485         JavaFileObject[] source = {
486                 JavaFileObjects.forSourceLines(
487                         "libcore.util.Compat",
488                         "package libcore.util;",
489                         "import android.compat.annotation.ChangeId;",
490                         "import android.compat.annotation.EnabledAfter;",
491                         "public class Compat {",
492                         "    @EnabledAfter(targetSdkVersion=29)",
493                         "    @ChangeId",
494                         "    final long MY_CHANGE_ID = 123456789l;",
495                         "}")
496         };
497         Compilation compilation =
498                 Compiler.javac()
499                         .withProcessors(new ChangeIdProcessor())
500                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
501         CompilationSubject.assertThat(compilation).hadErrorContaining(
502                 "Non static variable annotated with @ChangeId.");
503     }
504 
505     @Test
testCompatConfigXmlOutput_hideTag()506     public void testCompatConfigXmlOutput_hideTag() {
507         JavaFileObject[] source = {
508                 JavaFileObjects.forSourceLines(
509                         "libcore.util.Compat",
510                         "package libcore.util;",
511                         "import android.compat.annotation.ChangeId;",
512                         "import android.compat.annotation.EnabledAfter;",
513                         "import android.compat.annotation.Disabled;",
514                         "public class Compat {",
515                         "    public class Inner {",
516                         "        /**",
517                         "        * description of MY_CHANGE_ID.",
518                         "        * @hide",
519                         "        */",
520                         "        @ChangeId",
521                         "        static final long MY_CHANGE_ID = 123456789l;",
522                         "    }",
523                         "}"),
524         };
525         String expectedFile = HEADER + "<config>" +
526                 "<compat-change description=\"description of MY_CHANGE_ID.\" "
527                 + "id=\"123456789\" name=\"MY_CHANGE_ID\"><meta-data definedIn=\"libcore.util"
528                 + ".Compat.Inner\" sourcePosition=\"libcore/util/Compat.java:11\"/>"
529                 + "</compat-change></config>";
530         Compilation compilation =
531                 Compiler.javac()
532                         .withProcessors(new ChangeIdProcessor())
533                         .compile(ObjectArrays.concat(mAnnotations, source, JavaFileObject.class));
534         CompilationSubject.assertThat(compilation).succeeded();
535         CompilationSubject.assertThat(compilation).generatedFile(CLASS_OUTPUT, "libcore.util",
536                 "Compat.Inner_compat_config.xml").contentsAsString(UTF_8).isEqualTo(expectedFile);
537     }
538 
539 }
540