| /* |
| * 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 android.arch.persistence.room.solver |
| |
| import COMMON |
| import android.arch.persistence.room.Entity |
| import android.arch.persistence.room.ext.L |
| import android.arch.persistence.room.ext.LifecyclesTypeNames |
| import android.arch.persistence.room.ext.PagingTypeNames |
| import android.arch.persistence.room.ext.ReactiveStreamsTypeNames |
| import android.arch.persistence.room.ext.RoomTypeNames.STRING_UTIL |
| import android.arch.persistence.room.ext.RxJava2TypeNames |
| import android.arch.persistence.room.ext.T |
| import android.arch.persistence.room.parser.SQLTypeAffinity |
| import android.arch.persistence.room.processor.Context |
| import android.arch.persistence.room.processor.ProcessorErrors |
| import android.arch.persistence.room.solver.binderprovider.DataSourceQueryResultBinderProvider |
| import android.arch.persistence.room.solver.binderprovider.FlowableQueryResultBinderProvider |
| import android.arch.persistence.room.solver.binderprovider.LiveDataQueryResultBinderProvider |
| import android.arch.persistence.room.solver.binderprovider.LivePagedListQueryResultBinderProvider |
| import android.arch.persistence.room.solver.types.CompositeAdapter |
| import android.arch.persistence.room.solver.types.TypeConverter |
| import android.arch.persistence.room.testing.TestInvocation |
| import android.arch.persistence.room.testing.TestProcessor |
| import android.arch.paging.DataSource |
| import android.arch.paging.TiledDataSource |
| import com.google.auto.common.MoreTypes |
| 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 org.hamcrest.CoreMatchers.`is` |
| import org.hamcrest.CoreMatchers.instanceOf |
| import org.hamcrest.CoreMatchers.notNullValue |
| 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 simpleRun |
| import testCodeGenScope |
| import javax.annotation.processing.ProcessingEnvironment |
| import javax.lang.model.type.TypeKind |
| |
| @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") |
| @RunWith(JUnit4::class) |
| class TypeAdapterStoreTest { |
| companion object { |
| fun tmp(index: Int) = CodeGenScope._tmpVar(index) |
| } |
| |
| @Test |
| fun testDirect() { |
| singleRun { invocation -> |
| val store = TypeAdapterStore.create(Context(invocation.processingEnv)) |
| val primitiveType = invocation.processingEnv.typeUtils.getPrimitiveType(TypeKind.INT) |
| val adapter = store.findColumnTypeAdapter(primitiveType, null) |
| assertThat(adapter, notNullValue()) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testVia1TypeAdapter() { |
| singleRun { invocation -> |
| val store = TypeAdapterStore.create(Context(invocation.processingEnv)) |
| val booleanType = invocation.processingEnv.typeUtils |
| .getPrimitiveType(TypeKind.BOOLEAN) |
| val adapter = store.findColumnTypeAdapter(booleanType, null) |
| assertThat(adapter, notNullValue()) |
| assertThat(adapter, instanceOf(CompositeAdapter::class.java)) |
| val bindScope = testCodeGenScope() |
| adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope) |
| assertThat(bindScope.generate().trim(), `is`(""" |
| final int ${tmp(0)}; |
| ${tmp(0)} = fooVar ? 1 : 0; |
| stmt.bindLong(41, ${tmp(0)}); |
| """.trimIndent())) |
| |
| val cursorScope = testCodeGenScope() |
| adapter.readFromCursor("res", "curs", "7", cursorScope) |
| assertThat(cursorScope.generate().trim(), `is`(""" |
| final int ${tmp(0)}; |
| ${tmp(0)} = curs.getInt(7); |
| res = ${tmp(0)} != 0; |
| """.trimIndent())) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testVia2TypeAdapters() { |
| singleRun { invocation -> |
| val store = TypeAdapterStore.create(Context(invocation.processingEnv), |
| pointTypeConverters(invocation.processingEnv)) |
| val pointType = invocation.processingEnv.elementUtils |
| .getTypeElement("foo.bar.Point").asType() |
| val adapter = store.findColumnTypeAdapter(pointType, null) |
| assertThat(adapter, notNullValue()) |
| assertThat(adapter, instanceOf(CompositeAdapter::class.java)) |
| |
| val bindScope = testCodeGenScope() |
| adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope) |
| assertThat(bindScope.generate().trim(), `is`(""" |
| final int ${tmp(0)}; |
| final boolean ${tmp(1)}; |
| ${tmp(1)} = foo.bar.Point.toBoolean(fooVar); |
| ${tmp(0)} = ${tmp(1)} ? 1 : 0; |
| stmt.bindLong(41, ${tmp(0)}); |
| """.trimIndent())) |
| |
| val cursorScope = testCodeGenScope() |
| adapter.readFromCursor("res", "curs", "11", cursorScope).toString() |
| assertThat(cursorScope.generate().trim(), `is`(""" |
| final int ${tmp(0)}; |
| ${tmp(0)} = curs.getInt(11); |
| final boolean ${tmp(1)}; |
| ${tmp(1)} = ${tmp(0)} != 0; |
| res = foo.bar.Point.fromBoolean(${tmp(1)}); |
| """.trimIndent())) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testDate() { |
| singleRun { (processingEnv) -> |
| val store = TypeAdapterStore.create(Context(processingEnv), |
| dateTypeConverters(processingEnv)) |
| val tDate = processingEnv.elementUtils.getTypeElement("java.util.Date").asType() |
| val adapter = store.findCursorValueReader(tDate, SQLTypeAffinity.INTEGER) |
| assertThat(adapter, notNullValue()) |
| assertThat(adapter?.typeMirror(), `is`(tDate)) |
| val bindScope = testCodeGenScope() |
| adapter!!.readFromCursor("outDate", "curs", "0", bindScope) |
| assertThat(bindScope.generate().trim(), `is`(""" |
| final java.lang.Long _tmp; |
| if (curs.isNull(0)) { |
| _tmp = null; |
| } else { |
| _tmp = curs.getLong(0); |
| } |
| // convert Long to Date; |
| """.trimIndent())) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testIntList() { |
| singleRun { invocation -> |
| val binders = createIntListToStringBinders(invocation) |
| val store = TypeAdapterStore.create(Context(invocation.processingEnv), binders[0], |
| binders[1]) |
| |
| val adapter = store.findColumnTypeAdapter(binders[0].from, null) |
| assertThat(adapter, notNullValue()) |
| |
| val bindScope = testCodeGenScope() |
| adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope) |
| assertThat(bindScope.generate().trim(), `is`(""" |
| final java.lang.String ${tmp(0)}; |
| ${tmp(0)} = android.arch.persistence.room.util.StringUtil.joinIntoString(fooVar); |
| if (${tmp(0)} == null) { |
| stmt.bindNull(41); |
| } else { |
| stmt.bindString(41, ${tmp(0)}); |
| } |
| """.trimIndent())) |
| |
| val converter = store.findTypeConverter(binders[0].from, |
| invocation.context.COMMON_TYPES.STRING) |
| assertThat(converter, notNullValue()) |
| assertThat(store.reverse(converter!!), `is`(binders[1])) |
| |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testOneWayConversion() { |
| singleRun { invocation -> |
| val binders = createIntListToStringBinders(invocation) |
| val store = TypeAdapterStore.create(Context(invocation.processingEnv), binders[0]) |
| val adapter = store.findColumnTypeAdapter(binders[0].from, null) |
| assertThat(adapter, nullValue()) |
| |
| val stmtBinder = store.findStatementValueBinder(binders[0].from, null) |
| assertThat(stmtBinder, notNullValue()) |
| |
| val converter = store.findTypeConverter(binders[0].from, |
| invocation.context.COMMON_TYPES.STRING) |
| assertThat(converter, notNullValue()) |
| assertThat(store.reverse(converter!!), nullValue()) |
| } |
| } |
| |
| @Test |
| fun testMissingRxRoom() { |
| simpleRun(jfos = *arrayOf(COMMON.PUBLISHER, COMMON.FLOWABLE)) { invocation -> |
| val publisherElement = invocation.processingEnv.elementUtils |
| .getTypeElement(ReactiveStreamsTypeNames.PUBLISHER.toString()) |
| assertThat(publisherElement, notNullValue()) |
| assertThat(FlowableQueryResultBinderProvider(invocation.context).matches( |
| MoreTypes.asDeclared(publisherElement.asType())), `is`(true)) |
| }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_ROOM_RXJAVA2_ARTIFACT) |
| } |
| |
| @Test |
| fun testFindPublisher() { |
| simpleRun(jfos = *arrayOf(COMMON.PUBLISHER, COMMON.FLOWABLE, COMMON.RX2_ROOM)) { |
| invocation -> |
| val publisher = invocation.processingEnv.elementUtils |
| .getTypeElement(ReactiveStreamsTypeNames.PUBLISHER.toString()) |
| assertThat(publisher, notNullValue()) |
| assertThat(FlowableQueryResultBinderProvider(invocation.context).matches( |
| MoreTypes.asDeclared(publisher.asType())), `is`(true)) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testFindFlowable() { |
| simpleRun(jfos = *arrayOf(COMMON.PUBLISHER, COMMON.FLOWABLE, COMMON.RX2_ROOM)) { |
| invocation -> |
| val flowable = invocation.processingEnv.elementUtils |
| .getTypeElement(RxJava2TypeNames.FLOWABLE.toString()) |
| assertThat(flowable, notNullValue()) |
| assertThat(FlowableQueryResultBinderProvider(invocation.context).matches( |
| MoreTypes.asDeclared(flowable.asType())), `is`(true)) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun testFindLiveData() { |
| simpleRun(jfos = *arrayOf(COMMON.COMPUTABLE_LIVE_DATA, COMMON.LIVE_DATA)) { |
| invocation -> |
| val liveData = invocation.processingEnv.elementUtils |
| .getTypeElement(LifecyclesTypeNames.LIVE_DATA.toString()) |
| assertThat(liveData, notNullValue()) |
| assertThat(LiveDataQueryResultBinderProvider(invocation.context).matches( |
| MoreTypes.asDeclared(liveData.asType())), `is`(true)) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun findDataSource() { |
| simpleRun { |
| invocation -> |
| val dataSource = invocation.processingEnv.elementUtils |
| .getTypeElement(DataSource::class.java.canonicalName) |
| assertThat(dataSource, notNullValue()) |
| assertThat(DataSourceQueryResultBinderProvider(invocation.context).matches( |
| MoreTypes.asDeclared(dataSource.asType())), `is`(true)) |
| }.failsToCompile().withErrorContaining(ProcessorErrors.PAGING_SPECIFY_DATA_SOURCE_TYPE) |
| } |
| |
| @Test |
| fun findTiledDataSource() { |
| simpleRun { |
| invocation -> |
| val dataSource = invocation.processingEnv.elementUtils |
| .getTypeElement(TiledDataSource::class.java.canonicalName) |
| assertThat(dataSource, notNullValue()) |
| assertThat(DataSourceQueryResultBinderProvider(invocation.context).matches( |
| MoreTypes.asDeclared(dataSource.asType())), `is`(true)) |
| }.compilesWithoutError() |
| } |
| |
| @Test |
| fun findPagedListProvider() { |
| simpleRun(jfos = COMMON.LIVE_PAGED_LIST_PROVIDER) { |
| invocation -> |
| val pagedListProvider = invocation.processingEnv.elementUtils |
| .getTypeElement(PagingTypeNames.LIVE_PAGED_LIST_PROVIDER.toString()) |
| assertThat(pagedListProvider, notNullValue()) |
| assertThat(LivePagedListQueryResultBinderProvider(invocation.context).matches( |
| MoreTypes.asDeclared(pagedListProvider.asType())), `is`(true)) |
| }.compilesWithoutError() |
| } |
| |
| private fun createIntListToStringBinders(invocation: TestInvocation): List<TypeConverter> { |
| val intType = invocation.processingEnv.elementUtils |
| .getTypeElement(Integer::class.java.canonicalName) |
| .asType() |
| val listType = invocation.processingEnv.elementUtils |
| .getTypeElement(java.util.List::class.java.canonicalName) |
| val listOfInts = invocation.processingEnv.typeUtils.getDeclaredType(listType, intType) |
| |
| val intListConverter = object : TypeConverter(listOfInts, |
| invocation.context.COMMON_TYPES.STRING) { |
| override fun convert(inputVarName: String, outputVarName: String, |
| scope: CodeGenScope) { |
| scope.builder().apply { |
| addStatement("$L = $T.joinIntoString($L)", outputVarName, STRING_UTIL, |
| inputVarName) |
| } |
| } |
| } |
| |
| val stringToIntListConverter = object : TypeConverter( |
| invocation.context.COMMON_TYPES.STRING, listOfInts) { |
| override fun convert(inputVarName: String, outputVarName: String, |
| scope: CodeGenScope) { |
| scope.builder().apply { |
| addStatement("$L = $T.splitToIntList($L)", outputVarName, STRING_UTIL, |
| inputVarName) |
| } |
| } |
| } |
| return listOf(intListConverter, stringToIntListConverter) |
| } |
| |
| fun singleRun(handler: (TestInvocation) -> Unit): CompileTester { |
| return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources()) |
| .that(listOf(JavaFileObjects.forSourceString("foo.bar.DummyClass", |
| """ |
| package foo.bar; |
| import android.arch.persistence.room.*; |
| @Entity |
| public class DummyClass {} |
| """ |
| ), JavaFileObjects.forSourceString("foo.bar.Point", |
| """ |
| package foo.bar; |
| import android.arch.persistence.room.*; |
| @Entity |
| public class Point { |
| public int x, y; |
| public Point(int x, int y) { |
| this.x = x; |
| this.y = y; |
| } |
| public static Point fromBoolean(boolean val) { |
| return val ? new Point(1, 1) : new Point(0, 0); |
| } |
| public static boolean toBoolean(Point point) { |
| return point.x > 0; |
| } |
| } |
| """ |
| ))) |
| .processedWith(TestProcessor.builder() |
| .forAnnotations(Entity::class) |
| .nextRunHandler { invocation -> |
| handler(invocation) |
| true |
| } |
| .build()) |
| } |
| |
| fun pointTypeConverters(env: ProcessingEnvironment): List<TypeConverter> { |
| val tPoint = env.elementUtils.getTypeElement("foo.bar.Point").asType() |
| val tBoolean = env.typeUtils.getPrimitiveType(TypeKind.BOOLEAN) |
| return listOf( |
| object : TypeConverter(tPoint, tBoolean) { |
| override fun convert(inputVarName: String, outputVarName: String, |
| scope: CodeGenScope) { |
| scope.builder().apply { |
| addStatement("$L = $T.toBoolean($L)", outputVarName, from, inputVarName) |
| } |
| } |
| |
| }, |
| object : TypeConverter(tBoolean, tPoint) { |
| override fun convert(inputVarName: String, outputVarName: String, |
| scope: CodeGenScope) { |
| scope.builder().apply { |
| addStatement("$L = $T.fromBoolean($L)", outputVarName, tPoint, |
| inputVarName) |
| } |
| } |
| } |
| ) |
| } |
| |
| fun dateTypeConverters(env: ProcessingEnvironment): List<TypeConverter> { |
| val tDate = env.elementUtils.getTypeElement("java.util.Date").asType() |
| val tLong = env.elementUtils.getTypeElement("java.lang.Long").asType() |
| return listOf( |
| object : TypeConverter(tDate, tLong) { |
| override fun convert(inputVarName: String, outputVarName: String, |
| scope: CodeGenScope) { |
| scope.builder().apply { |
| addStatement("// convert Date to Long") |
| } |
| } |
| |
| }, |
| object : TypeConverter(tLong, tDate) { |
| override fun convert(inputVarName: String, outputVarName: String, |
| scope: CodeGenScope) { |
| scope.builder().apply { |
| addStatement("// convert Long to Date") |
| } |
| } |
| } |
| ) |
| } |
| } |