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