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