Skip to content

Commit 56fb55a

Browse files
authored
feat(configstore): add testing utilities and improve ConfigKey behavior (#10519)
2 parents 36f476c + 959a2e6 commit 56fb55a

File tree

8 files changed

+249
-1
lines changed

8 files changed

+249
-1
lines changed

core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/Config.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@ package net.thunderbird.core.configstore
33
/**
44
* A configuration holds key-value pairs of [ConfigKey] and their corresponding values.
55
*
6-
* It is used to store and retrieve configuration settings in a type-safe manner.
6+
* This is used to store and retrieve configuration settings in a type-safe manner.
77
*/
88
class Config(
99
private val entries: MutableMap<ConfigKey<*>, Any?> = mutableMapOf(),
1010
) {
11+
/**
12+
* Returns the value associated with the given [ConfigKey], or null if the key is not present.
13+
*/
1114
@Suppress("UNCHECKED_CAST")
1215
operator fun <T> get(key: ConfigKey<T>): T? = entries[key] as? T
1316

17+
/**
18+
* Sets the value for the given [ConfigKey]. The value must be of the correct type corresponding to the key.
19+
*/
1420
operator fun <T> set(key: ConfigKey<T>, value: T) {
1521
entries[key] = value
1622
}
@@ -19,4 +25,9 @@ class Config(
1925
* Returns a map representation of the configuration.
2026
*/
2127
fun toMap(): Map<ConfigKey<*>, Any?> = entries.toMap()
28+
29+
/**
30+
* Returns a copy of the configuration.
31+
*/
32+
fun copy(): Config = Config(entries.toMutableMap())
2233
}

core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigKey.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,19 @@ sealed class ConfigKey<T>(val name: String) {
1616
class LongKey(name: String) : ConfigKey<Long>(name)
1717
class FloatKey(name: String) : ConfigKey<Float>(name)
1818
class DoubleKey(name: String) : ConfigKey<Double>(name)
19+
20+
override fun equals(other: Any?): Boolean {
21+
if (this === other) return true
22+
if (other == null || this::class != other::class) return false
23+
other as ConfigKey<*>
24+
return name == other.name
25+
}
26+
27+
override fun hashCode(): Int {
28+
return name.hashCode() + 31 * this::class.hashCode()
29+
}
30+
31+
override fun toString(): String {
32+
return "${this::class.simpleName}(name='$name')"
33+
}
1934
}

core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/ConfigKeyTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package net.thunderbird.core.configstore
33
import assertk.assertThat
44
import assertk.assertions.isEqualTo
55
import assertk.assertions.isInstanceOf
6+
import assertk.assertions.isNotEqualTo
67
import kotlin.test.Test
78

89
class ConfigKeyTest {
@@ -84,4 +85,36 @@ class ConfigKeyTest {
8485
assertThat(key.name).isEqualTo(name)
8586
assertThat(key).isInstanceOf(ConfigKey.DoubleKey::class)
8687
}
88+
89+
@Test
90+
fun `equals should return true for same key type and same name`() {
91+
val key1 = ConfigKey.StringKey("test")
92+
val key2 = ConfigKey.StringKey("test")
93+
94+
assertThat(key1).isEqualTo(key2)
95+
assertThat(key1.hashCode()).isEqualTo(key2.hashCode())
96+
}
97+
98+
@Test
99+
fun `equals should return false for different key type and same name`() {
100+
val key1 = ConfigKey.StringKey("test")
101+
val key2 = ConfigKey.IntKey("test")
102+
103+
assertThat(key1).isNotEqualTo(key2)
104+
}
105+
106+
@Test
107+
fun `equals should return false for same key type and different name`() {
108+
val key1 = ConfigKey.StringKey("test1")
109+
val key2 = ConfigKey.StringKey("test2")
110+
111+
assertThat(key1).isNotEqualTo(key2)
112+
}
113+
114+
@Test
115+
fun `toString should return correct representation`() {
116+
val key = ConfigKey.IntKey("myKey")
117+
118+
assertThat(key.toString()).isEqualTo("IntKey(name='myKey')")
119+
}
87120
}

core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/ConfigTest.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package net.thunderbird.core.configstore
22

33
import assertk.assertThat
44
import assertk.assertions.isEqualTo
5+
import assertk.assertions.isNotSameInstanceAs
56
import assertk.assertions.isNull
67
import kotlin.test.Test
78

@@ -97,4 +98,19 @@ class ConfigTest {
9798
assertThat(map[key1]).isEqualTo("value1")
9899
assertThat(map[key2]).isEqualTo(2)
99100
}
101+
102+
@Test
103+
fun `copy should return a new instance with the same entries`() {
104+
// Arrange
105+
val config = Config()
106+
val key = ConfigKey.StringKey("key")
107+
config[key] = "value"
108+
109+
// Act
110+
val copy = config.copy()
111+
112+
// Assert
113+
assertThat(copy).isNotSameInstanceAs(config)
114+
assertThat(copy[key]).isEqualTo("value")
115+
}
100116
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
plugins {
2+
id(ThunderbirdPlugins.Library.kmp)
3+
}
4+
5+
kotlin {
6+
androidLibrary {
7+
namespace = "net.thunderbird.core.configstore.testing"
8+
withHostTest {}
9+
}
10+
11+
sourceSets {
12+
commonMain.dependencies {
13+
api(projects.core.configstore.api)
14+
}
15+
}
16+
}
17+
18+
codeCoverage {
19+
branchCoverage = 0
20+
lineCoverage = 0
21+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package net.thunderbird.core.configstore.testing
2+
3+
import kotlinx.coroutines.flow.Flow
4+
import kotlinx.coroutines.flow.MutableStateFlow
5+
import kotlinx.coroutines.flow.asStateFlow
6+
import kotlinx.coroutines.flow.update
7+
import net.thunderbird.core.configstore.Config
8+
import net.thunderbird.core.configstore.ConfigKey
9+
import net.thunderbird.core.configstore.backend.ConfigBackend
10+
11+
/**
12+
* A mutable, in-memory implementation of [ConfigBackend] designed for testing.
13+
*
14+
* This implementation allows tests to simulate configuration storage without relying on
15+
* actual persistence mechanisms like DataStore. It stores configuration in a [MutableStateFlow],
16+
* providing reactive updates and thread-safe access.
17+
*
18+
* Use this in unit tests where you need to verify how components interact with the config store
19+
* or to provide a controlled environment for configuration-dependent logic.
20+
*
21+
* @param initialConfig The starting configuration state. Defaults to an empty [Config].
22+
*/
23+
class TestConfigBackend(
24+
initialConfig: Config = Config(),
25+
) : ConfigBackend {
26+
27+
private val config: MutableStateFlow<Config> = MutableStateFlow(initialConfig)
28+
29+
override fun read(keys: List<ConfigKey<*>>): Flow<Config> = config.asStateFlow()
30+
31+
override suspend fun update(
32+
keys: List<ConfigKey<*>>,
33+
transform: (Config) -> Config,
34+
) {
35+
config.update { transform(it) }
36+
}
37+
38+
override suspend fun clear() {
39+
config.value = Config()
40+
}
41+
42+
override suspend fun readVersion(versionKey: String): Int {
43+
return config.value[ConfigKey.IntKey(versionKey)] ?: 0
44+
}
45+
46+
override suspend fun writeVersion(versionKey: String, version: Int) {
47+
config.update { current ->
48+
val newConfig = current.copy()
49+
newConfig[ConfigKey.IntKey(versionKey)] = version
50+
newConfig
51+
}
52+
}
53+
54+
override suspend fun removeKeys(keys: Set<ConfigKey<*>>) {
55+
config.update { current ->
56+
val newMap = current.toMap().toMutableMap()
57+
keys.forEach { newMap.remove(it) }
58+
Config(newMap)
59+
}
60+
}
61+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package net.thunderbird.core.configstore.testing
2+
3+
import assertk.assertThat
4+
import assertk.assertions.isEqualTo
5+
import assertk.assertions.isNull
6+
import kotlin.test.Test
7+
import kotlinx.coroutines.flow.first
8+
import kotlinx.coroutines.test.runTest
9+
import net.thunderbird.core.configstore.Config
10+
import net.thunderbird.core.configstore.ConfigKey
11+
12+
class TestConfigBackendTest {
13+
14+
private val booleanKey = ConfigKey.BooleanKey("boolean")
15+
private val intKey = ConfigKey.IntKey("int")
16+
private val stringKey = ConfigKey.StringKey("string")
17+
18+
@Test
19+
fun `read should return initial config`() = runTest {
20+
val initialConfig = Config().apply {
21+
set(booleanKey, true)
22+
}
23+
val backend = TestConfigBackend(initialConfig)
24+
25+
val config = backend.read(listOf(booleanKey)).first()
26+
27+
assertThat(config[booleanKey]).isEqualTo(true)
28+
}
29+
30+
@Test
31+
fun `update should modify config`() = runTest {
32+
val backend = TestConfigBackend()
33+
34+
backend.update(listOf(intKey)) { config ->
35+
val newConfig = config.copy()
36+
newConfig[intKey] = 42
37+
newConfig
38+
}
39+
40+
val config = backend.read(listOf(intKey)).first()
41+
assertThat(config[intKey]).isEqualTo(42)
42+
}
43+
44+
@Test
45+
fun `clear should reset config`() = runTest {
46+
val initialConfig = Config().apply {
47+
set(stringKey, "value")
48+
}
49+
val backend = TestConfigBackend(initialConfig)
50+
51+
backend.clear()
52+
53+
val config = backend.read(listOf(stringKey)).first()
54+
assertThat(config[stringKey]).isNull()
55+
}
56+
57+
@Test
58+
fun `readVersion should return 0 by default`() = runTest {
59+
val backend = TestConfigBackend()
60+
61+
val version = backend.readVersion("version_key")
62+
63+
assertThat(version).isEqualTo(0)
64+
}
65+
66+
@Test
67+
fun `writeVersion should update version`() = runTest {
68+
val backend = TestConfigBackend()
69+
70+
backend.writeVersion("version_key", 5)
71+
72+
val version = backend.readVersion("version_key")
73+
assertThat(version).isEqualTo(5)
74+
}
75+
76+
@Test
77+
fun `removeKeys should remove specified keys`() = runTest {
78+
val initialConfig = Config().apply {
79+
set(booleanKey, true)
80+
set(intKey, 42)
81+
}
82+
val backend = TestConfigBackend(initialConfig)
83+
84+
backend.removeKeys(setOf(booleanKey))
85+
86+
val config = backend.read(listOf(booleanKey, intKey)).first()
87+
assertThat(config[booleanKey]).isNull()
88+
assertThat(config[intKey]).isEqualTo(42)
89+
}
90+
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ include(
161161
":core:common",
162162
":core:configstore:api",
163163
":core:configstore:impl-backend",
164+
":core:configstore:testing",
164165
":core:featureflag",
165166
":core:logging:api",
166167
":core:logging:config",

0 commit comments

Comments
 (0)