mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-19 18:58:19 +00:00
Merge "Add testing for GridMigration." into main
This commit is contained in:
committed by
Android (Google) Code Review
commit
bbdcade30d
@@ -53,6 +53,13 @@ public class DeviceGridState implements Comparable<DeviceGridState> {
|
||||
private final @DeviceType int mDeviceType;
|
||||
private final String mDbFile;
|
||||
|
||||
public DeviceGridState(int columns, int row, int numHotseat, int deviceType, String dbFile) {
|
||||
mGridSizeString = String.format(Locale.ENGLISH, "%d,%d", columns, row);
|
||||
mNumHotseat = numHotseat;
|
||||
mDeviceType = deviceType;
|
||||
mDbFile = dbFile;
|
||||
}
|
||||
|
||||
public DeviceGridState(InvariantDeviceProfile idp) {
|
||||
mGridSizeString = String.format(Locale.ENGLISH, "%d,%d", idp.numColumns, idp.numRows);
|
||||
mNumHotseat = idp.numDatabaseHotseatIcons;
|
||||
|
||||
@@ -38,6 +38,7 @@ import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.launcher3.InvariantDeviceProfile;
|
||||
import com.android.launcher3.LauncherPrefs;
|
||||
@@ -94,6 +95,15 @@ public class GridSizeMigrationUtil {
|
||||
return needsToMigrate;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static List<DbEntry> readAllEntries(SQLiteDatabase db, String tableName,
|
||||
Context context) {
|
||||
DbReader dbReader = new DbReader(db, tableName, context, getValidPackages(context));
|
||||
List<DbEntry> result = dbReader.loadAllWorkspaceEntries();
|
||||
result.addAll(dbReader.loadHotseatEntries());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* When migrating the grid, we copy the table
|
||||
* {@link LauncherSettings.Favorites#TABLE_NAME} from {@code source} into
|
||||
@@ -654,7 +664,7 @@ public class GridSizeMigrationUtil {
|
||||
}
|
||||
}
|
||||
|
||||
protected static class DbEntry extends ItemInfo implements Comparable<DbEntry> {
|
||||
public static class DbEntry extends ItemInfo implements Comparable<DbEntry> {
|
||||
|
||||
private String mIntent;
|
||||
private String mProvider;
|
||||
|
||||
BIN
tests/assets/databases/GridMigrationTest/result5x5to3x3.db
Normal file
BIN
tests/assets/databases/GridMigrationTest/result5x5to3x3.db
Normal file
Binary file not shown.
BIN
tests/assets/databases/GridMigrationTest/result5x5to4x7.db
Normal file
BIN
tests/assets/databases/GridMigrationTest/result5x5to4x7.db
Normal file
Binary file not shown.
BIN
tests/assets/databases/GridMigrationTest/result5x5to5x8.db
Normal file
BIN
tests/assets/databases/GridMigrationTest/result5x5to5x8.db
Normal file
Binary file not shown.
BIN
tests/assets/databases/GridMigrationTest/test_launcher.db
Normal file
BIN
tests/assets/databases/GridMigrationTest/test_launcher.db
Normal file
Binary file not shown.
@@ -199,6 +199,19 @@ public class CellLayoutBoard implements Comparable<CellLayoutBoard> {
|
||||
return 'z';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given area is empty.
|
||||
*/
|
||||
public boolean isEmpty(int x, int y, int spanX, int spanY) {
|
||||
for (int xi = x; xi < x + spanX; xi++) {
|
||||
for (int yi = y; yi < y + spanY; yi++) {
|
||||
if (mWidget[xi][yi] == CellType.IGNORE) continue;
|
||||
if (mWidget[xi][yi] != CellType.EMPTY) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void addWidget(int x, int y, int spanX, int spanY, char type) {
|
||||
Rect rect = new Rect(x, y + spanY - 1, x + spanX - 1, y);
|
||||
removeOverlappingItems(rect);
|
||||
|
||||
195
tests/src/com/android/launcher3/model/GridMigrationTest.kt
Normal file
195
tests/src/com/android/launcher3/model/GridMigrationTest.kt
Normal file
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 com.android.launcher3.model
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.android.launcher3.InvariantDeviceProfile.TYPE_PHONE
|
||||
import com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME
|
||||
import com.android.launcher3.celllayout.board.CellLayoutBoard
|
||||
import com.android.launcher3.pm.UserCache
|
||||
import com.android.launcher3.util.rule.TestToPhoneFileCopier
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
private val phoneContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
data class EntryData(val x: Int, val y: Int, val spanX: Int, val spanY: Int, val rank: Int)
|
||||
|
||||
/**
|
||||
* Holds the data needed to run a test in GridMigrationTest, usually we would have a src
|
||||
* GridMigrationData and a dst GridMigrationData meaning the data after a migration has occurred.
|
||||
* This class holds a gridState, which is the size of the grid like 5x5 (among other things). a
|
||||
* dbHelper which contains the readable database and writable database used to migrate the
|
||||
* databases.
|
||||
*
|
||||
* You can also get all the entries defined in the dbHelper database.
|
||||
*/
|
||||
class GridMigrationData(dbFileName: String?, val gridState: DeviceGridState) {
|
||||
|
||||
val dbHelper: DatabaseHelper =
|
||||
DatabaseHelper(
|
||||
phoneContext,
|
||||
dbFileName,
|
||||
{ UserCache.INSTANCE.get(phoneContext).getSerialNumberForUser(it) },
|
||||
{}
|
||||
)
|
||||
|
||||
fun readEntries(): List<GridSizeMigrationUtil.DbEntry> =
|
||||
GridSizeMigrationUtil.readAllEntries(dbHelper.readableDatabase, TABLE_NAME, phoneContext)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the migration of a database from one size to another. It reads a database from the test
|
||||
* assets, uploads it into the phone and migrates the database to a database in memory which is
|
||||
* later compared against a database in the test assets to make sure they are identical.
|
||||
*/
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class GridMigrationTest {
|
||||
private val DB_FILE = "test_launcher.db"
|
||||
|
||||
// Copying the src db for all tests.
|
||||
@JvmField
|
||||
@Rule
|
||||
val fileCopier =
|
||||
TestToPhoneFileCopier("databases/GridMigrationTest/$DB_FILE", "databases/$DB_FILE", true)
|
||||
|
||||
private fun migrate(src: GridMigrationData, dst: GridMigrationData) {
|
||||
GridSizeMigrationUtil.migrateGridIfNeeded(
|
||||
phoneContext,
|
||||
src.gridState,
|
||||
dst.gridState,
|
||||
dst.dbHelper,
|
||||
src.dbHelper.readableDatabase
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure that none of the items overlaps on the result, i.e. no widget or icons share the
|
||||
* same space in the db.
|
||||
*/
|
||||
private fun validateDb(data: GridMigrationData) {
|
||||
val cellLayoutBoard = CellLayoutBoard(data.gridState.columns, data.gridState.rows)
|
||||
data.readEntries().forEach {
|
||||
assert(cellLayoutBoard.isEmpty(it.cellX, it.cellY, it.spanX, it.spanY)) {
|
||||
"Db has overlapping items"
|
||||
}
|
||||
cellLayoutBoard.addWidget(it.cellX, it.cellY, it.spanX, it.spanY)
|
||||
}
|
||||
}
|
||||
|
||||
private fun compare(dst: GridMigrationData, target: GridMigrationData) {
|
||||
val sortX = { it: GridSizeMigrationUtil.DbEntry -> it.cellX }
|
||||
val sortY = { it: GridSizeMigrationUtil.DbEntry -> it.cellX }
|
||||
val mapF = { it: GridSizeMigrationUtil.DbEntry ->
|
||||
EntryData(it.cellX, it.cellY, it.spanX, it.spanY, it.rank)
|
||||
}
|
||||
val entriesDst = dst.readEntries().sortedBy(sortX).sortedBy(sortY).map(mapF)
|
||||
val entriesTarget = target.readEntries().sortedBy(sortX).sortedBy(sortY).map(mapF)
|
||||
assert(entriesDst == entriesTarget) {
|
||||
"The elements on the dst database is not the same as in the target"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate src into dst and compare to target. This method validates 3 things:
|
||||
* 1. dst has the same number of items as src after the migration, meaning, none of the items
|
||||
* were removed during the migration.
|
||||
* 2. dst is valid, meaning that none of the items overlap with each other.
|
||||
* 3. dst is equal to target to ensure we don't unintentionally change the migration logic.
|
||||
*/
|
||||
private fun runTest(src: GridMigrationData, dst: GridMigrationData, target: GridMigrationData) {
|
||||
migrate(src, dst)
|
||||
assert(src.readEntries().size == dst.readEntries().size) {
|
||||
"Source db and destination db do not contain the same number of elements"
|
||||
}
|
||||
validateDb(dst)
|
||||
compare(dst, target)
|
||||
}
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
val result5x5to3x3 =
|
||||
TestToPhoneFileCopier(
|
||||
"databases/GridMigrationTest/result5x5to3x3.db",
|
||||
"databases/result5x5to3x3.db",
|
||||
true
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `5x5 to 3x3`() =
|
||||
runTest(
|
||||
src = GridMigrationData(DB_FILE, DeviceGridState(5, 5, 5, TYPE_PHONE, DB_FILE)),
|
||||
dst =
|
||||
GridMigrationData(
|
||||
null, // in memory db, to download a new db change null for the filename of the
|
||||
// db name to store it. Do not use existing names.
|
||||
DeviceGridState(3, 3, 3, TYPE_PHONE, "")
|
||||
),
|
||||
target =
|
||||
GridMigrationData("result5x5to3x3.db", DeviceGridState(3, 3, 3, TYPE_PHONE, ""))
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
val result5x5to4x7 =
|
||||
TestToPhoneFileCopier(
|
||||
"databases/GridMigrationTest/result5x5to4x7.db",
|
||||
"databases/result5x5to4x7.db",
|
||||
true
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `5x5 to 4x7`() =
|
||||
runTest(
|
||||
src = GridMigrationData(DB_FILE, DeviceGridState(5, 5, 5, TYPE_PHONE, DB_FILE)),
|
||||
dst =
|
||||
GridMigrationData(
|
||||
null, // in memory db, to download a new db change null for the filename of the
|
||||
// db name to store it. Do not use existing names.
|
||||
DeviceGridState(4, 7, 4, TYPE_PHONE, "")
|
||||
),
|
||||
target =
|
||||
GridMigrationData("result5x5to4x7.db", DeviceGridState(4, 7, 4, TYPE_PHONE, ""))
|
||||
)
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
val result5x5to5x8 =
|
||||
TestToPhoneFileCopier(
|
||||
"databases/GridMigrationTest/result5x5to5x8.db",
|
||||
"databases/result5x5to5x8.db",
|
||||
true
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `5x5 to 5x8`() =
|
||||
runTest(
|
||||
src = GridMigrationData(DB_FILE, DeviceGridState(5, 5, 5, TYPE_PHONE, DB_FILE)),
|
||||
dst =
|
||||
GridMigrationData(
|
||||
null, // in memory db, to download a new db change null for the filename of the
|
||||
// db name to store it. Do not use existing names.
|
||||
DeviceGridState(5, 8, 5, TYPE_PHONE, "")
|
||||
),
|
||||
target =
|
||||
GridMigrationData("result5x5to5x8.db", DeviceGridState(5, 8, 5, TYPE_PHONE, ""))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 com.android.launcher3.util.rule
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import java.io.File
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
/** Copy a file from the tests assets folder to the phone. */
|
||||
class TestToPhoneFileCopier(
|
||||
val src: String,
|
||||
dest: String,
|
||||
private val removeOnFinish: Boolean = false
|
||||
) : TestRule {
|
||||
|
||||
private val dstFile =
|
||||
File(InstrumentationRegistry.getInstrumentation().targetContext.dataDir, dest)
|
||||
|
||||
fun getDst() = dstFile.absolutePath
|
||||
|
||||
fun before() =
|
||||
dstFile.writeBytes(
|
||||
InstrumentationRegistry.getInstrumentation().context.assets.open(src).readBytes()
|
||||
)
|
||||
|
||||
fun after() {
|
||||
if (removeOnFinish) {
|
||||
dstFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement =
|
||||
object : Statement() {
|
||||
override fun evaluate() {
|
||||
before()
|
||||
base.evaluate()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user