1 /*
<lambda>null2  * Copyright (C) 2016 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 androidx.room.processor
18 
19 import androidx.room.Entity
20 import androidx.room.parser.Collate
21 import androidx.room.parser.SQLTypeAffinity
22 import androidx.room.solver.types.ColumnTypeAdapter
23 import androidx.room.testing.TestInvocation
24 import androidx.room.testing.TestProcessor
25 import androidx.room.vo.Field
26 import com.google.auto.common.MoreElements
27 import com.google.auto.common.MoreTypes
28 import com.google.common.truth.Truth
29 import com.google.testing.compile.CompileTester
30 import com.google.testing.compile.JavaFileObjects
31 import com.google.testing.compile.JavaSourcesSubjectFactory
32 import org.hamcrest.CoreMatchers.`is`
33 import org.hamcrest.MatcherAssert.assertThat
34 import org.junit.Test
35 import org.junit.runner.RunWith
36 import org.junit.runners.JUnit4
37 import org.mockito.Mockito.mock
38 import simpleRun
39 import javax.lang.model.element.Element
40 import javax.lang.model.element.ElementKind
41 import javax.lang.model.type.TypeKind
42 import javax.lang.model.type.TypeMirror
43 
44 @Suppress("HasPlatformType")
45 @RunWith(JUnit4::class)
46 class FieldProcessorTest {
47     companion object {
48         const val ENTITY_PREFIX = """
49                 package foo.bar;
50                 import androidx.room.*;
51                 @Entity
52                 abstract class MyEntity {
53                 """
54         const val ENTITY_SUFFIX = "}"
55         val ALL_PRIMITIVES = arrayListOf(
56                 TypeKind.INT,
57                 TypeKind.BYTE,
58                 TypeKind.SHORT,
59                 TypeKind.LONG,
60                 TypeKind.CHAR,
61                 TypeKind.FLOAT,
62                 TypeKind.DOUBLE)
63         val ARRAY_CONVERTER = JavaFileObjects.forSourceLines("foo.bar.MyConverter",
64                 """
65                 package foo.bar;
66                 import androidx.room.*;
67                 public class MyConverter {
68                 ${ALL_PRIMITIVES.joinToString("\n") {
69                     val arrayDef = "${it.name.toLowerCase()}[]"
70                     "@TypeConverter public static String" +
71                             " arrayIntoString($arrayDef input) { return null;}" +
72                             "@TypeConverter public static $arrayDef" +
73                             " stringIntoArray${it.name}(String input) { return null;}"
74                 }}
75                 ${ALL_PRIMITIVES.joinToString("\n") {
76                     val arrayDef = "${it.box()}[]"
77                     "@TypeConverter public static String" +
78                             " arrayIntoString($arrayDef input) { return null;}" +
79                             "@TypeConverter public static $arrayDef" +
80                             " stringIntoArray${it.name}Boxed(String input) { return null;}"
81                 }}
82                 }
83                 """)
84 
85         private fun TypeKind.box(): String {
86             return "java.lang." + when (this) {
87                 TypeKind.INT -> "Integer"
88                 TypeKind.CHAR -> "Character"
89                 else -> this.name.toLowerCase().capitalize()
90             }
91         }
92 
93         // these 2 box methods are ugly but makes tests nicer and they are private
94         private fun TypeKind.typeMirror(invocation: TestInvocation): TypeMirror {
95             return invocation.processingEnv.typeUtils.getPrimitiveType(this)
96         }
97 
98         private fun TypeKind.affinity(): SQLTypeAffinity {
99             return when (this) {
100                 TypeKind.FLOAT, TypeKind.DOUBLE -> SQLTypeAffinity.REAL
101                 else -> SQLTypeAffinity.INTEGER
102             }
103         }
104 
105         private fun TypeKind.box(invocation: TestInvocation): TypeMirror {
106             return invocation.processingEnv.elementUtils.getTypeElement(box()).asType()
107         }
108     }
109 
110     @Test
111     fun primitives() {
112         ALL_PRIMITIVES.forEach { primitive ->
113             singleEntity("${primitive.name.toLowerCase()} x;") { field, invocation ->
114                 assertThat(field, `is`(
115                         Field(name = "x",
116                                 type = primitive.typeMirror(invocation),
117                                 element = field.element,
118                                 affinity = primitive.affinity()
119                         )))
120             }.compilesWithoutError()
121         }
122     }
123 
124     @Test
125     fun boxed() {
126         ALL_PRIMITIVES.forEach { primitive ->
127             singleEntity("${primitive.box()} y;") { field, invocation ->
128                 assertThat(field, `is`(
129                         Field(name = "y",
130                                 type = primitive.box(invocation),
131                                 element = field.element,
132                                 affinity = primitive.affinity())))
133             }.compilesWithoutError()
134         }
135     }
136 
137     @Test
138     fun columnName() {
139         singleEntity("""
140             @ColumnInfo(name = "foo")
141             @PrimaryKey
142             int x;
143             """) { field, invocation ->
144             assertThat(field, `is`(
145                     Field(name = "x",
146                             type = TypeKind.INT.typeMirror(invocation),
147                             element = field.element,
148                             columnName = "foo",
149                             affinity = SQLTypeAffinity.INTEGER)))
150         }.compilesWithoutError()
151     }
152 
153     @Test
154     fun indexed() {
155         singleEntity("""
156             @ColumnInfo(name = "foo", index = true)
157             int x;
158             """) { field, invocation ->
159             assertThat(field, `is`(
160                     Field(name = "x",
161                             type = TypeKind.INT.typeMirror(invocation),
162                             element = field.element,
163                             columnName = "foo",
164                             affinity = SQLTypeAffinity.INTEGER,
165                             indexed = true)))
166         }.compilesWithoutError()
167     }
168 
169     @Test
170     fun emptyColumnName() {
171         singleEntity("""
172             @ColumnInfo(name = "")
173             int x;
174             """) { _, _ ->
175         }.failsToCompile().withErrorContaining(ProcessorErrors.COLUMN_NAME_CANNOT_BE_EMPTY)
176     }
177 
178     @Test
179     fun byteArrayWithEnforcedType() {
180         singleEntity("@TypeConverters(foo.bar.MyConverter.class)" +
181                 "@ColumnInfo(typeAffinity = ColumnInfo.TEXT) byte[] arr;") { field, invocation ->
182             assertThat(field, `is`(Field(name = "arr",
183                     type = invocation.processingEnv.typeUtils.getArrayType(
184                             TypeKind.BYTE.typeMirror(invocation)),
185                     element = field.element,
186                     affinity = SQLTypeAffinity.TEXT)))
187             assertThat((field.cursorValueReader as? ColumnTypeAdapter)?.typeAffinity,
188                     `is`(SQLTypeAffinity.TEXT))
189         }.compilesWithoutError()
190     }
191 
192     @Test
193     fun primitiveArray() {
194         ALL_PRIMITIVES.forEach { primitive ->
195             singleEntity("@TypeConverters(foo.bar.MyConverter.class) " +
196                     "${primitive.toString().toLowerCase()}[] arr;") { field, invocation ->
197                 assertThat(field, `is`(
198                         Field(name = "arr",
199                                 type = invocation.processingEnv.typeUtils.getArrayType(
200                                         primitive.typeMirror(invocation)),
201                                 element = field.element,
202                                 affinity = if (primitive == TypeKind.BYTE) {
203                                     SQLTypeAffinity.BLOB
204                                 } else {
205                                     SQLTypeAffinity.TEXT
206                                 })))
207             }.compilesWithoutError()
208         }
209     }
210 
211     @Test
212     fun boxedArray() {
213         ALL_PRIMITIVES.forEach { primitive ->
214             singleEntity("@TypeConverters(foo.bar.MyConverter.class) " +
215                     "${primitive.box()}[] arr;") { field, invocation ->
216                 assertThat(field, `is`(
217                         Field(name = "arr",
218                                 type = invocation.processingEnv.typeUtils.getArrayType(
219                                         primitive.box(invocation)),
220                                 element = field.element,
221                                 affinity = SQLTypeAffinity.TEXT)))
222             }.compilesWithoutError()
223         }
224     }
225 
226     @Test
227     fun generic() {
228         singleEntity("""
229                 static class BaseClass<T> {
230                     T item;
231                 }
232                 @Entity
233                 static class Extending extends BaseClass<java.lang.Integer> {
234                 }
235                 """) { field, invocation ->
236             assertThat(field, `is`(Field(name = "item",
237                     type = TypeKind.INT.box(invocation),
238                     element = field.element,
239                     affinity = SQLTypeAffinity.INTEGER)))
240         }.compilesWithoutError()
241     }
242 
243     @Test
244     fun unboundGeneric() {
245         singleEntity("""
246                 @Entity
247                 static class BaseClass<T> {
248                     T item;
249                 }
250                 """) { _, _ -> }.failsToCompile()
251                 .withErrorContaining(ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_ENTITY_FIELDS)
252     }
253 
254     @Test
255     fun nameVariations() {
256         simpleRun {
257             assertThat(Field(mock(Element::class.java), "x", TypeKind.INT.typeMirror(it),
258                     SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("x")))
259             assertThat(Field(mock(Element::class.java), "x", TypeKind.BOOLEAN.typeMirror(it),
260                     SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("x")))
261             assertThat(Field(mock(Element::class.java), "xAll",
262                     TypeKind.BOOLEAN.typeMirror(it), SQLTypeAffinity.INTEGER)
263                     .nameWithVariations, `is`(arrayListOf("xAll")))
264         }
265     }
266 
267     @Test
268     fun nameVariations_is() {
269         val elm = mock(Element::class.java)
270         simpleRun {
271             assertThat(Field(elm, "isX", TypeKind.BOOLEAN.typeMirror(it),
272                     SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("isX", "x")))
273             assertThat(Field(elm, "isX", TypeKind.INT.typeMirror(it),
274                     SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("isX")))
275             assertThat(Field(elm, "is", TypeKind.BOOLEAN.typeMirror(it),
276                     SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("is")))
277             assertThat(Field(elm, "isAllItems", TypeKind.BOOLEAN.typeMirror(it),
278                     SQLTypeAffinity.INTEGER).nameWithVariations,
279                     `is`(arrayListOf("isAllItems", "allItems")))
280         }
281     }
282 
283     @Test
284     fun nameVariations_has() {
285         val elm = mock(Element::class.java)
286         simpleRun {
287             assertThat(Field(elm, "hasX", TypeKind.BOOLEAN.typeMirror(it),
288                     SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("hasX", "x")))
289             assertThat(Field(elm, "hasX", TypeKind.INT.typeMirror(it),
290                     SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("hasX")))
291             assertThat(Field(elm, "has", TypeKind.BOOLEAN.typeMirror(it),
292                     SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("has")))
293             assertThat(Field(elm, "hasAllItems", TypeKind.BOOLEAN.typeMirror(it),
294                     SQLTypeAffinity.INTEGER).nameWithVariations,
295                     `is`(arrayListOf("hasAllItems", "allItems")))
296         }
297     }
298 
299     @Test
300     fun nameVariations_m() {
301         val elm = mock(Element::class.java)
302         simpleRun {
303             assertThat(Field(elm, "mall", TypeKind.BOOLEAN.typeMirror(it),
304                     SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("mall")))
305             assertThat(Field(elm, "mallVars", TypeKind.BOOLEAN.typeMirror(it),
306                     SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("mallVars")))
307             assertThat(Field(elm, "mAll", TypeKind.BOOLEAN.typeMirror(it),
308                     SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("mAll", "all")))
309             assertThat(Field(elm, "m", TypeKind.INT.typeMirror(it),
310                     SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("m")))
311             assertThat(Field(elm, "mallItems", TypeKind.BOOLEAN.typeMirror(it),
312                     SQLTypeAffinity.INTEGER).nameWithVariations,
313                     `is`(arrayListOf("mallItems")))
314             assertThat(Field(elm, "mAllItems", TypeKind.BOOLEAN.typeMirror(it),
315                     SQLTypeAffinity.INTEGER).nameWithVariations,
316                     `is`(arrayListOf("mAllItems", "allItems")))
317         }
318     }
319 
320     @Test
321     fun nameVariations_underscore() {
322         val elm = mock(Element::class.java)
323         simpleRun {
324             assertThat(Field(elm, "_all", TypeKind.BOOLEAN.typeMirror(it),
325                     SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("_all", "all")))
326             assertThat(Field(elm, "_", TypeKind.INT.typeMirror(it),
327                     SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("_")))
328             assertThat(Field(elm, "_allItems", TypeKind.BOOLEAN.typeMirror(it),
329                     SQLTypeAffinity.INTEGER).nameWithVariations,
330                     `is`(arrayListOf("_allItems", "allItems")))
331         }
332     }
333 
334     @Test
335     fun collate() {
336         Collate.values().forEach { collate ->
337             singleEntity("""
338             @PrimaryKey
339             @ColumnInfo(collate = ColumnInfo.${collate.name})
340             String code;
341             """) { field, invocation ->
342                 assertThat(field, `is`(
343                         Field(name = "code",
344                                 type = invocation.context.COMMON_TYPES.STRING,
345                                 element = field.element,
346                                 columnName = "code",
347                                 collate = collate,
348                                 affinity = SQLTypeAffinity.TEXT)))
349             }.compilesWithoutError()
350         }
351     }
352 
353     fun singleEntity(vararg input: String, handler: (Field, invocation: TestInvocation) -> Unit):
354             CompileTester {
355         return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
356                 .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyEntity",
357                         ENTITY_PREFIX + input.joinToString("\n") + ENTITY_SUFFIX
358                 ), ARRAY_CONVERTER))
359                 .processedWith(TestProcessor.builder()
360                         .forAnnotations(androidx.room.Entity::class)
361                         .nextRunHandler { invocation ->
362                             val (owner, field) = invocation.roundEnv
363                                     .getElementsAnnotatedWith(Entity::class.java)
364                                     .map {
365                                         Pair(it, invocation.processingEnv.elementUtils
366                                                 .getAllMembers(MoreElements.asType(it))
367                                                 .firstOrNull { it.kind == ElementKind.FIELD })
368                                     }
369                                     .first { it.second != null }
370                             val entityContext =
371                                     EntityProcessor(invocation.context, MoreElements.asType(owner))
372                                             .context
373                             val parser = FieldProcessor(
374                                     baseContext = entityContext,
375                                     containing = MoreTypes.asDeclared(owner.asType()),
376                                     element = field!!,
377                                     bindingScope = FieldProcessor.BindingScope.TWO_WAY,
378                                     fieldParent = null)
379                             handler(parser.process(), invocation)
380                             true
381                         }
382                         .build())
383     }
384 }
385