Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4c47c8f

Browse files
authoredApr 7, 2025
Fix null fallbacks on param stores (#301)
* Write more tests for param stores null fallbacks * Fix build breaks * Update how parameter store get their values * Fix lint * Test for wront types * Fix lint error * Add more tests, update casts to nullable
1 parent edd5a71 commit 4c47c8f

File tree

2 files changed

+172
-112
lines changed

2 files changed

+172
-112
lines changed
 

‎src/main/java/com/statsig/androidsdk/ParameterStore.kt

Lines changed: 42 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -57,53 +57,53 @@ class ParameterStore(
5757
public val options: ParameterStoreEvaluationOptions?,
5858
) {
5959
fun getBoolean(paramName: String, fallback: Boolean): Boolean {
60-
return getValue(paramName, fallback)
60+
return getValueFromRef(paramName, fallback, Layer::getBoolean, DynamicConfig::getBoolean)
6161
}
6262

6363
fun getString(paramName: String, fallback: String?): String? {
64-
return getValue(paramName, fallback)
64+
return getValueFromRef(paramName, fallback, Layer::getString, DynamicConfig::getString)
6565
}
6666

6767
fun getDouble(paramName: String, fallback: Double): Double {
68-
return getValue(paramName, fallback)
68+
return getValueFromRef(paramName, fallback, Layer::getDouble, DynamicConfig::getDouble)
6969
}
7070

7171
fun getDictionary(paramName: String, fallback: Map<String, Any>?): Map<String, Any>? {
72-
return getValue(paramName, fallback)
72+
return getValueFromRef(paramName, fallback, Layer::getDictionary, DynamicConfig::getDictionary)
7373
}
7474

7575
fun getArray(paramName: String, fallback: Array<*>?): Array<*>? {
76-
return getValue(paramName, fallback)
76+
return getValueFromRef(paramName, fallback, Layer::getArray, DynamicConfig::getArray)
7777
}
7878

7979
// --------evaluation--------
80-
private inline fun <reified T> getValue(topLevelParamName: String, fallback: T): T {
81-
try {
82-
val param = paramStore[topLevelParamName] ?: return fallback
83-
val referenceTypeString = param["ref_type"] as? String ?: return fallback
84-
val paramTypeString = param["param_type"] as? String ?: return fallback
85-
val refType = RefType.fromString(referenceTypeString)
86-
val paramType = ParamType.fromString(paramTypeString)
8780

88-
when (paramType) {
89-
ParamType.BOOLEAN -> if (fallback != null && fallback !is Boolean) return fallback
90-
ParamType.STRING -> if (fallback != null && fallback !is String) return fallback
91-
ParamType.NUMBER -> if (fallback != null && fallback !is Number) return fallback
92-
ParamType.OBJECT -> if (fallback != null && fallback !is Map<*, *>) return fallback
93-
ParamType.ARRAY -> if (fallback != null && fallback !is Array<*> && fallback !is List<*>) return fallback
94-
else -> return fallback
81+
private inline fun <reified T> getValueFromRef(
82+
topLevelParamName: String,
83+
fallback: T,
84+
getLayerValue: Layer.(String, T) -> T,
85+
getDynamicConfigValue: DynamicConfig.(String, T) -> T,
86+
): T {
87+
val param = paramStore[topLevelParamName] ?: return fallback
88+
val referenceTypeString = param["ref_type"] as? String ?: return fallback
89+
val paramTypeString = param["param_type"] as? String ?: return fallback
90+
val refType = RefType.fromString(referenceTypeString)
91+
val paramType = ParamType.fromString(paramTypeString)
92+
93+
return when (refType) {
94+
RefType.GATE -> evaluateFeatureGate(paramType, param, fallback)
95+
RefType.STATIC -> evaluateStaticValue(paramType, param, fallback)
96+
RefType.LAYER -> evaluateLayerParameter(param, fallback) { layer, paramName ->
97+
var v = layer.getLayerValue(paramName, fallback)
98+
return v
9599
}
96-
97-
return when (refType) {
98-
RefType.GATE -> evaluateFeatureGate(paramType, param, fallback)
99-
RefType.STATIC -> evaluateStaticValue(paramType, param, fallback)
100-
RefType.LAYER -> evaluateLayerParameter(paramType, param, fallback)
101-
RefType.DYNAMIC_CONFIG -> evaluateDynamicConfigParameter(paramType, param, fallback)
102-
RefType.EXPERIMENT -> evaluateExperimentParameter(paramType, param, fallback)
103-
else -> fallback
100+
RefType.DYNAMIC_CONFIG -> evaluateDynamicConfigParameter(param, fallback) { config, paramName ->
101+
config.getDynamicConfigValue(paramName, fallback)
104102
}
105-
} catch (e: Exception) {
106-
return fallback
103+
RefType.EXPERIMENT -> evaluateExperimentParameter(param, fallback) { experiment, paramName ->
104+
experiment.getDynamicConfigValue(paramName, fallback)
105+
}
106+
else -> fallback
107107
}
108108
}
109109

@@ -125,15 +125,15 @@ class ParameterStore(
125125
}
126126
val retVal = if (passes) passValue else failValue
127127
if (paramType == ParamType.NUMBER) {
128-
return (retVal as Number).toDouble() as T
128+
return (retVal as? Number)?.toDouble() as? T ?: fallback
129129
} else if (paramType == ParamType.ARRAY) {
130130
return when (retVal) {
131-
is Array<*> -> return retVal as T
132-
is ArrayList<*> -> return retVal.toTypedArray() as T
131+
is Array<*> -> return retVal as? T ?: fallback
132+
is ArrayList<*> -> return retVal.toTypedArray() as? T ?: fallback
133133
else -> fallback
134134
}
135135
}
136-
return retVal as T
136+
return retVal as? T ?: fallback
137137
}
138138

139139
private inline fun <reified T> evaluateStaticValue(
@@ -144,12 +144,12 @@ class ParameterStore(
144144
return when (paramType) {
145145
ParamType.BOOLEAN -> param["value"] as? T ?: fallback
146146
ParamType.STRING -> param["value"] as? T ?: fallback
147-
ParamType.NUMBER -> (param["value"] as Number).toDouble() as? T ?: fallback
147+
ParamType.NUMBER -> (param["value"] as? Number)?.toDouble() as? T ?: fallback
148148
ParamType.OBJECT -> param["value"] as? T ?: fallback
149149
ParamType.ARRAY -> {
150150
when (val returnValue = param["value"]) {
151-
is Array<*> -> returnValue as T
152-
is ArrayList<*> -> (returnValue.toTypedArray()) as T
151+
is Array<*> -> returnValue as? T ?: fallback
152+
is ArrayList<*> -> (returnValue.toTypedArray()) as? T ?: fallback
153153
else -> fallback
154154
}
155155
}
@@ -158,9 +158,9 @@ class ParameterStore(
158158
}
159159

160160
private inline fun <reified T> evaluateLayerParameter(
161-
paramType: ParamType,
162161
param: Map<String, Any>,
163162
fallback: T,
163+
getValue: (Layer, String) -> T,
164164
): T {
165165
val layerName = param["layer_name"] as? String
166166
val paramName = param["param_name"] as? String
@@ -172,35 +172,13 @@ class ParameterStore(
172172
} else {
173173
statsigClient.getLayer(layerName)
174174
}
175-
return when (paramType) {
176-
ParamType.BOOLEAN -> layer.getBoolean(
177-
paramName,
178-
fallback as? Boolean ?: return fallback,
179-
) as T
180-
ParamType.STRING -> layer.getString(
181-
paramName,
182-
fallback as? String ?: return fallback,
183-
) as T
184-
ParamType.NUMBER -> layer.getDouble(
185-
paramName,
186-
fallback as? Double ?: return fallback,
187-
) as T
188-
ParamType.OBJECT -> layer.getDictionary(
189-
paramName,
190-
fallback as? Map<String, Any> ?: return fallback,
191-
) as T
192-
ParamType.ARRAY -> layer.getArray(
193-
paramName,
194-
fallback as? Array<*> ?: return fallback,
195-
) as T
196-
else -> fallback
197-
}
175+
return getValue(layer, paramName)
198176
}
199177

200178
private inline fun <reified T> evaluateDynamicConfigParameter(
201-
paramType: ParamType,
202179
param: Map<String, Any>,
203180
fallback: T,
181+
getValue: (DynamicConfig, String) -> T,
204182
): T {
205183
val configName = param["config_name"] as? String
206184
val paramName = param["param_name"] as? String
@@ -212,35 +190,13 @@ class ParameterStore(
212190
} else {
213191
statsigClient.getConfig(configName)
214192
}
215-
return when (paramType) {
216-
ParamType.BOOLEAN -> config.getBoolean(
217-
paramName,
218-
fallback as? Boolean ?: return fallback,
219-
) as T
220-
ParamType.STRING -> config.getString(
221-
paramName,
222-
fallback as? String ?: return fallback,
223-
) as T
224-
ParamType.NUMBER -> config.getDouble(
225-
paramName,
226-
fallback as? Double ?: return fallback,
227-
) as T
228-
ParamType.OBJECT -> config.getDictionary(
229-
paramName,
230-
fallback as? Map<String, Any> ?: return fallback,
231-
) as T
232-
ParamType.ARRAY -> config.getArray(
233-
paramName,
234-
fallback as? Array<*> ?: return fallback,
235-
) as T
236-
else -> fallback
237-
}
193+
return getValue(config, paramName)
238194
}
239195

240196
private inline fun <reified T> evaluateExperimentParameter(
241-
paramType: ParamType,
242197
param: Map<String, Any>,
243198
fallback: T,
199+
getValue: (DynamicConfig, String) -> T,
244200
): T {
245201
val experimentName = param["experiment_name"] as? String
246202
val paramName = param["param_name"] as? String
@@ -252,28 +208,6 @@ class ParameterStore(
252208
} else {
253209
statsigClient.getExperiment(experimentName)
254210
}
255-
return when (paramType) {
256-
ParamType.BOOLEAN -> experiment.getBoolean(
257-
paramName,
258-
fallback as? Boolean ?: return fallback,
259-
) as T
260-
ParamType.STRING -> experiment.getString(
261-
paramName,
262-
fallback as? String ?: return fallback,
263-
) as T
264-
ParamType.NUMBER -> experiment.getDouble(
265-
paramName,
266-
fallback as? Double ?: return fallback,
267-
) as T
268-
ParamType.OBJECT -> experiment.getDictionary(
269-
paramName,
270-
fallback as? Map<String, Any> ?: return fallback,
271-
) as T
272-
ParamType.ARRAY -> experiment.getArray(
273-
paramName,
274-
fallback as? Array<*> ?: return fallback,
275-
) as T
276-
else -> fallback
277-
}
211+
return getValue(experiment, paramName)
278212
}
279213
}
Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,159 @@
11
package com.statsig.androidsdk
22

3+
import android.app.Application
4+
import io.mockk.mockk
5+
import kotlinx.coroutines.runBlocking
36
import org.junit.Assert.*
47
import org.junit.Before
58
import org.junit.Test
69

710
class ParameterStoreTest {
811

12+
private var initUser: StatsigUser? = null
13+
private var app: Application = mockk()
14+
private val user = StatsigUser(userID = "a-user")
15+
private var client: StatsigClient = StatsigClient()
16+
917
private lateinit var paramStore: ParameterStore
1018

1119
@Before
12-
internal fun setup() {
20+
internal fun setup() = runBlocking {
21+
app = mockk()
22+
TestUtil.stubAppFunctions(app)
23+
24+
TestUtil.mockStatsigUtil()
25+
TestUtil.mockDispatchers()
26+
27+
val statsigNetwork = TestUtil.mockNetwork()
28+
29+
client = StatsigClient()
30+
client.statsigNetwork = statsigNetwork
31+
32+
client.initialize(app, "test-key")
33+
34+
TestUtil.startStatsigAndWait(app, user, StatsigOptions(overrideStableID = "custom_stable_id"), network = TestUtil.mockNetwork())
1335
paramStore = ParameterStore(
14-
statsigClient = StatsigClient(),
36+
statsigClient = client,
1537
paramStore = mapOf(
16-
"testString" to mapOf(
38+
"static_value" to mapOf(
1739
"value" to "test",
1840
"ref_type" to "static",
1941
"param_type" to "string",
2042
),
43+
"static_bool" to mapOf(
44+
"value" to true,
45+
"ref_type" to "static",
46+
"param_type" to "boolean",
47+
),
48+
"gate_value" to mapOf(
49+
"ref_type" to "gate",
50+
"param_type" to "string",
51+
"gate_name" to "always_on",
52+
"pass_value" to "pass",
53+
"fail_value" to "fail",
54+
),
55+
"gate_bool" to mapOf(
56+
"ref_type" to "gate",
57+
"param_type" to "string",
58+
"gate_name" to "always_on",
59+
"pass_value" to true,
60+
"fail_value" to false,
61+
),
62+
"gate_number" to mapOf(
63+
"ref_type" to "gate",
64+
"param_type" to "number",
65+
"gate_name" to "always_on",
66+
"pass_value" to 1,
67+
"fail_value" to -1,
68+
),
69+
"layer_value" to mapOf(
70+
"ref_type" to "layer",
71+
"param_type" to "string",
72+
"layer_name" to "allocated_layer",
73+
"param_name" to "string",
74+
),
75+
"experiment_value" to mapOf(
76+
"ref_type" to "experiment",
77+
"param_type" to "string",
78+
"experiment_name" to "exp",
79+
"param_name" to "string",
80+
),
81+
"dynamic_config_value" to mapOf(
82+
"ref_type" to "dynamic_config",
83+
"param_type" to "string",
84+
"config_name" to "test_config",
85+
"param_name" to "string",
86+
),
2187
),
2288
name = "test_parameter_store",
2389
evaluationDetails = EvaluationDetails(EvaluationReason.Network, lcut = 0),
2490
options = ParameterStoreEvaluationOptions(disableExposureLog = true),
2591
)
92+
93+
return@runBlocking
2694
}
2795

2896
@Test
2997
fun testNullFallback() {
3098
assertNull(paramStore.getString("nonexistent", null))
31-
assertEquals("test", paramStore.getString("testString", null))
99+
assertEquals("test", paramStore.getString("static_value", null))
100+
}
101+
102+
@Test
103+
fun testNullFallbackForGates() {
104+
assertEquals("pass", paramStore.getString("gate_value", null))
105+
}
106+
107+
@Test
108+
fun testNullFallbackForLayers() {
109+
assertEquals("test", paramStore.getString("layer_value", null))
110+
}
111+
112+
@Test
113+
fun testNullFallbackForExperiments() {
114+
assertEquals("test", paramStore.getString("experiment_value", null))
115+
}
116+
117+
@Test
118+
fun testNullFallbackForDynamicConfigs() {
119+
assertEquals("test", paramStore.getString("dynamic_config_value", null))
120+
}
121+
122+
@Test
123+
fun testWrongStaticType() {
124+
assertEquals("DEFAULT", paramStore.getString("gate_bool", "DEFAULT"))
125+
}
126+
127+
@Test
128+
fun testGetBoolean() {
129+
assertEquals(true, paramStore.getBoolean("gate_bool", false))
130+
}
131+
132+
@Test
133+
fun testGetBooleanFallback() {
134+
assertEquals(true, paramStore.getBoolean("nonexistent", true))
135+
assertEquals(false, paramStore.getBoolean("nonexistent", false))
136+
}
137+
138+
@Test
139+
fun testGetNumber() {
140+
assertEquals(1.0, paramStore.getDouble("gate_number", 2.0), 0.01)
141+
}
142+
143+
@Test
144+
fun testGetNumberFallback() {
145+
assertEquals(2.0, paramStore.getDouble("nonexistent", 2.0), 0.01)
146+
assertEquals(0.0, paramStore.getDouble("nonexistent", 0.0), 0.01)
147+
}
148+
149+
@Test
150+
fun testGetNumberWrongType() {
151+
assertEquals("DEFAULT", paramStore.getString("gate_number", "DEFAULT"))
152+
assertEquals(true, paramStore.getBoolean("gate_number", true))
153+
}
154+
155+
@Test
156+
fun testWrongGateType() {
157+
assertEquals("DEFAULT", paramStore.getString("static_bool", "DEFAULT"))
32158
}
33159
}

0 commit comments

Comments
 (0)
Please sign in to comment.