[go: nahoru, domu]

blob: a4a393c3ad15f8df112dfb082ca37ed4f0e4a6bb [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.compiler.processing.XFieldElement
import androidx.room.compiler.processing.XTypeElement
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.XTestInvocation
import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.parser.Collate
import androidx.room.parser.SQLTypeAffinity
import androidx.room.solver.types.ColumnTypeAdapter
import androidx.room.testing.context
import androidx.room.vo.Field
import com.squareup.javapoet.ArrayTypeName
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 java.util.Locale
@Suppress("HasPlatformType")
@RunWith(JUnit4::class)
class FieldProcessorTest {
companion object {
const val ENTITY_PREFIX = """
package foo.bar;
import androidx.room.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@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 = Source.java(
"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: XTestInvocation) =
typeMirror(invocation).boxed()
private fun TypeName.typeMirror(invocation: XTestInvocation) =
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()
)
)
)
}
}
}
@Test
fun boxed() {
ALL_PRIMITIVES.forEach { primitive ->
singleEntity("@Nullable ${primitive.box()} y;") { field, invocation ->
assertThat(
field,
`is`(
Field(
name = "y",
type = primitive.box(invocation).makeNullable(),
element = field.element,
affinity = primitive.affinity()
)
)
)
}
}
}
@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
)
)
)
}
}
@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
)
)
)
}
}
@Test
fun emptyColumnName() {
singleEntity(
"""
@ColumnInfo(name = "")
int x;
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.COLUMN_NAME_CANNOT_BE_EMPTY
)
}
}
}
@Test
fun byteArrayWithEnforcedType() {
singleEntity(
"@TypeConverters(foo.bar.MyConverter.class)" +
"@ColumnInfo(typeAffinity = ColumnInfo.TEXT) @NonNull byte[] arr;"
) { field, invocation ->
assertThat(
field,
`is`(
Field(
name = "arr",
type = invocation.processingEnv.getArrayType(TypeName.BYTE)
.makeNonNullable(),
element = field.element,
affinity = SQLTypeAffinity.TEXT
)
)
)
assertThat(
(field.cursorValueReader as? ColumnTypeAdapter)?.typeAffinity,
`is`(SQLTypeAffinity.TEXT)
)
}
}
@Test
fun primitiveArray() {
ALL_PRIMITIVES.forEach { primitive ->
singleEntity(
"@TypeConverters(foo.bar.MyConverter.class) @NonNull " +
"${primitive.toString().lowercase(Locale.US)}[] arr;"
) { field, invocation ->
assertThat(
field,
`is`(
Field(
name = "arr",
type = invocation.processingEnv.getArrayType(primitive)
.makeNonNullable(),
element = field.element,
affinity = if (primitive == TypeName.BYTE) {
SQLTypeAffinity.BLOB
} else {
SQLTypeAffinity.TEXT
}
)
)
)
}
}
}
@Test
fun boxedArray() {
ALL_PRIMITIVES.forEach { primitive ->
singleEntity(
"@TypeConverters(foo.bar.MyConverter.class) " +
"${primitive.box()}[] arr;"
) { field, invocation ->
val expected = Field(
name = "arr",
type = invocation.processingEnv.getArrayType(
primitive.box()
),
element = field.element,
affinity = SQLTypeAffinity.TEXT,
nonNull = false // no annotation
)
// When source is parsed, it will have a flexible type in KSP and we have no way of
// obtaining a flexible type of:
// (Array<(kotlin.Int..kotlin.Int?)>..Array<out (kotlin.Int..kotlin.Int?)>?)
// as a workaround in test,
assertThat(
field,
`is`(
expected.copy(
// don't compare type
type = field.type
)
)
)
assertThat(
field.type.typeName,
`is`(ArrayTypeName.of(primitive.box()))
)
}
}
}
@Test
fun generic() {
singleEntity(
"""
static class BaseClass<T> {
@NonNull
T item;
}
@Entity
static class Extending extends BaseClass<java.lang.Integer> {
}
"""
) { field, invocation ->
assertThat(
field,
`is`(
Field(
name = "item",
type = TypeName.INT.box(invocation).makeNonNullable(),
element = field.element,
affinity = SQLTypeAffinity.INTEGER
)
)
)
}
}
@Test
fun unboundGeneric() {
singleEntity(
"""
@Entity
static class BaseClass<T> {
T item;
}
"""
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
// unbounded generics do not exist in KSP so the error is different
if (invocation.isKsp) {
ProcessorErrors.CANNOT_FIND_COLUMN_TYPE_ADAPTER
} else {
ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_ENTITY_FIELDS
}
)
}
}
}
@Test
fun nameVariations() {
runProcessorTest {
val fieldElement = mock(XFieldElement::class.java)
assertThat(
Field(
fieldElement, "x", TypeName.INT.typeMirror(it),
SQLTypeAffinity.INTEGER
).nameWithVariations,
`is`(arrayListOf("x"))
)
assertThat(
Field(
fieldElement, "x", TypeName.BOOLEAN.typeMirror(it),
SQLTypeAffinity.INTEGER
).nameWithVariations,
`is`(arrayListOf("x"))
)
assertThat(
Field(
fieldElement, "xAll",
TypeName.BOOLEAN.typeMirror(it), SQLTypeAffinity.INTEGER
)
.nameWithVariations,
`is`(arrayListOf("xAll"))
)
}
}
@Test
fun nameVariations_is() {
val elm = mock(XFieldElement::class.java)
runProcessorTest {
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(XFieldElement::class.java)
runProcessorTest {
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(XFieldElement::class.java)
runProcessorTest {
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(XFieldElement::class.java)
runProcessorTest {
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})
@Nullable
String code;
"""
) { field, invocation ->
assertThat(
field,
`is`(
Field(
name = "code",
type = invocation.context.COMMON_TYPES.STRING.makeNullable(),
element = field.element,
columnName = "code",
collate = collate,
affinity = SQLTypeAffinity.TEXT
)
)
)
}
}
}
@Test
fun defaultValues_number() {
testDefaultValue("\"1\"", "int") { defaultValue, _ ->
assertThat(defaultValue, `is`(equalTo("1")))
}
testDefaultValue("\"\"", "int") { defaultValue, _ ->
assertThat(defaultValue, `is`(nullValue()))
}
testDefaultValue("\"null\"", "Integer") { defaultValue, _ ->
assertThat(defaultValue, `is`(equalTo("null")))
}
testDefaultValue("ColumnInfo.VALUE_UNSPECIFIED", "int") { defaultValue, _ ->
assertThat(defaultValue, `is`(nullValue()))
}
testDefaultValue("\"CURRENT_TIMESTAMP\"", "long") { defaultValue, _ ->
assertThat(defaultValue, `is`(equalTo("CURRENT_TIMESTAMP")))
}
testDefaultValue("\"true\"", "boolean") { defaultValue, _ ->
assertThat(defaultValue, `is`(equalTo("true")))
}
testDefaultValue("\"false\"", "boolean") { defaultValue, _ ->
assertThat(defaultValue, `is`(equalTo("false")))
}
}
@Test
fun defaultValues_nonNull() {
testDefaultValue("\"null\"", "int") { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.DEFAULT_VALUE_NULLABILITY
)
}
}
testDefaultValue("\"null\"", "@NonNull String") { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(ProcessorErrors.DEFAULT_VALUE_NULLABILITY)
}
}
}
@Test
fun defaultValues_text() {
testDefaultValue("\"a\"", "String") { defaultValue, _ ->
assertThat(defaultValue, `is`(equalTo("'a'")))
}
testDefaultValue("\"'a'\"", "String") { defaultValue, _ ->
assertThat(defaultValue, `is`(equalTo("'a'")))
}
testDefaultValue("\"\"", "String") { defaultValue, _ ->
assertThat(defaultValue, `is`(equalTo("''")))
}
testDefaultValue("\"null\"", "String") { defaultValue, _ ->
assertThat(defaultValue, `is`(equalTo("null")))
}
testDefaultValue("ColumnInfo.VALUE_UNSPECIFIED", "String") { defaultValue, _ ->
assertThat(defaultValue, `is`(nullValue()))
}
testDefaultValue("\"CURRENT_TIMESTAMP\"", "String") { defaultValue, _ ->
assertThat(defaultValue, `is`(equalTo("CURRENT_TIMESTAMP")))
}
testDefaultValue(
defaultValue = "\"('Created at ' || CURRENT_TIMESTAMP)\"", "String"
) { defaultValue, _ ->
assertThat(defaultValue, `is`(equalTo("('Created at ' || CURRENT_TIMESTAMP)")))
}
}
private fun testDefaultValue(
defaultValue: String,
fieldType: String,
body: (String?, XTestInvocation) -> Unit
) {
singleEntity(
"""
@ColumnInfo(defaultValue = $defaultValue)
$fieldType name;
"""
) { field, invocation ->
body(field.defaultValue, invocation)
}
}
fun singleEntity(
vararg input: String,
handler: (Field, invocation: XTestInvocation) -> Unit
) {
val sources = listOf(
Source.java(
"foo.bar.MyEntity",
ENTITY_PREFIX + input.joinToString("\n") + ENTITY_SUFFIX
),
ARRAY_CONVERTER
)
runProcessorTest(
sources = sources
) { invocation ->
val (owner, fieldElement) = invocation.roundEnv
.getElementsAnnotatedWith(Entity::class.qualifiedName!!)
.filterIsInstance<XTypeElement>()
.map {
Pair(
it,
it.getAllFieldsIncludingPrivateSupers().firstOrNull()
)
}
.first { it.second != null }
val entityContext =
TableEntityProcessor(
baseContext = invocation.context,
element = owner
).context
val parser = FieldProcessor(
baseContext = entityContext,
containing = owner.type,
element = fieldElement!!,
bindingScope = FieldProcessor.BindingScope.TWO_WAY,
fieldParent = null,
onBindingError = { field, errorMsg ->
invocation.context.logger.e(field.element, errorMsg)
}
)
handler(parser.process(), invocation)
}
}
}