| /* |
| * Copyright 2020 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.sqlite.inspection.test |
| |
| import android.database.sqlite.SQLiteDatabase |
| import androidx.sqlite.inspection.test.MessageFactory.createGetSchemaCommand |
| import androidx.sqlite.inspection.test.MessageFactory.createTrackDatabasesCommand |
| import androidx.test.ext.junit.runners.AndroidJUnit4 |
| import androidx.test.filters.MediumTest |
| import androidx.test.filters.SdkSuppress |
| import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation |
| import com.google.common.truth.Truth.assertThat |
| import kotlinx.coroutines.runBlocking |
| import org.junit.Rule |
| import org.junit.Test |
| import org.junit.rules.TemporaryFolder |
| import org.junit.runner.RunWith |
| |
| @MediumTest |
| @RunWith(AndroidJUnit4::class) |
| @SdkSuppress(minSdkVersion = 26) |
| class GetSchemaTest { |
| @get:Rule |
| val testEnvironment = SqliteInspectorTestEnvironment() |
| |
| @get:Rule |
| val temporaryFolder = TemporaryFolder(getInstrumentation().context.cacheDir) |
| |
| @Test |
| fun test_get_schema_complex_tables() { |
| test_get_schema( |
| listOf( |
| Database( |
| "db1", |
| Table( |
| "table1", |
| Column("t", "TEXT"), |
| Column("nu", "NUMERIC"), |
| Column("i", "INTEGER"), |
| Column("r", "REAL"), |
| Column("b", "BLOB") |
| ), |
| Table( |
| "table2", |
| Column("id", "INTEGER"), |
| Column("name", "TEXT") |
| |
| ), |
| Table( |
| "table3a", |
| Column("c1", "INT"), |
| Column("c2", "INT", primaryKey = 1) |
| ), |
| Table( |
| "table3b", // compound-primary-key |
| Column("c1", "INT", primaryKey = 2), |
| Column("c2", "INT", primaryKey = 1) |
| ), |
| Table( |
| "table4", // compound-primary-key, two unique columns |
| Column("c1", "INT", primaryKey = 1), |
| Column("c2", "INT", primaryKey = 2, isUnique = true), |
| Column("c3", "INT", isUnique = true) |
| ), |
| Table( |
| "table5", // mix: unique, primary key, notNull |
| Column("c1", "INT", isNotNull = true), |
| Column("c2", "INT", primaryKey = 1, isUnique = true), |
| Column("c3", "INT", isUnique = true, isNotNull = true) |
| ), |
| Table( |
| "table6", // compound-unique-constraint-indices in [onDatabaseCreated] |
| Column("c1", "INT"), |
| Column("c2uuu", "INT", isUnique = true), |
| Column("c3", "INT"), |
| Column("c4u", "INT", isUnique = true) |
| ) |
| ) |
| ), |
| onDatabaseCreated = { db -> |
| // compound-unique-constraint-indices |
| listOf( |
| "create index index6_12 on 'table6' ('c1', 'c2uuu')", |
| "create index index6_23 on 'table6' ('c2uuu', 'c3')" |
| ).forEach { query -> |
| db.execSQL(query, emptyArray()) |
| } |
| |
| // sanity check: verifies if the above index adding operations succeeded |
| val indexCountTable6 = |
| db.rawQuery("select count(*) from pragma_index_list('table6')", null).let { |
| it.moveToNext() |
| val count = it.getString(0) |
| it.close() |
| count |
| } |
| |
| assertThat(indexCountTable6).isEqualTo("4") |
| } |
| ) |
| } |
| |
| @Test |
| fun test_get_schema_multiple_databases() { |
| test_get_schema( |
| listOf( |
| Database("db3", Table("t3", Column("c3", "BLOB"))), |
| Database("db2", Table("t2", Column("c2", "TEXT"))), |
| Database("db1", Table("t1", Column("c1", "TEXT"))) |
| ) |
| ) |
| } |
| |
| @Test |
| fun test_get_schema_wrong_database_id() = runBlocking { |
| val databaseId = 123456789 |
| testEnvironment.sendCommand(createGetSchemaCommand(databaseId)).let { response -> |
| assertThat(response.hasErrorOccurred()).isEqualTo(true) |
| val error = response.errorOccurred.content |
| assertThat(error.message).isEqualTo("No database with id=$databaseId") |
| assertThat(error.recoverability.isRecoverable).isEqualTo(true) |
| } |
| } |
| |
| private fun test_get_schema( |
| alreadyOpenDatabases: List<Database>, |
| onDatabaseCreated: (SQLiteDatabase) -> Unit = {} |
| ) = |
| runBlocking { |
| assertThat(alreadyOpenDatabases).isNotEmpty() // sanity check |
| |
| testEnvironment.registerAlreadyOpenDatabases(alreadyOpenDatabases.map { |
| it.createInstance(temporaryFolder).also { db -> onDatabaseCreated(db) } |
| }) |
| testEnvironment.sendCommand(createTrackDatabasesCommand()) |
| val databaseConnections = |
| alreadyOpenDatabases.indices.map { testEnvironment.receiveEvent().databaseOpened } |
| |
| val schemas = |
| databaseConnections |
| .sortedBy { it.name } |
| .map { |
| testEnvironment.sendCommand(createGetSchemaCommand(it.databaseId)).getSchema |
| } |
| |
| alreadyOpenDatabases |
| .sortedBy { it.name } |
| .zipSameSize(schemas) |
| .forEach { (expectedSchema, actualSchema) -> |
| val expectedTables = expectedSchema.tables.sortedBy { it.name } |
| val actualTables = actualSchema.tablesList.sortedBy { it.name } |
| |
| expectedTables |
| .zipSameSize(actualTables) |
| .forEach { (expectedTable, actualTable) -> |
| assertThat(actualTable.name).isEqualTo(expectedTable.name) |
| |
| val expectedColumns = expectedTable.columns.sortedBy { it.name } |
| val actualColumns = actualTable.columnsList.sortedBy { it.name } |
| |
| expectedColumns |
| .adjustForSinglePrimaryKey() |
| .zipSameSize(actualColumns) |
| .forEach { (expectedColumn, actualColumnProto) -> |
| val actualColumn = Column( |
| name = actualColumnProto.name, |
| type = actualColumnProto.type, |
| primaryKey = actualColumnProto.primaryKey, |
| isNotNull = actualColumnProto.isNotNull, |
| isUnique = actualColumnProto.isUnique |
| ) |
| assertThat(actualColumn).isEqualTo(expectedColumn) |
| } |
| } |
| } |
| } |
| |
| // The sole primary key in a table is by definition unique |
| private fun List<Column>.adjustForSinglePrimaryKey(): List<Column> = |
| if (this.count { it.isPrimaryKey } > 1) this |
| else this.map { |
| if (it.isPrimaryKey) it.copy(isUnique = true) |
| else it |
| } |
| |
| /** Same as [List.zip] but ensures both lists are the same size. */ |
| private fun <A, B> List<A>.zipSameSize(other: List<B>): List<Pair<A, B>> { |
| assertThat(this.size).isEqualTo(other.size) |
| return this.zip(other) |
| } |
| } |