Jakub Gielzak | 45513aa | 2020-01-03 14:16:12 +0000 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2020 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 | |
| 17 | package androidx.sqlite.inspection.test |
| 18 | |
Jakub Gielzak | 21ca398 | 2020-02-07 15:10:33 +0000 | [diff] [blame] | 19 | import android.database.sqlite.SQLiteDatabase |
Jakub Gielzak | 45513aa | 2020-01-03 14:16:12 +0000 | [diff] [blame] | 20 | import androidx.sqlite.inspection.test.MessageFactory.createGetSchemaCommand |
| 21 | import androidx.sqlite.inspection.test.MessageFactory.createTrackDatabasesCommand |
| 22 | import androidx.test.ext.junit.runners.AndroidJUnit4 |
| 23 | import androidx.test.filters.MediumTest |
Jakub Gielzak | 21ca398 | 2020-02-07 15:10:33 +0000 | [diff] [blame] | 24 | import androidx.test.filters.SdkSuppress |
Jakub Gielzak | b5456bf | 2020-01-13 17:39:03 +0000 | [diff] [blame] | 25 | import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation |
Jakub Gielzak | 45513aa | 2020-01-03 14:16:12 +0000 | [diff] [blame] | 26 | import com.google.common.truth.Truth.assertThat |
Jakub Gielzak | 45513aa | 2020-01-03 14:16:12 +0000 | [diff] [blame] | 27 | import kotlinx.coroutines.runBlocking |
| 28 | import org.junit.Rule |
| 29 | import org.junit.Test |
| 30 | import org.junit.rules.TemporaryFolder |
| 31 | import org.junit.runner.RunWith |
| 32 | |
| 33 | @MediumTest |
| 34 | @RunWith(AndroidJUnit4::class) |
Jakub Gielzak | 21ca398 | 2020-02-07 15:10:33 +0000 | [diff] [blame] | 35 | @SdkSuppress(minSdkVersion = 26) |
Jakub Gielzak | 45513aa | 2020-01-03 14:16:12 +0000 | [diff] [blame] | 36 | class GetSchemaTest { |
| 37 | @get:Rule |
| 38 | val testEnvironment = SqliteInspectorTestEnvironment() |
| 39 | |
| 40 | @get:Rule |
Jakub Gielzak | b5456bf | 2020-01-13 17:39:03 +0000 | [diff] [blame] | 41 | val temporaryFolder = TemporaryFolder(getInstrumentation().context.cacheDir) |
Jakub Gielzak | 45513aa | 2020-01-03 14:16:12 +0000 | [diff] [blame] | 42 | |
| 43 | @Test |
| 44 | fun test_get_schema_complex_tables() { |
| 45 | test_get_schema( |
| 46 | listOf( |
| 47 | Database( |
| 48 | "db1", |
| 49 | Table( |
| 50 | "table1", |
| 51 | Column("t", "TEXT"), |
| 52 | Column("nu", "NUMERIC"), |
| 53 | Column("i", "INTEGER"), |
| 54 | Column("r", "REAL"), |
| 55 | Column("b", "BLOB") |
| 56 | ), |
| 57 | Table( |
| 58 | "table2", |
| 59 | Column("id", "INTEGER"), |
| 60 | Column("name", "TEXT") |
| 61 | |
Jakub Gielzak | 21ca398 | 2020-02-07 15:10:33 +0000 | [diff] [blame] | 62 | ), |
| 63 | Table( |
| 64 | "table3a", |
| 65 | Column("c1", "INT"), |
| 66 | Column("c2", "INT", primaryKey = 1) |
| 67 | ), |
| 68 | Table( |
| 69 | "table3b", // compound-primary-key |
| 70 | Column("c1", "INT", primaryKey = 2), |
| 71 | Column("c2", "INT", primaryKey = 1) |
| 72 | ), |
| 73 | Table( |
| 74 | "table4", // compound-primary-key, two unique columns |
| 75 | Column("c1", "INT", primaryKey = 1), |
| 76 | Column("c2", "INT", primaryKey = 2, isUnique = true), |
| 77 | Column("c3", "INT", isUnique = true) |
| 78 | ), |
| 79 | Table( |
| 80 | "table5", // mix: unique, primary key, notNull |
| 81 | Column("c1", "INT", isNotNull = true), |
| 82 | Column("c2", "INT", primaryKey = 1, isUnique = true), |
| 83 | Column("c3", "INT", isUnique = true, isNotNull = true) |
| 84 | ), |
| 85 | Table( |
| 86 | "table6", // compound-unique-constraint-indices in [onDatabaseCreated] |
| 87 | Column("c1", "INT"), |
| 88 | Column("c2uuu", "INT", isUnique = true), |
| 89 | Column("c3", "INT"), |
| 90 | Column("c4u", "INT", isUnique = true) |
Jakub Gielzak | 45513aa | 2020-01-03 14:16:12 +0000 | [diff] [blame] | 91 | ) |
| 92 | ) |
Jakub Gielzak | 21ca398 | 2020-02-07 15:10:33 +0000 | [diff] [blame] | 93 | ), |
| 94 | onDatabaseCreated = { db -> |
| 95 | // compound-unique-constraint-indices |
| 96 | listOf( |
| 97 | "create index index6_12 on 'table6' ('c1', 'c2uuu')", |
| 98 | "create index index6_23 on 'table6' ('c2uuu', 'c3')" |
| 99 | ).forEach { query -> |
| 100 | db.execSQL(query, emptyArray()) |
| 101 | } |
| 102 | |
| 103 | // sanity check: verifies if the above index adding operations succeeded |
| 104 | val indexCountTable6 = |
| 105 | db.rawQuery("select count(*) from pragma_index_list('table6')", null).let { |
| 106 | it.moveToNext() |
| 107 | val count = it.getString(0) |
| 108 | it.close() |
| 109 | count |
| 110 | } |
| 111 | |
| 112 | assertThat(indexCountTable6).isEqualTo("4") |
| 113 | } |
Jakub Gielzak | 45513aa | 2020-01-03 14:16:12 +0000 | [diff] [blame] | 114 | ) |
| 115 | } |
| 116 | |
| 117 | @Test |
| 118 | fun test_get_schema_multiple_databases() { |
| 119 | test_get_schema( |
| 120 | listOf( |
| 121 | Database("db3", Table("t3", Column("c3", "BLOB"))), |
| 122 | Database("db2", Table("t2", Column("c2", "TEXT"))), |
| 123 | Database("db1", Table("t1", Column("c1", "TEXT"))) |
| 124 | ) |
| 125 | ) |
| 126 | } |
| 127 | |
Jakub Gielzak | b467c12 | 2020-03-03 16:17:48 +0000 | [diff] [blame] | 128 | @Test |
| 129 | fun test_get_schema_wrong_database_id() = runBlocking { |
| 130 | val databaseId = 123456789 |
| 131 | testEnvironment.sendCommand(createGetSchemaCommand(databaseId)).let { response -> |
| 132 | assertThat(response.hasErrorOccurred()).isEqualTo(true) |
| 133 | val error = response.errorOccurred.content |
| 134 | assertThat(error.message).isEqualTo("No database with id=$databaseId") |
Jakub Gielzak | 99b7415 | 2020-03-27 16:30:35 +0000 | [diff] [blame^] | 135 | assertThat(error.recoverability.isRecoverable).isEqualTo(true) |
Jakub Gielzak | b467c12 | 2020-03-03 16:17:48 +0000 | [diff] [blame] | 136 | } |
| 137 | } |
| 138 | |
Jakub Gielzak | 21ca398 | 2020-02-07 15:10:33 +0000 | [diff] [blame] | 139 | private fun test_get_schema( |
| 140 | alreadyOpenDatabases: List<Database>, |
| 141 | onDatabaseCreated: (SQLiteDatabase) -> Unit = {} |
| 142 | ) = |
| 143 | runBlocking { |
Jakub Gielzak | 45513aa | 2020-01-03 14:16:12 +0000 | [diff] [blame] | 144 | assertThat(alreadyOpenDatabases).isNotEmpty() // sanity check |
| 145 | |
| 146 | testEnvironment.registerAlreadyOpenDatabases(alreadyOpenDatabases.map { |
Jakub Gielzak | 21ca398 | 2020-02-07 15:10:33 +0000 | [diff] [blame] | 147 | it.createInstance(temporaryFolder).also { db -> onDatabaseCreated(db) } |
Jakub Gielzak | 45513aa | 2020-01-03 14:16:12 +0000 | [diff] [blame] | 148 | }) |
| 149 | testEnvironment.sendCommand(createTrackDatabasesCommand()) |
| 150 | val databaseConnections = |
| 151 | alreadyOpenDatabases.indices.map { testEnvironment.receiveEvent().databaseOpened } |
| 152 | |
| 153 | val schemas = |
| 154 | databaseConnections |
| 155 | .sortedBy { it.name } |
| 156 | .map { |
| 157 | testEnvironment.sendCommand(createGetSchemaCommand(it.databaseId)).getSchema |
| 158 | } |
| 159 | |
| 160 | alreadyOpenDatabases |
| 161 | .sortedBy { it.name } |
| 162 | .zipSameSize(schemas) |
| 163 | .forEach { (expectedSchema, actualSchema) -> |
| 164 | val expectedTables = expectedSchema.tables.sortedBy { it.name } |
| 165 | val actualTables = actualSchema.tablesList.sortedBy { it.name } |
| 166 | |
| 167 | expectedTables |
| 168 | .zipSameSize(actualTables) |
| 169 | .forEach { (expectedTable, actualTable) -> |
| 170 | assertThat(actualTable.name).isEqualTo(expectedTable.name) |
| 171 | |
| 172 | val expectedColumns = expectedTable.columns.sortedBy { it.name } |
| 173 | val actualColumns = actualTable.columnsList.sortedBy { it.name } |
| 174 | |
| 175 | expectedColumns |
Jakub Gielzak | 21ca398 | 2020-02-07 15:10:33 +0000 | [diff] [blame] | 176 | .adjustForSinglePrimaryKey() |
Jakub Gielzak | 45513aa | 2020-01-03 14:16:12 +0000 | [diff] [blame] | 177 | .zipSameSize(actualColumns) |
Jakub Gielzak | 21ca398 | 2020-02-07 15:10:33 +0000 | [diff] [blame] | 178 | .forEach { (expectedColumn, actualColumnProto) -> |
| 179 | val actualColumn = Column( |
| 180 | name = actualColumnProto.name, |
| 181 | type = actualColumnProto.type, |
| 182 | primaryKey = actualColumnProto.primaryKey, |
| 183 | isNotNull = actualColumnProto.isNotNull, |
| 184 | isUnique = actualColumnProto.isUnique |
| 185 | ) |
| 186 | assertThat(actualColumn).isEqualTo(expectedColumn) |
Jakub Gielzak | 45513aa | 2020-01-03 14:16:12 +0000 | [diff] [blame] | 187 | } |
| 188 | } |
| 189 | } |
| 190 | } |
| 191 | |
Jakub Gielzak | 21ca398 | 2020-02-07 15:10:33 +0000 | [diff] [blame] | 192 | // The sole primary key in a table is by definition unique |
| 193 | private fun List<Column>.adjustForSinglePrimaryKey(): List<Column> = |
| 194 | if (this.count { it.isPrimaryKey } > 1) this |
| 195 | else this.map { |
| 196 | if (it.isPrimaryKey) it.copy(isUnique = true) |
| 197 | else it |
| 198 | } |
| 199 | |
Jakub Gielzak | 45513aa | 2020-01-03 14:16:12 +0000 | [diff] [blame] | 200 | /** Same as [List.zip] but ensures both lists are the same size. */ |
| 201 | private fun <A, B> List<A>.zipSameSize(other: List<B>): List<Pair<A, B>> { |
| 202 | assertThat(this.size).isEqualTo(other.size) |
| 203 | return this.zip(other) |
| 204 | } |
| 205 | } |