blob: 25b6947bd8a15be5fb6df7974525eec46d3ac51e [file] [log] [blame]
/*
* Copyright (C) 2016 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 androidx.room.processor
import androidx.room.Entity
import androidx.room.parser.Collate
import androidx.room.parser.SQLTypeAffinity
import androidx.room.compiler.processing.XVariableElement
import androidx.room.solver.types.ColumnTypeAdapter
import androidx.room.testing.TestInvocation
import androidx.room.testing.TestProcessor
import androidx.room.vo.Field
import com.google.common.truth.Truth
import com.google.testing.compile.CompileTester
import com.google.testing.compile.JavaFileObjects
import com.google.testing.compile.JavaSourcesSubjectFactory
import com.squareup.javapoet.TypeName
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.nullValue
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mockito.mock
import simpleRun
@Suppress("HasPlatformType")
@RunWith(JUnit4::class)
class FieldProcessorTest {
companion object {
const val ENTITY_PREFIX = """
package foo.bar;
import androidx.room.*;
import androidx.annotation.NonNull;
@Entity
abstract class MyEntity {
"""
const val ENTITY_SUFFIX = "}"
val ALL_PRIMITIVES = arrayListOf(
TypeName.INT,
TypeName.BYTE,
TypeName.SHORT,
TypeName.LONG,
TypeName.CHAR,
TypeName.FLOAT,
TypeName.DOUBLE
)
val ARRAY_CONVERTER = JavaFileObjects.forSourceLines("foo.bar.MyConverter",
"""
package foo.bar;
import androidx.room.*;
public class MyConverter {
${ALL_PRIMITIVES.joinToString("\n") {
val arrayDef = "$it[]"
"@TypeConverter public static String" +
" arrayIntoString($arrayDef input) { return null;}" +
"@TypeConverter public static $arrayDef" +
" stringIntoArray$it(String input) { return null;}"
}}
${ALL_PRIMITIVES.joinToString("\n") {
val arrayDef = "${it.box()}[]"
"@TypeConverter public static String" +
" arrayIntoString($arrayDef input) { return null;}" +
"@TypeConverter public static $arrayDef" +
" stringIntoArray${it}Boxed(String input) { return null;}"
}}
}
""")
private fun TypeName.affinity(): SQLTypeAffinity {
return when (this) {
TypeName.FLOAT, TypeName.DOUBLE -> SQLTypeAffinity.REAL
else -> SQLTypeAffinity.INTEGER
}
}
private fun TypeName.box(invocation: TestInvocation) =
typeMirror(invocation).boxed()
private fun TypeName.typeMirror(invocation: TestInvocation) =
invocation.processingEnv.requireType(this)
}
@Test
fun primitives() {
ALL_PRIMITIVES.forEach { primitive ->
singleEntity("$primitive x;") { field, invocation ->
assertThat(field, `is`(
Field(name = "x",
type = primitive.typeMirror(invocation),
element = field.element,
affinity = primitive.affinity()
)))
}.compilesWithoutError()
}
}
@Test
fun boxed() {
ALL_PRIMITIVES.forEach { primitive ->
singleEntity("${primitive.box()} y;") { field, invocation ->
assertThat(field, `is`(
Field(name = "y",
type = primitive.box(invocation),
element = field.element,
affinity = primitive.affinity())))
}.compilesWithoutError()
}
}
@Test
fun columnName() {
singleEntity("""
@ColumnInfo(name = "foo")
@PrimaryKey
int x;
""") { field, invocation ->
assertThat(field, `is`(
Field(name = "x",
type = TypeName.INT.typeMirror(invocation),
element = field.element,
columnName = "foo",
affinity = SQLTypeAffinity.INTEGER)))
}.compilesWithoutError()
}
@Test
fun indexed() {
singleEntity("""
@ColumnInfo(name = "foo", index = true)
int x;
""") { field, invocation ->
assertThat(field, `is`(
Field(name = "x",
type = TypeName.INT.typeMirror(invocation),
element = field.element,
columnName = "foo",
affinity = SQLTypeAffinity.INTEGER,
indexed = true)))
}.compilesWithoutError()
}
@Test
fun emptyColumnName() {
singleEntity("""
@ColumnInfo(name = "")
int x;
""") { _, _ ->
}.failsToCompile().withErrorContaining(ProcessorErrors.COLUMN_NAME_CANNOT_BE_EMPTY)
}
@Test
fun byteArrayWithEnforcedType() {
singleEntity("@TypeConverters(foo.bar.MyConverter.class)" +
"@ColumnInfo(typeAffinity = ColumnInfo.TEXT) byte[] arr;") { field, invocation ->
assertThat(field, `is`(Field(name = "arr",
type = invocation.processingEnv.getArrayType(TypeName.BYTE),
element = field.element,
affinity = SQLTypeAffinity.TEXT)))
assertThat((field.cursorValueReader as? ColumnTypeAdapter)?.typeAffinity,
`is`(SQLTypeAffinity.TEXT))
}.compilesWithoutError()
}
@Test
fun primitiveArray() {
ALL_PRIMITIVES.forEach { primitive ->
singleEntity("@TypeConverters(foo.bar.MyConverter.class) " +
"${primitive.toString().toLowerCase()}[] arr;") { field, invocation ->
assertThat(field, `is`(
Field(name = "arr",
type = invocation.processingEnv.getArrayType(primitive),
element = field.element,
affinity = if (primitive == TypeName.BYTE) {
SQLTypeAffinity.BLOB
} else {
SQLTypeAffinity.TEXT
})))
}.compilesWithoutError()
}
}
@Test
fun boxedArray() {
ALL_PRIMITIVES.forEach { primitive ->
singleEntity("@TypeConverters(foo.bar.MyConverter.class) " +
"${primitive.box()}[] arr;") { field, invocation ->
assertThat(field, `is`(
Field(name = "arr",
type = invocation.processingEnv.getArrayType(
primitive.box()),
element = field.element,
affinity = SQLTypeAffinity.TEXT)))
}.compilesWithoutError()
}
}
@Test
fun generic() {
singleEntity("""
static class BaseClass<T> {
T item;
}
@Entity
static class Extending extends BaseClass<java.lang.Integer> {
}
""") { field, invocation ->
assertThat(field, `is`(Field(name = "item",
type = TypeName.INT.box(invocation),
element = field.element,
affinity = SQLTypeAffinity.INTEGER)))
}.compilesWithoutError()
}
@Test
fun unboundGeneric() {
singleEntity("""
@Entity
static class BaseClass<T> {
T item;
}
""") { _, _ -> }.failsToCompile()
.withErrorContaining(ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_ENTITY_FIELDS)
}
@Test
fun nameVariations() {
simpleRun {
val variableElement = mock(XVariableElement::class.java)
assertThat(Field(variableElement, "x", TypeName.INT.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("x")))
assertThat(Field(variableElement, "x", TypeName.BOOLEAN.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("x")))
assertThat(Field(variableElement, "xAll",
TypeName.BOOLEAN.typeMirror(it), SQLTypeAffinity.INTEGER)
.nameWithVariations, `is`(arrayListOf("xAll")))
}
}
@Test
fun nameVariations_is() {
val elm = mock(XVariableElement::class.java)
simpleRun {
assertThat(Field(elm, "isX", TypeName.BOOLEAN.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("isX", "x")))
assertThat(Field(elm, "isX", TypeName.INT.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("isX")))
assertThat(Field(elm, "is", TypeName.BOOLEAN.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("is")))
assertThat(Field(elm, "isAllItems", TypeName.BOOLEAN.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations,
`is`(arrayListOf("isAllItems", "allItems")))
}
}
@Test
fun nameVariations_has() {
val elm = mock(XVariableElement::class.java)
simpleRun {
assertThat(Field(elm, "hasX", TypeName.BOOLEAN.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("hasX", "x")))
assertThat(Field(elm, "hasX", TypeName.INT.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("hasX")))
assertThat(Field(elm, "has", TypeName.BOOLEAN.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("has")))
assertThat(Field(elm, "hasAllItems", TypeName.BOOLEAN.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations,
`is`(arrayListOf("hasAllItems", "allItems")))
}
}
@Test
fun nameVariations_m() {
val elm = mock(XVariableElement::class.java)
simpleRun {
assertThat(Field(elm, "mall", TypeName.BOOLEAN.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("mall")))
assertThat(Field(elm, "mallVars", TypeName.BOOLEAN.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("mallVars")))
assertThat(Field(elm, "mAll", TypeName.BOOLEAN.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("mAll", "all")))
assertThat(Field(elm, "m", TypeName.INT.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("m")))
assertThat(Field(elm, "mallItems", TypeName.BOOLEAN.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations,
`is`(arrayListOf("mallItems")))
assertThat(Field(elm, "mAllItems", TypeName.BOOLEAN.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations,
`is`(arrayListOf("mAllItems", "allItems")))
}
}
@Test
fun nameVariations_underscore() {
val elm = mock(XVariableElement::class.java)
simpleRun {
assertThat(Field(elm, "_all", TypeName.BOOLEAN.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("_all", "all")))
assertThat(Field(elm, "_", TypeName.INT.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations, `is`(arrayListOf("_")))
assertThat(Field(elm, "_allItems", TypeName.BOOLEAN.typeMirror(it),
SQLTypeAffinity.INTEGER).nameWithVariations,
`is`(arrayListOf("_allItems", "allItems")))
}
}
@Test
fun collate() {
Collate.values().forEach { collate ->
singleEntity("""
@PrimaryKey
@ColumnInfo(collate = ColumnInfo.${collate.name})
String code;
""") { field, invocation ->
assertThat(field, `is`(
Field(name = "code",
type = invocation.context.COMMON_TYPES.STRING,
element = field.element,
columnName = "code",
collate = collate,
affinity = SQLTypeAffinity.TEXT)))
}.compilesWithoutError()
}
}
@Test
fun defaultValues_number() {
testDefaultValue("\"1\"", "int") { defaultValue ->
assertThat(defaultValue, `is`(equalTo("1")))
}.compilesWithoutError()
testDefaultValue("\"\"", "int") { defaultValue ->
assertThat(defaultValue, `is`(nullValue()))
}.compilesWithoutError()
testDefaultValue("\"null\"", "Integer") { defaultValue ->
assertThat(defaultValue, `is`(equalTo("null")))
}.compilesWithoutError()
testDefaultValue("ColumnInfo.VALUE_UNSPECIFIED", "int") { defaultValue ->
assertThat(defaultValue, `is`(nullValue()))
}.compilesWithoutError()
testDefaultValue("\"CURRENT_TIMESTAMP\"", "long") { defaultValue ->
assertThat(defaultValue, `is`(equalTo("CURRENT_TIMESTAMP")))
}.compilesWithoutError()
testDefaultValue("\"true\"", "boolean") { defaultValue ->
assertThat(defaultValue, `is`(equalTo("true")))
}.compilesWithoutError()
testDefaultValue("\"false\"", "boolean") { defaultValue ->
assertThat(defaultValue, `is`(equalTo("false")))
}.compilesWithoutError()
}
@Test
fun defaultValues_nonNull() {
testDefaultValue("\"null\"", "int") {
}.failsToCompile().withErrorContaining(ProcessorErrors.DEFAULT_VALUE_NULLABILITY)
testDefaultValue("\"null\"", "@NonNull String") {
}.failsToCompile().withErrorContaining(ProcessorErrors.DEFAULT_VALUE_NULLABILITY)
}
@Test
fun defaultValues_text() {
testDefaultValue("\"a\"", "String") { defaultValue ->
assertThat(defaultValue, `is`(equalTo("'a'")))
}.compilesWithoutError()
testDefaultValue("\"'a'\"", "String") { defaultValue ->
assertThat(defaultValue, `is`(equalTo("'a'")))
}.compilesWithoutError()
testDefaultValue("\"\"", "String") { defaultValue ->
assertThat(defaultValue, `is`(equalTo("''")))
}.compilesWithoutError()
testDefaultValue("\"null\"", "String") { defaultValue ->
assertThat(defaultValue, `is`(equalTo("null")))
}.compilesWithoutError()
testDefaultValue("ColumnInfo.VALUE_UNSPECIFIED", "String") { defaultValue ->
assertThat(defaultValue, `is`(nullValue()))
}.compilesWithoutError()
testDefaultValue("\"CURRENT_TIMESTAMP\"", "String") { defaultValue ->
assertThat(defaultValue, `is`(equalTo("CURRENT_TIMESTAMP")))
}.compilesWithoutError()
testDefaultValue("\"('Created at ' || CURRENT_TIMESTAMP)\"", "String") { defaultValue ->
assertThat(defaultValue, `is`(equalTo("('Created at ' || CURRENT_TIMESTAMP)")))
}.compilesWithoutError()
}
private fun testDefaultValue(
defaultValue: String,
fieldType: String,
body: (String?) -> Unit
): CompileTester {
return singleEntity(
"""
@ColumnInfo(defaultValue = $defaultValue)
$fieldType name;
"""
) { field, _ ->
body(field.defaultValue)
}
}
fun singleEntity(vararg input: String, handler: (Field, invocation: TestInvocation) -> Unit):
CompileTester {
return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
.that(listOf(JavaFileObjects.forSourceString("foo.bar.MyEntity",
ENTITY_PREFIX + input.joinToString("\n") + ENTITY_SUFFIX
), ARRAY_CONVERTER))
.processedWith(TestProcessor.builder()
.forAnnotations(androidx.room.Entity::class)
.nextRunHandler { invocation ->
val (owner, fieldElement) = invocation.roundEnv
.getElementsAnnotatedWith(Entity::class.java)
.map {
Pair(it, it.asTypeElement()
.getAllFieldsIncludingPrivateSupers().firstOrNull())
}
.first { it.second != null }
val entityContext =
TableEntityProcessor(
baseContext = invocation.context,
element = owner.asTypeElement()
).context
val parser = FieldProcessor(
baseContext = entityContext,
containing = owner.asDeclaredType(),
element = fieldElement!!.asVariableElement(),
bindingScope = FieldProcessor.BindingScope.TWO_WAY,
fieldParent = null,
onBindingError = { field, errorMsg ->
invocation.context.logger.e(field.element, errorMsg) }
)
handler(parser.process(), invocation)
true
}
.build())
}
}