blob: 169bdf9d6db27512dad7b0209b8a98cb070e0405 [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 COMMON
import androidx.room.DatabaseProcessingStep
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeElement
import androidx.room.compiler.processing.util.CompilationResultSubject
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.XTestInvocation
import androidx.room.compiler.processing.util.compileFiles
import androidx.room.compiler.processing.util.compileFilesIntoJar
import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.parser.ParsedQuery
import androidx.room.parser.QueryType
import androidx.room.parser.Table
import androidx.room.processor.ProcessorErrors.autoMigrationSchemasMustBeRoomGenerated
import androidx.room.solver.query.result.EntityRowAdapter
import androidx.room.solver.query.result.PojoRowAdapter
import androidx.room.testing.context
import androidx.room.vo.Database
import androidx.room.vo.DatabaseView
import androidx.room.vo.ReadQueryMethod
import androidx.room.vo.Warning
import com.google.auto.service.processor.AutoServiceProcessor
import com.google.common.truth.Truth.assertThat
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.TypeName
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.hasItems
import org.hamcrest.CoreMatchers.instanceOf
import org.hamcrest.CoreMatchers.not
import org.hamcrest.CoreMatchers.notNullValue
import org.hamcrest.CoreMatchers.sameInstance
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mockito.mock
import java.io.File
import java.io.FileOutputStream
import java.net.URL
import java.net.URLClassLoader
@RunWith(JUnit4::class)
class DatabaseProcessorTest {
companion object {
const val DATABASE_PREFIX = """
package foo.bar;
import androidx.room.*;
"""
val DB1 = Source.java(
"foo.bar.Db1",
"""
$DATABASE_PREFIX
@Database(entities = {Book.class}, version = 42)
public abstract class Db1 extends RoomDatabase {
abstract BookDao bookDao();
}
"""
)
val DB2 = Source.java(
"foo.bar.Db2",
"""
$DATABASE_PREFIX
@Database(entities = {Book.class}, version = 42)
public abstract class Db2 extends RoomDatabase {
abstract BookDao bookDao();
}
"""
)
val DB3 = Source.java(
"foo.bar.Db3",
"""
$DATABASE_PREFIX
@Database(entities = {Book.class}, version = 42)
public abstract class Db3 extends RoomDatabase {
}
"""
)
val USER = Source.java(
"foo.bar.User",
"""
package foo.bar;
import androidx.room.*;
@Entity
public class User {
@PrimaryKey
int uid;
String name;
}
"""
)
val USER_DAO = Source.java(
"foo.bar.UserDao",
"""
package foo.bar;
import androidx.room.*;
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
public java.util.List<User> loadAll();
@Insert
public void insert(User... users);
@Query("SELECT * FROM user where uid = :uid")
public User loadOne(int uid);
@TypeConverters(Converter.class)
@Query("SELECT * FROM user where uid = :uid")
public User loadWithConverter(int uid);
@Query("SELECT * FROM user where uid = :uid")
public Pojo loadOnePojo(int uid);
@Query("SELECT * FROM user")
public java.util.List<Pojo> loadAllPojos();
@TypeConverters(Converter.class)
@Query("SELECT * FROM user where uid = :uid")
public Pojo loadPojoWithConverter(int uid);
public static class Converter {
@TypeConverter
public static java.util.Date foo(Long input) {return null;}
}
public static class Pojo {
public int uid;
}
}
"""
)
val BOOK = Source.java(
"foo.bar.Book",
"""
package foo.bar;
import androidx.room.*;
@Entity
public class Book {
@PrimaryKey
int bookId;
}
"""
)
val BOOK_DAO = Source.java(
"foo.bar.BookDao",
"""
package foo.bar;
import androidx.room.*;
@Dao
public interface BookDao {
@Query("SELECT * FROM book")
public java.util.List<Book> loadAllBooks();
@Insert
public void insert(Book book);
}
"""
)
val AUTOMIGRATION = Source.java(
"foo.bar.MyAutoMigration",
"""
package foo.bar;
import androidx.annotation.NonNull;
import androidx.room.migration.AutoMigrationSpec;
import androidx.sqlite.db.SupportSQLiteDatabase;
class MyAutoMigration implements AutoMigrationSpec {
@Override
public void onPostMigrate(@NonNull SupportSQLiteDatabase db) {}
}
"""
)
}
@get:Rule
var schemaFolder: TemporaryFolder = TemporaryFolder()
@Test
fun simple() {
singleDb(
"""
@Database(entities = {User.class}, version = 42)
public abstract class MyDb extends RoomDatabase {
abstract UserDao userDao();
}
""",
USER, USER_DAO
) { db, _ ->
assertThat(db.daoMethods.size, `is`(1))
assertThat(db.entities.size, `is`(1))
}
}
@Test
fun multiple() {
singleDb(
"""
@Database(entities = {User.class, Book.class}, version = 42)
public abstract class MyDb extends RoomDatabase {
abstract UserDao userDao();
abstract BookDao bookDao();
}
""",
USER, USER_DAO, BOOK, BOOK_DAO
) { db, _ ->
assertThat(db.daoMethods.size, `is`(2))
assertThat(db.entities.size, `is`(2))
assertThat(db.daoMethods.map { it.element.jvmName }, `is`(listOf("userDao", "bookDao")))
assertThat(
db.entities.map { it.type.typeName.toString() },
`is`(listOf("foo.bar.User", "foo.bar.Book"))
)
}
}
@Test
fun detectMissingBaseClass() {
singleDb(
"""
@Database(entities = {User.class, Book.class}, version = 42)
public abstract class MyDb {
}
""",
USER, BOOK
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(ProcessorErrors.DB_MUST_EXTEND_ROOM_DB)
}
}
}
@Test
fun detectMissingTable() {
singleDb(
"""
@Database(entities = {Book.class}, version = 42)
public abstract class MyDb extends RoomDatabase {
abstract BookDao bookDao();
}
""",
BOOK,
Source.java(
"foo.bar.BookDao",
"""
package foo.bar;
import androidx.room.*;
@Dao
public interface BookDao {
@Query("SELECT * FROM nonExistentTable")
public java.util.List<Book> loadAllBooks();
}
"""
)
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining("no such table: nonExistentTable")
}
}
}
@Test
fun detectDuplicateTableNames() {
singleDb(
"""
@Database(entities = {User.class, AnotherClass.class}, version = 42)
public abstract class MyDb extends RoomDatabase {
abstract UserDao userDao();
}
""",
USER, USER_DAO,
Source.java(
"foo.bar.AnotherClass",
"""
package foo.bar;
import androidx.room.*;
@Entity(tableName="user")
public class AnotherClass {
@PrimaryKey
int uid;
}
"""
)
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.duplicateTableNames(
"user",
listOf("foo.bar.User", "foo.bar.AnotherClass")
)
)
}
}
}
@Test
fun detectMissingEntityAnnotationInLibraryClass() {
val librarySource = Source.java(
"test.library.MissingEntityAnnotationPojo",
"""
package test.library;
import androidx.room.*;
public class MissingEntityAnnotationPojo {
@PrimaryKey
private long id;
public void setId(int id) {this.id = id;}
public long getId() {return this.id;}
}
""".trimIndent()
)
val libraryClasspath = compileFiles(
sources = listOf(librarySource)
)
singleDb(
"""
@Database(entities = {test.library.MissingEntityAnnotationPojo.class}, version = 1)
public abstract class MyDb extends RoomDatabase {}
""",
classpath = libraryClasspath
) { _, invocation ->
invocation.assertCompilationResult {
compilationDidFail()
hasRawOutputContaining(
ProcessorErrors.ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY +
" - test.library.MissingEntityAnnotationPojo"
)
}
}
}
@Test
fun detectMissingDaoAnnotationInLibraryClass() {
val librarySource = Source.java(
"test.library.MissingAnnotationsBaseDao",
"""
package test.library;
import androidx.room.*;
public interface MissingAnnotationsBaseDao {
int getFoo();
}
""".trimIndent()
)
val libraryClasspath = compileFiles(
sources = listOf(librarySource)
)
singleDb(
"""
@Database(entities = {User.class}, version = 1)
public abstract class MyDb extends RoomDatabase {
abstract test.library.MissingAnnotationsBaseDao getBadDao();
}
""",
USER, classpath = libraryClasspath
) { _, invocation ->
invocation.assertCompilationResult {
compilationDidFail()
hasRawOutputContaining(
ProcessorErrors.DAO_MUST_BE_ANNOTATED_WITH_DAO +
" - test.library.MissingAnnotationsBaseDao"
)
}
}
}
@Test
fun detectMissingExternalContentEntity() {
val userNameFtsSrc = Source.java(
"foo.bar.UserNameFts",
"""
package foo.bar;
import androidx.room.*;
@Entity
@Fts4(contentEntity = User.class)
public class UserNameFts {
String name;
}
"""
)
singleDb(
"""
@Database(entities = {UserNameFts.class}, version = 1)
public abstract class MyDb extends RoomDatabase {}
""",
USER, userNameFtsSrc
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.missingExternalContentEntity(
"foo.bar.UserNameFts", "foo.bar.User"
)
)
}
}
}
@Test
fun skipBadQueryVerification() {
singleDb(
"""
@SkipQueryVerification
@Database(entities = {Book.class}, version = 42)
public abstract class MyDb extends RoomDatabase {
abstract BookDao bookDao();
}
""",
BOOK,
Source.java(
"foo.bar.BookDao",
"""
package foo.bar;
import androidx.room.*;
@Dao
public interface BookDao {
@Query("SELECT nonExistingField FROM Book")
public java.util.List<Book> loadAllBooks();
}
"""
)
) { _, _ ->
}
}
@Test
fun multipleDatabases() {
val db1_2 = Source.java(
"foo.barx.Db1",
"""
package foo.barx;
import androidx.room.*;
import foo.bar.*;
@Database(entities = {Book.class}, version = 42)
public abstract class Db1 extends RoomDatabase {
abstract BookDao bookDao();
}
"""
)
runProcessorTest(
sources = listOf(BOOK, BOOK_DAO, DB1, DB2, db1_2),
createProcessingStep = { DatabaseProcessingStep() }
) { result ->
result.generatedSourceFileWithPath("foo/bar/Db1_Impl.java")
result.generatedSourceFileWithPath("foo/bar/Db2_Impl.java")
result.generatedSourceFileWithPath("foo/barx/Db1_Impl.java")
result.generatedSourceFileWithPath("foo/bar/BookDao_Db1_0_Impl.java")
result.generatedSourceFileWithPath("foo/bar/BookDao_Db1_1_Impl.java")
result.generatedSourceFileWithPath("foo/bar/BookDao_Db2_Impl.java")
}
}
@Test
fun twoDaoMethodsForTheSameDao() {
singleDb(
"""
@Database(entities = {User.class}, version = 42)
public abstract class MyDb extends RoomDatabase {
abstract UserDao userDao();
abstract UserDao userDao2();
}
""",
USER, USER_DAO
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.DAO_METHOD_CONFLICTS_WITH_OTHERS
)
hasErrorContaining(
ProcessorErrors.duplicateDao(
ClassName.get("foo.bar", "UserDao"), listOf("userDao", "userDao2")
)
)
}
}
}
@Test
fun suppressedWarnings() {
singleDb(
"""
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
@Database(entities = {User.class}, version = 42)
public abstract class MyDb extends RoomDatabase {
abstract UserDao userDao();
}
""",
USER, USER_DAO
) { db, invocation ->
assertThat(
DatabaseProcessor(invocation.context, db.element)
.context.logger.suppressedWarnings,
`is`(setOf(Warning.CURSOR_MISMATCH))
)
}
}
@Test
fun duplicateIndexNames() {
val entity1 = Source.java(
"foo.bar.Entity1",
"""
package foo.bar;
import androidx.room.*;
@Entity(indices = {@Index(name ="index_name", value = {"name"})})
public class Entity1 {
@PrimaryKey
int uid;
String name;
}
"""
)
val entity2 = Source.java(
"foo.bar.Entity2",
"""
package foo.bar;
import androidx.room.*;
@Entity(indices = {@Index(name ="index_name", value = {"anotherName"})})
public class Entity2 {
@PrimaryKey
int uid;
String anotherName;
}
"""
)
singleDb(
"""
@Database(entities = {Entity1.class, Entity2.class}, version = 42)
public abstract class MyDb extends RoomDatabase {
}
""",
entity1, entity2
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.duplicateIndexInDatabase(
"index_name",
listOf("foo.bar.Entity1 > index_name", "foo.bar.Entity2 > index_name")
)
)
}
}
}
@Test
fun foreignKey_missingParent() {
val entity1 = Source.java(
"foo.bar.Entity1",
"""
package foo.bar;
import androidx.room.*;
@Entity(foreignKeys = @ForeignKey(entity = ${COMMON.USER_TYPE_NAME}.class,
parentColumns = "lastName",
childColumns = "name"))
public class Entity1 {
@PrimaryKey
int uid;
String name;
}
"""
)
singleDb(
"""
@Database(entities = {Entity1.class}, version = 42)
public abstract class MyDb extends RoomDatabase {
}
""",
entity1, COMMON.USER
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.foreignKeyMissingParentEntityInDatabase(
"User",
"foo.bar.Entity1"
)
)
}
}
}
@Test
fun foreignKey_missingParentIndex() {
val entity1 = Source.java(
"foo.bar.Entity1",
"""
package foo.bar;
import androidx.room.*;
@Entity(foreignKeys = @ForeignKey(entity = ${COMMON.USER_TYPE_NAME}.class,
parentColumns = "lastName",
childColumns = "name"))
public class Entity1 {
@PrimaryKey
int uid;
String name;
}
"""
)
singleDb(
"""
@Database(entities = {Entity1.class, User.class}, version = 42)
public abstract class MyDb extends RoomDatabase {
}
""",
entity1, COMMON.USER
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.foreignKeyMissingIndexInParent(
parentEntity = COMMON.USER_TYPE_NAME.toString(),
parentColumns = listOf("lastName"),
childEntity = "foo.bar.Entity1",
childColumns = listOf("name")
)
)
}
}
}
@Test
fun foreignKey_goodWithPrimaryKey() {
val entity1 = Source.java(
"foo.bar.Entity1",
"""
package foo.bar;
import androidx.room.*;
@Entity(foreignKeys = @ForeignKey(entity = Entity2.class,
parentColumns = "uid",
childColumns = "parentId"))
public class Entity1 {
@PrimaryKey
int uid;
int parentId;
String name;
}
"""
)
val entity2 = Source.java(
"foo.bar.Entity2",
"""
package foo.bar;
import androidx.room.*;
@Entity
public class Entity2 {
@PrimaryKey
int uid;
String anotherName;
}
"""
)
singleDb(
"""
@Database(entities = {Entity1.class, Entity2.class}, version = 42)
public abstract class MyDb extends RoomDatabase {
}
""",
entity1, entity2
) { _, _ ->
}
}
@Test
fun foreignKey_missingParentColumn() {
val entity1 = Source.java(
"foo.bar.Entity1",
"""
package foo.bar;
import androidx.room.*;
@Entity(foreignKeys = @ForeignKey(entity = Entity2.class,
parentColumns = {"anotherName", "anotherName2"},
childColumns = {"name", "name2"}))
public class Entity1 {
@PrimaryKey
int uid;
String name;
String name2;
}
"""
)
val entity2 = Source.java(
"foo.bar.Entity2",
"""
package foo.bar;
import androidx.room.*;
@Entity
public class Entity2 {
@PrimaryKey
int uid;
String anotherName;
}
"""
)
singleDb(
"""
@Database(entities = {Entity1.class, Entity2.class}, version = 42)
public abstract class MyDb extends RoomDatabase {
}
""",
entity1, entity2
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.foreignKeyParentColumnDoesNotExist(
"foo.bar.Entity2",
"anotherName2", listOf("uid", "anotherName")
)
)
}
}
}
@Test
fun foreignKey_goodWithIndex() {
val entity1 = Source.java(
"foo.bar.Entity1",
"""
package foo.bar;
import androidx.room.*;
@Entity(foreignKeys = @ForeignKey(entity = Entity2.class,
parentColumns = {"anotherName", "anotherName2"},
childColumns = {"name", "name2"}))
public class Entity1 {
@PrimaryKey
int uid;
String name;
String name2;
}
"""
)
val entity2 = Source.java(
"foo.bar.Entity2",
"""
package foo.bar;
import androidx.room.*;
@Entity(indices = @Index(value = {"anotherName2", "anotherName"}, unique = true))
public class Entity2 {
@PrimaryKey
int uid;
String anotherName;
String anotherName2;
}
"""
)
singleDb(
"""
@Database(entities = {Entity1.class, Entity2.class}, version = 42)
public abstract class MyDb extends RoomDatabase {
}
""",
entity1, entity2
) { _, _ ->
}
}
@Test
fun insertNotAReferencedEntity() {
singleDb(
"""
@Database(entities = {User.class}, version = 42)
public abstract class MyDb extends RoomDatabase {
abstract BookDao bookDao();
}
""",
USER, USER_DAO, BOOK, BOOK_DAO
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.shortcutEntityIsNotInDatabase(
database = "foo.bar.MyDb",
dao = "foo.bar.BookDao",
entity = "foo.bar.Book"
)
)
}
}
}
@Test
fun cache_entity() {
singleDb(
"""
@Database(entities = {User.class}, version = 42)
@SkipQueryVerification
public abstract class MyDb extends RoomDatabase {
public abstract MyUserDao userDao();
@Dao
interface MyUserDao {
@Insert
public void insert(User... users);
@Query("SELECT * FROM user where uid = :uid")
public User loadOne(int uid);
@TypeConverters(Converter.class)
@Query("SELECT * FROM user where uid = :uid")
public User loadWithConverter(int uid);
}
public static class Converter {
@TypeConverter
public static java.util.Date foo(Long input) {return null;}
}
}
""",
USER, USER_DAO
) { db, _ ->
val userDao = db.daoMethods.first().dao
val insertionMethod = userDao.insertionMethods.find { it.element.jvmName == "insert" }
assertThat(insertionMethod, notNullValue())
val loadOne = userDao.queryMethods
.filterIsInstance<ReadQueryMethod>()
.find { it.element.jvmName == "loadOne" }
assertThat(loadOne, notNullValue())
val adapter = loadOne?.queryResultBinder?.adapter?.rowAdapters?.single()
assertThat("test sanity", adapter, instanceOf(EntityRowAdapter::class.java))
val adapterEntity = (adapter as EntityRowAdapter).entity
assertThat(
adapterEntity,
sameInstance(insertionMethod?.entities?.values?.first()?.pojo)
)
val withConverter = userDao.queryMethods
.filterIsInstance<ReadQueryMethod>()
.find { it.element.jvmName == "loadWithConverter" }
assertThat(withConverter, notNullValue())
val convAdapter = withConverter?.queryResultBinder?.adapter?.rowAdapters?.single()
assertThat("test sanity", adapter, instanceOf(EntityRowAdapter::class.java))
val convAdapterEntity = (convAdapter as EntityRowAdapter).entity
assertThat(
convAdapterEntity,
not(sameInstance(insertionMethod?.entities?.values?.first()?.pojo))
)
assertThat(convAdapterEntity, notNullValue())
assertThat(adapterEntity, notNullValue())
}
}
@Test
fun cache_pojo() {
singleDb(
"""
@Database(entities = {User.class}, version = 42)
public abstract class MyDb extends RoomDatabase {
public abstract UserDao userDao();
}
""",
USER, USER_DAO
) { db, _ ->
val userDao = db.daoMethods.first().dao
val loadOne = userDao.queryMethods
.filterIsInstance<ReadQueryMethod>()
.find { it.element.jvmName == "loadOnePojo" }
assertThat(loadOne, notNullValue())
val adapter = loadOne?.queryResultBinder?.adapter?.rowAdapters?.single()
assertThat("test sanity", adapter, instanceOf(PojoRowAdapter::class.java))
val adapterPojo = (adapter as PojoRowAdapter).pojo
val loadAll = userDao.queryMethods
.filterIsInstance<ReadQueryMethod>()
.find { it.element.jvmName == "loadAllPojos" }
assertThat(loadAll, notNullValue())
val loadAllAdapter = loadAll?.queryResultBinder?.adapter?.rowAdapters?.single()
assertThat("test sanity", loadAllAdapter, instanceOf(PojoRowAdapter::class.java))
val loadAllPojo = (loadAllAdapter as PojoRowAdapter).pojo
assertThat(adapter, not(sameInstance(loadAllAdapter)))
assertThat(adapterPojo, sameInstance(loadAllPojo))
val withConverter = userDao.queryMethods
.filterIsInstance<ReadQueryMethod>()
.find { it.element.jvmName == "loadPojoWithConverter" }
assertThat(withConverter, notNullValue())
val convAdapter = withConverter?.queryResultBinder?.adapter?.rowAdapters?.single()
assertThat("test sanity", adapter, instanceOf(PojoRowAdapter::class.java))
val convAdapterPojo = (convAdapter as PojoRowAdapter).pojo
assertThat(convAdapterPojo, notNullValue())
assertThat(convAdapterPojo, not(sameInstance(adapterPojo)))
}
}
@Test
fun daoConstructor_RoomDatabase() {
assertConstructor(listOf(DB1), "BookDao(RoomDatabase db) {}")
}
@Test
fun daoConstructor_specificDatabase() {
assertConstructor(listOf(DB1), "BookDao(Db1 db) {}")
}
@Test
fun daoConstructor_wrongDatabase() {
assertConstructor(listOf(DB1, DB3), "BookDao(Db3 db) {}") { result ->
result.hasErrorContaining(
ProcessorErrors
.daoMustHaveMatchingConstructor("foo.bar.BookDao", "foo.bar.Db1")
)
}
}
@Test
fun daoConstructor_multipleDatabases_RoomDatabase() {
assertConstructor(listOf(DB1, DB2), "BookDao(RoomDatabase db) {}")
}
@Test
fun daoConstructor_multipleDatabases_specificDatabases() {
assertConstructor(
listOf(DB1, DB2),
"""
BookDao(Db1 db) {}
BookDao(Db2 db) {}
"""
)
}
@Test
fun daoConstructor_multipleDatabases_empty() {
assertConstructor(
listOf(DB1, DB2),
"""
BookDao(Db1 db) {}
BookDao() {} // Db2 uses this
"""
)
}
@Test
fun daoConstructor_multipleDatabases_noMatch() {
assertConstructor(
listOf(DB1, DB2),
"""
BookDao(Db1 db) {}
"""
) { result ->
result.hasErrorContaining(
ProcessorErrors
.daoMustHaveMatchingConstructor("foo.bar.BookDao", "foo.bar.Db2")
)
}
}
@Test
fun view_duplicateNames() {
val view1 = Source.java(
"foo.bar.View1",
"""
package foo.bar;
import androidx.room.*;
@DatabaseView(value = "SELECT * FROM User", viewName = "SameName")
public class View1 {}
"""
)
val view2 = Source.java(
"foo.bar.View2",
"""
package foo.bar;
import androidx.room.*;
@DatabaseView(value = "SELECT * FROM User", viewName = "SameName")
public class View2 {}
"""
)
singleDb(
"""
@Database(entities = {User.class},
views = {View1.class, View2.class},
version = 42)
public abstract class MyDb extends RoomDatabase {
}
""",
USER, view1, view2
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.duplicateTableNames(
"samename",
listOf("foo.bar.View1", "foo.bar.View2")
)
)
}
}
}
@Test
fun view_duplicateNamesWithEntity() {
val view1 = Source.java(
"foo.bar.View1",
"""
package foo.bar;
import androidx.room.*;
@DatabaseView(value = "SELECT * FROM User", viewName = "Book")
public class View1 {}
"""
)
singleDb(
"""
@Database(entities = {User.class, Book.class},
views = {View1.class},
version = 42)
public abstract class MyDb extends RoomDatabase {
}
""",
USER, BOOK, view1
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.duplicateTableNames(
"book",
listOf("foo.bar.Book", "foo.bar.View1")
)
)
}
}
}
@Test
fun view_circularReference() {
val view1 = Source.java(
"foo.bar.View1",
"""
package foo.bar;
import androidx.room.*;
@DatabaseView("SELECT * FROM View2")
public class View1 {}
"""
)
val view2 = Source.java(
"foo.bar.View2",
"""
package foo.bar;
import androidx.room.*;
@DatabaseView("SELECT * FROM View1")
public class View2 {}
"""
)
singleDb(
"""
@Database(entities = {User.class},
views = {View1.class, View2.class},
version = 42)
public abstract class MyDb extends RoomDatabase {
}
""",
USER, view1, view2
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.viewCircularReferenceDetected(listOf("View1", "View2"))
)
}
}
}
@Test
fun resolveDatabaseViews_nested() {
resolveDatabaseViews(
mapOf(
"Q" to setOf("B", "P"),
"P" to setOf("A"),
"S" to setOf("A", "Q"),
"R" to setOf("C", "Q")
)
) { views, _ ->
assertThat(views.size, `is`(4))
views[0].let {
assertThat(it.viewName, `is`(equalTo("P")))
assertThat(it.tables.size, `is`(1))
assertThat(it.tables, hasItems("A"))
}
views[1].let {
assertThat(it.viewName, `is`(equalTo("Q")))
assertThat(it.tables.size, `is`(2))
assertThat(it.tables, hasItems("A", "B"))
}
// The order here is not important.
val (viewR, viewS) = if (views[2].viewName == "R") {
listOf(views[2], views[3])
} else {
listOf(views[3], views[2])
}
viewR.let {
assertThat(it.viewName, `is`(equalTo("R")))
assertThat(it.tables.size, `is`(3))
assertThat(it.tables, hasItems("A", "B", "C"))
}
viewS.let {
assertThat(it.viewName, `is`(equalTo("S")))
assertThat(it.tables.size, `is`(2))
assertThat(it.tables, hasItems("A", "B"))
}
}
}
@Test
fun resolveDatabaseViews_empty() {
resolveDatabaseViews(emptyMap()) { views, _ ->
assertThat(views.size, `is`(0))
}
}
@Test
fun resolveDatabaseViews_circular() {
resolveDatabaseViews(
mapOf(
"P" to setOf("Q"),
"Q" to setOf("P"),
"R" to setOf("A"),
"S" to setOf("R", "B")
)
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.viewCircularReferenceDetected(listOf("P", "Q"))
)
}
}
}
@Test
fun daoMethod_nonDeclaredReturnType() {
val badDaoType = Source.java(
"foo.bar.MyDb",
"""
package foo.bar;
import androidx.room.*;
@Database(version = 1, entities = {})
public abstract class MyDb extends RoomDatabase {
abstract long getDao();
}
"""
)
runProcessorTest(sources = listOf(badDaoType)) { invocation ->
val element = invocation.processingEnv.requireTypeElement("foo.bar.MyDb")
val result = DatabaseProcessor(
baseContext = invocation.context,
element = element
).process()
assertThat(result.daoMethods).hasSize(
// for KSP, it will still show as a method, just bad return type
if (invocation.isKsp) 1 else 0
)
invocation.assertCompilationResult {
hasErrorContaining(
if (invocation.isKsp) {
// no primitives in KSP hence we'll get another error
ProcessorErrors.DAO_MUST_BE_ANNOTATED_WITH_DAO
} else {
ProcessorErrors.DATABASE_INVALID_DAO_METHOD_RETURN_TYPE
}
)
}
}
}
@Test
fun nonDeclaredEntity() {
val badDaoType = Source.java(
"foo.bar.MyDb",
"""
package foo.bar;
import androidx.room.*;
@Database(version = 1, entities = {long.class}, views = {int.class})
public abstract class MyDb extends RoomDatabase {
}
"""
)
runProcessorTest(listOf(badDaoType)) { invocation ->
val element = invocation.processingEnv.requireTypeElement("foo.bar.MyDb")
val result = DatabaseProcessor(
baseContext = invocation.context,
element = element
).process()
assertThat(result.entities).hasSize(
if (invocation.isKsp) 1 else 0
)
assertThat(result.views).hasSize(
if (invocation.isKsp) 1 else 0
)
if (invocation.isKsp) {
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY
)
hasErrorContaining(
ProcessorErrors.VIEW_MUST_BE_ANNOTATED_WITH_DATABASE_VIEW
)
}
} else {
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.invalidEntityTypeInDatabaseAnnotation(
TypeName.LONG
)
)
hasErrorContaining(
ProcessorErrors.invalidViewTypeInDatabaseAnnotation(
TypeName.INT
)
)
}
}
}
}
@Test
fun autoMigrationDefinedButDatabaseSchemaExportOff() {
singleDb(
"""
@Database(entities = {User.class}, version = 42, exportSchema = false,
autoMigrations = {@AutoMigration(from = 1, to = 2, spec = MyAutoMigration
.class), @AutoMigration(from = 2, to = 3)})
public abstract class MyDb extends RoomDatabase {}
""",
USER, AUTOMIGRATION
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorContaining(
ProcessorErrors.AUTO_MIGRATION_FOUND_BUT_EXPORT_SCHEMA_OFF
)
}
}
}
@Test
fun autoMigrationSchemasNotFound() {
singleDb(
"""
@Database(entities = {User.class}, version = 42, exportSchema = true,
autoMigrations = {@AutoMigration(from = 1, to = 2, spec = MyAutoMigration
.class)})
public abstract class MyDb extends RoomDatabase {}
""",
USER, AUTOMIGRATION
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorCount(1)
hasErrorContaining(
ProcessorErrors.autoMigrationSchemasNotFound(
"1.json",
schemaFolder.root.absolutePath + File.separator + "foo.bar.MyDb"
)
)
}
}
}
@Test
fun autoMigrationFromSchemaNotFound() {
schemaFolder.newFolder("foo.bar.MyDb")
schemaFolder.newFile("foo.bar.MyDb" + File.separator + "2.json")
singleDb(
"""
@Database(entities = {User.class}, version = 42, exportSchema = true,
autoMigrations = {@AutoMigration(from = 1, to = 2, spec = MyAutoMigration
.class)})
public abstract class MyDb extends RoomDatabase {}
""",
USER, AUTOMIGRATION,
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorCount(1)
hasErrorContaining(
ProcessorErrors.autoMigrationSchemasNotFound(
"1.json",
schemaFolder.root.absolutePath + File.separator + "foo.bar.MyDb"
)
)
}
}
}
@Test
fun autoMigrationToSchemaNotFound() {
schemaFolder.newFolder("foo.bar.MyDb")
val createdFile: File = schemaFolder.newFile("foo.bar.MyDb" + File.separator + "1.json")
FileOutputStream(createdFile).bufferedWriter().use {
it.write("{}")
}
singleDb(
"""
@Database(entities = {User.class}, version = 42, exportSchema = true,
autoMigrations = {@AutoMigration(from = 1, to = 2, spec = MyAutoMigration
.class)})
public abstract class MyDb extends RoomDatabase {}
""",
USER, AUTOMIGRATION
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorCount(1)
hasErrorContaining(
ProcessorErrors.autoMigrationSchemasNotFound(
"2.json",
schemaFolder.root.absolutePath + File.separator + "foo.bar.MyDb"
)
)
}
}
}
@Test
fun autoMigrationEmptySchemaFiles() {
schemaFolder.newFolder("foo.bar.MyDb")
schemaFolder.newFile("foo.bar.MyDb" + File.separator + "1.json")
schemaFolder.newFile("foo.bar.MyDb" + File.separator + "2.json")
singleDb(
"""
@Database(entities = {User.class}, version = 42, exportSchema = true,
autoMigrations = {@AutoMigration(from = 1, to = 2, spec = MyAutoMigration
.class)})
public abstract class MyDb extends RoomDatabase {}
""",
USER, AUTOMIGRATION,
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorCount(1)
hasErrorContaining(
ProcessorErrors.autoMigrationSchemaIsEmpty(
"1.json",
schemaFolder.root.absolutePath + File.separator + "foo.bar.MyDb"
)
)
}
}
}
@Test
fun allAutoMigrationSchemasProvidedButNotRoomGenerated() {
schemaFolder.newFolder("foo.bar.MyDb")
val from: File = schemaFolder.newFile("foo.bar.MyDb" + File.separator + "1.json")
val to: File = schemaFolder.newFile("foo.bar.MyDb" + File.separator + "2.json")
FileOutputStream(from).bufferedWriter().use {
it.write("{}")
}
FileOutputStream(to).bufferedWriter().use {
it.write("{}")
}
singleDb(
"""
@Database(entities = {User.class}, version = 42, exportSchema = true,
autoMigrations = {@AutoMigration(from = 1, to = 2)})
public abstract class MyDb extends RoomDatabase {}
""",
USER, AUTOMIGRATION,
) { _, invocation ->
invocation.assertCompilationResult {
hasErrorCount(1)
hasErrorContaining(
autoMigrationSchemasMustBeRoomGenerated(1, 2)
)
}
}
}
@Test
fun differentSchemaResolver() {
val tempDirPath = schemaFolder.root.absolutePath
val serviceSource = Source.java(
"foo.bar.TestResolver",
"""
package foo.bar;
import androidx.room.util.SchemaFileResolver;
import com.google.auto.service.AutoService;
import java.io.File;
import java.nio.file.Path;
@AutoService(SchemaFileResolver.class)
public class TestResolver implements SchemaFileResolver {
@Override
public File getFile(Path path) {
return Path.of("$tempDirPath").resolve(path).toFile();
}
}
""".trimIndent()
)
val resolverLib = compileFilesIntoJar(
outputDirectory = schemaFolder.root,
sources = listOf(serviceSource),
annotationProcessors = listOf(AutoServiceProcessor()),
)
val dbSource = Source.java(
"foo.bar.MyDb",
"""
package foo.bar;
import androidx.room.*;
@Database(entities = {User.class}, version = 1, exportSchema = true)
public abstract class MyDb extends RoomDatabase {}
""".trimIndent()
)
runProcessorTest(
sources = listOf(dbSource, USER),
options = mapOf("room.schemaLocation" to "schemas/")
) { invocation ->
val dbAnnotationName = "androidx.room.Database"
val roundElements = mapOf(
dbAnnotationName to invocation.roundEnv.getElementsAnnotatedWith(dbAnnotationName)
)
// Temporarily change the current's thread class loader so that the ServiceLocator can
// find the compiled TestResolver. It is not enough to pass the compiled jar in the
// classpath argument when running processors because the processor classpath is
// different from the user classpath and specifically the processor classes are already
// loaded.
val currentClassLoader = Thread.currentThread().contextClassLoader
try {
Thread.currentThread().contextClassLoader = URLClassLoader(
arrayOf(URL("file://${resolverLib.absolutePath}")),
currentClassLoader,
)
DatabaseProcessingStep().process(invocation.processingEnv, roundElements)
} finally {
Thread.currentThread().contextClassLoader = currentClassLoader
}
}
// Assert schema file is created in a directory different from the room.schemaLocation
// verbatim since it was resolved by the compiled TestResolver.
assertThat(
File(schemaFolder.root, "schemas/foo.bar.MyDb/1.json").exists()
).isTrue()
}
private fun resolveDatabaseViews(
views: Map<String, Set<String>>,
body: (List<DatabaseView>, XTestInvocation) -> Unit
) {
runProcessorTest(
sources = listOf(DB3, BOOK)
) { invocation ->
val database = invocation.roundEnv
.getElementsAnnotatedWith(
androidx.room.Database::class.qualifiedName!!
)
.filterIsInstance<XTypeElement>()
.first()
val processor = DatabaseProcessor(
invocation.context,
database
)
val list = views.map { (viewName, names) ->
DatabaseView(
element = mock(XTypeElement::class.java),
viewName = viewName,
query = ParsedQuery(
"", QueryType.SELECT, emptyList(),
names.map { Table(it, it) }.toSet(), null,
emptyList()
),
type = mock(XType::class.java),
fields = emptyList(),
embeddedFields = emptyList(),
constructor = null
)
}
val resolvedViews = processor.resolveDatabaseViews(list)
body(resolvedViews, invocation)
}
}
private fun assertConstructor(
dbs: List<Source>,
constructor: String,
onCompilationResult: ((result: CompilationResultSubject) -> Unit)? = null
) {
val bookDao = Source.java(
"foo.bar.BookDao",
"""
package foo.bar;
import androidx.room.*;
@Dao
public abstract class BookDao {
$constructor
}
"""
)
runProcessorTest(
sources = listOf(BOOK, bookDao) + dbs,
createProcessingStep = { DatabaseProcessingStep() },
) {
onCompilationResult?.invoke(it)
}
}
fun singleDb(
input: String,
vararg otherFiles: Source,
classpath: List<File> = emptyList(),
handler: (Database, XTestInvocation) -> Unit
) {
runProcessorTest(
sources = otherFiles.toList() +
Source.java(
"foo.bar.MyDb",
DATABASE_PREFIX + input
),
classpath = classpath,
options = mapOf(
"room.schemaLocation" to schemaFolder.root.absolutePath
)
) { invocation ->
val entity = invocation.roundEnv
.getElementsAnnotatedWith(
androidx.room.Database::class.qualifiedName!!
)
.filterIsInstance<XTypeElement>()
.first()
val parser = DatabaseProcessor(
invocation.context,
entity
)
val parsedDb = parser.process()
handler(parsedDb, invocation)
}
}
}