Skip to content

Commit b825f75

Browse files
committed
refactor: error messaging, tests, cleaning
Signed-off-by: Marcin Stepien <marcin.stepien@fluxon.com>
1 parent f9a501f commit b825f75

4 files changed

Lines changed: 130 additions & 26 deletions

File tree

kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/providers/memory/Flag.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ data class Flag<T>(
1212
val flagMetadata: EvaluationMetadata? = null,
1313
val disabled: Boolean = false
1414
) {
15+
init {
16+
if (defaultVariant != null && !variants.containsKey(defaultVariant)) {
17+
throw IllegalArgumentException("defaultVariant ($defaultVariant) is not present in variants map")
18+
}
19+
}
20+
1521
companion object {
1622
fun <T> builder() = Builder<T>()
1723
}
@@ -31,11 +37,7 @@ data class Flag<T>(
3137
fun disabled(disabled: Boolean) = apply { this.disabled = disabled }
3238

3339
fun build(): Flag<T> {
34-
val dv = defaultVariant
35-
if (dv != null && !variants.containsKey(dv)) {
36-
throw IllegalArgumentException("defaultVariant ($dv) is not present in variants map")
37-
}
38-
return Flag(variants.toMap(), dv, contextEvaluator, flagMetadata, disabled)
40+
return Flag(variants.toMap(), defaultVariant, contextEvaluator, flagMetadata, disabled)
3941
}
4042
}
4143
}

kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/providers/memory/InMemoryProvider.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dev.openfeature.kotlin.sdk.providers.memory
22

33
import dev.openfeature.kotlin.sdk.EvaluationContext
4+
import dev.openfeature.kotlin.sdk.EvaluationMetadata
45
import dev.openfeature.kotlin.sdk.FeatureProvider
56
import dev.openfeature.kotlin.sdk.Hook
67
import dev.openfeature.kotlin.sdk.OpenFeatureStatus
@@ -12,6 +13,7 @@ import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents
1213
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents.EventDetails
1314
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents.ProviderConfigurationChanged
1415
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents.ProviderReady
16+
import dev.openfeature.kotlin.sdk.exceptions.ErrorCode
1517
import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError.FlagNotFoundError
1618
import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError.ProviderNotReadyError
1719
import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError.TypeMismatchError
@@ -139,12 +141,14 @@ class InMemoryProvider(initialFlags: Map<String, Flag<*>> = emptyMap()) : Featur
139141
value = defaultValue,
140142
reason = Reason.DISABLED.toString(),
141143
metadata = flag.flagMetadata
142-
?: dev.openfeature.kotlin.sdk.EvaluationMetadata.EMPTY
144+
?: EvaluationMetadata.EMPTY
143145
)
144146
}
145147

146148
var value: Any? = null
147149
var reason = Reason.STATIC
150+
var errorCode: ErrorCode? = null
151+
var errorMessage: String? = null
148152

149153
if (flag.contextEvaluator != null) {
150154
try {
@@ -156,11 +160,12 @@ class InMemoryProvider(initialFlags: Map<String, Flag<*>> = emptyMap()) : Featur
156160
)
157161
reason = Reason.TARGETING_MATCH
158162
} catch (e: Exception) {
159-
value = null
163+
errorCode = ErrorCode.GENERAL
164+
errorMessage = e.message ?: "Error evaluating context"
160165
}
161166
if (value == null) {
162167
value = flag.defaultVariant?.let { flag.variants[it] }
163-
reason = Reason.DEFAULT
168+
reason = if (errorCode != null) Reason.ERROR else Reason.DEFAULT
164169
}
165170
} else {
166171
value = flag.defaultVariant?.let { flag.variants[it] }
@@ -179,9 +184,11 @@ class InMemoryProvider(initialFlags: Map<String, Flag<*>> = emptyMap()) : Featur
179184
@Suppress("UNCHECKED_CAST")
180185
return ProviderEvaluation(
181186
value = value as T,
182-
variant = if (reason == Reason.DEFAULT) flag.defaultVariant else null,
187+
variant = if (reason == Reason.DEFAULT || reason == Reason.ERROR) flag.defaultVariant else null,
183188
reason = reason.toString(),
184-
metadata = flag.flagMetadata ?: dev.openfeature.kotlin.sdk.EvaluationMetadata.EMPTY
189+
errorCode = errorCode,
190+
errorMessage = errorMessage,
191+
metadata = flag.flagMetadata ?: EvaluationMetadata.EMPTY
185192
)
186193
}
187194

kotlin-sdk/src/commonTest/kotlin/dev/openfeature/kotlin/sdk/providers/memory/InMemoryProviderTest.kt

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
package dev.openfeature.kotlin.sdk.providers.memory
22

33
import dev.openfeature.kotlin.sdk.OpenFeatureAPI
4+
import dev.openfeature.kotlin.sdk.Reason
45
import dev.openfeature.kotlin.sdk.Value
6+
import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents
57
import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError.FlagNotFoundError
68
import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError.ProviderNotReadyError
79
import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError.TypeMismatchError
810
import kotlinx.coroutines.ExperimentalCoroutinesApi
11+
import kotlinx.coroutines.flow.first
12+
import kotlinx.coroutines.launch
13+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
914
import kotlinx.coroutines.test.runTest
1015
import kotlin.test.AfterTest
1116
import kotlin.test.BeforeTest
1217
import kotlin.test.Test
1318
import kotlin.test.assertEquals
1419
import kotlin.test.assertFailsWith
20+
import kotlin.test.assertTrue
1521

1622
@OptIn(ExperimentalCoroutinesApi::class)
1723
class InMemoryProviderTest {
@@ -105,4 +111,83 @@ class InMemoryProviderTest {
105111
newProvider.getBooleanEvaluation("some_flag", false, null)
106112
}
107113
}
114+
115+
@Test
116+
fun `disabled flag returns default value and reason DISABLED`() = runTest {
117+
val disabledFlag = Flag.builder<Boolean>()
118+
.variant("on", true)
119+
.variant("off", false)
120+
.defaultVariant("on")
121+
.disabled(true)
122+
.build()
123+
124+
val localProvider = InMemoryProvider(mapOf("disabled-flag" to disabledFlag))
125+
localProvider.initialize(null)
126+
127+
val eval = localProvider.getBooleanEvaluation("disabled-flag", false, null)
128+
assertEquals(false, eval.value)
129+
assertEquals(Reason.DISABLED.toString(), eval.reason)
130+
}
131+
132+
@Test
133+
fun `contextEvaluator returning null falls back to defaultVariant with Reason DEFAULT`() = runTest {
134+
val evaluatorFlag = Flag.builder<Boolean>()
135+
.variant("on", true)
136+
.variant("off", false)
137+
.defaultVariant("on")
138+
.contextEvaluator { _, _ -> null }
139+
.build()
140+
141+
val localProvider = InMemoryProvider(mapOf("evaluator-flag" to evaluatorFlag))
142+
localProvider.initialize(null)
143+
144+
val eval = localProvider.getBooleanEvaluation("evaluator-flag", false, null)
145+
assertEquals(true, eval.value) // defaultVariant "on" has value true
146+
assertEquals(Reason.DEFAULT.toString(), eval.reason)
147+
}
148+
149+
@Test
150+
fun `updateFlags and updateFlag fire ProviderConfigurationChanged event`() = runTest {
151+
val newFlag = Flag.builder<Boolean>()
152+
.variant("on", true)
153+
.defaultVariant("on")
154+
.build()
155+
156+
val job = launch(UnconfinedTestDispatcher(testScheduler)) {
157+
val event = provider.observe().first { it is OpenFeatureProviderEvents.ProviderConfigurationChanged }
158+
assertTrue(event is OpenFeatureProviderEvents.ProviderConfigurationChanged)
159+
}
160+
161+
provider.updateFlag("new-flag", newFlag)
162+
job.join()
163+
164+
val multiFlags = mapOf(
165+
"another-flag" to Flag.builder<Boolean>().variant("on", true).defaultVariant("on").build()
166+
)
167+
val job2 = launch(UnconfinedTestDispatcher(testScheduler)) {
168+
val event = provider.observe().first { it is OpenFeatureProviderEvents.ProviderConfigurationChanged }
169+
assertTrue(event is OpenFeatureProviderEvents.ProviderConfigurationChanged)
170+
}
171+
provider.updateFlags(multiFlags)
172+
job2.join()
173+
}
174+
175+
@Test
176+
fun `context evaluator exception defaults to fallback and Reason ERROR`() = runTest {
177+
val evaluatorFlag = Flag.builder<Boolean>()
178+
.variant("on", true)
179+
.variant("off", false)
180+
.defaultVariant("on")
181+
.contextEvaluator { _, _ -> throw Exception("Simulated evaluation error") }
182+
.build()
183+
184+
val localProvider = InMemoryProvider(mapOf("evaluator-error-flag" to evaluatorFlag))
185+
localProvider.initialize(null)
186+
187+
val eval = localProvider.getBooleanEvaluation("evaluator-error-flag", false, null)
188+
assertEquals(true, eval.value) // defaultVariant "on" has value true
189+
assertEquals(Reason.ERROR.toString(), eval.reason)
190+
assertEquals(dev.openfeature.kotlin.sdk.exceptions.ErrorCode.GENERAL, eval.errorCode)
191+
assertEquals("Simulated evaluation error", eval.errorMessage)
192+
}
108193
}

kotlin-sdk/src/jvmTest/kotlin/dev/openfeature/kotlin/sdk/e2e/EvaluationSteps.kt

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
/* ktlint-disable max-line-length */
1+
22
package dev.openfeature.kotlin.sdk.e2e
33

44
import dev.openfeature.kotlin.sdk.Client
55
import dev.openfeature.kotlin.sdk.EvaluationContext
6+
import dev.openfeature.kotlin.sdk.FlagEvaluationDetails
67
import dev.openfeature.kotlin.sdk.OpenFeatureAPI
78
import dev.openfeature.kotlin.sdk.Reason
89
import dev.openfeature.kotlin.sdk.Value
@@ -20,8 +21,11 @@ class EvaluationSteps {
2021
@Given("a stable provider")
2122
fun setup(): Unit = runBlocking {
2223
val flags = mapOf(
23-
"boolean-flag" to Flag.builder<Boolean>().variant("on", true).variant("off", false).defaultVariant("on").build(),
24-
"string-flag" to Flag.builder<String>().variant("greeting", "hi").defaultVariant("greeting").build(),
24+
"boolean-flag" to
25+
Flag.builder<Boolean>().variant("on", true)
26+
.variant("off", false).defaultVariant("on").build(),
27+
"string-flag" to
28+
Flag.builder<String>().variant("greeting", "hi").defaultVariant("greeting").build(),
2529
"integer-flag" to Flag.builder<Int>().variant("ten", 10).defaultVariant("ten").build(),
2630
"float-flag" to Flag.builder<Double>().variant("half", 0.5).defaultVariant("half").build(),
2731
"object-flag" to Flag.builder<Value>().variant(
@@ -59,11 +63,11 @@ class EvaluationSteps {
5963
private var doubleFlagValue: Double = 0.0
6064
private var objectFlagValue: Value? = null
6165

62-
private var booleanFlagDetails: dev.openfeature.kotlin.sdk.FlagEvaluationDetails<Boolean>? = null
63-
private var stringFlagDetails: dev.openfeature.kotlin.sdk.FlagEvaluationDetails<String>? = null
64-
private var intFlagDetails: dev.openfeature.kotlin.sdk.FlagEvaluationDetails<Int>? = null
65-
private var doubleFlagDetails: dev.openfeature.kotlin.sdk.FlagEvaluationDetails<Double>? = null
66-
private var objectFlagDetails: dev.openfeature.kotlin.sdk.FlagEvaluationDetails<Value>? = null
66+
private var booleanFlagDetails: FlagEvaluationDetails<Boolean>? = null
67+
private var stringFlagDetails: FlagEvaluationDetails<String>? = null
68+
private var intFlagDetails: FlagEvaluationDetails<Int>? = null
69+
private var doubleFlagDetails: FlagEvaluationDetails<Double>? = null
70+
private var objectFlagDetails: FlagEvaluationDetails<Value>? = null
6771

6872
private var contextAwareFlagKey: String = ""
6973
private var contextAwareDefaultValue: String = ""
@@ -72,11 +76,11 @@ class EvaluationSteps {
7276

7377
private var notFoundFlagKey: String = ""
7478
private var notFoundDefaultValue: String = ""
75-
private var notFoundDetails: dev.openfeature.kotlin.sdk.FlagEvaluationDetails<String>? = null
79+
private var notFoundDetails: FlagEvaluationDetails<String>? = null
7680

7781
private var typeErrorFlagKey: String = ""
7882
private var typeErrorDefaultValue: Int = 0
79-
private var typeErrorDetails: dev.openfeature.kotlin.sdk.FlagEvaluationDetails<Int>? = null
83+
private var typeErrorDetails: FlagEvaluationDetails<Int>? = null
8084

8185
@When("a boolean flag with key {string} is evaluated with default value {string}")
8286
fun evaluate_boolean(flagKey: String, defaultValue: String) {
@@ -124,7 +128,8 @@ class EvaluationSteps {
124128
}
125129

126130
@Then(
127-
"the resolved object value should be contain fields {string}, {string}, and {string}, with values {string}, {string} and {int}, respectively"
131+
"the resolved object value should be contain fields {string}, {string}, and {string}, " +
132+
"with values {string}, {string} and {int}, respectively"
128133
)
129134
fun assert_object_value(
130135
boolField: String,
@@ -146,7 +151,8 @@ class EvaluationSteps {
146151
}
147152

148153
@Then(
149-
"the resolved boolean details value should be {string}, the variant should be {string}, and the reason should be {string}"
154+
"the resolved boolean details value should be {string}, " +
155+
"the variant should be {string}, and the reason should be {string}"
150156
)
151157
fun assert_boolean_details(expectedValue: String, expectedVariant: String, expectedReason: String) {
152158
assertEquals(expectedValue.toBoolean(), booleanFlagDetails?.value)
@@ -160,7 +166,8 @@ class EvaluationSteps {
160166
}
161167

162168
@Then(
163-
"the resolved string details value should be {string}, the variant should be {string}, and the reason should be {string}"
169+
"the resolved string details value should be {string}, the variant should be {string}, " +
170+
"and the reason should be {string}"
164171
)
165172
fun assert_string_details(expectedValue: String, expectedVariant: String, expectedReason: String) {
166173
assertEquals(expectedValue, stringFlagDetails?.value)
@@ -174,7 +181,8 @@ class EvaluationSteps {
174181
}
175182

176183
@Then(
177-
"the resolved integer details value should be {int}, the variant should be {string}, and the reason should be {string}"
184+
"the resolved integer details value should be {int}, " +
185+
"the variant should be {string}, and the reason should be {string}"
178186
)
179187
fun assert_integer_details(expectedValue: Int, expectedVariant: String, expectedReason: String) {
180188
assertEquals(expectedValue, intFlagDetails?.value)
@@ -188,7 +196,8 @@ class EvaluationSteps {
188196
}
189197

190198
@Then(
191-
"the resolved float details value should be {double}, the variant should be {string}, and the reason should be {string}"
199+
"the resolved float details value should be {double}, the variant should be {string}, " +
200+
"and the reason should be {string}"
192201
)
193202
fun assert_double_details(expectedValue: Double, expectedVariant: String, expectedReason: String) {
194203
assertEquals(expectedValue, doubleFlagDetails?.value)
@@ -202,7 +211,8 @@ class EvaluationSteps {
202211
}
203212

204213
@Then(
205-
"the resolved object details value should be contain fields {string}, {string}, and {string}, with values {string}, {string} and {int}, respectively"
214+
"the resolved object details value should be contain fields {string}, {string}, and {string}, " +
215+
"with values {string}, {string} and {int}, respectively"
206216
)
207217
fun assert_object_details(
208218
boolField: String,

0 commit comments

Comments
 (0)