From 1e24e525f69c4085873a41650412e187e9d14ebf Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Sat, 27 Jan 2024 19:24:17 +0200 Subject: [PATCH 01/12] WIP: Setup integration tests (androidTest) --- .gitignore | 3 +- gradle/libs.versions.toml | 13 ++++++ ivy-data/build.gradle.kts | 7 ++- .../data/db/IvyRoomDatabaseMigrationTest.kt | 43 ++++++++++++++++++ .../java/com/ivy/data/db/IvyRoomDatabase.kt | 44 ++++++++++--------- 5 files changed, 87 insertions(+), 23 deletions(-) create mode 100644 ivy-data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt diff --git a/.gitignore b/.gitignore index 2a71d3359c..808b8405cb 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ captures/ # Android Studio 3 in .gitignore file. .idea/caches .idea/modules.xml +.idea/androidTestResultsUserPreferences.xml # Comment next line if keeping position of elements in Navigation Editor is relevant for you .idea/navEditor.xml .idea/codeStyles @@ -93,4 +94,4 @@ lint/tmp/ lint/reports/ # JS -node_modules/* \ No newline at end of file +node_modules/* diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b5b9da856e..de2f4a3670 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ hilt = "2.50" room = "2.6.1" androidx-work = "2.9.0" kotlinx-collections = "0.3.7" +androidx-test = "1.4.0" # Android min-sdk = "28" @@ -63,6 +64,11 @@ kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-te cashapp-molecule-plugin = { module = "app.cash.molecule:molecule-gradle-plugin", version = "1.3.2" } cashapp-turbine = { module = "app.cash.turbine:turbine", version = "1.0.0" } +# Integartion (Android) testing +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test" } +androidx-test-core = { module = "androidx.test:core-ktx", version.ref = "androidx-test" } +androidx-test-ext = { module = "androidx.test.ext:junit-ktx", version = "1.1.5" } + # Compose compose-animation = { module = "androidx.compose.animation:animation", version.ref = "compose" } compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } @@ -84,6 +90,7 @@ datastore = { module = "androidx.datastore:datastore-preferences", version = "1. room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } +room-testing = { module = "androidx.room:room-testing", version.ref = "room" } # Hilt hilt = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } @@ -162,6 +169,12 @@ testing = [ "mockk", "cashapp-turbine" ] +integration-testing = [ + "kotest-assertions", + "androidx-test-core", + "androidx-test-runner", + "androidx-test-ext" +] compose = [ "compose-animation", "compose-foundation", diff --git a/ivy-data/build.gradle.kts b/ivy-data/build.gradle.kts index a6ba9590a3..699eb4419b 100644 --- a/ivy-data/build.gradle.kts +++ b/ivy-data/build.gradle.kts @@ -4,7 +4,10 @@ plugins { } android { - namespace = "com.ivy.persistence" + namespace = "com.ivy.data" + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } } dependencies { @@ -13,5 +16,7 @@ dependencies { implementation(libs.datastore) implementation(libs.bundles.ktor) + androidTestImplementation(libs.bundles.integration.testing) + androidTestImplementation(libs.room.testing) testImplementation(projects.ivyTesting) } diff --git a/ivy-data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt b/ivy-data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt new file mode 100644 index 0000000000..6bf514f61e --- /dev/null +++ b/ivy-data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt @@ -0,0 +1,43 @@ +package com.ivy.data.db + +import androidx.room.Room +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class IvyRoomDatabaseMigrationTest { + private val TEST_DB = "migration-test" + + @get:Rule + val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + IvyRoomDatabase::class.java, + listOf(IvyRoomDatabase.DeleteSEMigration()), + FrameworkSQLiteOpenHelperFactory() + ) + + @Test + @Throws(IOException::class) + fun migrateAll() { + // Create earliest version of the database. + helper.createDatabase(TEST_DB, 1).apply { + close() + } + + // Open latest version of the database. Room validates the schema + // once all migrations execute. + Room.databaseBuilder( + InstrumentationRegistry.getInstrumentation().targetContext, + IvyRoomDatabase::class.java, + TEST_DB + ).addMigrations(*IvyRoomDatabase.migrations()).build().apply { + openHelper.writableDatabase.close() + } + } +} \ No newline at end of file diff --git a/ivy-data/src/main/java/com/ivy/data/db/IvyRoomDatabase.kt b/ivy-data/src/main/java/com/ivy/data/db/IvyRoomDatabase.kt index 309081fb25..a73fff68d6 100644 --- a/ivy-data/src/main/java/com/ivy/data/db/IvyRoomDatabase.kt +++ b/ivy-data/src/main/java/com/ivy/data/db/IvyRoomDatabase.kt @@ -96,6 +96,28 @@ abstract class IvyRoomDatabase : RoomDatabase() { companion object { const val DB_NAME = "ivywallet.db" + fun migrations() = arrayOf( + Migration105to106_TrnRecurringRules(), + Migration106to107_Wishlist(), + Migration107to108_Sync(), + Migration108to109_Users(), + Migration109to110_PlannedPayments(), + Migration110to111_PlannedPaymentRule(), + Migration111to112_User_testUser(), + Migration112to113_ExchangeRates(), + Migration113to114_Multi_Currency(), + Migration114to115_Category_Account_Icons(), + Migration115to116_Account_Include_In_Balance(), + Migration116to117_SalteEdgeIntgration(), + Migration117to118_Budgets(), + Migration118to119_Loans(), + Migration119to120_LoanTransactions(), + Migration120to121_DropWishlistItem(), + Migration122to123_ExchangeRates(), + Migration123to124_LoanIncludeDateTime(), + Migration124to125_LoanEditDateTime() + ) + fun create(applicationContext: Context): IvyRoomDatabase { return Room .databaseBuilder( @@ -103,27 +125,7 @@ abstract class IvyRoomDatabase : RoomDatabase() { IvyRoomDatabase::class.java, DB_NAME ) - .addMigrations( - Migration105to106_TrnRecurringRules(), - Migration106to107_Wishlist(), - Migration107to108_Sync(), - Migration108to109_Users(), - Migration109to110_PlannedPayments(), - Migration110to111_PlannedPaymentRule(), - Migration111to112_User_testUser(), - Migration112to113_ExchangeRates(), - Migration113to114_Multi_Currency(), - Migration114to115_Category_Account_Icons(), - Migration115to116_Account_Include_In_Balance(), - Migration116to117_SalteEdgeIntgration(), - Migration117to118_Budgets(), - Migration118to119_Loans(), - Migration119to120_LoanTransactions(), - Migration120to121_DropWishlistItem(), - Migration122to123_ExchangeRates(), - Migration123to124_LoanIncludeDateTime(), - Migration124to125_LoanEditDateTime() - ) + .addMigrations(*migrations()) .build() } } From 7f3258b10a15f96305171ed8268cca5688b10401 Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Sat, 27 Jan 2024 19:30:47 +0200 Subject: [PATCH 02/12] WIP: Setup Room migration tests --- buildSrc/build.gradle.kts | 1 + buildSrc/src/main/kotlin/ivy.room.gradle.kts | 7 +++++-- gradle/libs.versions.toml | 6 ++++-- ivy-data/build.gradle.kts | 1 - 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index b1f937da56..b4232dd2a4 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(libs.kotlinx.serialization.plugin) implementation(libs.ksp.plugin) implementation(libs.cashapp.molecule.plugin) + implementation(libs.room.plugin) // Make version catalog available in precompiled scripts // https://github.com/gradle/gradle/issues/15383#issuecomment-1567461389 diff --git a/buildSrc/src/main/kotlin/ivy.room.gradle.kts b/buildSrc/src/main/kotlin/ivy.room.gradle.kts index 0cd44e0092..9f6c48ca92 100644 --- a/buildSrc/src/main/kotlin/ivy.room.gradle.kts +++ b/buildSrc/src/main/kotlin/ivy.room.gradle.kts @@ -1,12 +1,15 @@ plugins { id("ivy.module") + id("androidx.room") } dependencies { implementation(libs.bundles.room) ksp(libs.room.compiler) + + androidTestImplementation(libs.room.testing) } -ksp { - arg("room.schemaLocation", "$projectDir/schemas") +room { + schemaDirectory("$projectDir/schemas") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index de2f4a3670..57721c00a7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -85,13 +85,15 @@ glance = { module = "androidx.glance:glance", version.ref = "glance" } glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glance" } glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "glance" } -# Local persistence -datastore = { module = "androidx.datastore:datastore-preferences", version = "1.0.0" } +# Room +room-plugin = {module="androidx.room:androidx.room.gradle.plugin", version.ref = "room"} room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } room-testing = { module = "androidx.room:room-testing", version.ref = "room" } +datastore = { module = "androidx.datastore:datastore-preferences", version = "1.0.0" } + # Hilt hilt = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-work = { module = "androidx.hilt:hilt-work", version = "1.1.0" } diff --git a/ivy-data/build.gradle.kts b/ivy-data/build.gradle.kts index 699eb4419b..7e8955729e 100644 --- a/ivy-data/build.gradle.kts +++ b/ivy-data/build.gradle.kts @@ -17,6 +17,5 @@ dependencies { implementation(libs.bundles.ktor) androidTestImplementation(libs.bundles.integration.testing) - androidTestImplementation(libs.room.testing) testImplementation(projects.ivyTesting) } From 3d254fca7be35d8d120a7e485dfeb1bd0ce55b1c Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Sat, 27 Jan 2024 19:52:12 +0200 Subject: [PATCH 03/12] Working room DB migration configuration --- buildSrc/src/main/kotlin/ivy.room.gradle.kts | 7 +++++++ .../java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/ivy.room.gradle.kts b/buildSrc/src/main/kotlin/ivy.room.gradle.kts index 9f6c48ca92..349a29f5d3 100644 --- a/buildSrc/src/main/kotlin/ivy.room.gradle.kts +++ b/buildSrc/src/main/kotlin/ivy.room.gradle.kts @@ -10,6 +10,13 @@ dependencies { androidTestImplementation(libs.room.testing) } +android { + sourceSets { + // Adds exported schema location as test app assets. + getByName("androidTest").assets.srcDirs(files("$projectDir/schemas")) + } +} + room { schemaDirectory("$projectDir/schemas") } diff --git a/ivy-data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt b/ivy-data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt index 6bf514f61e..906858169f 100644 --- a/ivy-data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt +++ b/ivy-data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt @@ -26,7 +26,7 @@ class IvyRoomDatabaseMigrationTest { @Throws(IOException::class) fun migrateAll() { // Create earliest version of the database. - helper.createDatabase(TEST_DB, 1).apply { + helper.createDatabase(TEST_DB, 101).apply { close() } From 64b6faf80413034faa3bf248d2c496584abec050 Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Sat, 27 Jan 2024 19:58:42 +0200 Subject: [PATCH 04/12] Add DB migrations tests --- .../ivy/data/db/IvyRoomDatabaseMigrationTest.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ivy-data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt b/ivy-data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt index 906858169f..f46e770dac 100644 --- a/ivy-data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt +++ b/ivy-data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt @@ -12,7 +12,6 @@ import java.io.IOException @RunWith(AndroidJUnit4::class) class IvyRoomDatabaseMigrationTest { - private val TEST_DB = "migration-test" @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( @@ -25,19 +24,24 @@ class IvyRoomDatabaseMigrationTest { @Test @Throws(IOException::class) fun migrateAll() { - // Create earliest version of the database. - helper.createDatabase(TEST_DB, 101).apply { + // Create earliest version of the database: + // for Ivy Wallet versions below 106 are broken :/ + helper.createDatabase(TestDb, 106).apply { close() } - // Open latest version of the database. Room validates the schema - // once all migrations execute. + // Open latest version of the database. + // Room validates and executes all migrations. Room.databaseBuilder( InstrumentationRegistry.getInstrumentation().targetContext, IvyRoomDatabase::class.java, - TEST_DB + TestDb ).addMigrations(*IvyRoomDatabase.migrations()).build().apply { openHelper.writableDatabase.close() } } + + companion object { + private const val TestDb = "migration-test" + } } \ No newline at end of file From 9fc24b43bd1b962e675b2e1c41df0c5318519f2c Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Sat, 27 Jan 2024 20:29:25 +0200 Subject: [PATCH 05/12] Add migration tests for 123 to 125 --- .../data/db/IvyRoomDatabaseMigrationTest.kt | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/ivy-data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt b/ivy-data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt index f46e770dac..5ddcd085c8 100644 --- a/ivy-data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt +++ b/ivy-data/src/androidTest/java/com/ivy/data/db/IvyRoomDatabaseMigrationTest.kt @@ -5,10 +5,14 @@ import androidx.room.testing.MigrationTestHelper import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import com.ivy.data.db.migration.Migration123to124_LoanIncludeDateTime +import com.ivy.data.db.migration.Migration124to125_LoanEditDateTime +import com.ivy.data.model.LoanType +import io.kotest.matchers.shouldBe import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import java.io.IOException +import java.util.UUID @RunWith(AndroidJUnit4::class) class IvyRoomDatabaseMigrationTest { @@ -22,14 +26,69 @@ class IvyRoomDatabaseMigrationTest { ) @Test - @Throws(IOException::class) + fun migrate123to125_LoanDateTime() { + // given + helper.createDatabase(TestDb, 123).apply { + // Database has schema version 1. Insert some data using SQL queries. + // You can't use DAO classes because they expect the latest schema. + val insertSql = """ + INSERT INTO loans (name, amount, type, color, icon, orderNum, accountId, isSynced, isDeleted, id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + """.trimIndent() + + // Assuming you have an instance of LoanEntity named loanEntity + val preparedStatement = compileStatement(insertSql).apply { + // Bind the values from your LoanEntity instance to the prepared statement + bindString(1, "Loan 1") + bindDouble(2, 123.50) + bindString(3, LoanType.BORROW.name) // Assuming you store enum as name + bindLong(4, 13) + bindString(5, "ic") + bindDouble(6, 3.14) + bindString(7, UUID.randomUUID().toString()) + bindLong(8, 1) + bindLong(9, 0) + bindString(10, UUID.randomUUID().toString()) + + } + preparedStatement.executeInsert() + close() + } + + // when + helper.runMigrationsAndValidate( + TestDb, + 124, + true, + Migration123to124_LoanIncludeDateTime() + ) + val newDb = helper.runMigrationsAndValidate( + TestDb, + 125, + true, + Migration124to125_LoanEditDateTime() + ) + + // then + newDb.query("SELECT * FROM loans").apply { + moveToFirst() shouldBe true + getString(0) shouldBe "Loan 1" + getDouble(1) shouldBe 123.50 + getString(2) shouldBe LoanType.BORROW.name + } + newDb.close() + } + + @Test fun migrateAll() { + // given: // Create earliest version of the database: // for Ivy Wallet versions below 106 are broken :/ helper.createDatabase(TestDb, 106).apply { close() } + // then: // Open latest version of the database. // Room validates and executes all migrations. Room.databaseBuilder( From f51787e8d47161086f53b89a7ac2af7535f86805 Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Sat, 27 Jan 2024 20:35:01 +0200 Subject: [PATCH 06/12] Add GitHub Action workflow that runs the integration tests --- .github/workflows/integration_test.yml | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/integration_test.yml diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml new file mode 100644 index 0000000000..adc2768191 --- /dev/null +++ b/.github/workflows/integration_test.yml @@ -0,0 +1,49 @@ +name: Integration tests (androidTest) + +on: + push: + branches: + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout GIT + uses: actions/checkout@v4 + + - name: Setup Java SDK + uses: actions/setup-java@v4 + with: + distribution: 'adopt' + java-version: '18' + + - name: Enable Gradle Wrapper caching (optimization) + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build Android code + run: ./gradlew assembleDebug assembleAndroidTest --stacktrace + + - name: Set up Android SDK + uses: android-actions/setup-android@v2 + + - name: Create and start emulator + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 29 + script: ./gradlew connectedCheck --stacktrace + target: default + arch: x86_64 + profile: Nexus 6 + disable-animations: true + disable-spellchecker: true + disable-android-watchers: true + disable-adb-kill-server: true From db3a991038bb2095937e226d054078edb6425f5f Mon Sep 17 00:00:00 2001 From: iliyangermanov Date: Sat, 27 Jan 2024 20:46:47 +0200 Subject: [PATCH 07/12] WIP: Fix Android linkage problems & the new workflow --- .github/workflows/integration_test.yml | 6 +++--- app/src/main/AndroidManifest.xml | 1 + {ivy-resources => app}/src/main/res/values/styles.xml | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) rename {ivy-resources => app}/src/main/res/values/styles.xml (99%) diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index adc2768191..5cee18a822 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -7,7 +7,7 @@ on: pull_request: jobs: - test: + integration_test: runs-on: ubuntu-latest steps: - name: Checkout GIT @@ -30,7 +30,7 @@ jobs: ${{ runner.os }}-gradle- - name: Build Android code - run: ./gradlew assembleDebug assembleAndroidTest --stacktrace + run: ./gradlew assembleDebug assembleAndroidTest - name: Set up Android SDK uses: android-actions/setup-android@v2 @@ -39,7 +39,7 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: 29 - script: ./gradlew connectedCheck --stacktrace + script: ./gradlew connectedDebugAndroidTest target: default arch: x86_64 profile: Nexus 6 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bbdddc5875..4bfc7f119e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -94,6 +94,7 @@ android:name="android.appwidget.provider" android:resource="@xml/wallet_balance_widget_info" /> + -