1 /* <lambda>null2 * Copyright (C) 2024 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 com.android.systemui.communal.data.db 18 19 import androidx.room.testing.MigrationTestHelper 20 import androidx.sqlite.db.SupportSQLiteDatabase 21 import androidx.test.ext.junit.runners.AndroidJUnit4 22 import androidx.test.filters.SmallTest 23 import androidx.test.platform.app.InstrumentationRegistry 24 import com.android.systemui.SysuiTestCase 25 import com.android.systemui.communal.shared.model.SpanValue 26 import com.android.systemui.communal.shared.model.toResponsive 27 import com.android.systemui.lifecycle.InstantTaskExecutorRule 28 import com.google.common.truth.Truth.assertThat 29 import org.junit.Rule 30 import org.junit.Test 31 import org.junit.runner.RunWith 32 33 @SmallTest 34 @RunWith(AndroidJUnit4::class) 35 class CommunalDatabaseMigrationsTest : SysuiTestCase() { 36 37 @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule() 38 39 @get:Rule 40 val migrationTestHelper = 41 MigrationTestHelper( 42 InstrumentationRegistry.getInstrumentation(), 43 CommunalDatabase::class.java.canonicalName, 44 ) 45 46 @Test 47 fun migrate1To2() { 48 // Create a communal database in version 1 49 val databaseV1 = migrationTestHelper.createDatabase(DATABASE_NAME, version = 1) 50 51 // Populate some fake data 52 val fakeWidgetsV1 = 53 listOf( 54 FakeCommunalWidgetItemV1(1, "test_widget_1", 11), 55 FakeCommunalWidgetItemV1(2, "test_widget_2", 12), 56 FakeCommunalWidgetItemV1(3, "test_widget_3", 13), 57 ) 58 databaseV1.insertWidgetsV1(fakeWidgetsV1) 59 60 // Verify fake widgets populated 61 databaseV1.verifyWidgetsV1(fakeWidgetsV1) 62 63 // Run migration and get database V2, the migration test helper verifies that the schema is 64 // updated correctly 65 val databaseV2 = 66 migrationTestHelper.runMigrationsAndValidate( 67 name = DATABASE_NAME, 68 version = 2, 69 validateDroppedTables = false, 70 CommunalDatabase.MIGRATION_1_2, 71 ) 72 73 // Verify data is migrated correctly 74 databaseV2.verifyWidgetsV2(fakeWidgetsV1.map { it.getV2() }) 75 } 76 77 @Test 78 fun migrate2To3_noGapBetweenRanks_ranksReversed() { 79 // Create a communal database in version 2 80 val databaseV2 = migrationTestHelper.createDatabase(DATABASE_NAME, version = 2) 81 82 // Populate some fake data 83 val fakeRanks = 84 listOf( 85 FakeCommunalItemRank(3), 86 FakeCommunalItemRank(2), 87 FakeCommunalItemRank(1), 88 FakeCommunalItemRank(0), 89 ) 90 databaseV2.insertRanks(fakeRanks) 91 92 // Verify fake ranks populated 93 databaseV2.verifyRanksInOrder(fakeRanks) 94 95 // Run migration and get database V3 96 val databaseV3 = 97 migrationTestHelper.runMigrationsAndValidate( 98 name = DATABASE_NAME, 99 version = 3, 100 validateDroppedTables = false, 101 CommunalDatabase.MIGRATION_2_3, 102 ) 103 104 // Verify ranks are reversed 105 databaseV3.verifyRanksInOrder( 106 listOf( 107 FakeCommunalItemRank(0), 108 FakeCommunalItemRank(1), 109 FakeCommunalItemRank(2), 110 FakeCommunalItemRank(3), 111 ) 112 ) 113 } 114 115 @Test 116 fun migrate2To3_withGapBetweenRanks_ranksReversed() { 117 // Create a communal database in version 2 118 val databaseV2 = migrationTestHelper.createDatabase(DATABASE_NAME, version = 2) 119 120 // Populate some fake data with gaps between ranks 121 val fakeRanks = 122 listOf( 123 FakeCommunalItemRank(9), 124 FakeCommunalItemRank(7), 125 FakeCommunalItemRank(2), 126 FakeCommunalItemRank(0), 127 ) 128 databaseV2.insertRanks(fakeRanks) 129 130 // Verify fake ranks populated 131 databaseV2.verifyRanksInOrder(fakeRanks) 132 133 // Run migration and get database V3 134 val databaseV3 = 135 migrationTestHelper.runMigrationsAndValidate( 136 name = DATABASE_NAME, 137 version = 3, 138 validateDroppedTables = false, 139 CommunalDatabase.MIGRATION_2_3, 140 ) 141 142 // Verify ranks are reversed 143 databaseV3.verifyRanksInOrder( 144 listOf( 145 FakeCommunalItemRank(0), 146 FakeCommunalItemRank(2), 147 FakeCommunalItemRank(7), 148 FakeCommunalItemRank(9), 149 ) 150 ) 151 } 152 153 @Test 154 fun migrate3To4_addSpanYColumn_defaultValuePopulated() { 155 val databaseV3 = migrationTestHelper.createDatabase(DATABASE_NAME, version = 3) 156 157 val fakeWidgetsV3 = 158 listOf( 159 FakeCommunalWidgetItemV3(1, "test_widget_1", 11, 0), 160 FakeCommunalWidgetItemV3(2, "test_widget_2", 12, 10), 161 FakeCommunalWidgetItemV3(3, "test_widget_3", 13, 0), 162 ) 163 databaseV3.insertWidgetsV3(fakeWidgetsV3) 164 165 databaseV3.verifyWidgetsV3(fakeWidgetsV3) 166 167 val databaseV4 = 168 migrationTestHelper.runMigrationsAndValidate( 169 name = DATABASE_NAME, 170 version = 4, 171 validateDroppedTables = false, 172 CommunalDatabase.MIGRATION_3_4, 173 ) 174 175 databaseV4.verifyWidgetsV4(fakeWidgetsV3.map { it.getV4() }) 176 } 177 178 @Test 179 fun migrate4To5_addNewSpanYColumn() { 180 val databaseV4 = migrationTestHelper.createDatabase(DATABASE_NAME, version = 4) 181 182 val fakeWidgetsV4 = 183 listOf( 184 FakeCommunalWidgetItemV4( 185 widgetId = 1, 186 componentName = "test_widget_1", 187 itemId = 11, 188 userSerialNumber = 0, 189 spanY = 3, 190 ), 191 FakeCommunalWidgetItemV4( 192 widgetId = 2, 193 componentName = "test_widget_2", 194 itemId = 12, 195 userSerialNumber = 10, 196 spanY = 6, 197 ), 198 FakeCommunalWidgetItemV4( 199 widgetId = 3, 200 componentName = "test_widget_3", 201 itemId = 13, 202 userSerialNumber = 0, 203 spanY = 0, 204 ), 205 ) 206 databaseV4.insertWidgetsV4(fakeWidgetsV4) 207 208 databaseV4.verifyWidgetsV4(fakeWidgetsV4) 209 210 val databaseV5 = 211 migrationTestHelper.runMigrationsAndValidate( 212 name = DATABASE_NAME, 213 version = 5, 214 validateDroppedTables = false, 215 CommunalDatabase.MIGRATION_4_5, 216 ) 217 218 databaseV5.verifyWidgetsV5(fakeWidgetsV4.map { it.getV5() }) 219 } 220 221 private fun SupportSQLiteDatabase.insertWidgetsV1(widgets: List<FakeCommunalWidgetItemV1>) { 222 widgets.forEach { widget -> 223 execSQL( 224 "INSERT INTO communal_widget_table(widget_id, component_name, item_id) " + 225 "VALUES(${widget.widgetId}, '${widget.componentName}', ${widget.itemId})" 226 ) 227 } 228 } 229 230 private fun SupportSQLiteDatabase.insertWidgetsV3(widgets: List<FakeCommunalWidgetItemV3>) { 231 widgets.forEach { widget -> 232 execSQL( 233 "INSERT INTO communal_widget_table(" + 234 "widget_id, " + 235 "component_name, " + 236 "item_id, " + 237 "user_serial_number) " + 238 "VALUES(${widget.widgetId}, " + 239 "'${widget.componentName}', " + 240 "${widget.itemId}, " + 241 "${widget.userSerialNumber})" 242 ) 243 } 244 } 245 246 private fun SupportSQLiteDatabase.insertWidgetsV4(widgets: List<FakeCommunalWidgetItemV4>) { 247 widgets.forEach { widget -> 248 execSQL( 249 "INSERT INTO communal_widget_table(" + 250 "widget_id, " + 251 "component_name, " + 252 "item_id, " + 253 "user_serial_number, " + 254 "span_y) " + 255 "VALUES(${widget.widgetId}, " + 256 "'${widget.componentName}', " + 257 "${widget.itemId}, " + 258 "${widget.userSerialNumber}," + 259 "${widget.spanY})" 260 ) 261 } 262 } 263 264 private fun SupportSQLiteDatabase.verifyWidgetsV1(widgets: List<FakeCommunalWidgetItemV1>) { 265 val cursor = query("SELECT * FROM communal_widget_table") 266 assertThat(cursor.moveToFirst()).isTrue() 267 268 widgets.forEach { widget -> 269 assertThat(cursor.getInt(cursor.getColumnIndex("widget_id"))).isEqualTo(widget.widgetId) 270 assertThat(cursor.getString(cursor.getColumnIndex("component_name"))) 271 .isEqualTo(widget.componentName) 272 assertThat(cursor.getInt(cursor.getColumnIndex("item_id"))).isEqualTo(widget.itemId) 273 274 cursor.moveToNext() 275 } 276 277 // Verify there is no more columns 278 assertThat(cursor.isAfterLast).isTrue() 279 } 280 281 private fun SupportSQLiteDatabase.verifyWidgetsV2(widgets: List<FakeCommunalWidgetItemV2>) { 282 val cursor = query("SELECT * FROM communal_widget_table") 283 assertThat(cursor.moveToFirst()).isTrue() 284 285 widgets.forEach { widget -> 286 assertThat(cursor.getInt(cursor.getColumnIndex("widget_id"))).isEqualTo(widget.widgetId) 287 assertThat(cursor.getString(cursor.getColumnIndex("component_name"))) 288 .isEqualTo(widget.componentName) 289 assertThat(cursor.getInt(cursor.getColumnIndex("item_id"))).isEqualTo(widget.itemId) 290 assertThat(cursor.getInt(cursor.getColumnIndex("user_serial_number"))) 291 .isEqualTo(widget.userSerialNumber) 292 293 cursor.moveToNext() 294 } 295 296 // Verify there is no more columns 297 assertThat(cursor.isAfterLast).isTrue() 298 } 299 300 private fun SupportSQLiteDatabase.verifyWidgetsV3(widgets: List<FakeCommunalWidgetItemV3>) { 301 val cursor = query("SELECT * FROM communal_widget_table") 302 assertThat(cursor.moveToFirst()).isTrue() 303 304 widgets.forEach { widget -> 305 assertThat(cursor.getInt(cursor.getColumnIndex("widget_id"))).isEqualTo(widget.widgetId) 306 assertThat(cursor.getString(cursor.getColumnIndex("component_name"))) 307 .isEqualTo(widget.componentName) 308 assertThat(cursor.getInt(cursor.getColumnIndex("item_id"))).isEqualTo(widget.itemId) 309 assertThat(cursor.getInt(cursor.getColumnIndex("user_serial_number"))) 310 .isEqualTo(widget.userSerialNumber) 311 312 cursor.moveToNext() 313 } 314 assertThat(cursor.isAfterLast).isTrue() 315 } 316 317 private fun SupportSQLiteDatabase.verifyWidgetsV4(widgets: List<FakeCommunalWidgetItemV4>) { 318 val cursor = query("SELECT * FROM communal_widget_table") 319 assertThat(cursor.moveToFirst()).isTrue() 320 321 widgets.forEach { widget -> 322 assertThat(cursor.getInt(cursor.getColumnIndex("widget_id"))).isEqualTo(widget.widgetId) 323 assertThat(cursor.getString(cursor.getColumnIndex("component_name"))) 324 .isEqualTo(widget.componentName) 325 assertThat(cursor.getInt(cursor.getColumnIndex("item_id"))).isEqualTo(widget.itemId) 326 assertThat(cursor.getInt(cursor.getColumnIndex("user_serial_number"))) 327 .isEqualTo(widget.userSerialNumber) 328 assertThat(cursor.getInt(cursor.getColumnIndex("span_y"))).isEqualTo(widget.spanY) 329 330 cursor.moveToNext() 331 } 332 333 assertThat(cursor.isAfterLast).isTrue() 334 } 335 336 private fun SupportSQLiteDatabase.verifyWidgetsV5(widgets: List<FakeCommunalWidgetItemV5>) { 337 val cursor = query("SELECT * FROM communal_widget_table") 338 assertThat(cursor.moveToFirst()).isTrue() 339 340 widgets.forEach { widget -> 341 assertThat(cursor.getInt(cursor.getColumnIndex("widget_id"))).isEqualTo(widget.widgetId) 342 assertThat(cursor.getString(cursor.getColumnIndex("component_name"))) 343 .isEqualTo(widget.componentName) 344 assertThat(cursor.getInt(cursor.getColumnIndex("item_id"))).isEqualTo(widget.itemId) 345 assertThat(cursor.getInt(cursor.getColumnIndex("user_serial_number"))) 346 .isEqualTo(widget.userSerialNumber) 347 assertThat(cursor.getInt(cursor.getColumnIndex("span_y"))).isEqualTo(widget.spanY) 348 assertThat(cursor.getInt(cursor.getColumnIndex("span_y_new"))) 349 .isEqualTo(widget.spanYNew) 350 351 cursor.moveToNext() 352 } 353 354 assertThat(cursor.isAfterLast).isTrue() 355 } 356 357 private fun SupportSQLiteDatabase.insertRanks(ranks: List<FakeCommunalItemRank>) { 358 ranks.forEach { rank -> 359 execSQL("INSERT INTO communal_item_rank_table(rank) VALUES(${rank.rank})") 360 } 361 } 362 363 private fun SupportSQLiteDatabase.verifyRanksInOrder(ranks: List<FakeCommunalItemRank>) { 364 val cursor = query("SELECT * FROM communal_item_rank_table ORDER BY uid") 365 assertThat(cursor.moveToFirst()).isTrue() 366 367 ranks.forEach { rank -> 368 assertThat(cursor.getInt(cursor.getColumnIndex("rank"))).isEqualTo(rank.rank) 369 cursor.moveToNext() 370 } 371 372 // Verify there is no more columns 373 assertThat(cursor.isAfterLast).isTrue() 374 } 375 376 /** 377 * Returns the expected data after migration from V1 to V2, which is simply that the new user 378 * serial number field is now set to [CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED]. 379 */ 380 private fun FakeCommunalWidgetItemV1.getV2(): FakeCommunalWidgetItemV2 { 381 return FakeCommunalWidgetItemV2( 382 widgetId, 383 componentName, 384 itemId, 385 CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED, 386 ) 387 } 388 389 private data class FakeCommunalWidgetItemV1( 390 val widgetId: Int, 391 val componentName: String, 392 val itemId: Int, 393 ) 394 395 private data class FakeCommunalWidgetItemV2( 396 val widgetId: Int, 397 val componentName: String, 398 val itemId: Int, 399 val userSerialNumber: Int, 400 ) 401 402 private fun FakeCommunalWidgetItemV3.getV4(): FakeCommunalWidgetItemV4 { 403 return FakeCommunalWidgetItemV4(widgetId, componentName, itemId, userSerialNumber, 3) 404 } 405 406 private data class FakeCommunalWidgetItemV3( 407 val widgetId: Int, 408 val componentName: String, 409 val itemId: Int, 410 val userSerialNumber: Int, 411 ) 412 413 private data class FakeCommunalWidgetItemV4( 414 val widgetId: Int, 415 val componentName: String, 416 val itemId: Int, 417 val userSerialNumber: Int, 418 val spanY: Int, 419 ) 420 421 private fun FakeCommunalWidgetItemV4.getV5(): FakeCommunalWidgetItemV5 { 422 val spanYFixed = SpanValue.Fixed(spanY) 423 return FakeCommunalWidgetItemV5( 424 widgetId = widgetId, 425 componentName = componentName, 426 itemId = itemId, 427 userSerialNumber = userSerialNumber, 428 spanY = spanYFixed.value, 429 spanYNew = spanYFixed.toResponsive().value, 430 ) 431 } 432 433 private data class FakeCommunalWidgetItemV5( 434 val widgetId: Int, 435 val componentName: String, 436 val itemId: Int, 437 val userSerialNumber: Int, 438 val spanY: Int, 439 val spanYNew: Int, 440 ) 441 442 private data class FakeCommunalItemRank(val rank: Int) 443 444 companion object { 445 private const val DATABASE_NAME = "communal_db" 446 } 447 } 448