Migrate tests to use new room v3.x APIs

Signed-off-by: Aayush Gupta <contact@aayush.io>
This commit is contained in:
Aayush Gupta
2026-10-16 19:03:27 +08:00
parent 84a8491aa9
commit 999ad8e3c0
8 changed files with 192 additions and 192 deletions
+2
View File
@@ -326,6 +326,8 @@ dependencies {
androidTestImplementation(libs.androidx.runner)
androidTestImplementation(libs.androidx.room.testing)
androidTestImplementation(libs.assertj.core)
androidTestImplementation(libs.androidx.sqlite.bundled)
androidTestImplementation(libs.jetbrains.coroutines.test)
}
aboutLibraries {
@@ -1,12 +1,13 @@
package org.schabi.newpipe.database
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import androidx.room.Room
import androidx.room.testing.MigrationTestHelper
import androidx.room3.Room
import androidx.room3.testing.MigrationTestHelper
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import androidx.sqlite.execSQL
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNull
@@ -37,95 +38,84 @@ class DatabaseMigrationTest {
private const val DEFAULT_THIRD_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}
private val instrumentation = InstrumentationRegistry.getInstrumentation()
@get:Rule
val testHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java
instrumentation = instrumentation,
databaseClass = AppDatabase::class,
driver = BundledSQLiteDriver(),
file = instrumentation.targetContext.getDatabasePath(AppDatabase.DATABASE_NAME)
)
@Test
fun migrateDatabaseFrom2to3() {
val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2)
fun migrateDatabaseFrom2to3() = runTest {
val databaseInV2 = testHelper.createDatabase(version = Migrations.DB_VER_2)
databaseInV2.run {
insert(
"streams",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SERVICE_ID)
put("url", DEFAULT_URL)
put("title", DEFAULT_TITLE)
put("stream_type", DEFAULT_TYPE.name)
put("duration", DEFAULT_DURATION)
put("uploader", DEFAULT_UPLOADER_NAME)
put("thumbnail_url", DEFAULT_THUMBNAIL)
}
)
insert(
"streams",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SECOND_SERVICE_ID)
put("url", DEFAULT_SECOND_URL)
}
)
insert(
"streams",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SERVICE_ID)
}
)
val sql = """
INSERT OR FAIL INTO streams
(service_id, url, title, stream_type, duration, uploader, thumbnail_url)
VALUES (?, ?, ?, ?, ?, ?, ?)
""".trimIndent()
prepare(sql).use { statement ->
statement.bindInt(1, DEFAULT_SERVICE_ID)
statement.bindText(2, DEFAULT_URL)
statement.bindText(3, DEFAULT_TITLE)
statement.bindText(4, DEFAULT_TYPE.name)
statement.bindLong(5, DEFAULT_DURATION)
statement.bindText(6, DEFAULT_UPLOADER_NAME)
statement.bindText(7, DEFAULT_THUMBNAIL)
statement.step()
}
prepare(sql).use { statement ->
statement.bindInt(1, DEFAULT_SECOND_SERVICE_ID)
statement.bindText(2, DEFAULT_SECOND_URL)
statement.step()
}
prepare(sql).use { statement ->
statement.bindInt(1, DEFAULT_SERVICE_ID)
statement.step()
}
close()
}
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_3,
true,
Migrations.MIGRATION_2_3
version = Migrations.DB_VER_3,
migrations = listOf(Migrations.MIGRATION_2_3)
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_4,
true,
Migrations.MIGRATION_3_4
version = Migrations.DB_VER_4,
migrations = listOf(Migrations.MIGRATION_3_4)
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_5,
true,
Migrations.MIGRATION_4_5
version = Migrations.DB_VER_5,
migrations = listOf(Migrations.MIGRATION_4_5)
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_6,
true,
Migrations.MIGRATION_5_6
version = Migrations.DB_VER_6,
migrations = listOf(Migrations.MIGRATION_5_6)
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_7,
true,
Migrations.MIGRATION_6_7
version = Migrations.DB_VER_7,
migrations = listOf(Migrations.MIGRATION_6_7)
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_8,
true,
Migrations.MIGRATION_7_8
version = Migrations.DB_VER_8,
migrations = listOf(Migrations.MIGRATION_7_8)
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_9,
true,
Migrations.MIGRATION_8_9
version = Migrations.DB_VER_9,
migrations = listOf(Migrations.MIGRATION_8_9)
)
val migratedDatabaseV3 = getMigratedDatabase()
@@ -163,8 +153,8 @@ class DatabaseMigrationTest {
}
@Test
fun migrateDatabaseFrom7to8() {
val databaseInV7 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_7)
fun migrateDatabaseFrom7to8() = runTest {
val databaseInV7 = testHelper.createDatabase(version = Migrations.DB_VER_7)
val defaultSearch1 = " abc "
val defaultSearch2 = " abc"
@@ -175,53 +165,43 @@ class DatabaseMigrationTest {
val otherServiceId = ServiceList.SoundCloud.serviceId
databaseInV7.run {
insert(
"search_history",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", serviceId)
put("search", defaultSearch1)
}
)
insert(
"search_history",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", serviceId)
put("search", defaultSearch2)
}
)
insert(
"search_history",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", otherServiceId)
put("search", defaultSearch1)
}
)
insert(
"search_history",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", otherServiceId)
put("search", defaultSearch2)
}
)
val sql = "INSERT OR FAIL INTO search_history (service_id, search) VALUES (?, ?)"
prepare(sql).use { statement ->
statement.bindInt(1, serviceId)
statement.bindText(2, defaultSearch1)
statement.step()
}
prepare(sql).use { statement ->
statement.bindInt(1, serviceId)
statement.bindText(2, defaultSearch2)
statement.step()
}
prepare(sql).use { statement ->
statement.bindInt(1, otherServiceId)
statement.bindText(2, defaultSearch1)
statement.step()
}
prepare(sql).use { statement ->
statement.bindInt(1, otherServiceId)
statement.bindText(2, defaultSearch2)
statement.step()
}
close()
}
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_8,
true,
Migrations.MIGRATION_7_8
version = Migrations.DB_VER_8,
migrations = listOf(Migrations.MIGRATION_7_8)
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_9,
true,
Migrations.MIGRATION_8_9
version = Migrations.DB_VER_9,
migrations = listOf(Migrations.MIGRATION_8_9)
)
val migratedDatabaseV8 = getMigratedDatabase()
@@ -234,66 +214,82 @@ class DatabaseMigrationTest {
}
@Test
fun migrateDatabaseFrom8to9() {
val databaseInV8 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_8)
fun migrateDatabaseFrom8to9() = runTest {
val databaseInV8 = testHelper.createDatabase(version = Migrations.DB_VER_8)
val localUid1: Long
val localUid2: Long
val remoteUid1: Long
val remoteUid2: Long
databaseInV8.run {
localUid1 = insert(
"playlists",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("name", DEFAULT_NAME + "1")
put("is_thumbnail_permanent", false)
put("thumbnail_stream_id", -1)
}
)
localUid2 = insert(
"playlists",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("name", DEFAULT_NAME + "2")
put("is_thumbnail_permanent", false)
put("thumbnail_stream_id", -1)
}
)
delete(
"playlists",
"uid = ?",
Array(1) { localUid1 }
)
remoteUid1 = insert(
"remote_playlists",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SERVICE_ID)
put("url", DEFAULT_URL)
}
)
remoteUid2 = insert(
"remote_playlists",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SECOND_SERVICE_ID)
put("url", DEFAULT_SECOND_URL)
}
)
delete(
"remote_playlists",
"uid = ?",
Array(1) { remoteUid2 }
)
val remotePlaylistsSQL = """
INSERT OR FAIL INTO remote_playlists
(service_id, url)
VALUES (?, ?)
""".trimIndent()
val playlistsSQL = """
INSERT OR FAIL INTO playlists
(name, is_thumbnail_permanent, thumbnail_stream_id)
VALUES (?, ?, ?)
""".trimIndent()
prepare(playlistsSQL).use { statement ->
statement.bindText(1, DEFAULT_NAME + "1")
statement.bindBoolean(2, false)
statement.bindInt(3, -1)
statement.step()
}
prepare("SELECT MAX(uid) FROM playlists").use { statement ->
statement.step()
localUid1 = statement.getLong(0)
}
prepare(playlistsSQL).use { statement ->
statement.bindText(1, DEFAULT_NAME + "2")
statement.bindBoolean(2, false)
statement.bindInt(3, -1)
statement.step()
}
prepare("SELECT MAX(uid) FROM playlists").use { statement ->
statement.step()
localUid2 = statement.getLong(0)
}
execSQL("DELETE FROM playlists WHERE uid = $localUid1")
prepare(remotePlaylistsSQL).use { statement ->
statement.bindInt(1, DEFAULT_SERVICE_ID)
statement.bindText(2, DEFAULT_URL)
statement.step()
}
prepare("SELECT MAX(uid) FROM remote_playlists").use { statement ->
statement.step()
remoteUid1 = statement.getLong(0)
}
prepare(remotePlaylistsSQL).use { statement ->
statement.bindInt(1, DEFAULT_SECOND_SERVICE_ID)
statement.bindText(2, DEFAULT_SECOND_URL)
statement.step()
}
prepare("SELECT MAX(uid) FROM remote_playlists").use { statement ->
statement.step()
remoteUid2 = statement.getLong(0)
}
execSQL("DELETE FROM remote_playlists WHERE uid = $remoteUid2")
close()
}
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_9,
true,
Migrations.MIGRATION_8_9
version = Migrations.DB_VER_9,
migrations = listOf(Migrations.MIGRATION_8_9)
)
val migratedDatabaseV9 = getMigratedDatabase()
@@ -1,11 +1,12 @@
package org.schabi.newpipe.database
import android.content.Context
import androidx.room.Room
import androidx.room3.Room
import androidx.test.core.app.ApplicationProvider
import io.reactivex.rxjava3.core.Single
import java.io.IOException
import java.time.OffsetDateTime
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
@@ -68,7 +69,7 @@ class FeedDAOTest {
}
@Test
fun testUnlinkStreamsOlderThan_KeepOne() {
fun testUnlinkStreamsOlderThan_KeepOne() = runTest {
setupUnlinkDelete("2023-08-15T00:00:00Z")
val streams = feedDAO.getStreams(
FeedGroupEntity.GROUP_ALL_ID,
@@ -82,7 +83,7 @@ class FeedDAOTest {
}
@Test
fun testUnlinkStreamsOlderThan_KeepMultiple() {
fun testUnlinkStreamsOlderThan_KeepMultiple() = runTest {
setupUnlinkDelete("2023-08-01T00:00:00Z")
val streams = feedDAO.getStreams(
FeedGroupEntity.GROUP_ALL_ID,
@@ -106,7 +107,7 @@ class FeedDAOTest {
)
}
private fun setupUnlinkDelete(time: String) {
private suspend fun setupUnlinkDelete(time: String) {
clearAndFillTables()
Single.fromCallable {
feedDAO.unlinkStreamsOlderThan(OffsetDateTime.parse(time))
@@ -116,7 +117,7 @@ class FeedDAOTest {
}.blockingSubscribe()
}
private fun clearAndFillTables() {
private suspend fun clearAndFillTables() {
db.clearAllTables()
streamDAO.insertAll(allStreams)
subscriptionDAO.insertAll(
@@ -56,7 +56,7 @@ class LocalPlaylistManagerTest {
// This should not behave like this.
// It should throw an error because currently the result is null
result.test().await().assertComplete()
manager.playlists.test().awaitCount(1).assertValue(emptyList())
manager.getPlaylists().test().awaitCount(1).assertValue(mutableListOf())
}
@Test()
@@ -1,6 +1,6 @@
package org.schabi.newpipe.testUtil
import androidx.room.Room
import androidx.room3.Room
import androidx.test.core.app.ApplicationProvider
import org.junit.Assert.assertSame
import org.schabi.newpipe.NewPipeDatabase
@@ -293,18 +293,18 @@ object Migrations {
// Create a temp table to initialize display_index.
connection.execSQL(
"CREATE TABLE `playlists_tmp` " +
"(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
"`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, " +
"`thumbnail_stream_id` INTEGER NOT NULL, " +
"`display_index` INTEGER NOT NULL)"
"(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
"`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, " +
"`thumbnail_stream_id` INTEGER NOT NULL, " +
"`display_index` INTEGER NOT NULL)"
)
connection.execSQL(
"INSERT INTO `playlists_tmp` " +
"(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " +
"`display_index`) " +
"SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " +
"-1 " +
"FROM `playlists`"
"(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " +
"`display_index`) " +
"SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " +
"-1 " +
"FROM `playlists`"
)
// Replace the old table, note that this also removes the index on the name which
@@ -316,18 +316,18 @@ object Migrations {
// Create a temp table to initialize display_index.
connection.execSQL(
"CREATE TABLE `remote_playlists_tmp` " +
"(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
"`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " +
"`thumbnail_url` TEXT, `uploader` TEXT, " +
"`display_index` INTEGER NOT NULL," +
"`stream_count` INTEGER)"
"(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
"`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " +
"`thumbnail_url` TEXT, `uploader` TEXT, " +
"`display_index` INTEGER NOT NULL," +
"`stream_count` INTEGER)"
)
connection.execSQL(
"INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, " +
"`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, " +
"`stream_count`)" +
"SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, " +
"-1, `stream_count` FROM `remote_playlists`"
"`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, " +
"`stream_count`)" +
"SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, " +
"-1, `stream_count` FROM `remote_playlists`"
)
// Replace the old table, note that this also removes the index on the name which
@@ -338,7 +338,7 @@ object Migrations {
// Create index on the new table.
connection.execSQL(
"CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " +
"ON `remote_playlists` (`service_id`, `url`)"
"ON `remote_playlists` (`service_id`, `url`)"
)
}
}
@@ -53,9 +53,9 @@ class HistoryRecordManager(context: Context) {
private val isStreamHistoryEnabled: Boolean
get() = sharedPreferences.getBoolean(streamHistoryKey, false)
/////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////
// Watch History
/////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////
/**
* Marks a stream item as watched such that it is hidden from the feed if watched videos are
@@ -165,9 +165,9 @@ class HistoryRecordManager(context: Context) {
return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io())
}
/////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////
// Search History
/////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////
fun onSearched(serviceId: Int, search: String): Maybe<Long> {
if (!isSearchHistoryEnabled) {
@@ -214,9 +214,9 @@ class HistoryRecordManager(context: Context) {
}
}
/////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////
// Stream State History
/////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////
fun loadStreamState(queueItem: PlayQueueItem): Maybe<StreamStateEntity> {
return queueItem.stream
@@ -281,9 +281,9 @@ class HistoryRecordManager(context: Context) {
}.subscribeOn(Schedulers.io())
}
/////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////
// Utility
/////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////
fun removeOrphanedRecords(): Single<Int> {
return Single.fromCallable { streamTable.deleteOrphans() }
+3 -2
View File
@@ -60,9 +60,9 @@ rxbinding = "4.0.0"
rxjava = "3.1.12"
settings = "1.3.0"
sonarqube = "7.5.0.8588"
sqlite = "2.7.1"
statesaver = "1.4.1" # TODO: Drop because it is deprecated and incompatible with KSP2
stetho = "1.6.0"
sqlite = "2.7.1"
swiperefreshlayout = "1.2.0"
# You can use a local version by uncommenting a few lines in settings.gradle
# Or you can use a commit you pushed to GitHub by just replacing TeamNewPipe with your GitHub
@@ -104,8 +104,8 @@ androidx-room-runtime = { module = "androidx.room3:room3-runtime", version.ref =
androidx-room-rxjava3 = { module = "androidx.room3:room3-rxjava3", version.ref = "room" }
androidx-room-testing = { module = "androidx.room3:room3-testing", version.ref = "room" }
androidx-runner = { module = "androidx.test:runner", version.ref = "runner" }
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" }
androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
@@ -139,6 +139,7 @@ jetbrains-compose-test-ui = { module = "org.jetbrains.compose.ui:ui-test", versi
jetbrains-compose-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "multiplatform" }
jetbrains-compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "multiplatform" }
jetbrains-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" }
jetbrains-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
jetbrains-lifecycle-navigation3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycle" }
jetbrains-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
jetbrains-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" }