Skip to content

Commit b2ae14b

Browse files
committed
Add test for storage migrations, set up CI for tests
1 parent 031d8e0 commit b2ae14b

File tree

5 files changed

+112
-1
lines changed

5 files changed

+112
-1
lines changed

.github/workflows/build-all.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ jobs:
2323
java-version: 21
2424
- name: Assemble desktop jar
2525
run: ./gradlew :shared:jvmJar
26+
- name: Run JVM tests
27+
run: ./gradlew jvmTest
2628

2729
build-android:
2830
name: Build Android

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
6565
koin-slf4j = { module = "io.insert-koin:koin-logger-slf4j", version.ref = "koin" }
6666
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
6767
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" }
68+
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
6869
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
6970
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" }
7071
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationCore" }
@@ -99,6 +100,7 @@ settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "mul
99100
settings-coroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "multiplatform-settings" }
100101
settings-observable = { module = "com.russhwolf:multiplatform-settings-make-observable", version.ref = "multiplatform-settings" }
101102
settings-serialization = { module = "com.russhwolf:multiplatform-settings-serialization", version.ref = "multiplatform-settings" }
103+
settings-test = { module = "com.russhwolf:multiplatform-settings-test", version.ref = "multiplatform-settings" }
102104
slf4j-nop = { module = "org.slf4j:slf4j-nop", version.ref = "slf4jNop" }
103105

104106
[plugins]

shared/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ kotlin {
105105

106106
commonTest.dependencies {
107107
implementation(kotlin("test"))
108+
implementation(libs.settings.test)
109+
implementation(libs.kotlinx.coroutines.test)
108110
}
109111

110112
val nonAndroidMain by creating {

shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/storage/MultiplatformSettingsStorage.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class MultiplatformSettingsStorage(
142142
},
143143
)
144144

145-
private companion object {
145+
companion object {
146146
const val V2025 = 2025_000
147147
const val V2026 = 2026_000
148148
const val CURRENT_STORAGE_VERSION: Int = V2026
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package org.jetbrains.kotlinconf.storage
2+
3+
import com.russhwolf.settings.ExperimentalSettingsApi
4+
import com.russhwolf.settings.MapSettings
5+
import com.russhwolf.settings.ObservableSettings
6+
import com.russhwolf.settings.observable.makeObservable
7+
import com.russhwolf.settings.set
8+
import kotlinx.coroutines.flow.first
9+
import kotlinx.coroutines.test.runTest
10+
import kotlinx.serialization.json.Json
11+
import org.jetbrains.kotlinconf.NotificationSettings
12+
import kotlin.test.Test
13+
import kotlin.test.assertEquals
14+
import kotlin.test.assertNull
15+
import kotlin.test.assertTrue
16+
17+
class MultiplatformSettingsStorageMigrationTest {
18+
19+
@OptIn(ExperimentalSettingsApi::class)
20+
private fun inMemorySettings(): ObservableSettings = MapSettings().makeObservable()
21+
22+
/**
23+
* Creates a storage object with data matching the older, 2025 storage version.
24+
*/
25+
private fun get2025Settings() = inMemorySettings().apply {
26+
this["storageVersion"] = MultiplatformSettingsStorage.V2025
27+
this["newsCache"] = "{\"dummy\":\"value\"}"
28+
this["notificationSettings"] = """
29+
{
30+
"sessionReminders": true,
31+
"scheduleUpdates": false,
32+
"jetBrainsNews": true,
33+
"kotlinConfNews": "true"
34+
}
35+
""".trimIndent()
36+
}
37+
38+
@Test
39+
fun migration_2025_to_2026_updates_version() {
40+
val settings = get2025Settings()
41+
val storage = MultiplatformSettingsStorage(settings)
42+
43+
// Run migrations
44+
storage.ensureCurrentVersion()
45+
46+
// Storage version should be successfully updated to the current version
47+
assertEquals(MultiplatformSettingsStorage.CURRENT_STORAGE_VERSION, settings.getInt("storageVersion", 0))
48+
}
49+
50+
@Test
51+
fun migration_2026_to_2026_removes_news_cache() {
52+
val settings = get2025Settings()
53+
val storage = MultiplatformSettingsStorage(settings)
54+
55+
// Run migrations
56+
storage.ensureCurrentVersion()
57+
58+
// The newsCache key should be removed by the migration
59+
assertNull(settings.getStringOrNull("newsCache"))
60+
}
61+
62+
@Test
63+
fun migration_2026_to_2026_migrates_notification_settings() = runTest {
64+
val settings = get2025Settings()
65+
val storage = MultiplatformSettingsStorage(settings)
66+
67+
// Run migrations
68+
storage.ensureCurrentVersion()
69+
70+
// Notification settings should still be readable with the current version
71+
val notiSettings = storage.getNotificationSettings().first()
72+
assertTrue(notiSettings != null)
73+
}
74+
75+
@Test
76+
fun unknown_storage_version_triggers_destructive_upgrade_and_clears_all_keys() {
77+
val settings = inMemorySettings()
78+
settings["userId2025"] = "user-123"
79+
settings["newsCache"] = "legacy-data"
80+
settings["notificationSettings"] = "{\"sessionReminders\":false,\"scheduleUpdates\":true}"
81+
val storage = MultiplatformSettingsStorage(settings)
82+
83+
// Run migrations
84+
storage.ensureCurrentVersion()
85+
86+
// Only the storageVersion should remain and be set to the current version
87+
assertEquals(setOf("storageVersion"), settings.keys)
88+
assertEquals(MultiplatformSettingsStorage.CURRENT_STORAGE_VERSION, settings.getInt("storageVersion", 0))
89+
}
90+
91+
@Test
92+
fun unknown_migration_path_triggers_destructive_upgrade() {
93+
val settings = inMemorySettings()
94+
settings["storageVersion"] = 2024_000 // A version we don't have a migration for
95+
settings["favorites"] = "[\"S1\",\"S2\"]"
96+
val storage = MultiplatformSettingsStorage(settings)
97+
98+
// Run migrations
99+
storage.ensureCurrentVersion()
100+
101+
// Only the storageVersion should remain and be set to the current version
102+
assertEquals(setOf("storageVersion"), settings.keys)
103+
assertEquals(MultiplatformSettingsStorage.CURRENT_STORAGE_VERSION, settings.getInt("storageVersion", 0))
104+
}
105+
}

0 commit comments

Comments
 (0)