diff --git a/go.mod b/go.mod index e841f1dcf..93e0d900a 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( github.com/derekparker/trie v0.0.0-20221221181808-1424fce0c981 + github.com/goccy/go-json v0.10.0 github.com/golang/glog v1.0.0 github.com/golang/protobuf v1.5.2 github.com/google/go-cmp v0.5.9 diff --git a/go.sum b/go.sum index 7776876f3..96b222c90 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,8 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= +github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= diff --git a/util/reflect.go b/util/reflect.go index 3ad849386..156d57423 100644 --- a/util/reflect.go +++ b/util/reflect.go @@ -256,6 +256,16 @@ func InsertIntoStruct(parentStruct interface{}, fieldName string, fieldValue int // generated code. Here we cast the value to the type in the generated code. if ft.Type.Kind() == reflect.Bool && t.Kind() == reflect.Bool { nv := reflect.New(ft.Type).Elem() + + // If the field is not nil, do not create a new pointer which modifies the + // field's memory address under its parent. + if pv.Elem().FieldByName(fieldName).Addr().UnsafePointer() != nil { + nv = reflect.NewAt( + ft.Type, + pv.Elem().FieldByName(fieldName).Addr().UnsafePointer(), + ).Elem() + } + nv.SetBool(v.Bool()) v = nv } @@ -265,6 +275,16 @@ func InsertIntoStruct(parentStruct interface{}, fieldName string, fieldValue int // This will also cast a []uint8 value since byte is an alias for uint8. if ft.Type.Kind() == reflect.Slice && t.Kind() == reflect.Slice && ft.Type.Elem().Kind() == reflect.Uint8 && t.Elem().Kind() == reflect.Uint8 { nv := reflect.New(ft.Type).Elem() + + // If the field is not nil, do not create a new pointer which modifies the + // field's memory address under its parent. + if pv.Elem().FieldByName(fieldName).Addr().UnsafePointer() != nil { + nv = reflect.NewAt( + ft.Type, + pv.Elem().FieldByName(fieldName).Addr().UnsafePointer(), + ).Elem() + } + nv.SetBytes(v.Bytes()) v = nv } @@ -272,6 +292,13 @@ func InsertIntoStruct(parentStruct interface{}, fieldName string, fieldValue int n := v if n.IsValid() && (ft.Type.Kind() == reflect.Ptr && t.Kind() != reflect.Ptr) { n = reflect.New(t) + + // If the field is not nil, do not create a new pointer which modifies the + // field's memory address under its parent. + if pv.Elem().FieldByName(fieldName).UnsafePointer() != nil { + n = reflect.NewAt(t, pv.Elem().FieldByName(fieldName).UnsafePointer()) + } + n.Elem().Set(v) } diff --git a/ytypes/gnmi.go b/ytypes/gnmi.go index 52a463a3e..bc316e067 100644 --- a/ytypes/gnmi.go +++ b/ytypes/gnmi.go @@ -44,25 +44,44 @@ func UnmarshalNotifications(schema *Schema, ns []*gpb.Notification, opts ...Unma // If an error occurs during unmarshalling, schema.Root may already be // modified. A rollback is not performed. func UnmarshalSetRequest(schema *Schema, req *gpb.SetRequest, opts ...UnmarshalOpt) error { - preferShadowPath := hasPreferShadowPath(opts) - ignoreExtraFields := hasIgnoreExtraFields(opts) if req == nil { return nil } + + // Use option slices instead of flags to pass options down to the function calls. + var getOrCreateOpts []GetOrCreateNodeOpt + var deleteOpts []DelNodeOpt + var updateOpts []SetNodeOpt + + for _, opt := range opts { + switch o := opt.(type) { + case *PreferShadowPath: + getOrCreateOpts = append(getOrCreateOpts, &PreferShadowPath{}) + deleteOpts = append(deleteOpts, &PreferShadowPath{}) + updateOpts = append(updateOpts, &PreferShadowPath{}) + case *IgnoreExtraFields: + updateOpts = append(updateOpts, &IgnoreExtraFields{}) + case *NodeCacheOpt: + getOrCreateOpts = append(getOrCreateOpts, o) + deleteOpts = append(deleteOpts, o) + updateOpts = append(updateOpts, o) + } + } + root := schema.Root - node, nodeName, err := getOrCreateNode(schema.RootSchema(), root, req.Prefix, preferShadowPath) + node, nodeName, err := getOrCreateNode(schema.RootSchema(), root, req.Prefix, getOrCreateOpts) if err != nil { return err } // Process deletes, then replace, then updates. - if err := deletePaths(schema.SchemaTree[nodeName], node, req.Delete, preferShadowPath); err != nil { + if err := deletePaths(schema.SchemaTree[nodeName], node, req.Delete, deleteOpts); err != nil { return err } - if err := replacePaths(schema.SchemaTree[nodeName], node, req.Replace, preferShadowPath, ignoreExtraFields); err != nil { + if err := replacePaths(schema.SchemaTree[nodeName], node, req.Replace, deleteOpts, updateOpts); err != nil { return err } - if err := updatePaths(schema.SchemaTree[nodeName], node, req.Update, preferShadowPath, ignoreExtraFields); err != nil { + if err := updatePaths(schema.SchemaTree[nodeName], node, req.Update, updateOpts); err != nil { return err } @@ -71,11 +90,7 @@ func UnmarshalSetRequest(schema *Schema, req *gpb.SetRequest, opts ...UnmarshalO // getOrCreateNode instantiates the node at the given path, and returns that // node along with its name. -func getOrCreateNode(schema *yang.Entry, goStruct ygot.GoStruct, path *gpb.Path, preferShadowPath bool) (ygot.GoStruct, string, error) { - var gcopts []GetOrCreateNodeOpt - if preferShadowPath { - gcopts = append(gcopts, &PreferShadowPath{}) - } +func getOrCreateNode(schema *yang.Entry, goStruct ygot.GoStruct, path *gpb.Path, gcopts []GetOrCreateNodeOpt) (ygot.GoStruct, string, error) { // Operate at the prefix level. nodeI, _, err := GetOrCreateNode(schema, goStruct, path, gcopts...) if err != nil { @@ -90,12 +105,7 @@ func getOrCreateNode(schema *yang.Entry, goStruct ygot.GoStruct, path *gpb.Path, } // deletePaths deletes a slice of paths from the given GoStruct. -func deletePaths(schema *yang.Entry, goStruct ygot.GoStruct, paths []*gpb.Path, preferShadowPath bool) error { - var dopts []DelNodeOpt - if preferShadowPath { - dopts = append(dopts, &PreferShadowPath{}) - } - +func deletePaths(schema *yang.Entry, goStruct ygot.GoStruct, paths []*gpb.Path, dopts []DelNodeOpt) error { for _, path := range paths { if err := DeleteNode(schema, goStruct, path, dopts...); err != nil { return err @@ -107,17 +117,12 @@ func deletePaths(schema *yang.Entry, goStruct ygot.GoStruct, paths []*gpb.Path, // replacePaths unmarshals a slice of updates into the given GoStruct. It // deletes the values at these paths before unmarshalling them. These updates // can either by JSON-encoded or gNMI-encoded values (scalars). -func replacePaths(schema *yang.Entry, goStruct ygot.GoStruct, updates []*gpb.Update, preferShadowPath, ignoreExtraFields bool) error { - var dopts []DelNodeOpt - if preferShadowPath { - dopts = append(dopts, &PreferShadowPath{}) - } - +func replacePaths(schema *yang.Entry, goStruct ygot.GoStruct, updates []*gpb.Update, dopts []DelNodeOpt, uopts []SetNodeOpt) error { for _, update := range updates { if err := DeleteNode(schema, goStruct, update.Path, dopts...); err != nil { return err } - if err := setNode(schema, goStruct, update, preferShadowPath, ignoreExtraFields); err != nil { + if err := setNode(schema, goStruct, update, uopts); err != nil { return err } } @@ -126,9 +131,9 @@ func replacePaths(schema *yang.Entry, goStruct ygot.GoStruct, updates []*gpb.Upd // updatePaths unmarshals a slice of updates into the given GoStruct. These // updates can either by JSON-encoded or gNMI-encoded values (scalars). -func updatePaths(schema *yang.Entry, goStruct ygot.GoStruct, updates []*gpb.Update, preferShadowPath, ignoreExtraFields bool) error { +func updatePaths(schema *yang.Entry, goStruct ygot.GoStruct, updates []*gpb.Update, uopts []SetNodeOpt) error { for _, update := range updates { - if err := setNode(schema, goStruct, update, preferShadowPath, ignoreExtraFields); err != nil { + if err := setNode(schema, goStruct, update, uopts); err != nil { return err } } @@ -137,14 +142,6 @@ func updatePaths(schema *yang.Entry, goStruct ygot.GoStruct, updates []*gpb.Upda // setNode unmarshals either a JSON-encoded value or a gNMI-encoded (scalar) // value into the given GoStruct. -func setNode(schema *yang.Entry, goStruct ygot.GoStruct, update *gpb.Update, preferShadowPath, ignoreExtraFields bool) error { - sopts := []SetNodeOpt{&InitMissingElements{}} - if preferShadowPath { - sopts = append(sopts, &PreferShadowPath{}) - } - if ignoreExtraFields { - sopts = append(sopts, &IgnoreExtraFields{}) - } - - return SetNode(schema, goStruct, update.Path, update.Val, sopts...) +func setNode(schema *yang.Entry, goStruct ygot.GoStruct, update *gpb.Update, uopts []SetNodeOpt) error { + return SetNode(schema, goStruct, update.Path, update.Val, append(uopts, &InitMissingElements{})...) } diff --git a/ytypes/gnmi_test.go b/ytypes/gnmi_test.go index c5d7ef621..669a72f3e 100644 --- a/ytypes/gnmi_test.go +++ b/ytypes/gnmi_test.go @@ -420,6 +420,714 @@ func TestUnmarshalSetRequest(t *testing.T) { } } +// TestUnmarshalSetRequestWithNodeCache verifies the behavior of UnmarshalSetRequest +// when node cache is used (optional). +// +// Since the basic tests for UnmarshalSetRequest are covered by TestUnmarshalSetRequest, +// this test function focuses on data changes in the cache. +func TestUnmarshalSetRequestWithNodeCache(t *testing.T) { + inSchema := &Schema{ + Root: &ListElemStruct1{ + Key1: ygot.String("hello"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafListName: []int32{43}, + }, + }, + }, + SchemaTree: map[string]*yang.Entry{ + "ListElemStruct1": simpleSchema(), + "OuterContainerType1": simpleSchema().Dir["outer"], + }, + } + + tests := []struct { + desc string + inSchema *Schema + inReq *gpb.SetRequest + inUnmarshalOpts []UnmarshalOpt + want ygot.GoStruct + wantNodeCacheStore map[string]*cachedNodeInfo // Only `key` and `nodes` (`Data` and `Path`) are compared. + wantErr bool + }{{ + desc: "updates to an empty struct", + inSchema: inSchema, + inReq: &gpb.SetRequest{ + Prefix: &gpb.Path{}, + Update: []*gpb.Update{{ + Path: mustPath("/key1"), + Val: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{StringVal: "hello"}}, + }, { + Path: mustPath("/outer/inner"), + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(` +{ + "int32-leaf-list": [42] +} + `), + }}, + }}, + }, + want: &ListElemStruct1{ + Key1: ygot.String("hello"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafListName: []int32{42}, + }, + }, + }, + wantNodeCacheStore: map[string]*cachedNodeInfo{ + `{"name":"key1"}`: { + nodes: []*TreeNode{{ + Data: func(s string) *string { return &s }("hello"), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{Name: "key1"}}, + }, + }}, + }, + `{"name":"outer"},{"name":"inner"}`: { + nodes: []*TreeNode{ + { + Data: &InnerContainerType1{ + Int32LeafListName: []int32{42}, + }, + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "outer"}, + {Name: "inner"}, + }, + }, + }, + }, + }, + }, + }, { + desc: "updates to non-empty struct", + inSchema: inSchema, + inReq: &gpb.SetRequest{ + Prefix: &gpb.Path{}, + Update: []*gpb.Update{{ + Path: mustPath("/key1"), + Val: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{StringVal: "hello"}}, + }, { + Path: mustPath("/outer/inner"), + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(` +{ + "int32-leaf-list": [43] +} + `), + }}, + }}, + }, + want: &ListElemStruct1{ + Key1: ygot.String("hello"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafListName: []int32{43}, + }, + }, + }, + wantNodeCacheStore: map[string]*cachedNodeInfo{ + `{"name":"key1"}`: { + nodes: []*TreeNode{{ + Data: func(s string) *string { return &s }("hello"), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{Name: "key1"}}, + }, + }}, + }, + `{"name":"outer"},{"name":"inner"}`: { + nodes: []*TreeNode{ + { + Data: &InnerContainerType1{ + Int32LeafListName: []int32{43}, + }, + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "outer"}, + {Name: "inner"}, + }, + }, + }, + }, + }, + }, + }, { + desc: "updates of invalid paths to non-empty struct with IgnoreExtraFields", + inSchema: inSchema, + inUnmarshalOpts: []UnmarshalOpt{&IgnoreExtraFields{}}, + inReq: &gpb.SetRequest{ + Prefix: &gpb.Path{}, + Update: []*gpb.Update{{ + Path: mustPath("/invalidkey1"), + Val: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{StringVal: "invalid"}}, + }, { + Path: mustPath("/outer/inner"), + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(` +{ + "int32-leaf-list": [41] +} + `), + }}, + }}, + }, + want: &ListElemStruct1{ + Key1: ygot.String("hello"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafListName: []int32{41}, + }, + }, + }, + wantNodeCacheStore: map[string]*cachedNodeInfo{ + `{"name":"key1"}`: { + nodes: []*TreeNode{{ + Data: func(s string) *string { return &s }("hello"), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{Name: "key1"}}, + }, + }}, + }, + `{"name":"outer"},{"name":"inner"}`: { + nodes: []*TreeNode{ + { + Data: &InnerContainerType1{ + Int32LeafListName: []int32{41}, + }, + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "outer"}, + {Name: "inner"}, + }, + }, + }, + }, + }, + }, + }, { + desc: "replaces and update to a non-empty struct", + inSchema: inSchema, + inReq: &gpb.SetRequest{ + Prefix: &gpb.Path{}, + Replace: []*gpb.Update{{ + Path: mustPath("/outer/inner"), + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(` +{ + "int32-leaf-list": [40] +} + `), + }}, + }}, + Update: []*gpb.Update{{ + Path: mustPath("/outer/inner/string-leaf-field"), + Val: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{ + StringVal: "foo", + }}, + }}, + }, + want: &ListElemStruct1{ + Key1: ygot.String("hello"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafListName: []int32{40}, + StringLeafName: ygot.String("foo"), + }, + }, + }, + wantNodeCacheStore: map[string]*cachedNodeInfo{ + `{"name":"key1"}`: { + nodes: []*TreeNode{{ + Data: func(s string) *string { return &s }("hello"), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{Name: "key1"}}, + }, + }}, + }, + `{"name":"outer"},{"name":"inner"}`: { + nodes: []*TreeNode{ + { + Data: &InnerContainerType1{ + Int32LeafListName: []int32{40}, + StringLeafName: func(s string) *string { return &s }("foo"), + }, + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "outer"}, + {Name: "inner"}, + }, + }, + }, + }, + }, + `{"name":"outer"},{"name":"inner"},{"name":"string-leaf-field"}`: { + nodes: []*TreeNode{ + { + Data: func(s string) *string { return &s }("foo"), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "outer"}, + {Name: "inner"}, + {Name: "string-leaf-field"}, + }, + }, + }, + }, + }, + }, + }, { + desc: "deletes to a non-empty struct", + inSchema: inSchema, + inReq: &gpb.SetRequest{ + Prefix: &gpb.Path{}, + Delete: []*gpb.Path{ + mustPath("/outer"), + }, + }, + want: &ListElemStruct1{ + Key1: ygot.String("hello"), + }, + wantNodeCacheStore: map[string]*cachedNodeInfo{ + `{"name":"key1"}`: { + nodes: []*TreeNode{{ + Data: func(s string) *string { return &s }("hello"), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{Name: "key1"}}, + }, + }}, + }, + }, + }, { + desc: "deletes, replaces and update to a non-empty struct", + inSchema: inSchema, + inReq: &gpb.SetRequest{ + Prefix: &gpb.Path{}, + Delete: []*gpb.Path{ + mustPath("/outer/inner"), + }, + Replace: []*gpb.Update{{ + Path: mustPath("/key1"), + Val: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{StringVal: "world"}}, + }}, + Update: []*gpb.Update{{ + Path: mustPath("/outer/inner/config/int32-leaf-field"), + Val: &gpb.TypedValue{Value: &gpb.TypedValue_IntVal{ + IntVal: 42, + }}, + }}, + }, + want: &ListElemStruct1{ + Key1: ygot.String("world"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + }, + }, + }, + wantNodeCacheStore: map[string]*cachedNodeInfo{ + `{"name":"key1"}`: { + nodes: []*TreeNode{{ + Data: func(s string) *string { return &s }("world"), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{Name: "key1"}}, + }, + }}, + }, + `{"name":"outer"},{"name":"inner"},{"name":"config"},{"name":"int32-leaf-field"}`: { + nodes: []*TreeNode{ + { + Data: func(val int32) *int32 { return &val }(42), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "outer"}, + {Name: "inner"}, + {Name: "config"}, + {Name: "int32-leaf-field"}, + }, + }, + }, + }, + }, + }, + }, { + desc: "deletes and update to a non-empty struct with preferShadowPath (no effect)", + inSchema: inSchema, + inReq: &gpb.SetRequest{ + Prefix: &gpb.Path{}, + Delete: []*gpb.Path{ + mustPath("/outer/inner/config/int32-leaf-field"), + }, + }, + inUnmarshalOpts: []UnmarshalOpt{&PreferShadowPath{}}, + want: &ListElemStruct1{ + Key1: ygot.String("world"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + }, + }, + }, + wantNodeCacheStore: map[string]*cachedNodeInfo{ + `{"name":"key1"}`: { + nodes: []*TreeNode{{ + Data: func(s string) *string { return &s }("world"), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{Name: "key1"}}, + }, + }}, + }, + `{"name":"outer"},{"name":"inner"},{"name":"config"},{"name":"int32-leaf-field"}`: { + nodes: []*TreeNode{ + { + Data: func(val int32) *int32 { return &val }(42), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "outer"}, + {Name: "inner"}, + {Name: "config"}, + {Name: "int32-leaf-field"}, + }, + }, + }, + }, + }, + }, + }, { + desc: "updates to a non-empty struct", + inSchema: inSchema, + inReq: &gpb.SetRequest{ + Prefix: &gpb.Path{}, + Update: []*gpb.Update{{ + Path: mustPath("/outer/inner"), + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(` +{ + "int32-leaf-list": [40] +} + `), + }}, + }, { + Path: mustPath("/outer/inner/string-leaf-field"), + Val: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{ + StringVal: "foo", + }}, + }}, + }, + want: &ListElemStruct1{ + Key1: ygot.String("world"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + Int32LeafListName: []int32{40}, + StringLeafName: ygot.String("foo"), + }, + }, + }, + wantNodeCacheStore: map[string]*cachedNodeInfo{ + `{"name":"key1"}`: { + nodes: []*TreeNode{{ + Data: func(s string) *string { return &s }("world"), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{Name: "key1"}}, + }, + }}, + }, + `{"name":"outer"},{"name":"inner"}`: { + nodes: []*TreeNode{ + { + Data: &InnerContainerType1{ + Int32LeafName: func(val int32) *int32 { return &val }(42), + Int32LeafListName: []int32{40}, + StringLeafName: func(s string) *string { return &s }("foo"), + }, + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "outer"}, + {Name: "inner"}, + }, + }, + }, + }, + }, + `{"name":"outer"},{"name":"inner"},{"name":"config"},{"name":"int32-leaf-field"}`: { + nodes: []*TreeNode{ + { + Data: func(val int32) *int32 { return &val }(42), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "outer"}, + {Name: "inner"}, + {Name: "config"}, + {Name: "int32-leaf-field"}, + }, + }, + }, + }, + }, + `{"name":"outer"},{"name":"inner"},{"name":"string-leaf-field"}`: { + nodes: []*TreeNode{ + { + Data: func(s string) *string { return &s }("foo"), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "outer"}, + {Name: "inner"}, + {Name: "string-leaf-field"}, + }, + }, + }, + }, + }, + }, + }, { + desc: "deletes from a non-empty struct", + inSchema: inSchema, + inReq: &gpb.SetRequest{ + Prefix: &gpb.Path{}, + Delete: []*gpb.Path{mustPath("/outer/inner/config/int32-leaf-field")}, + }, + want: &ListElemStruct1{ + Key1: ygot.String("world"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafListName: []int32{40}, + StringLeafName: ygot.String("foo"), + }, + }, + }, + wantNodeCacheStore: map[string]*cachedNodeInfo{ + `{"name":"key1"}`: { + nodes: []*TreeNode{{ + Data: func(s string) *string { return &s }("world"), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{Name: "key1"}}, + }, + }}, + }, + `{"name":"outer"},{"name":"inner"}`: { + nodes: []*TreeNode{ + { + Data: &InnerContainerType1{ + Int32LeafListName: []int32{40}, + StringLeafName: func(s string) *string { return &s }("foo"), + }, + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "outer"}, + {Name: "inner"}, + }, + }, + }, + }, + }, + `{"name":"outer"},{"name":"inner"},{"name":"string-leaf-field"}`: { + nodes: []*TreeNode{ + { + Data: func(s string) *string { return &s }("foo"), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "outer"}, + {Name: "inner"}, + {Name: "string-leaf-field"}, + }, + }, + }, + }, + }, + }, + }, { + desc: "replaces to a non-empty struct with prefix", + inSchema: inSchema, + inReq: &gpb.SetRequest{ + Prefix: mustPath("/outer"), + Replace: []*gpb.Update{{ + Path: mustPath("inner/string-leaf-field"), + Val: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{ + StringVal: "bar", + }}, + }}, + }, + want: &ListElemStruct1{ + Key1: ygot.String("world"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafListName: []int32{40}, + StringLeafName: ygot.String("bar"), + }, + }, + }, + wantNodeCacheStore: map[string]*cachedNodeInfo{ + `{"name":"key1"}`: { + nodes: []*TreeNode{{ + Data: func(s string) *string { return &s }("world"), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{Name: "key1"}}, + }, + }}, + }, + `{"name":"outer"}`: { + nodes: []*TreeNode{{ + Data: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafListName: []int32{40}, + StringLeafName: func(s string) *string { return &s }("bar"), + }, + }, + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{Name: "outer"}}, + }, + }}, + }, + `{"name":"outer"},{"name":"inner"}`: { + nodes: []*TreeNode{ + { + Data: &InnerContainerType1{ + Int32LeafListName: []int32{40}, + StringLeafName: func(s string) *string { return &s }("bar"), + }, + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "outer"}, + {Name: "inner"}, + }, + }, + }, + }, + }, + `{"name":"inner"},{"name":"string-leaf-field"}`: { + nodes: []*TreeNode{ + { + Data: func(s string) *string { return &s }("bar"), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "inner"}, + {Name: "string-leaf-field"}, + }, + }, + }, + }, + }, + }, + }, { + desc: "replaces to a non-existent path", + inSchema: inSchema, + inReq: &gpb.SetRequest{ + Prefix: mustPath("/outer-planets"), + Replace: []*gpb.Update{{ + Path: mustPath("inner"), + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(` +{ + "int32-leaf-list": [42] +} + `), + }}, + }}, + }, + wantErr: true, + }, { + desc: "delete string-leaf-field", + inSchema: inSchema, + inReq: &gpb.SetRequest{ + Prefix: &gpb.Path{}, + Delete: []*gpb.Path{mustPath("/outer/inner/string-leaf-field")}, + }, + want: &ListElemStruct1{ + Key1: ygot.String("world"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafListName: []int32{40}, + }, + }, + }, + wantNodeCacheStore: map[string]*cachedNodeInfo{ + `{"name":"key1"}`: { + nodes: []*TreeNode{{ + Data: func(s string) *string { return &s }("world"), + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{Name: "key1"}}, + }, + }}, + }, + `{"name":"outer"}`: { + nodes: []*TreeNode{{ + Data: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafListName: []int32{40}, + }, + }, + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{Name: "outer"}}, + }, + }}, + }, + `{"name":"outer"},{"name":"inner"}`: { + nodes: []*TreeNode{ + { + Data: &InnerContainerType1{ + Int32LeafListName: []int32{40}, + }, + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + {Name: "outer"}, + {Name: "inner"}, + }, + }, + }, + }, + }, + }, + }} + + // Instantiate node cache. + nodeCache := NewNodeCache() + + // Note: these test cases should not be running in parallel because of sequential + // dependencies on working with the same node cache. + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + err := UnmarshalSetRequest( + tt.inSchema, + tt.inReq, + append(tt.inUnmarshalOpts, &NodeCacheOpt{NodeCache: nodeCache})..., + ) + if gotErr := err != nil; gotErr != tt.wantErr { + t.Fatalf("got error: %v, want: %v", err, tt.wantErr) + } + if !tt.wantErr { + if diff := cmp.Diff(tt.inSchema.Root, tt.want); diff != "" { + t.Errorf("(-got, +want):\n%s", diff) + } + + // Check the data in the node cache. + if len(nodeCache.store) != len(tt.wantNodeCacheStore) { + t.Errorf("wanted node cache store size %d (%v), got %d (%v)", len(tt.wantNodeCacheStore), tt.wantNodeCacheStore, len(nodeCache.store), nodeCache.store) + return + } + + for key, info := range tt.wantNodeCacheStore { + if infoGot, ok := nodeCache.store[key]; !ok { + t.Errorf("missing expected key `%s` in the node cache store (%v)", key, nodeCache.store) + continue + } else { + for i := 0; i < len(info.nodes); i++ { + if diff := cmp.Diff(infoGot.nodes[i].Data, info.nodes[i].Data); diff != "" { + t.Errorf("key %s: (-got, +want):\n%s", key, diff) + } + + if diff := cmp.Diff(infoGot.nodes[i].Path.String(), info.nodes[i].Path.String()); diff != "" { + t.Errorf("key %s: (-got, +want):\n%s", key, diff) + } + } + } + } + } + }) + } +} + func TestUnmarshalNotifications(t *testing.T) { tests := []struct { desc string @@ -689,6 +1397,7 @@ func TestUnmarshalNotifications(t *testing.T) { if gotErr := err != nil; gotErr != tt.wantErr { t.Fatalf("got error: %v, want: %v", err, tt.wantErr) } + if !tt.wantErr { if diff := cmp.Diff(tt.inSchema.Root, tt.want); diff != "" { t.Errorf("(-got, +want):\n%s", diff) diff --git a/ytypes/node.go b/ytypes/node.go index fc174394d..89a45157c 100644 --- a/ytypes/node.go +++ b/ytypes/node.go @@ -15,7 +15,6 @@ package ytypes import ( - "encoding/json" "reflect" "github.com/openconfig/goyang/pkg/yang" @@ -25,6 +24,7 @@ import ( "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" + json "github.com/goccy/go-json" gpb "github.com/openconfig/gnmi/proto/gnmi" ) @@ -64,6 +64,14 @@ type retrieveNodeArgs struct { // ignoreExtraFields avoids generating an error when the input path // refers to a field that does not exist in the GoStruct. ignoreExtraFields bool + + // nodeCache is a data structure that can be passed in by the caller to create fast-paths + // for node modifications. + nodeCache *NodeCache + + // uniquePathRepresentation can be used by the node cache to reduce duplicated + // path string representation processing. + uniquePathRepresentation *string } // retrieveNode is an internal function that retrieves the node specified by @@ -71,7 +79,7 @@ type retrieveNodeArgs struct { // retrieveNodeArgs change the way retrieveNode works. // retrieveNode returns the list of matching nodes and their schemas, and error. // Note that retrieveNode may mutate the tree even if it fails. -func retrieveNode(schema *yang.Entry, root interface{}, path, traversedPath *gpb.Path, args retrieveNodeArgs) ([]*TreeNode, error) { +func retrieveNode(schema *yang.Entry, parent, root interface{}, path, traversedPath *gpb.Path, args retrieveNodeArgs) ([]*TreeNode, error) { switch { case path == nil || len(path.Elem) == 0: // When args.val is non-nil and the schema isn't nil, further check whether @@ -100,11 +108,26 @@ func retrieveNode(schema *yang.Entry, root interface{}, path, traversedPath *gpb return nil, status.Errorf(codes.Unknown, "path %v points to a node with non-leaf schema %v", traversedPath, schema) } } - return []*TreeNode{{ + + nodes := []*TreeNode{{ Path: traversedPath, Schema: schema, Data: root, - }}, nil + }} + + if args.nodeCache != nil && traversedPath != nil && parent != nil && root != nil { + // Note: appending to the slice will change the field's address and there is currently no good solution + // to work around this limitation. + // + // With the memory address being changed, the node cache will lose track of the config tree's fields' + // memory addresses which creates a synchronization issue in the node cache. + // + // To work around this, leaf-list and list schemas are not handled by the node cache for now. + if schema != nil && !schema.IsLeafList() && !schema.IsList() { + args.nodeCache.update(nodes, path, traversedPath, parent, root, args.uniquePathRepresentation) + } + } + return nodes, nil case util.IsValueNil(root): if args.delete { // No-op in case of a delete on a field whose value is not populated. @@ -198,6 +221,11 @@ func retrieveNodeContainer(schema *yang.Entry, root interface{}, path *gpb.Path, // any node type, whether leaf or non-leaf. if args.delete && len(path.Elem) == to { fv.Set(reflect.Zero(ft.Type)) + + // Delete the nodeCache entry. + if args.nodeCache != nil && len(path.GetElem()) > 0 { + args.nodeCache.delete(appendElem(traversedPath, path.GetElem()[0])) + } return nil, nil } @@ -246,10 +274,12 @@ func retrieveNodeContainer(schema *yang.Entry, root interface{}, path *gpb.Path, // the struct rather than having to use the parent struct. } - matches, err := retrieveNode(cschema, fv.Interface(), util.TrimGNMIPathPrefix(path, p[0:to]), np, args) + tp := util.TrimGNMIPathPrefix(path, p[0:to]) + matches, err := retrieveNode(cschema, root, fv.Interface(), tp, np, args) if err != nil { return nil, err } + // If the child container struct or list map is empty // after the deletion operation is executed, then set // it to its zero value (nil). @@ -261,10 +291,16 @@ func retrieveNodeContainer(schema *yang.Entry, root interface{}, path *gpb.Path, case cschema.IsContainer() || (cschema.IsList() && util.IsTypeStructPtr(reflect.TypeOf(fv.Interface()))): if fv.Elem().IsZero() { fv.Set(reflect.Zero(ft.Type)) + if args.nodeCache != nil && len(tp.GetElem()) > 0 { + args.nodeCache.delete(appendElem(np, tp.GetElem()[0])) + } } case cschema.IsList(): if fv.Len() == 0 { fv.Set(reflect.Zero(ft.Type)) + if args.nodeCache != nil && len(tp.GetElem()) > 0 { + args.nodeCache.delete(appendElem(np, tp.GetElem()[0])) + } } } } @@ -353,13 +389,13 @@ func retrieveNodeList(schema *yang.Entry, root interface{}, path, traversedPath if err != nil { return nil, status.Errorf(codes.Unknown, "could not get path keys at %v: %v", traversedPath, err) } - nodes, err := retrieveNode(schema, listElemV.Interface(), util.PopGNMIPath(path), appendElem(traversedPath, &gpb.PathElem{Name: path.GetElem()[0].Name, Key: keys}), args) + + nodes, err := retrieveNode(schema, root, listElemV.Interface(), util.PopGNMIPath(path), appendElem(traversedPath, &gpb.PathElem{Name: path.GetElem()[0].Name, Key: keys}), args) if err != nil { return nil, err } matches = append(matches, nodes...) - continue } @@ -388,17 +424,27 @@ func retrieveNodeList(schema *yang.Entry, root interface{}, path, traversedPath remainingPath := util.PopGNMIPath(path) if args.delete && len(remainingPath.GetElem()) == 0 { rv.SetMapIndex(k, reflect.Value{}) + if args.nodeCache != nil { + args.nodeCache.delete(traversedPath) + } + return nil, nil } - nodes, err := retrieveNode(schema, listElemV.Interface(), remainingPath, appendElem(traversedPath, path.GetElem()[0]), args) + + np := appendElem(traversedPath, path.GetElem()[0]) + nodes, err := retrieveNode(schema, root, listElemV.Interface(), remainingPath, np, args) if err != nil { return nil, err } + // If the map element is empty after the // deletion operation is executed, then remove // the map element from the map. if args.delete && rv.MapIndex(k).Elem().IsZero() { rv.SetMapIndex(k, reflect.Value{}) + if args.nodeCache != nil { + args.nodeCache.delete(traversedPath) + } } return nodes, nil } @@ -460,17 +506,27 @@ func retrieveNodeList(schema *yang.Entry, root interface{}, path, traversedPath remainingPath := util.PopGNMIPath(path) if args.delete && len(remainingPath.GetElem()) == 0 { rv.SetMapIndex(k, reflect.Value{}) + if args.nodeCache != nil { + args.nodeCache.delete(traversedPath) + } + return nil, nil } - nodes, err := retrieveNode(schema, listElemV.Interface(), remainingPath, appendElem(traversedPath, &gpb.PathElem{Name: path.GetElem()[0].Name, Key: keys}), args) + + np := appendElem(traversedPath, &gpb.PathElem{Name: path.GetElem()[0].Name, Key: keys}) + nodes, err := retrieveNode(schema, root, listElemV.Interface(), remainingPath, np, args) if err != nil { return nil, err } + // If the map element is empty after the // deletion operation is executed, then remove // the map element from the map. if args.delete && rv.MapIndex(k).Elem().IsZero() { rv.SetMapIndex(k, reflect.Value{}) + if args.nodeCache != nil { + args.nodeCache.delete(np) + } } if nodes != nil { @@ -484,10 +540,16 @@ func retrieveNodeList(schema *yang.Entry, root interface{}, path, traversedPath if err != nil { return nil, err } - nodes, err := retrieveNode(schema, rv.MapIndex(reflect.ValueOf(key)).Interface(), util.PopGNMIPath(path), appendElem(traversedPath, path.GetElem()[0]), args) + + tp := util.PopGNMIPath(path) + np := appendElem(traversedPath, path.GetElem()[0]) + valInterface := rv.MapIndex(reflect.ValueOf(key)).Interface() + + nodes, err := retrieveNode(schema, root, valInterface, tp, np, args) if err != nil { return nil, err } + matches = append(matches, nodes...) } @@ -512,10 +574,24 @@ type GetOrCreateNodeOpt interface { // were created so that a failed call or a call to a shadow path can later undo // this. This applies to SetNode as well. func GetOrCreateNode(schema *yang.Entry, root interface{}, path *gpb.Path, opts ...GetOrCreateNodeOpt) (interface{}, *yang.Entry, error) { - nodes, err := retrieveNode(schema, root, path, nil, retrieveNodeArgs{ + var c *NodeCache = nil + for _, opt := range opts { + switch nodeCacheOpt := opt.(type) { + case *NodeCacheOpt: + nodeCache := nodeCacheOpt.NodeCache + if nodeCache == nil { + continue + } + + c = nodeCache + } + } + + nodes, err := retrieveNode(schema, nil, root, path, nil, retrieveNodeArgs{ modifyRoot: true, initializeLeafs: true, preferShadowPath: hasGetOrCreateNodePreferShadowPath(opts), + nodeCache: c, }) if err != nil { return nil, nil, err @@ -539,13 +615,37 @@ type TreeNode struct { // also be supplied. It takes a set of options which can be used to specify get behaviours, such as // allowing partial match. If there are no matches for the path, an error is returned. func GetNode(schema *yang.Entry, root interface{}, path *gpb.Path, opts ...GetNodeOpt) ([]*TreeNode, error) { - return retrieveNode(schema, root, path, nil, retrieveNodeArgs{ + for _, opt := range opts { + switch nodeCacheOpt := opt.(type) { + case *NodeCacheOpt: + nodeCache := nodeCacheOpt.NodeCache + if nodeCache == nil { + continue + } + + cached, err := nodeCache.get(path) + if err != nil { + return nil, err + } + + if cached != nil { + return cached, nil + } + } + } + + nodes, err := retrieveNode(schema, nil, root, path, nil, retrieveNodeArgs{ // We never want to modify the input root, so we specify modifyRoot. modifyRoot: false, partialKeyMatch: hasPartialKeyMatch(opts), handleWildcards: hasHandleWildcards(opts), preferShadowPath: hasGetNodePreferShadowPath(opts), }) + if err != nil { + return nil, err + } + + return nodes, nil } // GetNodeOpt defines an interface that can be used to supply arguments to functions using GetNode. @@ -607,12 +707,32 @@ func appendElem(p *gpb.Path, e *gpb.PathElem) *gpb.Path { // Note that SetNode does not do a full validation -- e.g., it does not do the string // regex restriction validation done by ytypes.Validate(). func SetNode(schema *yang.Entry, root interface{}, path *gpb.Path, val interface{}, opts ...SetNodeOpt) error { - nodes, err := retrieveNode(schema, root, path, nil, retrieveNodeArgs{ + var c *NodeCache = nil + for _, opt := range opts { + switch nodeCacheOpt := opt.(type) { + case *NodeCacheOpt: + nodeCache := nodeCacheOpt.NodeCache + if nodeCache == nil { + continue + } + + c = nodeCache + + if setComplete, err := nodeCache.set(path, val, opts...); err != nil { + return err + } else if setComplete { + return nil + } + } + } + + nodes, err := retrieveNode(schema, nil, root, path, nil, retrieveNodeArgs{ modifyRoot: hasInitMissingElements(opts), val: val, tolerateJSONInconsistenciesForVal: hasTolerateJSONInconsistencies(opts), preferShadowPath: hasSetNodePreferShadowPath(opts), ignoreExtraFields: hasIgnoreExtraFieldsSetNode(opts), + nodeCache: c, }) if err != nil { @@ -759,6 +879,17 @@ func hasDelNodePreferShadowPath(opts []DelNodeOpt) bool { return false } +// findNodeCache finds the `NodeCacheOpt` and returns the node cache pointer inside of +// the `NodeCacheOpt`. If no `NodeCacheOpt` is found, nil is returned. +func findNodeCache(opts []DelNodeOpt) *NodeCache { + for _, o := range opts { + if nodeCacheOpt, ok := o.(*NodeCacheOpt); ok { + return nodeCacheOpt.NodeCache + } + } + return nil +} + // DeleteNode zeroes the value of the node specified by the supplied path from // the specified root, whose schema must also be supplied. If the node // specified by that path is already its zero value, or an intermediate node @@ -769,9 +900,10 @@ func hasDelNodePreferShadowPath(opts []DelNodeOpt) bool { // non-leaf nodes traversed by the path that is equal to the empty struct or // map will be set to nil, similar to the behaviour of ygot.PruneEmptyBranches. func DeleteNode(schema *yang.Entry, root interface{}, path *gpb.Path, opts ...DelNodeOpt) error { - _, err := retrieveNode(schema, root, path, nil, retrieveNodeArgs{ + _, err := retrieveNode(schema, nil, root, path, nil, retrieveNodeArgs{ delete: true, preferShadowPath: hasDelNodePreferShadowPath(opts), + nodeCache: findNodeCache(opts), }) return err diff --git a/ytypes/node_cache.go b/ytypes/node_cache.go new file mode 100644 index 000000000..1e6853422 --- /dev/null +++ b/ytypes/node_cache.go @@ -0,0 +1,300 @@ +package ytypes + +import ( + "fmt" + "strings" + "sync" + + json "github.com/goccy/go-json" + "github.com/openconfig/ygot/util" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + gpb "github.com/openconfig/gnmi/proto/gnmi" +) + +// NodeCacheOpt is an option that can be usd with calls such as `SetNode` and `GetNode`. +// +// The `node cache` potentially provides fast-paths for config tree traversals and offers +// noticeable performance boosts on a busy server that calls functions such as `SetNode` +// and `GetNode` frequently. +// +// Passing in the pointer of the `node cache` would prevent making the `ytypes` package +// stateful. The applications that use the `ytypes` package maintain the `node cache`. +type NodeCacheOpt struct { + NodeCache *NodeCache +} + +// IsGetNodeOpt implements the GetNodeOpt interface. +func (*NodeCacheOpt) IsGetNodeOpt() {} + +// IsSetNodeOpt implements the SetNodeOpt interface. +func (*NodeCacheOpt) IsSetNodeOpt() {} + +// IsUnmarshalOpt marks IgnoreExtraFields as a valid UnmarshalOpt. +func (*NodeCacheOpt) IsUnmarshalOpt() {} + +// IsGetOrCreateNodeOpt implements the GetOrCreateNodeOpt interface. +func (*NodeCacheOpt) IsGetOrCreateNodeOpt() {} + +// IsDelNodeOpt implements the DelNodeOpt interface. +func (*NodeCacheOpt) IsDelNodeOpt() {} + +// cachedNodeInfo is used to provide shortcuts to making operations +// to the nodes without having to traverse the config tree for every operation. +type cachedNodeInfo struct { + parent interface{} + root interface{} + nodes []*TreeNode +} + +// NodeCache is a thread-safe struct that's used for providing fast-paths for config tree traversals. +type NodeCache struct { + mu *sync.RWMutex + store map[string]*cachedNodeInfo +} + +// NewNodeCache returns the pointer of a new `NodeCache` instance. +func NewNodeCache() *NodeCache { + return &NodeCache{ + mu: &sync.RWMutex{}, + store: map[string]*cachedNodeInfo{}, + } +} + +// Reset resets the cache. The cache will be repopulated after the reset. +func (c *NodeCache) Reset() { + c.mu.Lock() + defer c.mu.Unlock() + + c.store = map[string]*cachedNodeInfo{} +} + +// Size returns the size of the cache (number of entries). +func (c *NodeCache) Size() int { + c.mu.RLock() + defer c.mu.RUnlock() + + return len(c.store) +} + +// setNodeCache uses the cached information to set the node instead of traversalling the config tree. +// This improves runtime performance of the library. +func (c *NodeCache) set(path *gpb.Path, val interface{}, opts ...SetNodeOpt) (setComplete bool, err error) { + switch val.(type) { + case *gpb.TypedValue: + default: + // Only TypedValue is supported by the node cache for now. + return + } + + // Get the unique path representation as the key of the cache store. + pathRep, err := uniquePathRepresentation(path) + if err != nil { + return + } + + c.mu.Lock() + + // If the path was cached, use the shortcut for setting the node value. + // Otherwise, return and continue the rest of `SetNode`. + nodeInfo, ok := c.store[pathRep] + if !ok || len(nodeInfo.nodes) == 0 { + c.mu.Unlock() + + // nil error is returned + return + } + + schema := &nodeInfo.nodes[0].Schema + parent := &nodeInfo.parent + root := &nodeInfo.root + + if schema == nil { + err = status.Error(codes.Internal, "cache: the schema is nil") + c.mu.Unlock() + return + } + + if parent == nil { + err = status.Error(codes.Internal, "cache: the parent is nil") + c.mu.Unlock() + return + } + + // Set value in the config tree. + // Condition: the parent is a container or a list. + if (*schema).Parent.IsContainer() || (*schema).Parent.IsList() { + var encoding Encoding + var options []UnmarshalOpt + if hasSetNodePreferShadowPath(opts) { + options = append(options, &PreferShadowPath{}) + } + + if hasIgnoreExtraFieldsSetNode(opts) { + options = append(options, &IgnoreExtraFields{}) + } + + if hasTolerateJSONInconsistencies(opts) { + encoding = gNMIEncodingWithJSONTolerance + } else { + encoding = GNMIEncoding + } + + // This call updates the node's value in the config tree. + if e := unmarshalGeneric(*schema, *parent, val, encoding, options...); e != nil { + // Note: the unmarshalling could still fail in certain cases. We may not want the node cache + // to block the `Set` so this error is not returned. + c.mu.Unlock() + return + } + } + + c.mu.Unlock() + + // Retrieve the node and update the cache. + var nodes []*TreeNode + nodes, err = retrieveNode(*schema, *parent, *root, nil, path, retrieveNodeArgs{ + modifyRoot: hasInitMissingElements(opts), + val: val, + tolerateJSONInconsistenciesForVal: hasTolerateJSONInconsistencies(opts), + preferShadowPath: hasSetNodePreferShadowPath(opts), + ignoreExtraFields: hasIgnoreExtraFieldsSetNode(opts), + uniquePathRepresentation: &pathRep, + nodeCache: c, + }) + if err != nil { + // Here it's assumed that the set was successful. Therefore, if an error is + // returned from retrieveNode the error should be escalated. + return + } + + if len(nodes) != 0 { + setComplete = true + return + } + + err = status.Errorf( + codes.Unknown, + "failed to retrieve node, parent %T, value %v (%T)", + parent, + val, + val, + ) + + return +} + +// update performs `NodeCache` update based on the input arguments. +func (c *NodeCache) update(nodes []*TreeNode, tp, np *gpb.Path, parent, root interface{}, pathStr *string) { + var pathRep string + if pathStr != nil { + pathRep = *pathStr + } else { + if tp != nil && len(tp.GetElem()) > 0 { + var err error + + pathRep, err = uniquePathRepresentation(appendElem(np, tp.GetElem()[0])) + if err != nil { + return + } + } else { + var err error + + pathRep, err = uniquePathRepresentation(np) + if err != nil { + return + } + } + } + + c.mu.Lock() + defer c.mu.Unlock() + + if _, ok := c.store[pathRep]; !ok { + c.store[pathRep] = &cachedNodeInfo{} + } + + if c.store[pathRep].nodes == nil || len(c.store[pathRep].nodes) == 0 { + c.store[pathRep].nodes = nodes + } else { + // Only update the data. + c.store[pathRep].nodes[0].Data = nodes[0].Data + } + + c.store[pathRep].parent = parent + c.store[pathRep].root = root +} + +// delete removes the path entry from the node cache. +func (c *NodeCache) delete(path *gpb.Path) { + // Delete in the nodeCache. + nodePath, err := uniquePathRepresentation(path) + if err != nil { + return + } + + c.mu.Lock() + defer c.mu.Unlock() + + keysToDelete := []string{} + for k := range c.store { + if strings.Contains(k, nodePath) || strings.HasSuffix(nodePath, k) { + keysToDelete = append(keysToDelete, k) + } + } + + for _, k := range keysToDelete { + delete(c.store, k) + } +} + +// get tries to retrieve the cached `TreeNode` slice. If the cache doesn't contain the +// target `TreeNode` slice or the cached data is `nil`, an error is returned. +func (c *NodeCache) get(path *gpb.Path) ([]*TreeNode, error) { + pathRep, err := uniquePathRepresentation(path) + if err != nil { + return nil, err + } + + c.mu.RLock() + defer c.mu.RUnlock() + + if nodeInfo, ok := c.store[pathRep]; ok { + ret := nodeInfo.nodes + if len(ret) == 0 { + return nil, status.Error(codes.NotFound, "cache: no node was found") + } else if util.IsValueNil(ret[0].Data) { + return nil, status.Error(codes.NotFound, "cache: nil value node was found") + } + + return ret, nil + } + + return nil, nil +} + +// uniquePathRepresentation returns the unique path representation. The current +// implementation uses json marshal for both simplicity and performance. +// +// https://pkg.go.dev/encoding/json#Marshal +// +// Map values encode as JSON objects. The map's key type must either be a string, +// an integer type, or implement encoding.TextMarshaler. The map keys are sorted +// and used as JSON object keys by applying the following rules, subject to the +// UTF-8 coercion described for string values above: +// +// - keys of any string type are used directly +// +// - encoding.TextMarshalers are marshaled +// +// - integer keys are converted to strings +func uniquePathRepresentation(path *gpb.Path) (string, error) { + b, err := json.Marshal(path.GetElem()) + if err != nil { + // This should never happen. + return "", status.Error(codes.Internal, fmt.Sprintf("cache: failed to compute unique path representation for path %v", path)) + } + + return strings.TrimRight(strings.TrimLeft(string(b), "["), "]"), nil +} diff --git a/ytypes/node_cache_test.go b/ytypes/node_cache_test.go new file mode 100644 index 000000000..04467801d --- /dev/null +++ b/ytypes/node_cache_test.go @@ -0,0 +1,41 @@ +package ytypes + +import ( + "testing" + + gpb "github.com/openconfig/gnmi/proto/gnmi" + "github.com/openconfig/goyang/pkg/yang" +) + +// TestNodeCacheSizeAndReset checks the 2 simple methods `Size` and `Reset`. +func TestNodeCacheSizeAndReset(t *testing.T) { + nodeCache := NewNodeCache() + inSchema := &Schema{ + Root: &ListElemStruct1{}, + SchemaTree: map[string]*yang.Entry{ + "ListElemStruct1": simpleSchema(), + }, + } + + err := SetNode(inSchema.RootSchema(), inSchema.Root, mustPath("/outer/inner"), &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(` +{ + "int32-leaf-list": [42] +} + `), + }}, &InitMissingElements{}, &NodeCacheOpt{NodeCache: nodeCache}) + + if err != nil { + t.Fatalf("node cache set error: %s", err) + } + + if nodeCache.Size() != 1 { + t.Fatalf("expected node cache size 1, got %d", nodeCache.Size()) + } + + nodeCache.Reset() + + if nodeCache.Size() != 0 { + t.Fatalf("expected node cache size 0, got %d", nodeCache.Size()) + } +} diff --git a/ytypes/node_test.go b/ytypes/node_test.go index 66e988046..a8a063414 100644 --- a/ytypes/node_test.go +++ b/ytypes/node_test.go @@ -3265,7 +3265,7 @@ func TestRetrieveNodeError(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - _, err := retrieveNode(tt.inSchema, tt.inRoot, tt.inPath, nil, tt.inArgs) + _, err := retrieveNode(tt.inSchema, nil, tt.inRoot, tt.inPath, nil, tt.inArgs) if diff := errdiff.Substring(err, tt.wantErrSubstring); diff != "" { t.Fatalf("did not get expected error, %s", diff) } @@ -3377,3 +3377,900 @@ func TestRetrieveContainerListError(t *testing.T) { }) } } + +func TestSetNodeWithNodeCache(t *testing.T) { + schema := simpleSchema() + schema2 := &yang.Entry{ + Name: "list-elem-struct4", + Kind: yang.DirectoryEntry, + Dir: map[string]*yang.Entry{ + "key1": { + Name: "key1", + Kind: yang.LeafEntry, + Type: &yang.YangType{Kind: yang.Yuint32}, + }, + }, + } + + containerWithStringKeySchema := containerWithStringKey() + + testParent := &ListElemStruct1{} + testParent2 := &ListElemStruct4{} + + containerWithStringKeyTestParent := &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + }, + }, + }, + }, + } + + tests := []struct { + inDesc string + inSchema *yang.Entry + inParentFn func() interface{} + inPath *gpb.Path + inVal interface{} + inValJSON interface{} + inOpts []SetNodeOpt + wantErrSubstring string + wantLeaf interface{} + wantParent interface{} + }{ + { + inDesc: "failed to set annotation in uninitialized node without InitMissingElements in SetNodeOpt", + inSchema: schema, + inParentFn: func() interface{} { return testParent }, + inPath: mustPath("/outer/inner/@annotation"), + inVal: &ExampleAnnotation{ConfigSource: "devicedemo"}, + wantErrSubstring: "could not find children", + wantParent: &ListElemStruct1{}, + }, + { + inDesc: "success setting string field in top node", + inSchema: schema, + inParentFn: func() interface{} { return testParent }, + inPath: mustPath("/key1"), + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{StringVal: "hello"}}, + wantLeaf: ygot.String("hello"), + wantParent: &ListElemStruct1{Key1: ygot.String("hello")}, + }, + { + inDesc: "success setting string field in top node with preferShadowPath=true where shadow-path doesn't exist", + inSchema: schema, + inParentFn: func() interface{} { return testParent }, + inPath: mustPath("/key1"), + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{StringVal: "world"}}, + inOpts: []SetNodeOpt{&PreferShadowPath{}}, + wantLeaf: ygot.String("world"), + wantParent: &ListElemStruct1{Key1: ygot.String("world")}, + }, + { + inDesc: "fail setting value for node with non-leaf schema", + inSchema: schema, + inParentFn: func() interface{} { return testParent }, + inPath: mustPath("/outer"), + inVal: &gpb.TypedValue{}, + wantErrSubstring: `path ` + (&gpb.Path{Elem: []*gpb.PathElem{{Name: "outer"}}}).String() + ` points to a node with non-leaf schema`, + wantParent: &ListElemStruct1{Key1: ygot.String("world")}, + }, + { + inDesc: "success setting annotation in top node", + inSchema: schema, + inParentFn: func() interface{} { return testParent }, + inPath: mustPath("/@annotation"), + inVal: &ExampleAnnotation{ConfigSource: "devicedemo"}, + wantLeaf: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + wantParent: &ListElemStruct1{ + Key1: ygot.String("world"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + }, + }, + { + inDesc: "success setting annotation in inner node", + inSchema: schema, + inParentFn: func() interface{} { return testParent }, + inPath: mustPath("/outer/inner/@annotation"), + inVal: &ExampleAnnotation{ConfigSource: "devicedemo"}, + inOpts: []SetNodeOpt{&InitMissingElements{}}, + wantLeaf: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + wantParent: &ListElemStruct1{ + Key1: ygot.String("world"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + }, + }, + }, + }, + { + inDesc: "success setting int32 field in inner node", + inSchema: schema, + inParentFn: func() interface{} { return testParent }, + inPath: mustPath("/outer/inner/int32-leaf-field"), + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_IntVal{IntVal: 42}}, + inOpts: []SetNodeOpt{&InitMissingElements{}}, + wantLeaf: ygot.Int32(42), + wantParent: &ListElemStruct1{ + Key1: ygot.String("world"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + }, + }, + }, + }, + { + inDesc: "success setting int32 leaf list field", + inSchema: schema, + inParentFn: func() interface{} { return testParent }, + inPath: mustPath("/outer/inner/int32-leaf-list"), + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_LeaflistVal{ + LeaflistVal: &gpb.ScalarArray{ + Element: []*gpb.TypedValue{ + {Value: &gpb.TypedValue_IntVal{IntVal: 42}}, + {Value: &gpb.TypedValue_IntVal{IntVal: 43}}, + }}, + }}, + inOpts: []SetNodeOpt{&InitMissingElements{}}, + wantLeaf: []int32{42, 43}, + wantParent: &ListElemStruct1{ + Key1: ygot.String("world"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Int32LeafListName: []int32{42, 43}, + }, + }, + }, + }, + { + inDesc: "success setting int32 leaf list field for an existing leaf list", + inSchema: schema, + inParentFn: func() interface{} { return testParent }, + inPath: mustPath("/outer/inner/int32-leaf-list"), + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_LeaflistVal{ + LeaflistVal: &gpb.ScalarArray{ + Element: []*gpb.TypedValue{ + {Value: &gpb.TypedValue_IntVal{IntVal: 43}}, + {Value: &gpb.TypedValue_IntVal{IntVal: 44}}, + {Value: &gpb.TypedValue_IntVal{IntVal: 45}}, + }}, + }}, + wantLeaf: []int32{43, 44, 45}, + wantParent: &ListElemStruct1{ + Key1: ygot.String("world"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Int32LeafListName: []int32{43, 44, 45}, + }, + }, + }, + }, + { + inDesc: "failed to set value on invalid node", + inSchema: schema, + inParentFn: func() interface{} { return testParent }, + inPath: mustPath("/invalidkey"), + inVal: ygot.String("hello"), + wantErrSubstring: "no match found in *ytypes.ListElemStruct1", + wantParent: &ListElemStruct1{ + Key1: ygot.String("world"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Int32LeafListName: []int32{43, 44, 45}, + }, + }, + }, + }, + { + inDesc: "set on invalid node OK when IgnoreExtraFields set", + inSchema: schema, + inParentFn: func() interface{} { return testParent }, + inPath: mustPath("/invalidkey"), + inVal: ygot.String("hello"), + inOpts: []SetNodeOpt{&IgnoreExtraFields{}}, + wantLeaf: nil, + wantParent: &ListElemStruct1{ + Key1: ygot.String("world"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Int32LeafListName: []int32{43, 44, 45}, + }, + }, + }, + }, + { + inDesc: "failed to set value with invalid type", + inSchema: schema, + inParentFn: func() interface{} { return testParent }, + inPath: mustPath("/@annotation"), + inVal: struct{ field string }{"hello"}, + wantErrSubstring: "failed to update struct field Annotation", + wantParent: &ListElemStruct1{ + Key1: ygot.String("world"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Int32LeafListName: []int32{43, 44, 45}, + }, + }, + }, + }, + { + inDesc: "failure setting uint field in top node with int value", + inSchema: schema2, + inParentFn: func() interface{} { return testParent2 }, + inPath: mustPath("/key1"), + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_IntVal{IntVal: 42}}, + wantErrSubstring: "failed to unmarshal", + wantParent: &ListElemStruct4{}, + }, + { + inDesc: "failure setting uint field in top node with int value with InitMissingElements", + inSchema: schema2, + inParentFn: func() interface{} { return testParent2 }, + inPath: mustPath("/key1"), + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_IntVal{IntVal: 42}}, + inOpts: []SetNodeOpt{&InitMissingElements{}}, + wantErrSubstring: "failed to unmarshal", + wantParent: &ListElemStruct4{}, + }, + { + inDesc: "success setting uint field in uint node with positive int value with JSON tolerance is set", + inSchema: schema2, + inParentFn: func() interface{} { return testParent2 }, + inPath: mustPath("/key1"), + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_IntVal{IntVal: 42}}, + inOpts: []SetNodeOpt{&TolerateJSONInconsistencies{}}, + wantLeaf: ygot.Uint32(42), + wantParent: &ListElemStruct4{Key1: ygot.Uint32(42)}, + }, + { + inDesc: "success setting uint field in uint node with 0 int value with JSON tolerance is set", + inSchema: schema2, + inParentFn: func() interface{} { return testParent2 }, + inPath: mustPath("/key1"), + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_IntVal{IntVal: 0}}, + inOpts: []SetNodeOpt{&TolerateJSONInconsistencies{}}, + wantLeaf: ygot.Uint32(0), + wantParent: &ListElemStruct4{Key1: ygot.Uint32(0)}, + }, + { + inDesc: "failure setting uint field in uint node with negative int value with JSON tolerance is set", + inSchema: schema2, + inParentFn: func() interface{} { return testParent2 }, + inPath: mustPath("/key1"), + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_IntVal{IntVal: -42}}, + inOpts: []SetNodeOpt{&TolerateJSONInconsistencies{}}, + wantErrSubstring: "failed to unmarshal", + wantParent: &ListElemStruct4{Key1: ygot.Uint32(0)}, + }, + { + inDesc: "success setting annotation in list element", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/@annotation"), + inVal: &ExampleAnnotation{ConfigSource: "devicedemo"}, + wantLeaf: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + }, + }, + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + }, + }, + }, + }, + { + inDesc: "success setting already-set dual non-shadow and shadow leaf", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer/inner/int32-leaf-field"), + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_IntVal{IntVal: 41}}, + wantLeaf: ygot.Int32(41), + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(41), + }, + }, + }, + }, + }, + }, + { + inDesc: "success setting already-set dual non-shadow and shadow leaf", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer/inner/int32-leaf-field"), + inValJSON: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("43")}}, + wantLeaf: ygot.Int32(43), + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(43), + }, + }, + }, + }, + }, + }, + { + inDesc: "fail setting with Json (non-ietf) value", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer/inner/int32-leaf-field"), + inValJSON: &gpb.TypedValue{Value: &gpb.TypedValue_JsonVal{JsonVal: []byte("43")}}, + wantErrSubstring: "json_val format is deprecated, please use json_ietf_val", + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(43), + }, + }, + }, + }, + }, + }, + { + inDesc: "success setting already-set non-shadow leaf", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer/inner/config/int32-leaf-field"), + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_IntVal{IntVal: 41}}, + wantLeaf: ygot.Int32(41), + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(41), + }, + }, + }, + }, + }, + }, + { + inDesc: "success setting already-set non-shadow leaf", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer/inner/config/int32-leaf-field"), + inValJSON: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("42")}}, + wantLeaf: ygot.Int32(42), + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + }, + }, + }, + }, + }, + }, + { + inDesc: "success ignoring already-set shadow leaf", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer/inner/state/int32-leaf-field"), + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_IntVal{IntVal: 43}}, + inValJSON: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("43")}}, + wantLeaf: nil, + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + }, + }, + }, + }, + }, + }, + { + inDesc: "success setting non-shadow leaf", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer/inner/string-leaf-field"), + inOpts: []SetNodeOpt{&InitMissingElements{}}, + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{StringVal: "world"}}, + wantLeaf: ygot.String("world"), + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + StringLeafName: ygot.String("world"), + }, + }, + }, + }, + }, + }, + { + inDesc: "success setting non-shadow leaf", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer/inner/string-leaf-field"), + inOpts: []SetNodeOpt{&InitMissingElements{}}, + inValJSON: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(`"hello"`)}}, + wantLeaf: ygot.String("hello"), + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + StringLeafName: ygot.String("hello"), + }, + }, + }, + }, + }, + }, + { + inDesc: "success ignore setting shadow leaf", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer/inner/state/string-leaf-field"), + inOpts: []SetNodeOpt{&InitMissingElements{}}, + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{StringVal: "world"}}, + inValJSON: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(`"world"`)}}, + wantLeaf: nil, + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + StringLeafName: ygot.String("hello"), + }, + }, + }, + }, + }, + }, + { + inDesc: "success setting already-set shadow leaf when preferShadowPath=true", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer/inner/state/int32-leaf-field"), + inOpts: []SetNodeOpt{&PreferShadowPath{}}, + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_IntVal{IntVal: 41}}, + wantLeaf: ygot.Int32(41), + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(41), + StringLeafName: ygot.String("hello"), + }, + }, + }, + }, + }, + }, + { + inDesc: "success setting already-set shadow leaf when preferShadowPath=true", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer/inner/state/int32-leaf-field"), + inOpts: []SetNodeOpt{&PreferShadowPath{}}, + inValJSON: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("43")}}, + wantLeaf: ygot.Int32(43), + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(43), + StringLeafName: ygot.String("hello"), + }, + }, + }, + }, + }, + }, + { + inDesc: "success ignoring non-shadow leaf when preferShadowPath=true", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer/inner/config/int32-leaf-field"), + inOpts: []SetNodeOpt{&InitMissingElements{}, &PreferShadowPath{}}, + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_IntVal{IntVal: 42}}, + wantLeaf: ygot.Int32(43), + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(43), + StringLeafName: ygot.String("hello"), + }, + }, + }, + }, + }, + }, + { + inDesc: "success ignoring non-shadow leaf when preferShadowPath=true", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer/inner/config/int32-leaf-field"), + inOpts: []SetNodeOpt{&InitMissingElements{}, &PreferShadowPath{}}, + inValJSON: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("42")}}, + wantLeaf: ygot.Int32(43), + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(43), + StringLeafName: ygot.String("hello"), + }, + }, + }, + }, + }, + }, + { + inDesc: "success writing shadow leaf when preferShadowPath=true", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer/inner/string-leaf-field"), + inOpts: []SetNodeOpt{&InitMissingElements{}, &PreferShadowPath{}}, + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{StringVal: "world1"}}, + wantLeaf: ygot.String("world1"), + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(43), + StringLeafName: ygot.String("world1"), + }, + }, + }, + }, + }, + }, + { + inDesc: "success writing shadow leaf when preferShadowPath=true", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer/inner/string-leaf-field"), + inOpts: []SetNodeOpt{&InitMissingElements{}, &PreferShadowPath{}}, + inValJSON: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(`"world"`)}}, + wantLeaf: ygot.String("world"), + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(43), + StringLeafName: ygot.String("world"), + }, + }, + }, + }, + }, + }, + { + inDesc: "fail setting leaf that doesn't exist when preferShadowPath=true", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inOpts: []SetNodeOpt{&InitMissingElements{}, &PreferShadowPath{}}, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer/inner/INVALID-LEAF"), + inVal: &gpb.TypedValue{Value: &gpb.TypedValue_StringVal{StringVal: "hello"}}, + inValJSON: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(`"hello"`)}}, + wantErrSubstring: "no match found in *ytypes.InnerContainerType1", + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(43), + StringLeafName: ygot.String("world"), + }, + }, + }, + }, + }, + }, + { + inDesc: "success setting struct", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer"), + inOpts: []SetNodeOpt{&InitMissingElements{}}, + inValJSON: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(`{ "config": { "inner": { "config": { "int32-leaf-field": 42 } } } }`)}}, + wantLeaf: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + StringLeafName: ygot.String("world"), + }, + }, + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + StringLeafName: ygot.String("world"), + }, + }, + }, + }, + }, + }, + { + inDesc: "failure setting JSON struct with unknown field", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer"), + inOpts: []SetNodeOpt{&InitMissingElements{}}, + inValJSON: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(`{ "config": { "inner": { "config": { "int32-leaf-field": 41, "unknown-field": 41 } } } }`)}}, + wantErrSubstring: "JSON contains unexpected field unknown-field", + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(41), + StringLeafName: ygot.String("world"), + }, + }, + }, + }, + }, + }, + { + inDesc: "OK setting JSON struct with unknown field with IgnoreExtraFields", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]/outer"), + inOpts: []SetNodeOpt{&InitMissingElements{}, &IgnoreExtraFields{}}, + inValJSON: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(`{ "config": { "inner": { "config": { "int32-leaf-field": 42, "unknown-field": 42 } } } }`)}}, + wantLeaf: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + StringLeafName: ygot.String("world"), + }, + }, + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + StringLeafName: ygot.String("world"), + }, + }, + }, + }, + }, + }, + { + inDesc: "success setting list element", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/config/simple-key-list[key1=forty-two]"), + inOpts: []SetNodeOpt{&InitMissingElements{}}, + inValJSON: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(`{ "outer": { "config": { "inner": { "config": { "int32-leaf-field": 41 } } } } }`)}}, + wantLeaf: &ListElemStruct1{ + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(41), + StringLeafName: ygot.String("world"), + }, + }, + }, + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Annotation: []ygot.Annotation{&ExampleAnnotation{ConfigSource: "devicedemo"}}, + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(41), + StringLeafName: ygot.String("world"), + }, + }, + }, + }, + }, + }, + { + inDesc: "success setting struct with a list field ignoring shadow path", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/"), + inOpts: []SetNodeOpt{&InitMissingElements{}}, + inValJSON: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(`{ "config": { "simple-key-list": [ { "key1": "forty-two", "outer": { "config": { "inner": { "config": { "int32-leaf-field": 42 }, "state": { "int32-leaf-field": 43 } } } } } ] } }`)}}, + wantLeaf: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + }, + }, + }, + }, + }, + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(42), + }, + }, + }, + }, + }, + }, + { + inDesc: "success setting struct with a list field unmarshalling shadow path", + inSchema: containerWithStringKeySchema, + inParentFn: func() interface{} { return containerWithStringKeyTestParent }, + inPath: mustPath("/"), + inOpts: []SetNodeOpt{&InitMissingElements{}, &PreferShadowPath{}}, + inValJSON: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(`{ "config": { "simple-key-list": [ { "key1": "forty-two", "outer": { "config": { "inner": { "config": { "int32-leaf-field": 42 }, "state": { "int32-leaf-field": 43 } } } } } ] } }`)}}, + wantLeaf: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(43), + }, + }, + }, + }, + }, + wantParent: &ContainerStruct1{ + StructKeyList: map[string]*ListElemStruct1{ + "forty-two": { + Key1: ygot.String("forty-two"), + Outer: &OuterContainerType1{ + Inner: &InnerContainerType1{ + Int32LeafName: ygot.Int32(43), + }, + }, + }, + }, + }, + }, + } + + // Instantiate node cache. + nodeCache := NewNodeCache() + + for _, tt := range tests { + for typeDesc, inVal := range map[string]interface{}{"scalar": tt.inVal, "JSON": tt.inValJSON} { + if inVal == nil { + continue + } + t.Run(tt.inDesc+" "+typeDesc, func(t *testing.T) { + parent := tt.inParentFn() + err := SetNode(tt.inSchema, parent, tt.inPath, inVal, append(tt.inOpts, &NodeCacheOpt{NodeCache: nodeCache})...) + if diff := errdiff.Substring(err, tt.wantErrSubstring); diff != "" { + t.Fatalf("got %v\nwant %v", err, tt.wantErrSubstring) + } + if diff := cmp.Diff(tt.wantParent, parent); diff != "" { + t.Errorf("(-wantParent, +got):\n%s", diff) + } + if err != nil { + return + } + if tt.wantLeaf == nil && hasIgnoreExtraFieldsSetNode(tt.inOpts) { + return + } + + var getNodeOpts []GetNodeOpt + if hasSetNodePreferShadowPath(tt.inOpts) { + getNodeOpts = append(getNodeOpts, &PreferShadowPath{}) + } + + treeNode, err := GetNode(tt.inSchema, parent, tt.inPath, append(getNodeOpts, &NodeCacheOpt{NodeCache: nodeCache})...) + if err != nil { + t.Fatalf("unexpected error returned from GetNode: %v", err) + } + switch { + case len(treeNode) == 1: + // Expected case for most tests. + break + case len(treeNode) == 0 && tt.wantLeaf == nil: + return + default: + t.Fatalf("did not get exactly one tree node: %v", treeNode) + } + got := treeNode[0].Data + if diff := cmp.Diff(tt.wantLeaf, got); diff != "" { + t.Errorf("(-wantLeaf, +got):\n%s", diff) + } + }) + } + } +} diff --git a/ytypes/schema_tests/set_test.go b/ytypes/schema_tests/set_test.go index d0261aaaa..3d74efb0e 100644 --- a/ytypes/schema_tests/set_test.go +++ b/ytypes/schema_tests/set_test.go @@ -72,7 +72,7 @@ func TestSet(t *testing.T) { }}, }, inValue: &gpb.TypedValue{ - Value: &gpb.TypedValue_StringVal{"XCVR-1-2"}, + Value: &gpb.TypedValue_StringVal{StringVal: "XCVR-1-2"}, }, inOpts: []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}}, wantNode: &ytypes.TreeNode{ @@ -114,7 +114,7 @@ func TestSet(t *testing.T) { }}, }, inValue: &gpb.TypedValue{ - Value: &gpb.TypedValue_StringVal{"XCVR-1-2"}, + Value: &gpb.TypedValue_StringVal{StringVal: "XCVR-1-2"}, }, inOpts: []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}}, wantNode: &ytypes.TreeNode{ @@ -163,7 +163,7 @@ func TestSet(t *testing.T) { }}, }, inValue: &gpb.TypedValue{ - Value: &gpb.TypedValue_UintVal{5}, + Value: &gpb.TypedValue_UintVal{UintVal: 5}, }, inOpts: []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}}, wantNode: &ytypes.TreeNode{ @@ -212,7 +212,7 @@ func TestSet(t *testing.T) { }}, }, inValue: &gpb.TypedValue{ - Value: &gpb.TypedValue_StringVal{"XCVR-1-2"}, + Value: &gpb.TypedValue_StringVal{StringVal: "XCVR-1-2"}, }, inOpts: []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}}, wantNode: &ytypes.TreeNode{ @@ -254,7 +254,7 @@ func TestSet(t *testing.T) { }}, }, inValue: &gpb.TypedValue{ - Value: &gpb.TypedValue_StringVal{"XCVR-1-2"}, + Value: &gpb.TypedValue_StringVal{StringVal: "XCVR-1-2"}, }, inOpts: []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}}, wantNode: &ytypes.TreeNode{ @@ -297,7 +297,7 @@ func TestSet(t *testing.T) { }}, }, inValue: &gpb.TypedValue{ - Value: &gpb.TypedValue_StringVal{"XCVR-1-2"}, + Value: &gpb.TypedValue_StringVal{StringVal: "XCVR-1-2"}, }, inOpts: []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}}, wantErrSubstring: "no match found", @@ -310,7 +310,7 @@ func TestSet(t *testing.T) { }}, }, inValue: &gpb.TypedValue{ - Value: &gpb.TypedValue_IntVal{42}, + Value: &gpb.TypedValue_IntVal{IntVal: 42}, }, wantErrSubstring: "no match found", }, { @@ -326,7 +326,7 @@ func TestSet(t *testing.T) { }}, }, inValue: &gpb.TypedValue{ - Value: &gpb.TypedValue_UintVal{42}, + Value: &gpb.TypedValue_UintVal{UintVal: 42}, }, inOpts: []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}}, wantErrSubstring: "failed to unmarshal", @@ -410,3 +410,291 @@ func TestSet(t *testing.T) { }) } } + +func TestSetWithNodeCache(t *testing.T) { + inSchema := mustSchema(exampleoc.Schema) + + tests := []struct { + desc string + inSchema *ytypes.Schema + inPath *gpb.Path + inValue *gpb.TypedValue + inOpts []ytypes.SetNodeOpt + wantNode *ytypes.TreeNode + wantNodeCacheSize int + }{{ + desc: "set leafref", + inSchema: inSchema, + inPath: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "components", + }, { + Name: "component", + Key: map[string]string{ + "name": "OCH-1-2", + }, + }, { + Name: "optical-channel", + }, { + Name: "config", + }, { + Name: "line-port", + }}, + }, + inValue: &gpb.TypedValue{ + Value: &gpb.TypedValue_StringVal{StringVal: "XCVR-1-2"}, + }, + inOpts: []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}}, + wantNode: &ytypes.TreeNode{ + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "components", + }, { + Name: "component", + Key: map[string]string{ + "name": "OCH-1-2", + }, + }, { + Name: "optical-channel", + }, { + Name: "config", + }, { + Name: "line-port", + }}, + }, + Data: ygot.String("XCVR-1-2"), + }, + wantNodeCacheSize: 1, + }, { + desc: "set(modify) leafref", + inSchema: inSchema, + inPath: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "components", + }, { + Name: "component", + Key: map[string]string{ + "name": "OCH-1-2", + }, + }, { + Name: "optical-channel", + }, { + Name: "config", + }, { + Name: "line-port", + }}, + }, + inValue: &gpb.TypedValue{ + Value: &gpb.TypedValue_StringVal{StringVal: "XCVR-1-1"}, + }, + inOpts: []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}}, + wantNode: &ytypes.TreeNode{ + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "components", + }, { + Name: "component", + Key: map[string]string{ + "name": "OCH-1-2", + }, + }, { + Name: "optical-channel", + }, { + Name: "config", + }, { + Name: "line-port", + }}, + }, + Data: ygot.String("XCVR-1-1"), + }, + wantNodeCacheSize: 1, + }, { + desc: "set list with union type", + inSchema: inSchema, + inPath: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "network-instances", + }, { + Name: "network-instance", + Key: map[string]string{ + "name": "OCH-1-2", + }, + }, { + Name: "afts", + }, { + Name: "mpls", + }, { + Name: "label-entry", + Key: map[string]string{ + "label": "483414", + }, + }, { + Name: "state", + }, { + Name: "next-hop-group", + }}, + }, + inValue: &gpb.TypedValue{ + Value: &gpb.TypedValue_UintVal{UintVal: 5}, + }, + inOpts: []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}}, + wantNode: &ytypes.TreeNode{ + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "network-instances", + }, { + Name: "network-instance", + Key: map[string]string{ + "name": "OCH-1-2", + }, + }, { + Name: "afts", + }, { + Name: "mpls", + }, { + Name: "label-entry", + Key: map[string]string{ + "label": "483414", + }, + }, { + Name: "state", + }, { + Name: "next-hop-group", + }}, + }, + Data: ygot.Uint64(5), + }, + wantNodeCacheSize: 2, + }, { + desc: "set(modify) list with union type", + inSchema: inSchema, + inPath: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "network-instances", + }, { + Name: "network-instance", + Key: map[string]string{ + "name": "OCH-1-2", + }, + }, { + Name: "afts", + }, { + Name: "mpls", + }, { + Name: "label-entry", + Key: map[string]string{ + "label": "483414", + }, + }, { + Name: "state", + }, { + Name: "next-hop-group", + }}, + }, + inValue: &gpb.TypedValue{ + Value: &gpb.TypedValue_UintVal{UintVal: 6}, + }, + inOpts: []ytypes.SetNodeOpt{}, + wantNode: &ytypes.TreeNode{ + Path: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "network-instances", + }, { + Name: "network-instance", + Key: map[string]string{ + "name": "OCH-1-2", + }, + }, { + Name: "afts", + }, { + Name: "mpls", + }, { + Name: "label-entry", + Key: map[string]string{ + "label": "483414", + }, + }, { + Name: "state", + }, { + Name: "next-hop-group", + }}, + }, + Data: ygot.Uint64(6), + }, + wantNodeCacheSize: 2, + }, { + desc: "bad path", + inSchema: inSchema, + inPath: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "doesnt-exist", + }}, + }, + inValue: &gpb.TypedValue{ + Value: &gpb.TypedValue_IntVal{IntVal: 42}, + }, + wantNodeCacheSize: 2, + }, { + desc: "wrong type", + inSchema: inSchema, + inPath: &gpb.Path{ + Elem: []*gpb.PathElem{{ + Name: "components", + }, { + Name: "component", + Key: map[string]string{ + "name": "OCH-1-2", + }, + }, { + Name: "optical-channel", + }, { + Name: "config", + }, { + Name: "line-port", + }}, + }, + inValue: &gpb.TypedValue{ + Value: &gpb.TypedValue_IntVal{IntVal: 42}, + }, + inOpts: []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}}, + wantNodeCacheSize: 2, + }} + + // Instantiate node cache. + nodeCache := ytypes.NewNodeCache() + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + err := ytypes.SetNode(tt.inSchema.RootSchema(), tt.inSchema.Root, tt.inPath, tt.inValue, append(tt.inOpts, &ytypes.NodeCacheOpt{NodeCache: nodeCache})...) + if tt.wantNode != nil && err != nil { + t.Fatalf("failed to set node: %s", err) + } + + if tt.wantNodeCacheSize != nodeCache.Size() { + t.Fatalf("did not get expected node cache size, got: %d, want: %d\n", nodeCache.Size(), tt.wantNodeCacheSize) + } + + if tt.wantNode == nil { + return + } + + got, err := ytypes.GetNode(tt.inSchema.RootSchema(), tt.inSchema.Root, tt.wantNode.Path, &ytypes.NodeCacheOpt{NodeCache: nodeCache}) + if err != nil { + t.Fatalf("cannot perform get, %v", err) + } + if len(got) != 1 { + t.Fatalf("unexpected number of nodes, want: 1, got: %d", len(got)) + } + + opts := []cmp.Option{ + cmpopts.IgnoreFields(ytypes.TreeNode{}, "Schema"), + cmp.Comparer(proto.Equal), + } + + if !cmp.Equal(got[0], tt.wantNode, opts...) { + diff := cmp.Diff(tt.wantNode, got[0], opts...) + t.Fatalf("did not get expected node, got: %v, want: %v, diff (-want, +got):\n%s", got[0], tt.wantNode, diff) + } + }) + } +}