Skip to content

Commit 1f980b3

Browse files
authored
Add ComparePath func (#787)
* Add ComparePath func * feedback * feedback
1 parent 4123dde commit 1f980b3

File tree

2 files changed

+241
-12
lines changed

2 files changed

+241
-12
lines changed

util/gnmi.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,101 @@ func JoinPaths(prefix, suffix *gpb.Path) (*gpb.Path, error) {
255255
}
256256
return joined, nil
257257
}
258+
259+
// CompareRelation describes of the relation between to gNMI paths.
260+
type CompareRelation int
261+
262+
const (
263+
// Equal means the paths are the same.
264+
Equal CompareRelation = iota
265+
// Disjoint means the paths do not overlap at all.
266+
Disjoint
267+
// Subset means a path is strictly smaller than the other.
268+
Subset
269+
// Superset means a path is strictly larger than the other.
270+
Superset
271+
// PartialIntersect means a path partial overlaps the other.
272+
PartialIntersect
273+
)
274+
275+
// ComparePaths returns the set relation between two paths.
276+
// It returns an error if the paths are invalid or not one of the relations.
277+
func ComparePaths(a, b *gpb.Path) CompareRelation {
278+
if a.Origin != b.Origin {
279+
return Disjoint
280+
}
281+
282+
shortestLen := len(a.Elem)
283+
284+
// If path a is longer than b, then we start by assuming a subset relationship, and vice versa.
285+
// Otherwise assume paths are equal.
286+
relation := Equal
287+
if len(a.Elem) > len(b.Elem) {
288+
relation = Subset
289+
shortestLen = len(b.Elem)
290+
} else if len(a.Elem) < len(b.Elem) {
291+
relation = Superset
292+
}
293+
294+
for i := 0; i < shortestLen; i++ {
295+
elemRelation := comparePathElem(a.Elem[i], b.Elem[i])
296+
switch elemRelation {
297+
case PartialIntersect, Disjoint:
298+
return elemRelation
299+
case Superset, Subset:
300+
if relation == Equal {
301+
relation = elemRelation
302+
} else if elemRelation != relation {
303+
return PartialIntersect
304+
}
305+
}
306+
}
307+
308+
return relation
309+
}
310+
311+
// comparePathElem compare two path elements a, b and returns:
312+
// subset: if every definite key in a is wildcard in b.
313+
// superset: if every wildcard key in b is non-wildcard in b.
314+
// error: not two keys are both subset and superset.
315+
func comparePathElem(a, b *gpb.PathElem) CompareRelation {
316+
if a.Name != b.Name {
317+
return Disjoint
318+
}
319+
320+
// Start by assuming a perfect match. Then relax this property as we scan through the key values.
321+
setRelation := Equal
322+
for k, aVal := range a.Key {
323+
bVal, ok := b.Key[k]
324+
switch {
325+
case aVal == bVal, aVal == "*" && !ok: // Values are equal (possibly be implicit wildcards).
326+
continue
327+
case aVal == "*": // Values not equal, a value is superset of b value.
328+
if setRelation == Subset {
329+
return PartialIntersect
330+
}
331+
setRelation = Superset
332+
case bVal == "*", !ok: // Values not equal, a value is subset of b value.
333+
if setRelation == Superset {
334+
return PartialIntersect
335+
}
336+
setRelation = Subset
337+
default: // Values not equal, but of the same size.
338+
return Disjoint
339+
}
340+
}
341+
for k, bVal := range b.Key {
342+
_, ok := a.Key[k]
343+
switch {
344+
case ok, bVal == "*": // Key has already been visited, or values are equal.
345+
continue
346+
case bVal != "*": // If a contains an implicit wildcard and b isn't.
347+
if setRelation == Subset {
348+
return PartialIntersect
349+
}
350+
setRelation = Superset
351+
}
352+
}
353+
354+
return setRelation
355+
}

util/gnmi_test.go

Lines changed: 143 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package util
15+
package util_test
1616

1717
import (
1818
"strings"
@@ -21,6 +21,8 @@ import (
2121
"github.com/google/go-cmp/cmp"
2222
"github.com/openconfig/gnmi/errdiff"
2323
"github.com/openconfig/goyang/pkg/yang"
24+
"github.com/openconfig/ygot/util"
25+
"github.com/openconfig/ygot/ygot"
2426
"google.golang.org/protobuf/encoding/prototext"
2527
"google.golang.org/protobuf/proto"
2628
"google.golang.org/protobuf/testing/protocmp"
@@ -123,7 +125,7 @@ func TestPathMatchesPrefix(t *testing.T) {
123125

124126
for _, tt := range tests {
125127
t.Run(tt.desc, func(t *testing.T) {
126-
if got, want := PathMatchesPrefix(pathNoKeysToGNMIPath(tt.path), strings.Split(tt.prefix, "/")), tt.want; got != want {
128+
if got, want := util.PathMatchesPrefix(pathNoKeysToGNMIPath(tt.path), strings.Split(tt.prefix, "/")), tt.want; got != want {
127129
t.Errorf("%s: got: %v want: %v", tt.desc, got, want)
128130
}
129131
})
@@ -191,7 +193,7 @@ func TestTrimGNMIPathPrefix(t *testing.T) {
191193
t.Run(tt.desc, func(t *testing.T) {
192194
path := pathNoKeysToGNMIPath(tt.path)
193195
prefix := strings.Split(tt.prefix, "/")
194-
got := gnmiPathNoKeysToPath(TrimGNMIPathPrefix(path, prefix))
196+
got := gnmiPathNoKeysToPath(util.TrimGNMIPathPrefix(path, prefix))
195197
if got != tt.want {
196198
t.Errorf("%s: got: %s want: %s", tt.desc, got, tt.want)
197199
}
@@ -229,7 +231,7 @@ func TestPopGNMIPath(t *testing.T) {
229231

230232
for _, tt := range tests {
231233
t.Run(tt.desc, func(t *testing.T) {
232-
if got, want := gnmiPathNoKeysToPath(PopGNMIPath(pathNoKeysToGNMIPath(tt.path))), tt.want; got != want {
234+
if got, want := gnmiPathNoKeysToPath(util.PopGNMIPath(pathNoKeysToGNMIPath(tt.path))), tt.want; got != want {
233235
t.Errorf("%s: got: %s want: %s", tt.desc, got, want)
234236
}
235237
})
@@ -325,7 +327,7 @@ func TestPathElemsEqual(t *testing.T) {
325327

326328
for _, tt := range tests {
327329
t.Run(tt.desc, func(t *testing.T) {
328-
if got := PathElemsEqual(tt.lhs, tt.rhs); got != tt.want {
330+
if got := util.PathElemsEqual(tt.lhs, tt.rhs); got != tt.want {
329331
t.Fatalf("did not get expected result, got: %v, want: %v", got, tt.want)
330332
}
331333
})
@@ -424,7 +426,7 @@ func TestPathElemSlicesEqual(t *testing.T) {
424426

425427
for _, tt := range tests {
426428
t.Run(tt.desc, func(t *testing.T) {
427-
if got := PathElemSlicesEqual(tt.inElemsA, tt.inElemsB); got != tt.want {
429+
if got := util.PathElemSlicesEqual(tt.inElemsA, tt.inElemsB); got != tt.want {
428430
t.Fatalf("did not get expected result, got: %v, want: %v", got, tt.want)
429431
}
430432
})
@@ -519,7 +521,7 @@ func TestPathMatchesPathElemPrefix(t *testing.T) {
519521

520522
for _, tt := range tests {
521523
t.Run(tt.desc, func(t *testing.T) {
522-
if got := PathMatchesPathElemPrefix(tt.inPath, tt.inPrefix); got != tt.want {
524+
if got := util.PathMatchesPathElemPrefix(tt.inPath, tt.inPrefix); got != tt.want {
523525
t.Fatalf("did not get expected result, got: %v, want: %v", got, tt.want)
524526
}
525527
})
@@ -746,7 +748,7 @@ func TestPathMatchesQuery(t *testing.T) {
746748
}}
747749
for _, tt := range tests {
748750
t.Run(tt.desc, func(t *testing.T) {
749-
if got := PathMatchesQuery(tt.inPath, tt.inQuery); got != tt.want {
751+
if got := util.PathMatchesQuery(tt.inPath, tt.inQuery); got != tt.want {
750752
t.Fatalf("did not get expected result, got: %v, want: %v", got, tt.want)
751753
}
752754
})
@@ -836,7 +838,7 @@ func TestTrimGNMIPathElemPrefix(t *testing.T) {
836838

837839
for _, tt := range tests {
838840
t.Run(tt.desc, func(t *testing.T) {
839-
if got := TrimGNMIPathElemPrefix(tt.inPath, tt.inPrefix); !proto.Equal(got, tt.want) {
841+
if got := util.TrimGNMIPathElemPrefix(tt.inPath, tt.inPrefix); !proto.Equal(got, tt.want) {
840842
t.Fatalf("did not get expected path, got: %s, want: %s", prototext.Format(got), prototext.Format(tt.want))
841843
}
842844
})
@@ -872,7 +874,7 @@ func TestFindPathElemPrefix(t *testing.T) {
872874
}}
873875

874876
for _, tt := range tests {
875-
if got := FindPathElemPrefix(tt.inPaths); !proto.Equal(got, tt.want) {
877+
if got := util.FindPathElemPrefix(tt.inPaths); !proto.Equal(got, tt.want) {
876878
t.Errorf("%s: FindPathElemPrefix(%v): did not get expected prefix, got: %s, want: %s", tt.name, tt.inPaths, prototext.Format(got), prototext.Format(tt.want))
877879
}
878880
}
@@ -979,7 +981,7 @@ func TestFindModelData(t *testing.T) {
979981
}}
980982

981983
for _, tt := range tests {
982-
got, err := FindModelData(tt.in)
984+
got, err := util.FindModelData(tt.in)
983985

984986
if diff := errdiff.Substring(err, tt.wantErrSubstring); diff != "" {
985987
t.Errorf("%s: FindModelData(%v): did not get expected error, %s", tt.name, tt.in, diff)
@@ -1039,7 +1041,7 @@ func TestJoinPaths(t *testing.T) {
10391041

10401042
for _, tt := range tests {
10411043
t.Run(tt.desc, func(t *testing.T) {
1042-
got, err := JoinPaths(tt.prefix, tt.suffix)
1044+
got, err := util.JoinPaths(tt.prefix, tt.suffix)
10431045
if diff := errdiff.Substring(err, tt.wantErrSubstring); diff != "" {
10441046
t.Errorf("JoinPaths(%v, %v) got unexpected error diff: %s", tt.prefix, tt.suffix, diff)
10451047
}
@@ -1052,3 +1054,132 @@ func TestJoinPaths(t *testing.T) {
10521054
})
10531055
}
10541056
}
1057+
1058+
func mustStringToPath(t testing.TB, path string) *gpb.Path {
1059+
p, err := ygot.StringToStructuredPath(path)
1060+
if err != nil {
1061+
t.Fatal(err)
1062+
}
1063+
return p
1064+
}
1065+
1066+
func TestComparePaths(t *testing.T) {
1067+
tests := []struct {
1068+
desc string
1069+
a, b *gpb.Path
1070+
want util.CompareRelation
1071+
}{{
1072+
desc: "different origins",
1073+
a: &gpb.Path{Origin: "foo"},
1074+
b: &gpb.Path{Origin: "bar"},
1075+
want: util.Disjoint,
1076+
}, {
1077+
desc: "disjoint paths",
1078+
a: mustStringToPath(t, "/foo"),
1079+
b: mustStringToPath(t, "/bar"),
1080+
want: util.Disjoint,
1081+
}, {
1082+
desc: "disjoint paths by list keys",
1083+
a: mustStringToPath(t, "/foo[a=1][b=2]"),
1084+
b: mustStringToPath(t, "/foo[a=1][b=3]"),
1085+
want: util.Disjoint,
1086+
}, {
1087+
desc: "equal paths",
1088+
a: mustStringToPath(t, "/foo"),
1089+
b: mustStringToPath(t, "/foo"),
1090+
want: util.Equal,
1091+
}, {
1092+
desc: "equal paths with list keys",
1093+
a: mustStringToPath(t, "/foo[a=1]"),
1094+
b: mustStringToPath(t, "/foo[a=1]"),
1095+
want: util.Equal,
1096+
}, {
1097+
desc: "equal paths with implicit wildcards",
1098+
a: mustStringToPath(t, "/foo[a=*]"),
1099+
b: mustStringToPath(t, "/foo"),
1100+
want: util.Equal,
1101+
}, {
1102+
desc: "equal paths with implicit wildcards",
1103+
a: mustStringToPath(t, "/foo"),
1104+
b: mustStringToPath(t, "/foo[a=*]"),
1105+
want: util.Equal,
1106+
}, {
1107+
desc: "superset by length",
1108+
a: mustStringToPath(t, "/foo"),
1109+
b: mustStringToPath(t, "/foo/bar"),
1110+
want: util.Superset,
1111+
}, {
1112+
desc: "superset by length and keys",
1113+
a: mustStringToPath(t, "/foo[a=*]"),
1114+
b: mustStringToPath(t, "/foo[b=1]/bar"),
1115+
want: util.Superset,
1116+
}, {
1117+
desc: "superset by list keys",
1118+
a: mustStringToPath(t, "/foo[a=*]"),
1119+
b: mustStringToPath(t, "/foo[a=1]"),
1120+
want: util.Superset,
1121+
}, {
1122+
desc: "superset by list keys implicit wildcard",
1123+
a: mustStringToPath(t, "/foo"),
1124+
b: mustStringToPath(t, "/foo[a=1]"),
1125+
want: util.Superset,
1126+
}, {
1127+
desc: "subset by length",
1128+
a: mustStringToPath(t, "/foo/bar"),
1129+
b: mustStringToPath(t, "/foo"),
1130+
want: util.Subset,
1131+
}, {
1132+
desc: "subset by length and keys",
1133+
a: mustStringToPath(t, "/foo[a=1]/bar"),
1134+
b: mustStringToPath(t, "/foo[b=*]"),
1135+
want: util.Subset,
1136+
}, {
1137+
desc: "subset by list keys",
1138+
a: mustStringToPath(t, "/foo[a=1]"),
1139+
b: mustStringToPath(t, "/foo[a=*]"),
1140+
want: util.Subset,
1141+
}, {
1142+
desc: "subset by list keys implicit wildcard",
1143+
a: mustStringToPath(t, "/foo[a=1]"),
1144+
b: mustStringToPath(t, "/foo"),
1145+
want: util.Subset,
1146+
}, {
1147+
desc: "error single elem is both subset and superset",
1148+
a: mustStringToPath(t, "/foo[a=1][b=*]"),
1149+
b: mustStringToPath(t, "/foo[a=*][b=1]"),
1150+
want: util.PartialIntersect,
1151+
}, {
1152+
desc: "error single elem is both subset and superset implicit wildcards",
1153+
a: mustStringToPath(t, "/foo[a=1]"),
1154+
b: mustStringToPath(t, "/foo[b=1]"),
1155+
want: util.PartialIntersect,
1156+
}, {
1157+
desc: "error path is both subset and superset",
1158+
a: mustStringToPath(t, "/foo[a=*]/bar[b=1]"),
1159+
b: mustStringToPath(t, "/foo[b=1]/bar[b=*]"),
1160+
want: util.PartialIntersect,
1161+
}, {
1162+
desc: "error path elem is both subset and superset",
1163+
a: mustStringToPath(t, "/foo[a=1]/bar[b=*]"),
1164+
b: mustStringToPath(t, "/foo[b=*]/bar[b=1]"),
1165+
want: util.PartialIntersect,
1166+
}, {
1167+
desc: "error shorter path is both subset and superset",
1168+
a: mustStringToPath(t, "/foo[a=*]/bar"),
1169+
b: mustStringToPath(t, "/foo[b=1]"),
1170+
want: util.PartialIntersect,
1171+
}, {
1172+
desc: "error path elem is both subset and superset",
1173+
a: mustStringToPath(t, "/foo[a=1]"),
1174+
b: mustStringToPath(t, "/foo[b=*]/bar"),
1175+
want: util.PartialIntersect,
1176+
}}
1177+
for _, tt := range tests {
1178+
t.Run(tt.desc, func(t *testing.T) {
1179+
got := util.ComparePaths(tt.a, tt.b)
1180+
if got != tt.want {
1181+
t.Errorf("ComparePaths(%v, %v) got unexpected result: got %v, want %v", tt.a, tt.b, got, tt.want)
1182+
}
1183+
})
1184+
}
1185+
}

0 commit comments

Comments
 (0)