/* Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package value import ( "encoding/base64" "encoding/json" "fmt" "reflect" "sort" "testing" "time" ) func MustReflect(i interface{}) Value { if i == nil { return NewValueInterface(nil) } v, err := wrapValueReflect(reflect.ValueOf(i), nil, nil) if err != nil { panic(err) } return v } func TestReflectPrimitives(t *testing.T) { rv := MustReflect("string") if !rv.IsString() { t.Error("expected IsString to be true") } if rv.AsString() != "string" { t.Errorf("expected rv.String to be 'string' but got %v", rv.Unstructured()) } rv = MustReflect([]byte("string")) if !rv.IsString() { t.Error("expected IsString to be true") } if rv.IsList() { t.Error("expected IsList to be false ([]byte is represented as a base64 encoded string)") } encoded := base64.StdEncoding.EncodeToString([]byte("string")) if rv.AsString() != encoded { t.Errorf("expected rv.String to be %v but got %v", []byte(encoded), rv.Unstructured()) } rv = MustReflect(1) if !rv.IsInt() { t.Error("expected IsInt to be true") } if rv.AsInt() != 1 { t.Errorf("expected rv.Int to be 1 but got %v", rv.Unstructured()) } rv = MustReflect(uint32(3000000000)) if !rv.IsInt() { t.Error("expected IsInt to be true") } if rv.AsInt() != 3000000000 { t.Errorf("expected rv.Int to be 3000000000 but got %v", rv.Unstructured()) } rv = MustReflect(1.5) if !rv.IsFloat() { t.Error("expected IsFloat to be true") } if rv.AsFloat() != 1.5 { t.Errorf("expected rv.Float to be 1.1 but got %v", rv.Unstructured()) } rv = MustReflect(true) if !rv.IsBool() { t.Error("expected IsBool to be true") } if rv.AsBool() != true { t.Errorf("expected rv.Bool to be true but got %v", rv.Unstructured()) } rv = MustReflect(nil) if !rv.IsNull() { t.Error("expected IsNull to be true") } } type Convertable struct { Value interface{} } func (t Convertable) MarshalJSON() ([]byte, error) { return json.Marshal(t.Value) } func (t Convertable) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &t.Value) } type PtrConvertable struct { Value interface{} } func (t *PtrConvertable) MarshalJSON() ([]byte, error) { return json.Marshal(t.Value) } func (t *PtrConvertable) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &t.Value) } type StringConvertable struct { Value string } func (t StringConvertable) MarshalJSON() ([]byte, error) { return json.Marshal(t.Value) } func (t StringConvertable) ToUnstructured() (string, bool) { return t.Value, true } type PtrStringConvertable struct { Value string } func (t PtrStringConvertable) MarshalJSON() ([]byte, error) { return json.Marshal(t.Value) } func (t *PtrStringConvertable) ToUnstructured() (string, bool) { return t.Value, true } func TestReflectCustomStringConversion(t *testing.T) { dateTime, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00") if err != nil { t.Fatal(err) } cases := []struct { name string convertable interface{} expected interface{} }{ { name: "marshalable-struct", convertable: Convertable{Value: "struct-test"}, expected: "struct-test", }, { name: "marshalable-pointer", convertable: &PtrConvertable{Value: "pointer-test"}, expected: "pointer-test", }, { name: "pointer-to-marshalable-struct", convertable: &Convertable{Value: "pointer-test"}, expected: "pointer-test", }, { name: "string-convertable-struct", convertable: StringConvertable{Value: "struct-test"}, expected: "struct-test", }, { name: "string-convertable-pointer", convertable: &PtrStringConvertable{Value: "struct-test"}, expected: "struct-test", }, { name: "pointer-to-string-convertable-struct", convertable: &StringConvertable{Value: "pointer-test"}, expected: "pointer-test", }, { name: "time", convertable: dateTime, expected: "2006-01-02T15:04:05+07:00", }, { name: "nil-marshalable-struct", convertable: Convertable{Value: nil}, expected: nil, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { rv := MustReflect(tc.convertable) if rv.Unstructured() != tc.expected { t.Errorf("expected rv.String to be %v but got %s", tc.expected, rv.AsString()) } }) } } func TestReflectPointers(t *testing.T) { s := "string" rv := MustReflect(&s) if !rv.IsString() { t.Error("expected IsString to be true") } if rv.AsString() != "string" { t.Errorf("expected rv.String to be 'string' but got %s", rv.AsString()) } } type T struct { I int64 `json:"int"` } type emptyStruct struct{} type testBasicStruct struct { I int64 `json:"int"` S string } type testOmitStruct struct { I int64 `json:"-"` S string } type testInlineStruct struct { Inline T `json:",inline"` S string } type testOmitemptyStruct struct { Noomit *string `json:"noomit"` Omit *string `json:"omit,omitempty"` } type testEmbeddedStruct struct { *testBasicStruct `json:",inline"` } func TestReflectStruct(t *testing.T) { cases := []struct { name string val interface{} expectedMap map[string]interface{} expectedUnstructured interface{} }{ { name: "empty", val: emptyStruct{}, expectedMap: map[string]interface{}{}, expectedUnstructured: map[string]interface{}{}, }, { name: "basic", val: testBasicStruct{I: 10, S: "string"}, expectedMap: map[string]interface{}{"int": int64(10), "S": "string"}, expectedUnstructured: map[string]interface{}{"int": int64(10), "S": "string"}, }, { name: "pointerToBasic", val: &testBasicStruct{I: 10, S: "string"}, expectedMap: map[string]interface{}{"int": int64(10), "S": "string"}, expectedUnstructured: map[string]interface{}{"int": int64(10), "S": "string"}, }, { name: "omit", val: testOmitStruct{I: 10, S: "string"}, expectedMap: map[string]interface{}{"S": "string"}, expectedUnstructured: map[string]interface{}{"S": "string"}, }, { name: "inline", val: &testInlineStruct{Inline: T{I: 10}, S: "string"}, expectedMap: map[string]interface{}{"int": int64(10), "S": "string"}, expectedUnstructured: map[string]interface{}{"int": int64(10), "S": "string"}, }, { name: "omitempty", val: testOmitemptyStruct{Noomit: nil, Omit: nil}, expectedMap: map[string]interface{}{"noomit": (*string)(nil)}, expectedUnstructured: map[string]interface{}{"noomit": nil}, }, { name: "embedded", val: testEmbeddedStruct{&testBasicStruct{I: 10, S: "string"}}, expectedMap: map[string]interface{}{"int": int64(10), "S": "string"}, expectedUnstructured: map[string]interface{}{"int": int64(10), "S": "string"}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { rv := MustReflect(tc.val) if !rv.IsMap() { t.Error("expected IsMap to be true") } m := rv.AsMap() if m.Length() != len(tc.expectedMap) { t.Errorf("expected map to be of length %d but got %d", len(tc.expectedMap), m.Length()) } iterateResult := map[string]interface{}{} m.Iterate(func(s string, value Value) bool { iterateResult[s] = value.(*valueReflect).Value.Interface() return true }) if !reflect.DeepEqual(iterateResult, tc.expectedMap) { t.Errorf("expected iterate to produce %#v but got %#v", tc.expectedMap, iterateResult) } unstructured := rv.Unstructured() if !reflect.DeepEqual(unstructured, tc.expectedUnstructured) { t.Errorf("expected iterate to produce %#v but got %#v", tc.expectedUnstructured, unstructured) } }) } } type testMutateStruct struct { I1 int64 `json:"key1,omitempty"` S1 string `json:"key2,omitempty"` S2 string `json:"key3,omitempty"` S3 string `json:"key4,omitempty"` } func TestReflectStructMutate(t *testing.T) { rv := MustReflect(&testMutateStruct{I1: 1, S1: "string1"}) if !rv.IsMap() { t.Error("expected IsMap to be true") } m := rv.AsMap() atKey1, ok := m.Get("key1") if !ok { t.Fatalf("expected map.Get(key1) to be 1 but got !ok") } if atKey1.AsInt() != 1 { t.Fatalf("expected map.Get(key1) to be 1 but got: %v", atKey1) } m.Set("key1", NewValueInterface(int64(2))) m.Delete("key2") m.Delete("key3") m.Set("key4", NewValueInterface("string4")) expectedMap := map[string]interface{}{"key1": int64(2), "key4": "string4"} unstructured := rv.Unstructured() if !reflect.DeepEqual(unstructured, expectedMap) { t.Errorf("expected %v but got: %v", expectedMap, unstructured) } } // TestReflectMutateNestedStruct ensures a structs field within various typed can be modified. func TestReflectMutateNestedStruct(t *testing.T) { type field struct { S string `json:"s,omitempty"` } cases := []struct { fieldName string root Value lookupField func(root Value) Value expectUpdated interface{} expectDeleted interface{} }{ { fieldName: "field", root: MustReflect(&struct { Field field `json:"field,omitempty"` }{ Field: field{S: "field"}, }), lookupField: func(rv Value) Value { field, _ := rv.AsMap().Get("field") return field }, expectUpdated: map[string]interface{}{ "field": map[string]interface{}{"s": "updatedValue"}, }, expectDeleted: map[string]interface{}{ "field": map[string]interface{}{}, }, }, { fieldName: "map", root: MustReflect(&struct { Map map[string]field `json:"map,omitempty"` }{ Map: map[string]field{"mapKey": {S: "mapItem"}}, }), lookupField: func(rv Value) Value { m, _ := rv.AsMap().Get("map") mapItem, _ := m.AsMap().Get("mapKey") return mapItem }, expectUpdated: map[string]interface{}{ "map": map[string]interface{}{"mapKey": map[string]interface{}{"s": "updatedValue"}}, }, expectDeleted: map[string]interface{}{ "map": map[string]interface{}{"mapKey": map[string]interface{}{}}, }, }, { fieldName: "mapiter", root: MustReflect(&struct { Mapiter map[string]field `json:"mapiter,omitempty"` }{ Mapiter: map[string]field{"mapKey": {S: "mapItem"}}, }), lookupField: func(rv Value) Value { mapItem := &valueReflect{} m, _ := rv.AsMap().Get("mapiter") m.AsMap().Iterate(func(key string, value Value) bool { if key == "mapKey" { *mapItem = *value.(*valueReflect) return false } return true }) if !mapItem.Value.IsValid() { t.Fatal("map item not found") } return mapItem }, expectUpdated: map[string]interface{}{ "mapiter": map[string]interface{}{"mapKey": map[string]interface{}{"s": "updatedValue"}}, }, expectDeleted: map[string]interface{}{ "mapiter": map[string]interface{}{"mapKey": map[string]interface{}{}}, }, }, { fieldName: "list", root: MustReflect(&struct { List []field `json:"list,omitempty"` }{ List: []field{{S: "listItem"}}, }), lookupField: func(rv Value) Value { list, _ := rv.AsMap().Get("list") return list.AsList().At(0) }, expectUpdated: map[string]interface{}{ "list": []interface{}{map[string]interface{}{"s": "updatedValue"}}, }, expectDeleted: map[string]interface{}{ "list": []interface{}{map[string]interface{}{}}, }, }, { fieldName: "mapOfMaps", root: MustReflect(&struct { MapOfMaps map[string]map[string]field `json:"mapOfMaps,omitempty"` }{ MapOfMaps: map[string]map[string]field{"outer": {"inner": {S: "mapOfMapItem"}}}, }), lookupField: func(rv Value) Value { mapOfMaps, _ := rv.AsMap().Get("mapOfMaps") innerMap, _ := mapOfMaps.AsMap().Get("outer") mapOfMapsItem, _ := innerMap.AsMap().Get("inner") return mapOfMapsItem }, expectUpdated: map[string]interface{}{ "mapOfMaps": map[string]interface{}{"outer": map[string]interface{}{"inner": map[string]interface{}{"s": "updatedValue"}}}, }, expectDeleted: map[string]interface{}{ "mapOfMaps": map[string]interface{}{"outer": map[string]interface{}{"inner": map[string]interface{}{}}}, }, }, { fieldName: "mapOfLists", root: MustReflect(&struct { MapOfLists map[string][]field `json:"mapOfLists,omitempty"` }{ MapOfLists: map[string][]field{"outer": {{S: "mapOfListsItem"}}}, }), lookupField: func(rv Value) Value { mapOfLists, _ := rv.AsMap().Get("mapOfLists") innerList, _ := mapOfLists.AsMap().Get("outer") mapOfListsItem := innerList.AsList().At(0) return mapOfListsItem }, expectUpdated: map[string]interface{}{ "mapOfLists": map[string]interface{}{"outer": []interface{}{map[string]interface{}{"s": "updatedValue"}}}, }, expectDeleted: map[string]interface{}{ "mapOfLists": map[string]interface{}{"outer": []interface{}{map[string]interface{}{}}}, }, }, } for _, tc := range cases { t.Run(tc.fieldName, func(t *testing.T) { root := tc.root field := tc.lookupField(root) field.AsMap().Set("s", NewValueInterface("updatedValue")) unstructured := root.Unstructured() if !reflect.DeepEqual(unstructured, tc.expectUpdated) { t.Errorf("expected %v but got: %v", tc.expectUpdated, unstructured) } field.AsMap().Delete("s") unstructured = root.Unstructured() if !reflect.DeepEqual(unstructured, tc.expectDeleted) { t.Errorf("expected %v but got: %v", tc.expectDeleted, unstructured) } }) } } func TestReflectMap(t *testing.T) { cases := []struct { name string val interface{} expectedMap map[string]interface{} expectedUnstructured interface{} length int }{ { name: "empty", val: map[string]string{}, expectedMap: map[string]interface{}{}, expectedUnstructured: map[string]interface{}{}, length: 0, }, { name: "stringMap", val: map[string]string{"key1": "value1", "key2": "value2"}, expectedMap: map[string]interface{}{"key1": "value1", "key2": "value2"}, expectedUnstructured: map[string]interface{}{"key1": "value1", "key2": "value2"}, length: 2, }, { name: "convertableMap", val: map[string]Convertable{"key1": {"converted1"}, "key2": {"converted2"}}, expectedMap: map[string]interface{}{"key1": "converted1", "key2": "converted2"}, expectedUnstructured: map[string]interface{}{"key1": "converted1", "key2": "converted2"}, length: 2, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { rv := MustReflect(tc.val) if !rv.IsMap() { t.Error("expected IsMap to be true") } m := rv.AsMap() if m.Length() != tc.length { t.Errorf("expected map to be of length %d but got %d", tc.length, m.Length()) } iterateResult := map[string]interface{}{} m.Iterate(func(s string, value Value) bool { iterateResult[s] = value.AsString() return true }) if !reflect.DeepEqual(iterateResult, tc.expectedMap) { t.Errorf("expected iterate to produce %#v but got %#v", tc.expectedMap, iterateResult) } unstructured := rv.Unstructured() if !reflect.DeepEqual(unstructured, tc.expectedUnstructured) { t.Errorf("expected iterate to produce %#v but got %#v", tc.expectedUnstructured, unstructured) } }) } } func TestReflectMapMutate(t *testing.T) { rv := MustReflect(map[string]string{"key1": "value1", "key2": "value2"}) if !rv.IsMap() { t.Error("expected IsMap to be true") } m := rv.AsMap() atKey1, ok := m.Get("key1") if !ok { t.Errorf("expected map.Get(key1) to be 'value1' but got !ok") } if atKey1.AsString() != "value1" { t.Errorf("expected map.Get(key1) to be 'value1' but got: %v", atKey1) } m.Set("key1", NewValueInterface("replacement")) m.Delete("key2") m.Delete("key3") m.Set("key4", NewValueInterface("value4")) expectedMap := map[string]interface{}{"key1": "replacement", "key4": "value4"} unstructured := rv.Unstructured() if !reflect.DeepEqual(unstructured, expectedMap) { t.Errorf("expected %v but got: %v", expectedMap, unstructured) } } func TestReflectList(t *testing.T) { cases := []struct { name string val interface{} expectedIterate []interface{} expectedUnstructured interface{} length int }{ { name: "empty", val: []string{}, expectedIterate: []interface{}{}, expectedUnstructured: []interface{}{}, length: 0, }, { name: "stringList", val: []string{"value1", "value2"}, expectedIterate: []interface{}{"value1", "value2"}, expectedUnstructured: []interface{}{"value1", "value2"}, length: 2, }, { name: "convertableList", val: []Convertable{{"converted1"}, {"converted2"}}, expectedIterate: []interface{}{"converted1", "converted2"}, expectedUnstructured: []interface{}{"converted1", "converted2"}, length: 2, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { rv := MustReflect(tc.val) if !rv.IsList() { t.Error("expected IsList to be true") } m := rv.AsList() if m.Length() != tc.length { t.Errorf("expected list to be of length %d but got %d", tc.length, m.Length()) } l := m.Length() iterateResult := make([]interface{}, l) for i := 0; i < l; i++ { iterateResult[i] = m.At(i).AsString() } if !reflect.DeepEqual(iterateResult, tc.expectedIterate) { t.Errorf("expected iterate to produce %#v but got %#v", tc.expectedIterate, iterateResult) } iter := m.Range() iterateResult = make([]interface{}, l) for iter.Next() { i, val := iter.Item() iterateResult[i] = val.AsString() } if !reflect.DeepEqual(iterateResult, tc.expectedIterate) { t.Errorf("expected iterate to produce %#v but got %#v", tc.expectedIterate, iterateResult) } unstructured := rv.Unstructured() if !reflect.DeepEqual(unstructured, tc.expectedUnstructured) { t.Errorf("expected iterate to produce %#v but got %#v", tc.expectedUnstructured, unstructured) } }) } } func TestReflectListAt(t *testing.T) { rv := MustReflect([]string{"one", "two"}) if !rv.IsList() { t.Error("expected IsList to be true") } list := rv.AsList() atOne := list.At(1) if atOne.AsString() != "two" { t.Errorf("expected list.At(1) to be 'two' but got: %v", atOne) } } func TestMapZip(t *testing.T) { type entry struct { key string lhs, rhs interface{} } type s struct { // deliberately unordered C string `json:"c,omitempty"` B string `json:"b,omitempty"` D string `json:"d,omitempty"` A string `json:"a,omitempty"` } cases := []struct { name string lhs interface{} rhs interface{} expectedZipped []entry }{ { name: "structZip", lhs: &s{A: "1", B: "3", C: "5"}, rhs: &s{A: "2", B: "4", D: "6"}, expectedZipped: []entry{ {"a", "1", "2"}, {"b", "3", "4"}, {"c", "5", interface{}(nil)}, {"d", interface{}(nil), "6"}, }, }, { name: "mapZip", lhs: &map[string]interface{}{"a": "1", "b": "3", "c": "5"}, rhs: &map[string]interface{}{"a": "2", "b": "4", "d": "6"}, expectedZipped: []entry{ {"a", "1", "2"}, {"b", "3", "4"}, {"c", "5", interface{}(nil)}, {"d", interface{}(nil), "6"}, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { lhs := MustReflect(tc.lhs) rhs := MustReflect(tc.rhs) for _, lhs := range []Value{lhs, NewValueInterface(lhs.Unstructured())} { for _, rhs := range []Value{rhs, NewValueInterface(rhs.Unstructured())} { t.Run(fmt.Sprintf("%s-%s", reflect.TypeOf(lhs).Elem().Name(), reflect.TypeOf(rhs).Elem().Name()), func(t *testing.T) { for _, order := range []MapTraverseOrder{Unordered, LexicalKeyOrder} { var zipped []entry var name string switch order { case Unordered: name = "Unordered" case LexicalKeyOrder: name = "LexicalKeyOrder" } t.Run(name, func(t *testing.T) { MapZip(lhs.AsMap(), rhs.AsMap(), order, func(key string, lhs, rhs Value) bool { var li, ri interface{} if lhs != nil { li = lhs.Unstructured() } if rhs != nil { ri = rhs.Unstructured() } zipped = append(zipped, entry{key, li, ri}) return true }) if order == Unordered { sort.Slice(zipped, func(i, j int) bool { return zipped[i].key < zipped[j].key }) } if !reflect.DeepEqual(zipped, tc.expectedZipped) { t.Errorf("expected zip to produce:\n%#v\nbut got\n%#v", tc.expectedZipped, zipped) } }) } }) } } }) } }