...

Source file src/github.com/growthbook/growthbook-golang/json_test.go

Documentation: github.com/growthbook/growthbook-golang

     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  // Main test function for running JSON-based tests. These all use a
    15  // jsonTest helper function to read and parse the JSON test case file.
    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  // Test functions driven from JSON cases. Each of this has a similar
    34  // structure, first extracting test data from the JSON data into typed
    35  // values, then performing the test.
    36  
    37  // Condition evaluation tests.
    38  //
    39  // Test parameters: name, condition, attributes, result
    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  // Version comparison tests.
    60  //
    61  // Test parameters: ...
    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  // Hash function tests.
    96  //
    97  // Test parameters: value, hash
    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  // Bucket range tests.
   129  //
   130  // Test parameters: name, args ([numVariations, coverage, weights]), result
   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  	// Handle expected warnings.
   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  // Feature tests.
   194  //
   195  // Test parameters: name, context, feature key, result
   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  	// fmt.Println("== RESULT ======================================================================")
   213  	// fmt.Println(retval)
   214  	// fmt.Println(retval.Experiment)
   215  	// fmt.Println(retval.ExperimentResult)
   216  	// fmt.Println("--------------------------------------------------------------------------------")
   217  	// fmt.Println(expected)
   218  	// fmt.Println(expected.Experiment)
   219  	// fmt.Println(expected.ExperimentResult)
   220  	// fmt.Println("== EXPECTED ====================================================================")
   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  // Experiment tests.
   234  //
   235  // Test parameters: name, context, experiment, value, inExperiment
   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  // Variation choice tests.
   267  //
   268  // Test parameters: name, hash, ranges, result
   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  // Query string override tests
   293  //
   294  // Test parameters: name, experiment key, url, numVariations, result
   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  // Namespace inclusion tests
   320  //
   321  // Test parameters: name, id, namespace, result
   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  // Equal weight calculation tests.
   338  //
   339  // Test parameters: numVariations, result
   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  // Decryption function tests.
   359  //
   360  // Test parameters: name, encryptedString, key, expected
   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  //  TEST UTILITIES
   397  //
   398  
   399  // Run a set of JSON test cases provided as a JSON array.
   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  	// Unmarshal all test cases at once.
   409  	allCases := map[string]interface{}{}
   410  	err = json.Unmarshal(content, &allCases)
   411  	if err != nil {
   412  		log.Fatal(err)
   413  	}
   414  
   415  	// Extract just the test cases for the test type we're working on.
   416  	cases := allCases[label].([]interface{})
   417  
   418  	// Extract the test data for each case as a JSON array and pass to
   419  	// the test function.
   420  	t.Run("json test suite: "+label, func(t *testing.T) {
   421  		// Run tests one at a time: each test's JSON data is an array,
   422  		// with the interpretation of the array entries depending on the
   423  		// test type.
   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  				// Handle logging during tests: reset log before each test,
   435  				// make sure there are no errors or warnings (some tests that
   436  				// check for correct handling of out-of-range parameters
   437  				// trigger warnings, but these are handled within the test
   438  				// themselves).
   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  // Run a set of JSON test cases provided as a JSON map.
   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  	// Unmarshal all test cases at once.
   462  	allCases := map[string]interface{}{}
   463  	err = json.Unmarshal(content, &allCases)
   464  	if err != nil {
   465  		log.Fatal(err)
   466  	}
   467  
   468  	// Extract just the test cases for the test type we're working on.
   469  	cases := allCases[label].(map[string]interface{})
   470  
   471  	// Extract the test data for each case as a JSON array and pass to
   472  	// the test function.
   473  	t.Run("json test suite: "+label, func(t *testing.T) {
   474  		// Run tests one at a time: each test's JSON data is an array,
   475  		// keyed by a string label, and the interpretation of the array
   476  		// entries depends on the test type.
   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  				// Handle logging during tests: reset log before each test,
   486  				// make sure there are no errors or warnings (some tests that
   487  				// check for correct handling of out-of-range parameters
   488  				// trigger warnings, but these are handled within the test
   489  				// themselves).
   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