| /* |
| * 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 COMMON |
| import androidx.room.ColumnInfo |
| import androidx.room.Dao |
| import androidx.room.Entity |
| import androidx.room.PrimaryKey |
| import androidx.room.Query |
| import androidx.room.Relation |
| import androidx.room.Transaction |
| import androidx.room.ext.CommonTypeNames |
| import androidx.room.ext.KotlinTypeNames |
| import androidx.room.ext.LifecyclesTypeNames |
| import androidx.room.ext.PagingTypeNames |
| import androidx.room.ext.hasAnnotation |
| import androidx.room.ext.typeName |
| import androidx.room.parser.QueryType |
| import androidx.room.parser.Table |
| import androidx.room.processor.ProcessorErrors.cannotFindQueryResultAdapter |
| import androidx.room.solver.query.result.DataSourceFactoryQueryResultBinder |
| import androidx.room.solver.query.result.ListQueryResultAdapter |
| import androidx.room.solver.query.result.LiveDataQueryResultBinder |
| import androidx.room.solver.query.result.PojoRowAdapter |
| import androidx.room.solver.query.result.SingleEntityQueryResultAdapter |
| import androidx.room.testing.TestInvocation |
| import androidx.room.testing.TestProcessor |
| import androidx.room.vo.Field |
| import androidx.room.vo.QueryMethod |
| import androidx.room.vo.ReadQueryMethod |
| import androidx.room.vo.Warning |
| import androidx.room.vo.WriteQueryMethod |
| import com.google.auto.common.MoreElements |
| import com.google.auto.common.MoreTypes |
| import com.google.common.truth.Truth.assertAbout |
| import com.google.testing.compile.CompileTester |
| import com.google.testing.compile.JavaFileObjects |
| import com.google.testing.compile.JavaSourcesSubjectFactory |
| import com.squareup.javapoet.ArrayTypeName |
| import com.squareup.javapoet.ClassName |
| import com.squareup.javapoet.ParameterizedTypeName |
| import com.squareup.javapoet.TypeName |
| import com.squareup.javapoet.TypeVariableName |
| import createInterpreterFromEntitiesAndViews |
| import createVerifierFromEntitiesAndViews |
| import mockElementAndType |
| import org.hamcrest.CoreMatchers.`is` |
| import org.hamcrest.CoreMatchers.hasItem |
| import org.hamcrest.CoreMatchers.instanceOf |
| import org.hamcrest.CoreMatchers.not |
| import org.hamcrest.CoreMatchers.notNullValue |
| import org.hamcrest.CoreMatchers.nullValue |
| import org.hamcrest.MatcherAssert.assertThat |
| import org.junit.Assert.assertEquals |
| import org.junit.Test |
| import org.junit.runner.RunWith |
| import org.junit.runners.Parameterized |
| import org.mockito.Mockito |
| import javax.lang.model.type.DeclaredType |
| import javax.lang.model.type.TypeKind.INT |
| import javax.lang.model.type.TypeMirror |
| import javax.tools.JavaFileObject |
| |
| @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") |
| @RunWith(Parameterized::class) |
| class QueryMethodProcessorTest(val enableVerification: Boolean) { |
| companion object { |
| const val DAO_PREFIX = """ |
| package foo.bar; |
| import androidx.annotation.NonNull; |
| import androidx.room.*; |
| @Dao |
| abstract class MyClass { |
| """ |
| const val DAO_SUFFIX = "}" |
| val POJO: ClassName = ClassName.get("foo.bar", "MyClass.Pojo") |
| @Parameterized.Parameters(name = "enableDbVerification={0}") |
| @JvmStatic |
| fun getParams() = arrayOf(true, false) |
| |
| fun createField(name: String, columnName: String? = null): Field { |
| val (element, type) = mockElementAndType() |
| return Field( |
| element = element, |
| name = name, |
| type = type, |
| columnName = columnName ?: name, |
| affinity = null |
| ) |
| } |
| } |
| |
| @Test |
| fun testReadNoParams() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("SELECT * from User") |
| abstract public int[] foo(); |
| """) { parsedQuery, _ -> |
| assertThat(parsedQuery.name, `is`("foo")) |
| assertThat(parsedQuery.parameters.size, `is`(0)) |
| assertThat(parsedQuery.returnType.typeName(), |
| `is`(ArrayTypeName.of(TypeName.INT) as TypeName)) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testSingleParam() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("SELECT * from User where uid = :x") |
| abstract public long foo(int x); |
| """) { parsedQuery, invocation -> |
| assertThat(parsedQuery.name, `is`("foo")) |
| assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.LONG)) |
| assertThat(parsedQuery.parameters.size, `is`(1)) |
| val param = parsedQuery.parameters.first() |
| assertThat(param.name, `is`("x")) |
| assertThat(param.sqlName, `is`("x")) |
| assertThat(param.type, |
| `is`(invocation.processingEnv.typeUtils.getPrimitiveType(INT) as TypeMirror)) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testVarArgs() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("SELECT * from User where uid in (:ids)") |
| abstract public long foo(int... ids); |
| """) { parsedQuery, invocation -> |
| assertThat(parsedQuery.name, `is`("foo")) |
| assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.LONG)) |
| assertThat(parsedQuery.parameters.size, `is`(1)) |
| val param = parsedQuery.parameters.first() |
| assertThat(param.name, `is`("ids")) |
| assertThat(param.sqlName, `is`("ids")) |
| val types = invocation.processingEnv.typeUtils |
| assertThat(param.type, |
| `is`(types.getArrayType(types.getPrimitiveType(INT)) as TypeMirror)) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testParamBindingMatchingNoName() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("SELECT uid from User where uid = :id") |
| abstract public long getIdById(int id); |
| """) { parsedQuery, _ -> |
| val section = parsedQuery.query.bindSections.first() |
| val param = parsedQuery.parameters.firstOrNull() |
| assertThat(section, notNullValue()) |
| assertThat(param, notNullValue()) |
| assertThat(parsedQuery.sectionToParamMapping, `is`(listOf(Pair(section, param)))) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testParamBindingMatchingSimpleBind() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("SELECT uid from User where uid = :id") |
| abstract public long getIdById(int id); |
| """) { parsedQuery, _ -> |
| val section = parsedQuery.query.bindSections.first() |
| val param = parsedQuery.parameters.firstOrNull() |
| assertThat(section, notNullValue()) |
| assertThat(param, notNullValue()) |
| assertThat(parsedQuery.sectionToParamMapping, |
| `is`(listOf(Pair(section, param)))) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testParamBindingTwoBindVarsIntoTheSameParameter() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("SELECT uid from User where uid = :id OR uid = :id") |
| abstract public long getIdById(int id); |
| """) { parsedQuery, _ -> |
| val section = parsedQuery.query.bindSections[0] |
| val section2 = parsedQuery.query.bindSections[1] |
| val param = parsedQuery.parameters.firstOrNull() |
| assertThat(section, notNullValue()) |
| assertThat(section2, notNullValue()) |
| assertThat(param, notNullValue()) |
| assertThat(parsedQuery.sectionToParamMapping, |
| `is`(listOf(Pair(section, param), Pair(section2, param)))) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testMissingParameterForBinding() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("SELECT uid from User where uid = :id OR uid = :uid") |
| abstract public long getIdById(int id); |
| """) { parsedQuery, _ -> |
| val section = parsedQuery.query.bindSections[0] |
| val section2 = parsedQuery.query.bindSections[1] |
| val param = parsedQuery.parameters.firstOrNull() |
| assertThat(section, notNullValue()) |
| assertThat(section2, notNullValue()) |
| assertThat(param, notNullValue()) |
| assertThat(parsedQuery.sectionToParamMapping, |
| `is`(listOf(Pair(section, param), Pair(section2, null)))) |
| } |
| .failsToCompile() |
| .withErrorContaining( |
| ProcessorErrors.missingParameterForBindVariable(listOf(":uid"))) |
| } |
| |
| @Test |
| fun test2MissingParameterForBinding() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("SELECT uid from User where name = :bar AND uid = :id OR uid = :uid") |
| abstract public long getIdById(int id); |
| """) { parsedQuery, _ -> |
| val bar = parsedQuery.query.bindSections[0] |
| val id = parsedQuery.query.bindSections[1] |
| val uid = parsedQuery.query.bindSections[2] |
| val param = parsedQuery.parameters.firstOrNull() |
| assertThat(bar, notNullValue()) |
| assertThat(id, notNullValue()) |
| assertThat(uid, notNullValue()) |
| assertThat(param, notNullValue()) |
| assertThat(parsedQuery.sectionToParamMapping, |
| `is`(listOf(Pair(bar, null), Pair(id, param), Pair(uid, null)))) |
| } |
| .failsToCompile() |
| .withErrorContaining( |
| ProcessorErrors.missingParameterForBindVariable(listOf(":bar", ":uid"))) |
| } |
| |
| @Test |
| fun testUnusedParameters() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("SELECT uid from User where name = :bar") |
| abstract public long getIdById(int bar, int whyNotUseMe); |
| """) { parsedQuery, _ -> |
| val bar = parsedQuery.query.bindSections[0] |
| val barParam = parsedQuery.parameters.firstOrNull() |
| assertThat(bar, notNullValue()) |
| assertThat(barParam, notNullValue()) |
| assertThat(parsedQuery.sectionToParamMapping, |
| `is`(listOf(Pair(bar, barParam)))) |
| }.failsToCompile().withErrorContaining( |
| ProcessorErrors.unusedQueryMethodParameter(listOf("whyNotUseMe"))) |
| } |
| |
| @Test |
| fun testNameWithUnderscore() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("select * from User where uid = :_blah") |
| abstract public long getSth(int _blah); |
| """ |
| ) { _, _ -> } |
| .failsToCompile() |
| .withErrorContaining(ProcessorErrors.QUERY_PARAMETERS_CANNOT_START_WITH_UNDERSCORE) |
| } |
| |
| @Test |
| fun testGenericReturnType() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("select * from User") |
| abstract public <T> ${CommonTypeNames.LIST}<T> foo(int x); |
| """) { parsedQuery, _ -> |
| val expected: TypeName = ParameterizedTypeName.get(ClassName.get(List::class.java), |
| TypeVariableName.get("T")) |
| assertThat(parsedQuery.returnType.typeName(), `is`(expected)) |
| }.failsToCompile() |
| .withErrorContaining(ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS) |
| } |
| |
| @Test |
| fun testBadQuery() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("select * from :1 :2") |
| abstract public long foo(int x); |
| """) { _, _ -> |
| // do nothing |
| }.failsToCompile() |
| .withErrorContaining("UNEXPECTED_CHAR=:") |
| } |
| |
| @Test |
| fun testLiveDataWithWithClause() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("WITH RECURSIVE tempTable(n, fact) AS (SELECT 0, 1 UNION ALL SELECT n+1," |
| + " (n+1)*fact FROM tempTable WHERE n < 9) SELECT fact FROM tempTable, User") |
| abstract public ${LifecyclesTypeNames.LIVE_DATA}<${CommonTypeNames.LIST}<Integer>> |
| getFactorialLiveData(); |
| """) { parsedQuery, _ -> |
| assertThat(parsedQuery.query.tables, hasItem(Table("User", "User"))) |
| assertThat(parsedQuery.query.tables, |
| not(hasItem(Table("tempTable", "tempTable")))) |
| assertThat(parsedQuery.query.tables.size, `is`(1)) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testLiveDataWithNothingToObserve() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("SELECT 1") |
| abstract public ${LifecyclesTypeNames.LIVE_DATA}<Integer> getOne(); |
| """) { _, _ -> |
| // do nothing |
| }.failsToCompile() |
| .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE) |
| } |
| |
| @Test |
| fun testLiveDataWithWithClauseAndNothingToObserve() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("WITH RECURSIVE tempTable(n, fact) AS (SELECT 0, 1 UNION ALL SELECT n+1," |
| + " (n+1)*fact FROM tempTable WHERE n < 9) SELECT fact FROM tempTable") |
| abstract public ${LifecyclesTypeNames.LIVE_DATA}<${CommonTypeNames.LIST}<Integer>> |
| getFactorialLiveData(); |
| """) { _, _ -> |
| // do nothing |
| }.failsToCompile() |
| .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE) |
| } |
| |
| @Test |
| fun testBoundGeneric() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| static abstract class BaseModel<T> { |
| @Query("select COUNT(*) from User") |
| abstract public T getT(); |
| } |
| @Dao |
| static abstract class ExtendingModel extends BaseModel<Integer> { |
| } |
| """) { parsedQuery, _ -> |
| assertThat(parsedQuery.returnType.typeName(), |
| `is`(ClassName.get(Integer::class.java) as TypeName)) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testBoundGenericParameter() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| static abstract class BaseModel<T> { |
| @Query("select COUNT(*) from User where :t") |
| abstract public int getT(T t); |
| } |
| @Dao |
| static abstract class ExtendingModel extends BaseModel<Integer> { |
| } |
| """) { parsedQuery, invocation -> |
| assertThat(parsedQuery.parameters.first().type, |
| `is`(invocation.processingEnv.elementUtils |
| .getTypeElement("java.lang.Integer").asType())) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testReadDeleteWithBadReturnType() { |
| singleQueryMethod<WriteQueryMethod>( |
| """ |
| @Query("DELETE from User where uid = :id") |
| abstract public float foo(int id); |
| """) { _, _ -> |
| }.failsToCompile().withErrorContaining( |
| ProcessorErrors.cannotFindPreparedQueryResultAdapter("float", QueryType.DELETE) |
| ) |
| } |
| |
| @Test |
| fun testSimpleDelete() { |
| singleQueryMethod<WriteQueryMethod>( |
| """ |
| @Query("DELETE from User where uid = :id") |
| abstract public int foo(int id); |
| """) { parsedQuery, _ -> |
| assertThat(parsedQuery.name, `is`("foo")) |
| assertThat(parsedQuery.parameters.size, `is`(1)) |
| assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.INT)) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testVoidDeleteQuery() { |
| singleQueryMethod<WriteQueryMethod>( |
| """ |
| @Query("DELETE from User where uid = :id") |
| abstract public void foo(int id); |
| """) { parsedQuery, _ -> |
| assertThat(parsedQuery.name, `is`("foo")) |
| assertThat(parsedQuery.parameters.size, `is`(1)) |
| assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.VOID)) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testVoidUpdateQuery() { |
| singleQueryMethod<WriteQueryMethod>( |
| """ |
| @Query("update user set name = :name") |
| abstract public void updateAllNames(String name); |
| """) { parsedQuery, invocation -> |
| assertThat(parsedQuery.name, `is`("updateAllNames")) |
| assertThat(parsedQuery.parameters.size, `is`(1)) |
| assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.VOID)) |
| assertThat(parsedQuery.parameters.first().type.typeName(), |
| `is`(invocation.context.COMMON_TYPES.STRING.typeName())) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testVoidInsertQuery() { |
| singleQueryMethod<WriteQueryMethod>( |
| """ |
| @Query("insert into user (name) values (:name)") |
| abstract public void insertUsername(String name); |
| """) { parsedQuery, invocation -> |
| assertThat(parsedQuery.name, `is`("insertUsername")) |
| assertThat(parsedQuery.parameters.size, `is`(1)) |
| assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.VOID)) |
| assertThat(parsedQuery.parameters.first().type.typeName(), |
| `is`(invocation.context.COMMON_TYPES.STRING.typeName())) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testLongInsertQuery() { |
| singleQueryMethod<WriteQueryMethod>( |
| """ |
| @Query("insert into user (name) values (:name)") |
| abstract public long insertUsername(String name); |
| """) { parsedQuery, invocation -> |
| assertThat(parsedQuery.name, `is`("insertUsername")) |
| assertThat(parsedQuery.parameters.size, `is`(1)) |
| assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.LONG)) |
| assertThat(parsedQuery.parameters.first().type.typeName(), |
| `is`(invocation.context.COMMON_TYPES.STRING.typeName())) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testInsertQueryWithBadReturnType() { |
| singleQueryMethod<WriteQueryMethod>( |
| """ |
| @Query("insert into user (name) values (:name)") |
| abstract public int insert(String name); |
| """) { parsedQuery, _ -> |
| assertThat(parsedQuery.returnType.typeName(), `is`(TypeName.INT)) |
| }.failsToCompile().withErrorContaining( |
| ProcessorErrors.cannotFindPreparedQueryResultAdapter("int", QueryType.INSERT) |
| ) |
| } |
| |
| @Test |
| fun testLiveDataQuery() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("select name from user where uid = :id") |
| abstract ${LifecyclesTypeNames.LIVE_DATA}<String> nameLiveData(String id); |
| """ |
| ) { parsedQuery, _ -> |
| assertThat(parsedQuery.returnType.typeName(), |
| `is`(ParameterizedTypeName.get(LifecyclesTypeNames.LIVE_DATA, |
| String::class.typeName()) as TypeName)) |
| assertThat(parsedQuery.queryResultBinder, |
| instanceOf(LiveDataQueryResultBinder::class.java)) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testBadReturnForDeleteQuery() { |
| singleQueryMethod<WriteQueryMethod>( |
| """ |
| @Query("delete from user where uid = :id") |
| abstract ${LifecyclesTypeNames.LIVE_DATA}<Integer> deleteLiveData(String id); |
| """ |
| ) { _, _ -> |
| }.failsToCompile() |
| .withErrorContaining(ProcessorErrors.cannotFindPreparedQueryResultAdapter( |
| "androidx.lifecycle.LiveData<java.lang.Integer>", QueryType.DELETE)) |
| } |
| |
| @Test |
| fun testBadReturnForUpdateQuery() { |
| singleQueryMethod<WriteQueryMethod>( |
| """ |
| @Query("update user set name = :name") |
| abstract ${LifecyclesTypeNames.LIVE_DATA}<Integer> updateNameLiveData(String name); |
| """ |
| ) { _, _ -> |
| }.failsToCompile() |
| .withErrorContaining(ProcessorErrors.cannotFindPreparedQueryResultAdapter( |
| "androidx.lifecycle.LiveData<java.lang.Integer>", QueryType.UPDATE)) |
| } |
| |
| @Test |
| fun testDataSourceFactoryQuery() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("select name from user") |
| abstract ${PagingTypeNames.DATA_SOURCE_FACTORY}<Integer, String> |
| nameDataSourceFactory(); |
| """ |
| ) { parsedQuery, _ -> |
| assertThat(parsedQuery.returnType.typeName(), |
| `is`(ParameterizedTypeName.get(PagingTypeNames.DATA_SOURCE_FACTORY, |
| Integer::class.typeName(), String::class.typeName()) as TypeName)) |
| assertThat(parsedQuery.queryResultBinder, |
| instanceOf(DataSourceFactoryQueryResultBinder::class.java)) |
| val tableNames = |
| (parsedQuery.queryResultBinder as DataSourceFactoryQueryResultBinder) |
| .positionalDataSourceQueryResultBinder.tableNames |
| assertEquals(setOf("user"), tableNames) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testMultiTableDataSourceFactoryQuery() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("select name from User u LEFT OUTER JOIN Book b ON u.uid == b.uid") |
| abstract ${PagingTypeNames.DATA_SOURCE_FACTORY}<Integer, String> |
| nameDataSourceFactory(); |
| """ |
| ) { parsedQuery, _ -> |
| assertThat(parsedQuery.returnType.typeName(), |
| `is`(ParameterizedTypeName.get(PagingTypeNames.DATA_SOURCE_FACTORY, |
| Integer::class.typeName(), String::class.typeName()) as TypeName)) |
| assertThat(parsedQuery.queryResultBinder, |
| instanceOf(DataSourceFactoryQueryResultBinder::class.java)) |
| val tableNames = |
| (parsedQuery.queryResultBinder as DataSourceFactoryQueryResultBinder) |
| .positionalDataSourceQueryResultBinder.tableNames |
| assertEquals(setOf("User", "Book"), tableNames) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testBadChannelReturnForQuery() { |
| singleQueryMethod<QueryMethod>( |
| """ |
| @Query("select * from user") |
| abstract ${KotlinTypeNames.CHANNEL}<User> getUsersChannel(); |
| """, |
| jfos = listOf(COMMON.CHANNEL) |
| ) { _, _ -> |
| }.failsToCompile() |
| .withErrorContaining(ProcessorErrors.invalidChannelType( |
| KotlinTypeNames.CHANNEL.toString())) |
| } |
| |
| @Test |
| fun testBadSendChannelReturnForQuery() { |
| singleQueryMethod<QueryMethod>( |
| """ |
| @Query("select * from user") |
| abstract ${KotlinTypeNames.SEND_CHANNEL}<User> getUsersChannel(); |
| """, |
| jfos = listOf(COMMON.SEND_CHANNEL) |
| ) { _, _ -> |
| }.failsToCompile() |
| .withErrorContaining(ProcessorErrors.invalidChannelType( |
| KotlinTypeNames.SEND_CHANNEL.toString())) |
| } |
| |
| @Test |
| fun testBadReceiveChannelReturnForQuery() { |
| singleQueryMethod<QueryMethod>( |
| """ |
| @Query("select * from user") |
| abstract ${KotlinTypeNames.RECEIVE_CHANNEL}<User> getUsersChannel(); |
| """, |
| jfos = listOf(COMMON.RECEIVE_CHANNEL) |
| ) { _, _ -> |
| }.failsToCompile() |
| .withErrorContaining(ProcessorErrors.invalidChannelType( |
| KotlinTypeNames.RECEIVE_CHANNEL.toString())) |
| } |
| |
| @Test |
| fun query_detectTransaction_select() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Query("select * from user") |
| abstract int loadUsers(); |
| """ |
| ) { method, _ -> |
| assertThat(method.inTransaction, `is`(false)) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun query_detectTransaction_selectInTransaction() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @Transaction |
| @Query("select * from user") |
| abstract int loadUsers(); |
| """ |
| ) { method, _ -> |
| assertThat(method.inTransaction, `is`(true)) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun skipVerification() { |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| @SkipQueryVerification |
| @Query("SELECT foo from User") |
| abstract public int[] foo(); |
| """) { parsedQuery, _ -> |
| assertThat(parsedQuery.name, `is`("foo")) |
| assertThat(parsedQuery.parameters.size, `is`(0)) |
| assertThat(parsedQuery.returnType.typeName(), |
| `is`(ArrayTypeName.of(TypeName.INT) as TypeName)) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun suppressWarnings() { |
| singleQueryMethod<ReadQueryMethod>(""" |
| @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) |
| @Query("SELECT uid from User") |
| abstract public int[] foo(); |
| """) { method, invocation -> |
| val queryInterpreter = createInterpreterFromEntitiesAndViews(invocation) |
| assertThat( |
| QueryMethodProcessor( |
| baseContext = invocation.context, |
| containing = Mockito.mock(DeclaredType::class.java), |
| executableElement = method.element, |
| queryInterpreter = queryInterpreter, |
| dbVerifier = null |
| ).context.logger.suppressedWarnings, |
| `is`(setOf(Warning.CURSOR_MISMATCH)) |
| ) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun relationWithExtendsBounds() { |
| if (!enableVerification) { |
| return |
| } |
| singleQueryMethod<ReadQueryMethod>( |
| """ |
| static class Merged extends User { |
| @Relation(parentColumn = "name", entityColumn = "lastName", |
| entity = User.class) |
| java.util.List<? extends User> users; |
| } |
| @Transaction |
| @Query("select * from user") |
| abstract java.util.List<Merged> loadUsers(); |
| """) { method, _ -> |
| assertThat(method.queryResultBinder.adapter, |
| instanceOf(ListQueryResultAdapter::class.java)) |
| val listAdapter = method.queryResultBinder.adapter as ListQueryResultAdapter |
| assertThat(listAdapter.rowAdapter, instanceOf(PojoRowAdapter::class.java)) |
| val pojoRowAdapter = listAdapter.rowAdapter as PojoRowAdapter |
| assertThat(pojoRowAdapter.relationCollectors.size, `is`(1)) |
| assertThat(pojoRowAdapter.relationCollectors[0].relationTypeName, `is`( |
| ParameterizedTypeName.get(ClassName.get(ArrayList::class.java), |
| COMMON.USER_TYPE_NAME) as TypeName |
| )) |
| }.compilesWithoutError() |
| .withWarningCount(0) |
| } |
| |
| @Test |
| fun pojo_renamedColumn() { |
| pojoTest(""" |
| String name; |
| String lName; |
| """, listOf("name", "lastName as lName")) { adapter, _, _ -> |
| assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList())) |
| assertThat(adapter?.mapping?.unusedFields, `is`(emptyList())) |
| }?.compilesWithoutError()?.withWarningCount(0) |
| } |
| |
| @Test |
| fun pojo_exactMatch() { |
| pojoTest(""" |
| String name; |
| String lastName; |
| """, listOf("name", "lastName")) { adapter, _, _ -> |
| assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList())) |
| assertThat(adapter?.mapping?.unusedFields, `is`(emptyList())) |
| }?.compilesWithoutError()?.withWarningCount(0) |
| } |
| |
| @Test |
| fun pojo_exactMatchWithStar() { |
| pojoTest(""" |
| String name; |
| String lastName; |
| int uid; |
| @ColumnInfo(name = "ageColumn") |
| int age; |
| """, listOf("*")) { adapter, _, _ -> |
| assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList())) |
| assertThat(adapter?.mapping?.unusedFields, `is`(emptyList())) |
| }?.compilesWithoutError()?.withWarningCount(0) |
| } |
| |
| @Test |
| fun pojo_nonJavaName() { |
| pojoTest(""" |
| @ColumnInfo(name = "MAX(ageColumn)") |
| int maxAge; |
| String name; |
| """, listOf("MAX(ageColumn)", "name")) { adapter, _, _ -> |
| assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList())) |
| assertThat(adapter?.mapping?.unusedFields, `is`(emptyList())) |
| }?.compilesWithoutError()?.withWarningCount(0) |
| } |
| |
| @Test |
| fun pojo_noMatchingFields() { |
| pojoTest(""" |
| String nameX; |
| String lastNameX; |
| """, listOf("name", "lastName")) { adapter, _, _ -> |
| assertThat(adapter?.mapping?.unusedColumns, `is`(listOf("name", "lastName"))) |
| assertThat(adapter?.mapping?.unusedFields, `is`(adapter?.pojo?.fields as List<Field>)) |
| }?.failsToCompile() |
| ?.withErrorContaining(cannotFindQueryResultAdapter("foo.bar.MyClass.Pojo")) |
| ?.and() |
| ?.withWarningContaining( |
| ProcessorErrors.cursorPojoMismatch( |
| pojoTypeName = POJO, |
| unusedColumns = listOf("name", "lastName"), |
| unusedFields = listOf(createField("nameX"), |
| createField("lastNameX")), |
| allColumns = listOf("name", "lastName"), |
| allFields = listOf( |
| createField("nameX"), |
| createField("lastNameX") |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun pojo_badQuery() { |
| // do not report mismatch if query is broken |
| pojoTest(""" |
| @ColumnInfo(name = "MAX(ageColumn)") |
| int maxAge; |
| String name; |
| """, listOf("MAX(age)", "name")) { _, _, _ -> |
| }?.failsToCompile() |
| ?.withErrorContaining("no such column: age") |
| ?.and()?.withErrorContaining(cannotFindQueryResultAdapter("foo.bar.MyClass.Pojo")) |
| ?.and()?.withErrorCount(2) |
| ?.withWarningCount(0) |
| } |
| |
| @Test |
| fun pojo_tooManyColumns() { |
| pojoTest(""" |
| String name; |
| String lastName; |
| """, listOf("uid", "name", "lastName")) { adapter, _, _ -> |
| assertThat(adapter?.mapping?.unusedColumns, `is`(listOf("uid"))) |
| assertThat(adapter?.mapping?.unusedFields, `is`(emptyList())) |
| }?.compilesWithoutError()?.withWarningContaining( |
| ProcessorErrors.cursorPojoMismatch( |
| pojoTypeName = POJO, |
| unusedColumns = listOf("uid"), |
| unusedFields = emptyList(), |
| allColumns = listOf("uid", "name", "lastName"), |
| allFields = listOf(createField("name"), createField("lastName")) |
| )) |
| } |
| |
| @Test |
| fun pojo_tooManyFields() { |
| pojoTest(""" |
| String name; |
| String lastName; |
| """, listOf("lastName")) { adapter, _, _ -> |
| assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList())) |
| assertThat(adapter?.mapping?.unusedFields, `is`( |
| adapter?.pojo?.fields?.filter { it.name == "name" } |
| )) |
| }?.compilesWithoutError()?.withWarningContaining( |
| ProcessorErrors.cursorPojoMismatch( |
| pojoTypeName = POJO, |
| unusedColumns = emptyList(), |
| unusedFields = listOf(createField("name")), |
| allColumns = listOf("lastName"), |
| allFields = listOf(createField("name"), createField("lastName")) |
| )) |
| } |
| |
| @Test |
| fun pojo_missingNonNull() { |
| pojoTest(""" |
| @NonNull |
| String name; |
| String lastName; |
| """, listOf("lastName")) { adapter, _, _ -> |
| assertThat(adapter?.mapping?.unusedColumns, `is`(emptyList())) |
| assertThat(adapter?.mapping?.unusedFields, `is`( |
| adapter?.pojo?.fields?.filter { it.name == "name" } |
| )) |
| }?.failsToCompile()?.withWarningContaining( |
| ProcessorErrors.cursorPojoMismatch( |
| pojoTypeName = POJO, |
| unusedColumns = emptyList(), |
| unusedFields = listOf(createField("name")), |
| allColumns = listOf("lastName"), |
| allFields = listOf(createField("name"), createField("lastName")) |
| ))?.and()?.withErrorContaining( |
| ProcessorErrors.pojoMissingNonNull(pojoTypeName = POJO, |
| missingPojoFields = listOf("name"), |
| allQueryColumns = listOf("lastName"))) |
| } |
| |
| @Test |
| fun pojo_tooManyFieldsAndColumns() { |
| pojoTest(""" |
| String name; |
| String lastName; |
| """, listOf("uid", "name")) { adapter, _, _ -> |
| assertThat(adapter?.mapping?.unusedColumns, `is`(listOf("uid"))) |
| assertThat(adapter?.mapping?.unusedFields, `is`( |
| adapter?.pojo?.fields?.filter { it.name == "lastName" } |
| )) |
| }?.compilesWithoutError()?.withWarningContaining( |
| ProcessorErrors.cursorPojoMismatch( |
| pojoTypeName = POJO, |
| unusedColumns = listOf("uid"), |
| unusedFields = listOf(createField("lastName")), |
| allColumns = listOf("uid", "name"), |
| allFields = listOf(createField("name"), createField("lastName")) |
| )) |
| } |
| |
| @Test |
| fun pojo_expandProjection() { |
| if (!enableVerification) return |
| pojoTest(""" |
| String uid; |
| String name; |
| """, |
| listOf("*"), |
| options = listOf("-Aroom.expandProjection=true") |
| ) { adapter, _, _ -> |
| adapter!! |
| assertThat(adapter.mapping.unusedColumns.size, `is`(0)) |
| assertThat(adapter.mapping.unusedFields.size, `is`(0)) |
| }!!.compilesWithoutWarnings() |
| } |
| |
| fun pojoTest( |
| pojoFields: String, |
| queryColumns: List<String>, |
| options: List<String> = emptyList(), |
| handler: (PojoRowAdapter?, QueryMethod, TestInvocation) -> Unit |
| ): CompileTester? { |
| val assertion = singleQueryMethod<ReadQueryMethod>( |
| """ |
| static class Pojo { |
| $pojoFields |
| } |
| @Query("SELECT ${queryColumns.joinToString(", ")} from User LIMIT 1") |
| abstract MyClass.Pojo getNameAndLastNames(); |
| """, |
| options = options |
| ) { parsedQuery, invocation -> |
| val adapter = parsedQuery.queryResultBinder.adapter |
| if (enableVerification) { |
| if (adapter is SingleEntityQueryResultAdapter) { |
| handler(adapter.rowAdapter as? PojoRowAdapter, parsedQuery, invocation) |
| } else { |
| handler(null, parsedQuery, invocation) |
| } |
| } else { |
| assertThat(adapter, nullValue()) |
| } |
| } |
| if (enableVerification) { |
| return assertion |
| } else { |
| assertion.failsToCompile() |
| .withErrorContaining(cannotFindQueryResultAdapter("foo.bar.MyClass.Pojo")) |
| return null |
| } |
| } |
| |
| private fun <T : QueryMethod> singleQueryMethod( |
| vararg input: String, |
| jfos: Iterable<JavaFileObject> = emptyList(), |
| options: List<String> = emptyList(), |
| handler: (T, TestInvocation) -> Unit |
| ): CompileTester { |
| return assertAbout(JavaSourcesSubjectFactory.javaSources()) |
| .that( |
| listOf( |
| JavaFileObjects.forSourceString( |
| "foo.bar.MyClass", |
| DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX |
| ), COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER, COMMON.BOOK |
| ) + jfos |
| ) |
| .withCompilerOptions(options) |
| .processedWith(TestProcessor.builder() |
| .forAnnotations( |
| Query::class, Dao::class, ColumnInfo::class, |
| Entity::class, PrimaryKey::class, Relation::class, |
| Transaction::class |
| ) |
| .nextRunHandler { invocation -> |
| val (owner, methods) = invocation.roundEnv |
| .getElementsAnnotatedWith(Dao::class.java) |
| .map { |
| Pair(it, |
| invocation.processingEnv.elementUtils |
| .getAllMembers(MoreElements.asType(it)) |
| .filter { |
| it.hasAnnotation(Query::class) |
| } |
| ) |
| }.first { it.second.isNotEmpty() } |
| val verifier = if (enableVerification) { |
| createVerifierFromEntitiesAndViews(invocation) |
| } else { |
| null |
| } |
| val queryInterpreter = createInterpreterFromEntitiesAndViews(invocation) |
| val parser = QueryMethodProcessor( |
| baseContext = invocation.context, |
| containing = MoreTypes.asDeclared(owner.asType()), |
| executableElement = MoreElements.asExecutable(methods.first()), |
| queryInterpreter = queryInterpreter, |
| dbVerifier = verifier |
| ) |
| val parsedQuery = parser.process() |
| @Suppress("UNCHECKED_CAST") |
| handler(parsedQuery as T, invocation) |
| true |
| } |
| .build()) |
| } |
| } |