Repository: openconfig/goyang
Pull request #219 (2022-01-04 17:23:43)

Add unit tests for FindGrouping

coveralls: 
[![Coverage Status](https://coveralls.io/builds/45368191/badge)](https://coveralls.io/builds/45368191)

Coverage increased (+1.5%) to 83.52% when pulling **642fbd69e56265fc6e03a2aeac944b7b64421126 on findgrouping-unittest** into **eff35533116315549c52ac5bc8d7bbdb6d652a7f on master**.


Patch:
diff --git a/pkg/yang/find.go b/pkg/yang/find.go
index 602c705..9f0575d 100644
--- a/pkg/yang/find.go
+++ b/pkg/yang/find.go
@@ -38,10 +38,15 @@ func trimLocalPrefix(n Node, name string) string {
 	return strings.TrimPrefix(name, pfx)
 }
 
-// FindGrouping finds the grouping named name in one of the parent node's
-// grouping fields, seen provides a list of the modules previously seen
-// by FindGrouping during traversal.  If no parent has the named grouping,
-// nil is returned. Imported and included modules are also checked.
+// FindGrouping finds the grouping named name according to YANG namespace rules
+// using the input node as the initial context node. The seen parameter
+// provides a list of the modules previously seen by FindGrouping during
+// traversal. If the named grouping cannot be found, nil is returned.
+//
+// FindGrouping works by recursively looking through the context node's parent
+// nodes for grouping fields, or in included or imported submodules/modules for
+// externally-defined groupings. Note that any prefix in the name must match
+// the module prefix of its import statement in the context node's module.
 func FindGrouping(n Node, name string, seen map[string]bool) *Grouping {
 	name = trimLocalPrefix(n, name)
 	for n != nil {
diff --git a/pkg/yang/yangtype.go b/pkg/yang/yangtype.go
index fedb934..1eedf03 100644
--- a/pkg/yang/yangtype.go
+++ b/pkg/yang/yangtype.go
@@ -255,6 +255,10 @@ type YangType struct {
 // Equal returns true if y and t describe the same type.
 func (y *YangType) Equal(t *YangType) bool {
 	switch {
+	case y == t:
+		return true
+	case y == nil || t == nil:
+		return false
 	case
 		// Don't check the Name, it contains no information
 		y.Kind != t.Kind,


Test patch:
diff --git a/pkg/yang/find_test.go b/pkg/yang/find_test.go
new file mode 100644
index 0000000..da39d50
--- /dev/null
+++ b/pkg/yang/find_test.go
@@ -0,0 +1,358 @@
+package yang
+
+import (
+	"testing"
+)
+
+func TestFindGrouping(t *testing.T) {
+	tests := []struct {
+		desc              string
+		inMods            map[string]string
+		inNode            func(*Modules) (Node, error)
+		inName            string
+		wantGroupNodePath string
+		// wantCannotFound indicates that the grouping cannot be found.
+		wantCannotFound bool
+	}{{
+		desc: "grouping within module",
+		inMods: map[string]string{
+			"dev": `
+				module dev {
+					prefix d;
+					namespace "urn:d";
+
+					revision 01-01-01 { description "the start of time"; }
+
+					grouping g { leaf a { type string; } }
+
+					container c { leaf b { type string; } }
+				}`,
+		},
+		inNode: func(ms *Modules) (Node, error) {
+			return FindNode(ms.Modules["dev"], "c")
+		},
+		inName:            "g",
+		wantGroupNodePath: "/dev/g",
+	}, {
+		desc: "nested grouping within module",
+		inMods: map[string]string{
+			"dev": `
+				module dev {
+					prefix d;
+					namespace "urn:d";
+
+					revision 01-01-01 { description "the start of time"; }
+
+					grouping g { grouping gg { leaf a { type string; } } uses gg; }
+
+					container c { leaf b { type string; } }
+				}`,
+		},
+		inNode: func(ms *Modules) (Node, error) {
+			return FindNode(ms.Modules["dev"], "g")
+		},
+		inName:            "gg",
+		wantGroupNodePath: "/dev/g/gg",
+	}, {
+		desc: "grouping that uses another grouping both within the same module",
+		inMods: map[string]string{
+			"dev": `
+				module dev {
+					prefix d;
+					namespace "urn:d";
+
+					revision 01-01-01 { description "the start of time"; }
+
+					grouping gg { leaf a { type string; } }
+
+					grouping g { uses gg; }
+
+					container c { leaf b { type string; } }
+				}`,
+		},
+		inNode: func(ms *Modules) (Node, error) {
+			return FindNode(ms.Modules["dev"], "g")
+		},
+		inName:            "gg",
+		wantGroupNodePath: "/dev/gg",
+	}, {
+		desc: "grouping in included submodule",
+		inMods: map[string]string{
+			"dev": `
+				module dev {
+					prefix d;
+					namespace "urn:d";
+					include sys;
+
+					container c { leaf b { type string; } }
+
+					revision 01-01-01 { description "the start of time"; }
+				}`,
+			"sys": `
+				submodule sys {
+					belongs-to dev {
+						prefix "d";
+					}
+
+					revision 01-01-01 { description "the start of time"; }
+
+					grouping g { leaf a { type string; } }
+				}`,
+		},
+		inNode: func(ms *Modules) (Node, error) {
+			return FindNode(ms.Modules["dev"], "c")
+		},
+		inName:            "g",
+		wantGroupNodePath: "/sys/g",
+	}, {
+		desc: "grouping in indirectly-included submodule",
+		inMods: map[string]string{
+			"dev": `
+				module dev {
+					prefix d;
+					namespace "urn:d";
+					include sys;
+
+					revision 01-01-01 { description "the start of time"; }
+
+					container c { leaf b { type string; } }
+				}`,
+			"sys": `
+				submodule sys {
+					belongs-to dev {
+						prefix "d";
+					}
+					include sysdb;
+
+					revision 01-01-01 { description "the start of time"; }
+				}`,
+			"sysdb": `
+				submodule sysdb {
+					belongs-to dev {
+						prefix "d";
+					}
+
+					revision 01-01-01 { description "the start of time"; }
+
+					grouping g { leaf a { type string; } }
+				}`,
+		},
+		inNode: func(ms *Modules) (Node, error) {
+			return FindNode(ms.Modules["dev"], "c")
+		},
+		inName:            "g",
+		wantGroupNodePath: "/sysdb/g",
+	}, {
+		desc: "grouping in indirectly-included submodule with node in submodule",
+		inMods: map[string]string{
+			"dev": `
+				module dev {
+					prefix d;
+					namespace "urn:d";
+					include sys;
+
+					revision 01-01-01 { description "the start of time"; }
+				}`,
+			"sys": `
+				submodule sys {
+					belongs-to dev {
+						prefix "d";
+					}
+					include sysdb;
+
+					revision 01-01-01 { description "the start of time"; }
+
+					container c { leaf b { type string; } }
+				}`,
+			"sysdb": `
+				submodule sysdb {
+					belongs-to dev {
+						prefix "d";
+					}
+
+					revision 01-01-01 { description "the start of time"; }
+
+					grouping g { leaf a { type string; } }
+				}`,
+		},
+		inNode: func(ms *Modules) (Node, error) {
+			return FindNode(ms.SubModules["sys"], "c")
+		},
+		inName:            "g",
+		wantGroupNodePath: "/sysdb/g",
+	}, {
+		desc: "grouping in submodule",
+		inMods: map[string]string{
+			"dev": `
+				module dev {
+					prefix d;
+					namespace "urn:d";
+					import sysdb { prefix "s"; }
+
+					revision 01-01-01 { description "the start of time"; }
+
+					container c { leaf b { type string; } uses s:g; }
+				}`,
+			"sysdb": `
+				module sysdb {
+					prefix sd;
+					namespace "urn:sd";
+
+					revision 01-01-01 { description "the start of time"; }
+
+					grouping g { leaf a { type string; } }
+				}`,
+		},
+		inNode: func(ms *Modules) (Node, error) {
+			return FindNode(ms.Modules["dev"], "c")
+		},
+		inName:            "s:g",
+		wantGroupNodePath: "/sysdb/g",
+	}, {
+		desc: "grouping that uses another grouping both in different modules",
+		inMods: map[string]string{
+			"dev": `
+				module dev {
+					prefix d;
+					namespace "urn:d";
+					import dev2 { prefix "de2"; }
+
+					revision 01-01-01 { description "the start of time"; }
+
+					container c { leaf l { type string; } uses de2:g; }
+				}`,
+			"dev2": `
+				module dev2 {
+					prefix d2;
+					namespace "urn:d2";
+					import dev3 { prefix "de3"; }
+
+					revision 01-01-01 { description "the start of time"; }
+
+					grouping g { leaf a { type string; } uses de3:gg; }
+				}`,
+			"dev3": `
+				module dev3 {
+					prefix d3;
+					namespace "urn:d3";
+
+					revision 01-01-01 { description "the start of time"; }
+
+					grouping gg { leaf b { type string; } }
+				}`,
+		},
+		inNode: func(ms *Modules) (Node, error) {
+			return FindNode(ms.Modules["dev2"], "g")
+		},
+		inName:            "de3:gg",
+		wantGroupNodePath: "/dev3/gg",
+	}, {
+		desc: "grouping that uses another grouping both in different modules but prefix is wrong",
+		inMods: map[string]string{
+			"dev": `
+				module dev {
+					prefix d;
+					namespace "urn:d";
+					import dev2 { prefix "de2"; }
+
+					revision 01-01-01 { description "the start of time"; }
+
+					container c { leaf l { type string; } uses de2:g; }
+				}`,
+			"dev2": `
+				module dev2 {
+					prefix d2;
+					namespace "urn:d2";
+					import dev3 { prefix "de3"; }
+
+					revision 01-01-01 { description "the start of time"; }
+
+					grouping g { leaf a { type string; } uses de3:gg; }
+				}`,
+			"dev3": `
+				module dev3 {
+					prefix dev3;
+					namespace "urn:dev3";
+
+					revision 01-01-01 { description "the start of time"; }
+
+					grouping gg { leaf b { type string; } }
+				}`,
+		},
+		inNode: func(ms *Modules) (Node, error) {
+			return FindNode(ms.Modules["dev2"], "g")
+		},
+		inName:          "d3:gg",
+		wantCannotFound: true,
+	}, {
+		desc: "grouping that uses another grouping both in different modules but uses wrong context node",
+		inMods: map[string]string{
+			"dev": `
+				module dev {
+					prefix d;
+					namespace "urn:d";
+					import dev2 { prefix "de2"; }
+
+					revision 01-01-01 { description "the start of time"; }
+
+					container c { leaf l { type string; } uses de2:g; }
+				}`,
+			"dev2": `
+				module dev2 {
+					prefix d2;
+					namespace "urn:d2";
+					import dev3 { prefix "dev3"; }
+
+					revision 01-01-01 { description "the start of time"; }
+
+					grouping g { leaf a { type string; } uses dev3:gg; }
+				}`,
+			"dev3": `
+				module dev3 {
+					prefix dev3;
+					namespace "urn:dev3";
+
+					revision 01-01-01 { description "the start of time"; }
+
+					grouping gg { leaf b { type string; } }
+				}`,
+		},
+		inNode: func(ms *Modules) (Node, error) {
+			return FindNode(ms.Modules["dev"], "c")
+		},
+		inName:          "dev3:gg",
+		wantCannotFound: true,
+	}}
+
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+			ms := NewModules()
+
+			for n, m := range tt.inMods {
+				if err := ms.Parse(m, n); err != nil {
+					t.Fatalf("cannot parse module %s, err: %v", n, err)
+				}
+			}
+
+			if errs := ms.Process(); errs != nil {
+				t.Fatalf("cannot process modules: %v", errs)
+			}
+
+			seen := map[string]bool{}
+			node, err := tt.inNode(ms)
+			if err != nil {
+				t.Fatalf("cannot find input node: %v", err)
+			}
+			g := FindGrouping(node, tt.inName, seen)
+			if got, want := g == nil, tt.wantCannotFound; got != want {
+				t.Fatalf("got grouping: %v, wantCannotFound: %v", got, want)
+			}
+			if tt.wantCannotFound {
+				return
+			}
+			if got, want := NodePath(g), tt.wantGroupNodePath; got != want {
+				t.Errorf("found grouping path doesn't match expected, got: %s, want: %s", got, want)
+			}
+		})
+	}
+}
diff --git a/pkg/yang/yangtype_test.go b/pkg/yang/yangtype_test.go
new file mode 100644
index 0000000..6d93530
--- /dev/null
+++ b/pkg/yang/yangtype_test.go
@@ -0,0 +1,92 @@
+// Copyright 2021 Google Inc.
+//
+// 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 yang
+
+import (
+	"testing"
+)
+
+func TestYangTypeEqual(t *testing.T) {
+
+	tests := []struct {
+		name      string
+		inLeft    *YangType
+		inRight   *YangType
+		wantEqual bool
+	}{{
+		name:      "both-nil",
+		inLeft:    nil,
+		inRight:   nil,
+		wantEqual: true,
+	}, {
+		name: "one-nil",
+		inLeft: &YangType{
+			Kind:           Ydecimal64,
+			FractionDigits: 5,
+		},
+		inRight:   nil,
+		wantEqual: false,
+	}, {
+		name: "name-unequal",
+		inLeft: &YangType{
+			Name:           "foo",
+			Kind:           Ydecimal64,
+			FractionDigits: 5,
+		},
+		inRight: &YangType{
+			Name:           "bar",
+			Kind:           Ydecimal64,
+			FractionDigits: 5,
+		},
+		wantEqual: true,
+	}, {
+		name: "fraction-digits-unequal",
+		inLeft: &YangType{
+			Name:           "foo",
+			Kind:           Ydecimal64,
+			FractionDigits: 5,
+		},
+		inRight: &YangType{
+			Name:           "foo",
+			Kind:           Ydecimal64,
+			FractionDigits: 4,
+		},
+		wantEqual: false,
+	}, {
+		name: "types-unequal",
+		inLeft: &YangType{
+			Name:           "foo",
+			Kind:           Ydecimal64,
+			FractionDigits: 5,
+		},
+		inRight: &YangType{
+			Name: "foo",
+			Kind: Yint64,
+		},
+		wantEqual: false,
+	}}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if gotEqual := tt.inLeft.Equal(tt.inRight); gotEqual != tt.wantEqual {
+				t.Errorf("gotEqual: %v, wantEqual: %v", gotEqual, tt.wantEqual)
+			}
+			// Must be symmetric
+			if reverseEqual := tt.inRight.Equal(tt.inLeft); reverseEqual != tt.wantEqual {
+				t.Errorf("got reverseEqual: %v, wantEqual: %v", reverseEqual, tt.wantEqual)
+			}
+		})
+	}
+}