1 package growthbook
2
3 import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "io/ioutil"
8 "log"
9 "net/url"
10 "reflect"
11 "testing"
12 )
13
14
15
16
17 func TestJSON(t *testing.T) {
18 SetLogger(&testLog)
19
20 jsonTest(t, "evalCondition", jsonTestEvalCondition)
21 jsonMapTest(t, "versionCompare", jsonTestVersionCompare)
22 jsonTest(t, "hash", jsonTestHash)
23 jsonTest(t, "getBucketRange", jsonTestGetBucketRange)
24 jsonTest(t, "feature", jsonTestFeature)
25 jsonTest(t, "run", jsonTestRun)
26 jsonTest(t, "chooseVariation", jsonTestChooseVariation)
27 jsonTest(t, "getQueryStringOverride", jsonTestQueryStringOverride)
28 jsonTest(t, "inNamespace", jsonTestInNamespace)
29 jsonTest(t, "getEqualWeights", jsonTestGetEqualWeights)
30 jsonTest(t, "decrypt", jsonTestDecrypt)
31 }
32
33
34
35
36
37
38
39
40 func jsonTestEvalCondition(t *testing.T, test []interface{}) {
41 condition, ok1 := test[1].(map[string]interface{})
42 value, ok2 := test[2].(map[string]interface{})
43 expected, ok3 := test[3].(bool)
44 if !ok1 || !ok2 || !ok3 {
45 log.Fatal("unpacking test data")
46 }
47
48 cond := BuildCondition(condition)
49 if cond == nil {
50 log.Fatal(errors.New("failed to build condition"))
51 }
52 attrs := Attributes(value)
53 result := cond.Eval(attrs)
54 if !reflect.DeepEqual(result, expected) {
55 t.Errorf("unexpected result: %v", result)
56 }
57 }
58
59
60
61
62 func jsonTestVersionCompare(t *testing.T, comparison string, test []interface{}) {
63 for _, oneTest := range test {
64 testData, ok := oneTest.([]interface{})
65 if !ok || len(testData) != 3 {
66 log.Fatal("unpacking test data")
67 }
68 v1, ok1 := testData[0].(string)
69 v2, ok2 := testData[1].(string)
70 expected, ok3 := testData[2].(bool)
71 if !ok1 || !ok2 || !ok3 {
72 log.Fatal("unpacking test data")
73 }
74
75 pv1 := paddedVersionString(v1)
76 pv2 := paddedVersionString(v2)
77
78 switch comparison {
79 case "eq":
80 if (pv1 == pv2) != expected {
81 t.Errorf("unexpected result: '%s' eq '%s' => %v", v1, v2, pv1 == pv2)
82 }
83 case "gt":
84 if (pv1 > pv2) != expected {
85 t.Errorf("unexpected result: '%s' gt '%s' => %v", v1, v2, pv1 == pv2)
86 }
87 case "lt":
88 if (pv1 < pv2) != expected {
89 t.Errorf("unexpected result: '%s' lt '%s' => %v", v1, v2, pv1 == pv2)
90 }
91 }
92 }
93 }
94
95
96
97
98 func jsonTestHash(t *testing.T, test []interface{}) {
99 seed, ok0 := test[0].(string)
100 value, ok1 := test[1].(string)
101 version, ok2 := test[2].(float64)
102 expectedValue, ok3 := test[3].(float64)
103 var expected *float64
104 if ok3 {
105 expected = &expectedValue
106 } else {
107 ok3 = test[3] == nil
108 }
109 if !ok0 || !ok1 || !ok2 || !ok3 {
110 log.Fatal("unpacking test data")
111 }
112
113 result := hash(seed, value, int(version))
114 if expected == nil {
115 if result != nil {
116 t.Errorf("expected nil result, got %v", result)
117 }
118 } else {
119 if result == nil {
120 t.Errorf("expected non-nil result, got nil")
121 }
122 if !reflect.DeepEqual(*result, *expected) {
123 t.Errorf("unexpected result: %v", *result)
124 }
125 }
126 }
127
128
129
130
131 func jsonTestGetBucketRange(t *testing.T, test []interface{}) {
132 args, ok1 := test[1].([]interface{})
133 result, ok2 := test[2].([]interface{})
134 if !ok1 || !ok2 {
135 log.Fatal("unpacking test data")
136 }
137
138 numVariations, argok0 := args[0].(float64)
139 coverage, argok1 := args[1].(float64)
140 if !argok0 || !argok1 {
141 log.Fatal("unpacking test data")
142 }
143 var weights []float64
144 totalWeights := 0.0
145 if args[2] != nil {
146 wgts, ok := args[2].([]interface{})
147 if !ok {
148 log.Fatal("unpacking test data")
149 }
150 weights = make([]float64, len(wgts))
151 for i, w := range wgts {
152 weights[i] = w.(float64)
153 totalWeights += w.(float64)
154 }
155 }
156
157 variations := make([]Range, len(result))
158 for i, v := range result {
159 vr, ok := v.([]interface{})
160 if !ok || len(vr) != 2 {
161 log.Fatal("unpacking test data")
162 }
163 variations[i] = Range{vr[0].(float64), vr[1].(float64)}
164 }
165
166 ranges := roundRanges(getBucketRanges(int(numVariations), coverage, weights))
167
168 if !reflect.DeepEqual(ranges, variations) {
169 t.Errorf("unexpected value: %v", result)
170 }
171
172
173 if coverage < 0 || coverage > 1 {
174 if len(testLog.errors) != 0 && len(testLog.warnings) != 1 {
175 t.Errorf("expected coverage log warning")
176 }
177 testLog.reset()
178 }
179 if totalWeights != 1 {
180 if len(testLog.errors) != 0 && len(testLog.warnings) != 1 {
181 t.Errorf("expected weight sum log warning")
182 }
183 testLog.reset()
184 }
185 if len(weights) != len(result) {
186 if len(testLog.errors) != 0 && len(testLog.warnings) != 1 {
187 t.Errorf("expected weight length log warning")
188 }
189 testLog.reset()
190 }
191 }
192
193
194
195
196 func jsonTestFeature(t *testing.T, test []interface{}) {
197 contextDict, ok1 := test[1].(map[string]interface{})
198 featureKey, ok2 := test[2].(string)
199 expectedDict, ok3 := test[3].(map[string]interface{})
200 if !ok1 || !ok2 || !ok3 {
201 log.Fatal("unpacking test data")
202 }
203
204 context := BuildContext(contextDict)
205 growthbook := New(context)
206 expected := BuildFeatureResult(expectedDict)
207 if expected == nil {
208 t.Errorf("unexpected nil from BuildFeatureResult")
209 }
210 retval := growthbook.Feature(featureKey)
211
212
213
214
215
216
217
218
219
220
221
222 if !reflect.DeepEqual(retval, expected) {
223 t.Errorf("unexpected value: %v", retval)
224 }
225
226 expectedWarnings := map[string]int{
227 "unknown feature key": 1,
228 "ignores empty rules": 1,
229 }
230 handleExpectedWarnings(t, test, expectedWarnings)
231 }
232
233
234
235
236 func jsonTestRun(t *testing.T, test []interface{}) {
237 contextDict, ok1 := test[1].(map[string]interface{})
238 experimentDict, ok2 := test[2].(map[string]interface{})
239 resultValue := test[3]
240 resultInExperiment, ok3 := test[4].(bool)
241 if !ok1 || !ok2 || !ok3 {
242 log.Fatal("unpacking test data")
243 }
244
245 context := BuildContext(contextDict)
246 growthbook := New(context)
247 experiment := BuildExperiment(experimentDict)
248 if experiment == nil {
249 t.Errorf("unexpected nil from BuildExperiment")
250 }
251 result := growthbook.Run(experiment)
252
253 if !reflect.DeepEqual(result.Value, resultValue) {
254 t.Errorf("unexpected result value: %v", result.Value)
255 }
256 if result.InExperiment != resultInExperiment {
257 t.Errorf("unexpected inExperiment value: %v", result.InExperiment)
258 }
259
260 expectedWarnings := map[string]int{
261 "single variation": 1,
262 }
263 handleExpectedWarnings(t, test, expectedWarnings)
264 }
265
266
267
268
269 func jsonTestChooseVariation(t *testing.T, test []interface{}) {
270 hash, ok1 := test[1].(float64)
271 ranges, ok2 := test[2].([]interface{})
272 result, ok3 := test[3].(float64)
273 if !ok1 || !ok2 || !ok3 {
274 log.Fatal("unpacking test data")
275 }
276
277 variations := make([]Range, len(ranges))
278 for i, v := range ranges {
279 vr, ok := v.([]interface{})
280 if !ok || len(vr) != 2 {
281 log.Fatal("unpacking test data")
282 }
283 variations[i] = Range{vr[0].(float64), vr[1].(float64)}
284 }
285
286 variation := chooseVariation(hash, variations)
287 if variation != int(result) {
288 t.Errorf("unexpected result: %d", variation)
289 }
290 }
291
292
293
294
295 func jsonTestQueryStringOverride(t *testing.T, test []interface{}) {
296 key, ok1 := test[1].(string)
297 rawURL, ok2 := test[2].(string)
298 numVariations, ok3 := test[3].(float64)
299 result := test[4]
300 var expected *int
301 if result != nil {
302 tmp := int(result.(float64))
303 expected = &tmp
304 }
305 if !ok1 || !ok2 || !ok3 {
306 log.Fatal("unpacking test data")
307 }
308 url, err := url.Parse(rawURL)
309 if err != nil {
310 log.Fatal("invalid URL")
311 }
312
313 override := getQueryStringOverride(key, url, int(numVariations))
314 if !reflect.DeepEqual(override, expected) {
315 t.Errorf("unexpected result: %v", override)
316 }
317 }
318
319
320
321
322 func jsonTestInNamespace(t *testing.T, test []interface{}) {
323 id, ok1 := test[1].(string)
324 ns, ok2 := test[2].([]interface{})
325 expected, ok3 := test[3].(bool)
326 if !ok1 || !ok2 || !ok3 {
327 log.Fatal("unpacking test data")
328 }
329
330 namespace := BuildNamespace(ns)
331 result := namespace.inNamespace(id)
332 if result != expected {
333 t.Errorf("unexpected result: %v", result)
334 }
335 }
336
337
338
339
340 func jsonTestGetEqualWeights(t *testing.T, test []interface{}) {
341 numVariations, ok0 := test[0].(float64)
342 exp, ok1 := test[1].([]interface{})
343 if !ok0 || !ok1 {
344 log.Fatal("unpacking test data")
345 }
346
347 expected := make([]float64, len(exp))
348 for i, e := range exp {
349 expected[i] = e.(float64)
350 }
351
352 result := getEqualWeights(int(numVariations))
353 if !reflect.DeepEqual(round(result), round(expected)) {
354 t.Errorf("unexpected value: %v", result)
355 }
356 }
357
358
359
360
361 func jsonTestDecrypt(t *testing.T, test []interface{}) {
362 encryptedString, ok1 := test[1].(string)
363 key, ok2 := test[2].(string)
364 if !ok1 || !ok2 {
365 log.Fatal("unpacking test data")
366 }
367 nilExpected := test[3] == nil
368 expected := ""
369 if !nilExpected {
370 expected, ok2 = test[3].(string)
371 if !ok2 {
372 log.Fatal("unpacking test data")
373 }
374 }
375
376 result, err := decrypt(encryptedString, key)
377 if nilExpected {
378 if err == nil {
379 t.Errorf("expected error return")
380 }
381 } else {
382 if err != nil {
383 t.Errorf("error in decrypt: %v", err)
384 } else if !reflect.DeepEqual(result, expected) {
385 t.Errorf("unexpected result: %v", result)
386 fmt.Printf("expected: '%s' (%d)\n", expected, len(expected))
387 fmt.Println([]byte(expected))
388 fmt.Printf(" got: '%s' (%d)\n", result, len(result))
389 fmt.Println([]byte(result))
390 }
391 }
392 }
393
394
395
396
397
398
399
400
401 func jsonTest(t *testing.T, label string,
402 fn func(t *testing.T, test []interface{})) {
403 content, err := ioutil.ReadFile("cases.json")
404 if err != nil {
405 log.Fatal(err)
406 }
407
408
409 allCases := map[string]interface{}{}
410 err = json.Unmarshal(content, &allCases)
411 if err != nil {
412 log.Fatal(err)
413 }
414
415
416 cases := allCases[label].([]interface{})
417
418
419
420 t.Run("json test suite: "+label, func(t *testing.T) {
421
422
423
424 for itest, gtest := range cases {
425 test, ok := gtest.([]interface{})
426 if !ok {
427 log.Fatal("unpacking JSON test data")
428 }
429 name, ok := test[0].(string)
430 if !ok {
431 name = ""
432 }
433 t.Run(fmt.Sprintf("[%d] %s", itest, name), func(t *testing.T) {
434
435
436
437
438
439 testLog.reset()
440 fn(t, test)
441 if len(testLog.errors) != 0 {
442 t.Errorf("test log has errors: %s", testLog.allErrors())
443 }
444 if len(testLog.warnings) != 0 {
445 t.Errorf("test log has warnings: %s", testLog.allWarnings())
446 }
447 })
448 }
449 })
450 }
451
452
453
454 func jsonMapTest(t *testing.T, label string,
455 fn func(t *testing.T, label string, test []interface{})) {
456 content, err := ioutil.ReadFile("cases.json")
457 if err != nil {
458 log.Fatal(err)
459 }
460
461
462 allCases := map[string]interface{}{}
463 err = json.Unmarshal(content, &allCases)
464 if err != nil {
465 log.Fatal(err)
466 }
467
468
469 cases := allCases[label].(map[string]interface{})
470
471
472
473 t.Run("json test suite: "+label, func(t *testing.T) {
474
475
476
477 itest := 1
478 for name, gtest := range cases {
479 test, ok := gtest.([]interface{})
480 if !ok {
481 log.Fatal("unpacking JSON test data")
482 }
483
484 t.Run(fmt.Sprintf("[%d] %s", itest, name), func(t *testing.T) {
485
486
487
488
489
490 testLog.reset()
491 fn(t, name, test)
492 if len(testLog.errors) != 0 {
493 t.Errorf("test log has errors: %s", testLog.allErrors())
494 }
495 if len(testLog.warnings) != 0 {
496 t.Errorf("test log has warnings: %s", testLog.allWarnings())
497 }
498 })
499 itest++
500 }
501 })
502 }
503
View as plain text