diff --git a/index_op_test.go b/index_op_test.go index 5f752da..cb99a54 100644 --- a/index_op_test.go +++ b/index_op_test.go @@ -1,11 +1,32 @@ package planout import ( - "testing" - "io/ioutil" "encoding/json" + "io/ioutil" + "testing" ) +func getInterpreter(filename string) (*Interpreter, error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + var js map[string]interface{} + err = json.Unmarshal(data, &js) + if err != nil { + return nil, err + } + + return &Interpreter{ + Name: "the name", + Salt: "the salt", + Evaluated: false, + Inputs: make(map[string]interface{}), + Outputs: make(map[string]interface{}), + Code: js, + }, nil +} + type Inner struct { Value string } @@ -19,18 +40,12 @@ type NestedStruct struct { } func TestNestedIndex(t *testing.T) { - data, err := ioutil.ReadFile("test/nested_index.json") + exp, err := getInterpreter("test/nested_index.json") if err != nil { t.Fatal(err) } - var js map[string]interface{} - err = json.Unmarshal(data, &js) - if (err != nil) { - t.Fatal(err) - } - inputs := make(map[string]interface{}) - inputs["s"] = &NestedStruct{ + exp.Inputs["s"] = &NestedStruct{ Outer: &Outer{ Inner: &Inner{ Value: "foo", @@ -38,20 +53,11 @@ func TestNestedIndex(t *testing.T) { }, } - exp := &Interpreter{ - Name: "nested_test", - Salt: "salt123", - Evaluated: false, - Inputs: inputs, - Outputs: make(map[string]interface{}), - Code: js, - } - if _, ok := exp.Run(); !ok { t.Fatal("Failed to run experiment") } - if (exp.Outputs["out"] != "foo") { + if exp.Outputs["out"] != "foo" { t.Fail() } } @@ -61,30 +67,16 @@ type StructWithArray struct { } func TestArrayInStruct(t *testing.T) { - data, err := ioutil.ReadFile("test/array_field_test.json") + exp, err := getInterpreter("test/array_field_test.json") if err != nil { t.Fatal(err) } - var js map[string]interface{} - err = json.Unmarshal(data, &js) - if (err != nil) { - t.Fatal(err) - } - inputs := make(map[string]interface{}) - i := 123 - inputs["s"] = &StructWithArray{ + i := int(123) + exp.Inputs["s"] = &StructWithArray{ Array: []*int{&i}, } - exp := &Interpreter{ - Name: "test_array_field", - Salt: "blasdfalks", - Inputs: inputs, - Outputs: make(map[string]interface{}), - Code: js, - } - if _, ok := exp.Run(); !ok { t.Fatal("Experiment run failed") } @@ -95,40 +87,51 @@ func TestArrayInStruct(t *testing.T) { } type StructWithMap struct { - Map map[string]string + Map map[string]int64 } func TestMapField(t *testing.T) { - data, err := ioutil.ReadFile("test/map_index_test.json") + exp, err := getInterpreter("test/map_index_test.json") if err != nil { t.Fatal(err) } - var js map[string]interface{} - err = json.Unmarshal(data, &js) - if (err != nil) { - t.Fatal(err) - } - inputs := make(map[string]interface{}) - mapField := make(map[string]string) - mapField["key"] = "value" - inputs["s"] = &StructWithMap{ + mapField := make(map[string]int64) + mapField["key"] = 42 + exp.Inputs["s"] = &StructWithMap{ Map: mapField, } - exp := &Interpreter{ - Name: "test_map_index", - Salt: "asdfkhjaslkdfjh", - Inputs: inputs, - Outputs: make(map[string]interface{}), - Code: js, + if _, ok := exp.Run(); !ok { + t.Fatal("Experiment run failed") + } + + if elem := exp.Outputs["element"]; elem != int64(42) { + t.Fail() + } + + if exp.Outputs["empty"] != nil { + t.Fail() + } +} + +type StructWithNilField struct { + None interface{} +} + +func TestStructWithNilField(t *testing.T) { + exp, err := getInterpreter("test/struct_with_nil_field.json") + if err != nil { + t.Fatal(err) } + exp.Inputs["struct"] = &StructWithNilField{} + if _, ok := exp.Run(); !ok { t.Fatal("Experiment run failed") } - if elem := exp.Outputs["element"]; elem != "value" { + if exp.Outputs["nil"] != nil { t.Fail() } -} \ No newline at end of file +} diff --git a/interpreter.go b/interpreter.go index b6a6f5d..48916ce 100644 --- a/interpreter.go +++ b/interpreter.go @@ -30,7 +30,7 @@ type Interpreter struct { Inputs, Outputs, Overrides map[string]interface{} Code interface{} Evaluated, InExperiment bool - parameterSalt string + ParameterSalt string } func (interpreter *Interpreter) Run(force ...bool) (map[string]interface{}, bool) { @@ -54,6 +54,14 @@ func (interpreter *Interpreter) Run(force ...bool) (map[string]interface{}, bool return interpreter.Outputs, true } +// ReSet方法重置experiment的三个参数Inputs、Overrides、Outputs,保留namespace的分区信息,就可以复用该namespace, +// 而不是每次都重新生成一个namespace,耗费性能 +func (interpreter *Interpreter) ReSet() { + // 置空参数 + interpreter.Inputs, interpreter.Overrides, interpreter.Outputs = make(map[string]interface{}), make(map[string]interface{}), make(map[string]interface{}) + interpreter.Evaluated, interpreter.InExperiment = false, false +} + func (interpreter *Interpreter) Get(name string) (interface{}, bool) { value, ok := interpreter.Overrides[name] if ok { diff --git a/namespace.go b/namespace.go index 8ff2b48..cab8efa 100644 --- a/namespace.go +++ b/namespace.go @@ -15,11 +15,11 @@ type SimpleNamespace struct { PrimaryUnit string NumSegments int Inputs map[string]interface{} - segmentAllocations map[uint64]string - availableSegments []int - currentExperiments map[string]*Interpreter - defaultExperiment *Interpreter - selectedExperiment uint64 + SegmentAllocations map[uint64]string + AvailableSegments []int + CurrentExperiments map[string]*Interpreter + DefaultExperiment *Interpreter + SelectedExperiment uint64 } func NewSimpleNamespace(name string, numSegments int, primaryUnit string, inputs map[string]interface{}) SimpleNamespace { @@ -40,21 +40,42 @@ func NewSimpleNamespace(name string, numSegments int, primaryUnit string, inputs PrimaryUnit: primaryUnit, NumSegments: numSegments, Inputs: inputs, - segmentAllocations: make(map[uint64]string), - availableSegments: avail, - currentExperiments: make(map[string]*Interpreter), - selectedExperiment: uint64(numSegments + 1), - defaultExperiment: noop, + SegmentAllocations: make(map[uint64]string), + AvailableSegments: avail, + CurrentExperiments: make(map[string]*Interpreter), + SelectedExperiment: uint64(numSegments + 1), + DefaultExperiment: noop, } } +// SetInputs方法可以手动设置namespace的输入值 +func (n *SimpleNamespace) SetInputs(inputs map[string]interface{}) { + for _, exp := range n.CurrentExperiments { + exp.Inputs = inputs + } + n.DefaultExperiment.Inputs = inputs + n.Inputs = inputs +} +// ReSet方法重置该namespace中的实验信息 +func (n *SimpleNamespace) ReSet() { + // 遍历namespace下所有experiments,进行重置 + for _, exp := range n.CurrentExperiments { + exp.ReSet() + } + defaultConfig := n.DefaultExperiment.Outputs + n.DefaultExperiment.ReSet() + n.DefaultExperiment.Outputs = defaultConfig + n.Inputs = make(map[string]interface{}) + n.SelectedExperiment = uint64(n.NumSegments + 1) +} + func (n *SimpleNamespace) Run() *Interpreter { - interpreter := n.defaultExperiment + interpreter := n.DefaultExperiment - if name, ok := n.segmentAllocations[n.getSegment()]; ok { - interpreter = n.currentExperiments[name] - interpreter.Name = n.Name + "-" + interpreter.Name - interpreter.Salt = n.Name + "." + interpreter.Name + if name, ok := n.SegmentAllocations[n.getSegment()]; ok { + interpreter = n.CurrentExperiments[name] + //interpreter.Name = n.Name + "-" + interpreter.Name + //interpreter.Salt = n.Name + "." + interpreter.Name } interpreter.Run() @@ -62,45 +83,46 @@ func (n *SimpleNamespace) Run() *Interpreter { } func (n *SimpleNamespace) AddDefaultExperiment(defaultExperiment *Interpreter) { - n.defaultExperiment = defaultExperiment + n.DefaultExperiment = defaultExperiment } func (n *SimpleNamespace) AddExperiment(name string, interpreter *Interpreter, segments int) error { - avail := len(n.availableSegments) + avail := len(n.AvailableSegments) if avail < segments { return fmt.Errorf("Not enough segments available %v to add the new experiment %v\n", avail, name) } - if _, ok := n.currentExperiments[name]; ok { + if _, ok := n.CurrentExperiments[name]; ok { return fmt.Errorf("There is already and experiment called %s\n", name) } n.allocateExperiment(name, segments) - n.currentExperiments[name] = interpreter + n.CurrentExperiments[name] = interpreter return nil } func (n *SimpleNamespace) RemoveExperiment(name string) error { - _, exists := n.currentExperiments[name] + _, exists := n.CurrentExperiments[name] if !exists { return fmt.Errorf("Experiment %v does not exists in the namespace\n", name) } segmentsToFree := make([]int, 0, n.NumSegments) - for i := range n.segmentAllocations { - if n.segmentAllocations[i] == name { + for i := range n.SegmentAllocations { + if n.SegmentAllocations[i] == name { segmentsToFree = append(segmentsToFree, int(i)) + delete(n.SegmentAllocations, i) } } for i := range segmentsToFree { - n.availableSegments = append(n.availableSegments, segmentsToFree[i]) + n.AvailableSegments = append(n.AvailableSegments, segmentsToFree[i]) } - sort.Ints(n.availableSegments) + sort.Ints(n.AvailableSegments) - delete(n.currentExperiments, name) + delete(n.CurrentExperiments, name) return nil } @@ -115,8 +137,8 @@ func (n *SimpleNamespace) allocateExperiment(name string, segments int) { } // Compile Sample operator - var availableSegmentsAsInterface []interface{} = make([]interface{}, len(n.availableSegments)) - for i, d := range n.availableSegments { + var availableSegmentsAsInterface = make([]interface{}, len(n.AvailableSegments)) + for i, d := range n.AvailableSegments { availableSegmentsAsInterface[i] = d } @@ -132,15 +154,15 @@ func (n *SimpleNamespace) allocateExperiment(name string, segments int) { // Remove segment from available_segments for i := range shuffle { j := shuffle[i].(int) - n.segmentAllocations[uint64(j)] = name - n.availableSegments = deallocateSegments(n.availableSegments, j) + n.SegmentAllocations[uint64(j)] = name + n.AvailableSegments = deallocateSegments(n.AvailableSegments, j) } } func (n *SimpleNamespace) getSegment() uint64 { - if n.selectedExperiment != uint64(n.NumSegments+1) { - return n.selectedExperiment + if n.SelectedExperiment != uint64(n.NumSegments+1) { + return n.SelectedExperiment } // generate random integer min=0, max=num_segments, unit=primary_unit @@ -160,8 +182,8 @@ func (n *SimpleNamespace) getSegment() uint64 { args["max"] = n.NumSegments - 1 args["unit"] = n.Inputs[n.PrimaryUnit] s := &randomInteger{} - n.selectedExperiment = s.execute(args, expt).(uint64) - return n.selectedExperiment + n.SelectedExperiment = s.execute(args, expt).(uint64) + return n.SelectedExperiment } func deallocateSegments(allocated []int, segmentToRemove int) []int { diff --git a/namespace_test.go b/namespace_test.go index 7c784be..61ac2d3 100644 --- a/namespace_test.go +++ b/namespace_test.go @@ -46,7 +46,7 @@ func TestSimpleNamespace(t *testing.T) { n.AddExperiment("random ops", e2, 10) n.AddExperiment("simple", e3, 80) - x := n.availableSegments + x := n.AvailableSegments seg := n.getSegment() if seg != 92 { @@ -62,7 +62,7 @@ func TestSimpleNamespace(t *testing.T) { n.RemoveExperiment("random ops") n.AddExperiment("random ops", e2, 10) - y := n.availableSegments + y := n.AvailableSegments if reflect.DeepEqual(x, y) == false { t.Errorf("Removing and re-adding experiment to a namespace resulted in mismatched allocations. X: %v, Y: %v\n", x, y) @@ -77,7 +77,36 @@ func TestSimpleNamespace(t *testing.T) { n.RemoveExperiment("random ops") n.RemoveExperiment("simple ops") n.RemoveExperiment("simple") - if len(n.availableSegments) != 100 { - t.Errorf("Expected all segments to be available. Actual %d\n", len(n.availableSegments)) + if len(n.AvailableSegments) != 100 { + t.Errorf("Expected all segments to be available. Actual %d\n", len(n.AvailableSegments)) } + + + inputs["userid"] = "test-id" + n = NewSimpleNamespace("test_removing_namespace", 100, "userid", inputs) + n.AddExperiment("simple ops", e1, 10) + n.AddExperiment("random ops", e2, 10) + n.AddExperiment("simple", e3, 80) + interpreter = n.Run() + for _, v := range n.SegmentAllocations { + if v == "simple" { + t.Log("added [simple] exp\n") + } + } + val, ok := interpreter.Get("output") + if !ok || val!= "test" { + t.Errorf("Namespace run was not successful out:[%+v]\n", interpreter) + } + n.RemoveExperiment("simple") + interpreter = n.Run() + for _, v := range n.SegmentAllocations { + if v == "simple" { + t.Error("didn't remove [simple] exp\n") + } + } + val, ok = interpreter.Get("output") + if ok { + t.Errorf("Namespace run was not successful out:[%+v]\n", interpreter) + } + } diff --git a/operators.go b/operators.go index 1a57175..02b3c28 100644 --- a/operators.go +++ b/operators.go @@ -102,7 +102,7 @@ type set struct{} func (s *set) execute(m map[string]interface{}, interpreter *Interpreter) interface{} { existOrPanic(m, []string{"var", "value"}, "Set") lhs := m["var"].(string) - interpreter.parameterSalt = lhs + interpreter.ParameterSalt = lhs value := interpreter.evaluate(m["value"]) interpreter.Outputs[lhs] = value return true @@ -213,7 +213,11 @@ func unwrapValue(value reflect.Value) interface{} { case reflect.Bool: return value.Bool() default: - return value.Interface() + if value.IsValid() { + return value.Interface() + } else { + return nil + } } } diff --git a/random.go b/random.go index 20aac8f..86851e5 100644 --- a/random.go +++ b/random.go @@ -18,7 +18,7 @@ package planout import ( "crypto/sha1" - "fmt" + "encoding/hex" "strconv" ) @@ -28,7 +28,8 @@ func hash(in string) uint64 { var x [20]byte = sha1.Sum([]byte(in)) // Get the first 15 characters of the hexdigest. - var y string = fmt.Sprintf("%x", x[0:8]) + //var y string = fmt.Sprintf("%x", x[0:8]) + y := hex.EncodeToString(x[:8]) y = y[0 : len(y)-1] // Convert hex string into uint64 @@ -73,7 +74,7 @@ func getUnit(args map[string]interface{}, interpreter *Interpreter) string { func getHash(args map[string]interface{}, interpreter *Interpreter, appended_units ...string) uint64 { unitstr := getUnit(args, interpreter) - salt := getSalt(args, interpreter.Salt, interpreter.parameterSalt) + salt := getSalt(args, interpreter.Salt, interpreter.ParameterSalt) name := generateNameToHash(unitstr, salt) if len(appended_units) > 0 { diff --git a/test/map_index_test.json b/test/map_index_test.json index 7ce7cd1..9481e0c 100644 --- a/test/map_index_test.json +++ b/test/map_index_test.json @@ -32,6 +32,18 @@ }, "index": "key" } + }, + { + "op": "set", + "var": "empty", + "value": { + "op": "index", + "base": { + "op": "get", + "var": "map" + }, + "index": "not there" + } } ] } \ No newline at end of file diff --git a/test/struct_with_nil_field.json b/test/struct_with_nil_field.json new file mode 100644 index 0000000..ad8e14b --- /dev/null +++ b/test/struct_with_nil_field.json @@ -0,0 +1,12 @@ +{ + "op": "set", + "var": "nil", + "value": { + "op": "index", + "base": { + "op": "get", + "var": "struct" + }, + "index": "None" + } +} \ No newline at end of file