1/* 2 * 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 17package android.arch.persistence.room.solver 18 19import COMMON 20import android.arch.persistence.room.Entity 21import android.arch.persistence.room.ext.L 22import android.arch.persistence.room.ext.LifecyclesTypeNames 23import android.arch.persistence.room.ext.PagingTypeNames 24import android.arch.persistence.room.ext.ReactiveStreamsTypeNames 25import android.arch.persistence.room.ext.RoomTypeNames.STRING_UTIL 26import android.arch.persistence.room.ext.RxJava2TypeNames 27import android.arch.persistence.room.ext.T 28import android.arch.persistence.room.parser.SQLTypeAffinity 29import android.arch.persistence.room.processor.Context 30import android.arch.persistence.room.processor.ProcessorErrors 31import android.arch.persistence.room.solver.binderprovider.DataSourceQueryResultBinderProvider 32import android.arch.persistence.room.solver.binderprovider.FlowableQueryResultBinderProvider 33import android.arch.persistence.room.solver.binderprovider.LiveDataQueryResultBinderProvider 34import android.arch.persistence.room.solver.binderprovider.LivePagedListQueryResultBinderProvider 35import android.arch.persistence.room.solver.types.CompositeAdapter 36import android.arch.persistence.room.solver.types.TypeConverter 37import android.arch.persistence.room.testing.TestInvocation 38import android.arch.persistence.room.testing.TestProcessor 39import android.arch.paging.DataSource 40import android.arch.paging.TiledDataSource 41import com.google.auto.common.MoreTypes 42import com.google.common.truth.Truth 43import com.google.testing.compile.CompileTester 44import com.google.testing.compile.JavaFileObjects 45import com.google.testing.compile.JavaSourcesSubjectFactory 46import org.hamcrest.CoreMatchers.`is` 47import org.hamcrest.CoreMatchers.instanceOf 48import org.hamcrest.CoreMatchers.notNullValue 49import org.hamcrest.CoreMatchers.nullValue 50import org.hamcrest.MatcherAssert.assertThat 51import org.junit.Test 52import org.junit.runner.RunWith 53import org.junit.runners.JUnit4 54import simpleRun 55import testCodeGenScope 56import javax.annotation.processing.ProcessingEnvironment 57import javax.lang.model.type.TypeKind 58 59@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") 60@RunWith(JUnit4::class) 61class TypeAdapterStoreTest { 62 companion object { 63 fun tmp(index: Int) = CodeGenScope._tmpVar(index) 64 } 65 66 @Test 67 fun testDirect() { 68 singleRun { invocation -> 69 val store = TypeAdapterStore.create(Context(invocation.processingEnv)) 70 val primitiveType = invocation.processingEnv.typeUtils.getPrimitiveType(TypeKind.INT) 71 val adapter = store.findColumnTypeAdapter(primitiveType, null) 72 assertThat(adapter, notNullValue()) 73 }.compilesWithoutError() 74 } 75 76 @Test 77 fun testVia1TypeAdapter() { 78 singleRun { invocation -> 79 val store = TypeAdapterStore.create(Context(invocation.processingEnv)) 80 val booleanType = invocation.processingEnv.typeUtils 81 .getPrimitiveType(TypeKind.BOOLEAN) 82 val adapter = store.findColumnTypeAdapter(booleanType, null) 83 assertThat(adapter, notNullValue()) 84 assertThat(adapter, instanceOf(CompositeAdapter::class.java)) 85 val bindScope = testCodeGenScope() 86 adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope) 87 assertThat(bindScope.generate().trim(), `is`(""" 88 final int ${tmp(0)}; 89 ${tmp(0)} = fooVar ? 1 : 0; 90 stmt.bindLong(41, ${tmp(0)}); 91 """.trimIndent())) 92 93 val cursorScope = testCodeGenScope() 94 adapter.readFromCursor("res", "curs", "7", cursorScope) 95 assertThat(cursorScope.generate().trim(), `is`(""" 96 final int ${tmp(0)}; 97 ${tmp(0)} = curs.getInt(7); 98 res = ${tmp(0)} != 0; 99 """.trimIndent())) 100 }.compilesWithoutError() 101 } 102 103 @Test 104 fun testVia2TypeAdapters() { 105 singleRun { invocation -> 106 val store = TypeAdapterStore.create(Context(invocation.processingEnv), 107 pointTypeConverters(invocation.processingEnv)) 108 val pointType = invocation.processingEnv.elementUtils 109 .getTypeElement("foo.bar.Point").asType() 110 val adapter = store.findColumnTypeAdapter(pointType, null) 111 assertThat(adapter, notNullValue()) 112 assertThat(adapter, instanceOf(CompositeAdapter::class.java)) 113 114 val bindScope = testCodeGenScope() 115 adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope) 116 assertThat(bindScope.generate().trim(), `is`(""" 117 final int ${tmp(0)}; 118 final boolean ${tmp(1)}; 119 ${tmp(1)} = foo.bar.Point.toBoolean(fooVar); 120 ${tmp(0)} = ${tmp(1)} ? 1 : 0; 121 stmt.bindLong(41, ${tmp(0)}); 122 """.trimIndent())) 123 124 val cursorScope = testCodeGenScope() 125 adapter.readFromCursor("res", "curs", "11", cursorScope).toString() 126 assertThat(cursorScope.generate().trim(), `is`(""" 127 final int ${tmp(0)}; 128 ${tmp(0)} = curs.getInt(11); 129 final boolean ${tmp(1)}; 130 ${tmp(1)} = ${tmp(0)} != 0; 131 res = foo.bar.Point.fromBoolean(${tmp(1)}); 132 """.trimIndent())) 133 }.compilesWithoutError() 134 } 135 136 @Test 137 fun testDate() { 138 singleRun { (processingEnv) -> 139 val store = TypeAdapterStore.create(Context(processingEnv), 140 dateTypeConverters(processingEnv)) 141 val tDate = processingEnv.elementUtils.getTypeElement("java.util.Date").asType() 142 val adapter = store.findCursorValueReader(tDate, SQLTypeAffinity.INTEGER) 143 assertThat(adapter, notNullValue()) 144 assertThat(adapter?.typeMirror(), `is`(tDate)) 145 val bindScope = testCodeGenScope() 146 adapter!!.readFromCursor("outDate", "curs", "0", bindScope) 147 assertThat(bindScope.generate().trim(), `is`(""" 148 final java.lang.Long _tmp; 149 if (curs.isNull(0)) { 150 _tmp = null; 151 } else { 152 _tmp = curs.getLong(0); 153 } 154 // convert Long to Date; 155 """.trimIndent())) 156 }.compilesWithoutError() 157 } 158 159 @Test 160 fun testIntList() { 161 singleRun { invocation -> 162 val binders = createIntListToStringBinders(invocation) 163 val store = TypeAdapterStore.create(Context(invocation.processingEnv), binders[0], 164 binders[1]) 165 166 val adapter = store.findColumnTypeAdapter(binders[0].from, null) 167 assertThat(adapter, notNullValue()) 168 169 val bindScope = testCodeGenScope() 170 adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope) 171 assertThat(bindScope.generate().trim(), `is`(""" 172 final java.lang.String ${tmp(0)}; 173 ${tmp(0)} = android.arch.persistence.room.util.StringUtil.joinIntoString(fooVar); 174 if (${tmp(0)} == null) { 175 stmt.bindNull(41); 176 } else { 177 stmt.bindString(41, ${tmp(0)}); 178 } 179 """.trimIndent())) 180 181 val converter = store.findTypeConverter(binders[0].from, 182 invocation.context.COMMON_TYPES.STRING) 183 assertThat(converter, notNullValue()) 184 assertThat(store.reverse(converter!!), `is`(binders[1])) 185 186 }.compilesWithoutError() 187 } 188 189 @Test 190 fun testOneWayConversion() { 191 singleRun { invocation -> 192 val binders = createIntListToStringBinders(invocation) 193 val store = TypeAdapterStore.create(Context(invocation.processingEnv), binders[0]) 194 val adapter = store.findColumnTypeAdapter(binders[0].from, null) 195 assertThat(adapter, nullValue()) 196 197 val stmtBinder = store.findStatementValueBinder(binders[0].from, null) 198 assertThat(stmtBinder, notNullValue()) 199 200 val converter = store.findTypeConverter(binders[0].from, 201 invocation.context.COMMON_TYPES.STRING) 202 assertThat(converter, notNullValue()) 203 assertThat(store.reverse(converter!!), nullValue()) 204 } 205 } 206 207 @Test 208 fun testMissingRxRoom() { 209 simpleRun(jfos = *arrayOf(COMMON.PUBLISHER, COMMON.FLOWABLE)) { invocation -> 210 val publisherElement = invocation.processingEnv.elementUtils 211 .getTypeElement(ReactiveStreamsTypeNames.PUBLISHER.toString()) 212 assertThat(publisherElement, notNullValue()) 213 assertThat(FlowableQueryResultBinderProvider(invocation.context).matches( 214 MoreTypes.asDeclared(publisherElement.asType())), `is`(true)) 215 }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_ROOM_RXJAVA2_ARTIFACT) 216 } 217 218 @Test 219 fun testFindPublisher() { 220 simpleRun(jfos = *arrayOf(COMMON.PUBLISHER, COMMON.FLOWABLE, COMMON.RX2_ROOM)) { 221 invocation -> 222 val publisher = invocation.processingEnv.elementUtils 223 .getTypeElement(ReactiveStreamsTypeNames.PUBLISHER.toString()) 224 assertThat(publisher, notNullValue()) 225 assertThat(FlowableQueryResultBinderProvider(invocation.context).matches( 226 MoreTypes.asDeclared(publisher.asType())), `is`(true)) 227 }.compilesWithoutError() 228 } 229 230 @Test 231 fun testFindFlowable() { 232 simpleRun(jfos = *arrayOf(COMMON.PUBLISHER, COMMON.FLOWABLE, COMMON.RX2_ROOM)) { 233 invocation -> 234 val flowable = invocation.processingEnv.elementUtils 235 .getTypeElement(RxJava2TypeNames.FLOWABLE.toString()) 236 assertThat(flowable, notNullValue()) 237 assertThat(FlowableQueryResultBinderProvider(invocation.context).matches( 238 MoreTypes.asDeclared(flowable.asType())), `is`(true)) 239 }.compilesWithoutError() 240 } 241 242 @Test 243 fun testFindLiveData() { 244 simpleRun(jfos = *arrayOf(COMMON.COMPUTABLE_LIVE_DATA, COMMON.LIVE_DATA)) { 245 invocation -> 246 val liveData = invocation.processingEnv.elementUtils 247 .getTypeElement(LifecyclesTypeNames.LIVE_DATA.toString()) 248 assertThat(liveData, notNullValue()) 249 assertThat(LiveDataQueryResultBinderProvider(invocation.context).matches( 250 MoreTypes.asDeclared(liveData.asType())), `is`(true)) 251 }.compilesWithoutError() 252 } 253 254 @Test 255 fun findDataSource() { 256 simpleRun { 257 invocation -> 258 val dataSource = invocation.processingEnv.elementUtils 259 .getTypeElement(DataSource::class.java.canonicalName) 260 assertThat(dataSource, notNullValue()) 261 assertThat(DataSourceQueryResultBinderProvider(invocation.context).matches( 262 MoreTypes.asDeclared(dataSource.asType())), `is`(true)) 263 }.failsToCompile().withErrorContaining(ProcessorErrors.PAGING_SPECIFY_DATA_SOURCE_TYPE) 264 } 265 266 @Test 267 fun findTiledDataSource() { 268 simpleRun { 269 invocation -> 270 val dataSource = invocation.processingEnv.elementUtils 271 .getTypeElement(TiledDataSource::class.java.canonicalName) 272 assertThat(dataSource, notNullValue()) 273 assertThat(DataSourceQueryResultBinderProvider(invocation.context).matches( 274 MoreTypes.asDeclared(dataSource.asType())), `is`(true)) 275 }.compilesWithoutError() 276 } 277 278 @Test 279 fun findPagedListProvider() { 280 simpleRun(jfos = COMMON.LIVE_PAGED_LIST_PROVIDER) { 281 invocation -> 282 val pagedListProvider = invocation.processingEnv.elementUtils 283 .getTypeElement(PagingTypeNames.LIVE_PAGED_LIST_PROVIDER.toString()) 284 assertThat(pagedListProvider, notNullValue()) 285 assertThat(LivePagedListQueryResultBinderProvider(invocation.context).matches( 286 MoreTypes.asDeclared(pagedListProvider.asType())), `is`(true)) 287 }.compilesWithoutError() 288 } 289 290 private fun createIntListToStringBinders(invocation: TestInvocation): List<TypeConverter> { 291 val intType = invocation.processingEnv.elementUtils 292 .getTypeElement(Integer::class.java.canonicalName) 293 .asType() 294 val listType = invocation.processingEnv.elementUtils 295 .getTypeElement(java.util.List::class.java.canonicalName) 296 val listOfInts = invocation.processingEnv.typeUtils.getDeclaredType(listType, intType) 297 298 val intListConverter = object : TypeConverter(listOfInts, 299 invocation.context.COMMON_TYPES.STRING) { 300 override fun convert(inputVarName: String, outputVarName: String, 301 scope: CodeGenScope) { 302 scope.builder().apply { 303 addStatement("$L = $T.joinIntoString($L)", outputVarName, STRING_UTIL, 304 inputVarName) 305 } 306 } 307 } 308 309 val stringToIntListConverter = object : TypeConverter( 310 invocation.context.COMMON_TYPES.STRING, listOfInts) { 311 override fun convert(inputVarName: String, outputVarName: String, 312 scope: CodeGenScope) { 313 scope.builder().apply { 314 addStatement("$L = $T.splitToIntList($L)", outputVarName, STRING_UTIL, 315 inputVarName) 316 } 317 } 318 } 319 return listOf(intListConverter, stringToIntListConverter) 320 } 321 322 fun singleRun(handler: (TestInvocation) -> Unit): CompileTester { 323 return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources()) 324 .that(listOf(JavaFileObjects.forSourceString("foo.bar.DummyClass", 325 """ 326 package foo.bar; 327 import android.arch.persistence.room.*; 328 @Entity 329 public class DummyClass {} 330 """ 331 ), JavaFileObjects.forSourceString("foo.bar.Point", 332 """ 333 package foo.bar; 334 import android.arch.persistence.room.*; 335 @Entity 336 public class Point { 337 public int x, y; 338 public Point(int x, int y) { 339 this.x = x; 340 this.y = y; 341 } 342 public static Point fromBoolean(boolean val) { 343 return val ? new Point(1, 1) : new Point(0, 0); 344 } 345 public static boolean toBoolean(Point point) { 346 return point.x > 0; 347 } 348 } 349 """ 350 ))) 351 .processedWith(TestProcessor.builder() 352 .forAnnotations(Entity::class) 353 .nextRunHandler { invocation -> 354 handler(invocation) 355 true 356 } 357 .build()) 358 } 359 360 fun pointTypeConverters(env: ProcessingEnvironment): List<TypeConverter> { 361 val tPoint = env.elementUtils.getTypeElement("foo.bar.Point").asType() 362 val tBoolean = env.typeUtils.getPrimitiveType(TypeKind.BOOLEAN) 363 return listOf( 364 object : TypeConverter(tPoint, tBoolean) { 365 override fun convert(inputVarName: String, outputVarName: String, 366 scope: CodeGenScope) { 367 scope.builder().apply { 368 addStatement("$L = $T.toBoolean($L)", outputVarName, from, inputVarName) 369 } 370 } 371 372 }, 373 object : TypeConverter(tBoolean, tPoint) { 374 override fun convert(inputVarName: String, outputVarName: String, 375 scope: CodeGenScope) { 376 scope.builder().apply { 377 addStatement("$L = $T.fromBoolean($L)", outputVarName, tPoint, 378 inputVarName) 379 } 380 } 381 } 382 ) 383 } 384 385 fun dateTypeConverters(env: ProcessingEnvironment): List<TypeConverter> { 386 val tDate = env.elementUtils.getTypeElement("java.util.Date").asType() 387 val tLong = env.elementUtils.getTypeElement("java.lang.Long").asType() 388 return listOf( 389 object : TypeConverter(tDate, tLong) { 390 override fun convert(inputVarName: String, outputVarName: String, 391 scope: CodeGenScope) { 392 scope.builder().apply { 393 addStatement("// convert Date to Long") 394 } 395 } 396 397 }, 398 object : TypeConverter(tLong, tDate) { 399 override fun convert(inputVarName: String, outputVarName: String, 400 scope: CodeGenScope) { 401 scope.builder().apply { 402 addStatement("// convert Long to Date") 403 } 404 } 405 } 406 ) 407 } 408} 409