github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/gnmi/path_test.go (about) 1 // Copyright (c) 2017 Arista Networks, Inc. 2 // Use of this source code is governed by the Apache License 2.0 3 // that can be found in the COPYING file. 4 5 package gnmi 6 7 import ( 8 "fmt" 9 "testing" 10 11 "github.com/aristanetworks/goarista/test" 12 13 pb "github.com/openconfig/gnmi/proto/gnmi" 14 ) 15 16 func p(s ...string) []string { 17 return s 18 } 19 20 func TestSplitPath(t *testing.T) { 21 for i, tc := range []struct { 22 in string 23 exp []string 24 }{{ 25 in: "/foo/bar", 26 exp: p("foo", "bar"), 27 }, { 28 in: "/foo/bar/", 29 exp: p("foo", "bar"), 30 }, { 31 in: "//foo//bar//", 32 exp: p("", "foo", "", "bar", ""), 33 }, { 34 in: "/foo[name=///]/bar", 35 exp: p("foo[name=///]", "bar"), 36 }, { 37 in: `/foo[name=[\\\]/]/bar`, 38 exp: p(`foo[name=[\\\]/]`, "bar"), 39 }, { 40 in: `/foo[name=[\\]/bar`, 41 exp: p(`foo[name=[\\]`, "bar"), 42 }, { 43 in: "/foo[a=1][b=2]/bar", 44 exp: p("foo[a=1][b=2]", "bar"), 45 }, { 46 in: "/foo[a=1\\]2][b=2]/bar", 47 exp: p("foo[a=1\\]2][b=2]", "bar"), 48 }, { 49 in: "/foo[a=1][b=2]/bar\\baz", 50 exp: p("foo[a=1][b=2]", "bar\\baz"), 51 }} { 52 got := SplitPath(tc.in) 53 if !test.DeepEqual(tc.exp, got) { 54 t.Errorf("[%d] unexpect split for %q. Expected: %v, Got: %v", 55 i, tc.in, tc.exp, got) 56 } 57 } 58 } 59 60 func TestStrPath(t *testing.T) { 61 for i, tc := range []struct { 62 path string 63 }{{ 64 path: "/", 65 }, { 66 path: "/foo/bar", 67 }, { 68 path: "/foo[name=a]/bar", 69 }, { 70 path: "/foo[a=1][b=2]/bar", 71 }, { 72 path: "/foo[a=1\\]2][b=2]/bar", 73 }, { 74 path: "/foo[a=1][b=2]/bar\\/baz", 75 }, { 76 path: `/foo[a\==1]`, 77 }, { 78 path: `/f\/o\[o[a\==1]`, 79 }} { 80 sElms := SplitPath(tc.path) 81 pbPath, err := ParseGNMIElements(sElms) 82 if err != nil { 83 t.Errorf("failed to parse %s: %s", sElms, err) 84 } 85 s := StrPath(pbPath) 86 if tc.path != s { 87 t.Errorf("[%d] want %s, got %s", i, tc.path, s) 88 } 89 } 90 } 91 92 func TestStrPathBackwardsCompat(t *testing.T) { 93 for i, tc := range []struct { 94 path *pb.Path 95 str string 96 }{{ 97 path: &pb.Path{ 98 Element: p("foo[a=1][b=2]", "bar"), 99 }, 100 str: "/foo[a=1][b=2]/bar", 101 }} { 102 got := StrPath(tc.path) 103 if got != tc.str { 104 t.Errorf("[%d] want %q, got %q", i, tc.str, got) 105 } 106 } 107 } 108 109 func TestParseElement(t *testing.T) { 110 // test cases 111 cases := []struct { 112 // name is the name of the test useful if you want to run a single test 113 // from the command line -run TestParseElement/<name> 114 name string 115 // in is the path element to be parsed 116 in string 117 // fieldName is field name (YANG node name) expected to be parsed from the path element. 118 // Normally this is simply the path element, or if the path element contains keys this is 119 // the text before the first [ 120 fieldName string 121 // keys is a map of the expected key value pairs from within the []s in the 122 // `path element. 123 // 124 // For example prefix[ip-prefix=10.0.0.0/24][masklength-range=26..28] 125 // fieldName would be "prefix" 126 // keys would be {"ip-prefix": "10.0.0.0/24", "masklength-range": "26..28"} 127 keys map[string]string 128 // expectedError is the exact error we expect. 129 expectedError error 130 }{{ 131 name: "no_elms", 132 in: "hello", 133 fieldName: "hello", 134 }, { 135 name: "single_open", 136 in: "[", 137 expectedError: fmt.Errorf("failed to find element name in %q", "["), 138 }, { 139 name: "no_equal_no_close", 140 in: "hello[there", 141 expectedError: fmt.Errorf("failed to find '=' in %q", "[there"), 142 }, { 143 name: "no_equals", 144 in: "hello[there]", 145 expectedError: fmt.Errorf("failed to find '=' in %q", "[there]"), 146 }, { 147 name: "no_left_side", 148 in: "hello[=there]", 149 fieldName: "hello", 150 keys: map[string]string{"": "there"}, 151 }, { 152 name: "no_right_side", 153 in: "hello[there=]", 154 fieldName: "hello", 155 keys: map[string]string{"there": ""}, 156 }, { 157 name: "hanging_escape", 158 in: "hello[there\\", 159 expectedError: fmt.Errorf("failed to find '=' in %q", "[there\\"), 160 }, { 161 name: "single_name_value", 162 in: "hello[there=where]", 163 fieldName: "hello", 164 keys: map[string]string{"there": "where"}, 165 }, { 166 name: "single_value_with=", 167 in: "hello[there=whe=r=e]", 168 fieldName: "hello", 169 keys: map[string]string{"there": "whe=r=e"}, 170 }, { 171 name: "single_value_with=_and_escaped_]", 172 in: `hello[there=whe=\]r=e]`, 173 fieldName: "hello", 174 keys: map[string]string{"there": `whe=]r=e`}, 175 }, { 176 name: "single_value_with[", 177 in: "hello[there=w[[here]", 178 fieldName: "hello", 179 keys: map[string]string{"there": "w[[here"}, 180 }, { 181 name: "value_single_open", 182 in: "hello[first=value][", 183 expectedError: fmt.Errorf("failed to find '=' in %q", "["), 184 }, { 185 name: "value_no_close", 186 in: "hello[there=where][somename", 187 expectedError: fmt.Errorf("failed to find '=' in %q", "[somename"), 188 }, { 189 name: "value_no_equals", 190 in: "hello[there=where][somename]", 191 expectedError: fmt.Errorf("failed to find '=' in %q", "[somename]"), 192 }, { 193 name: "no_left_side", 194 in: "hello[there=where][=somevalue]", 195 fieldName: "hello", 196 keys: map[string]string{"there": "where", "": "somevalue"}, 197 }, { 198 name: "no_right_side", 199 in: "hello[there=where][somename=]", 200 fieldName: "hello", 201 keys: map[string]string{"there": "where", "somename": ""}, 202 }, { 203 name: "two_name_values", 204 in: "hello[there=where][somename=somevalue]", 205 fieldName: "hello", 206 keys: map[string]string{"there": "where", "somename": "somevalue"}, 207 }, { 208 name: "three_name_values", 209 in: "hello[there=where][somename=somevalue][anothername=value]", 210 fieldName: "hello", 211 keys: map[string]string{"there": "where", "somename": "somevalue", 212 "anothername": "value"}, 213 }, { 214 name: "aserisk_value", 215 in: "hello[there=*][somename=somevalue][anothername=value]", 216 fieldName: "hello", 217 keys: map[string]string{"there": "*", "somename": "somevalue", 218 "anothername": "value"}, 219 }, { 220 name: "escaped =", 221 in: `hello[foo\==bar]`, 222 fieldName: "hello", 223 keys: map[string]string{"foo=": "bar"}, 224 }, { 225 name: "escaped [", 226 in: `hell\[o[foo=bar]`, 227 fieldName: "hell[o", 228 keys: map[string]string{"foo": "bar"}, 229 }} 230 231 for _, tc := range cases { 232 t.Run(tc.name, func(t *testing.T) { 233 fieldName, keys, err := parseElement(tc.in) 234 if !test.DeepEqual(tc.expectedError, err) { 235 t.Fatalf("[%s] expected err %#v, got %#v", tc.name, tc.expectedError, err) 236 } 237 if !test.DeepEqual(tc.keys, keys) { 238 t.Fatalf("[%s] expected output %#v, got %#v", tc.name, tc.keys, keys) 239 } 240 if tc.fieldName != fieldName { 241 t.Fatalf("[%s] expected field name %s, got %s", tc.name, tc.fieldName, fieldName) 242 } 243 }) 244 } 245 } 246 247 func TestParseKeys(t *testing.T) { 248 // test cases 249 cases := []struct { 250 // name is the name of the test useful if you want to run a single test 251 // from the command line -run TestParseElement/<name> 252 name string 253 // in is the path element to be parsed 254 in string 255 // keys is a map of the expected key value pairs from within the []s in the 256 // `path element. 257 // 258 // For example prefix[ip-prefix=10.0.0.0/24][masklength-range=26..28] 259 // fieldName would be "prefix" 260 // keys would be {"ip-prefix": "10.0.0.0/24", "masklength-range": "26..28"} 261 keys map[string]string 262 // expectedError is the exact error we expect. 263 expectedError error 264 }{{ 265 name: "no_equal_no_close", 266 in: "[there", 267 expectedError: fmt.Errorf("failed to find '=' in %q", "[there"), 268 }, { 269 name: "no_equals", 270 in: "[there]", 271 expectedError: fmt.Errorf("failed to find '=' in %q", "[there]"), 272 }, { 273 name: "no_left_side", 274 in: "[=there]", 275 keys: map[string]string{"": "there"}, 276 }, { 277 name: "no_right_side", 278 in: "[there=]", 279 keys: map[string]string{"there": ""}, 280 }, { 281 name: "hanging_escape", 282 in: "[there\\", 283 expectedError: fmt.Errorf("failed to find '=' in %q", "[there\\"), 284 }, { 285 name: "single_name_value", 286 in: "[there=where]", 287 keys: map[string]string{"there": "where"}, 288 }, { 289 name: "single_value_with=", 290 in: "[there=whe=r=e]", 291 keys: map[string]string{"there": "whe=r=e"}, 292 }, { 293 name: "single_value_with=_and_escaped_]", 294 in: `[there=whe=\]r=e]`, 295 keys: map[string]string{"there": `whe=]r=e`}, 296 }, { 297 name: "single_value_with[", 298 in: "[there=w[[here]", 299 keys: map[string]string{"there": "w[[here"}, 300 }, { 301 name: "value_single_open", 302 in: "[first=value][", 303 expectedError: fmt.Errorf("failed to find '=' in %q", "["), 304 }, { 305 name: "value_no_close", 306 in: "[there=where][somename", 307 expectedError: fmt.Errorf("failed to find '=' in %q", "[somename"), 308 }, { 309 name: "value_no_equals", 310 in: "[there=where][somename]", 311 expectedError: fmt.Errorf("failed to find '=' in %q", "[somename]"), 312 }, { 313 name: "no_left_side", 314 in: "[there=where][=somevalue]", 315 keys: map[string]string{"there": "where", "": "somevalue"}, 316 }, { 317 name: "no_right_side", 318 in: "[there=where][somename=]", 319 keys: map[string]string{"there": "where", "somename": ""}, 320 }, { 321 name: "two_name_values", 322 in: "[there=where][somename=somevalue]", 323 keys: map[string]string{"there": "where", "somename": "somevalue"}, 324 }, { 325 name: "three_name_values", 326 in: "[there=where][somename=somevalue][anothername=value]", 327 keys: map[string]string{"there": "where", "somename": "somevalue", 328 "anothername": "value"}, 329 }, { 330 name: "aserisk_value", 331 in: "[there=*][somename=somevalue][anothername=value]", 332 keys: map[string]string{"there": "*", "somename": "somevalue", 333 "anothername": "value"}, 334 }, { 335 name: "escaped =", 336 in: `[foo\==bar]`, 337 keys: map[string]string{"foo=": "bar"}, 338 }} 339 340 for _, tc := range cases { 341 t.Run(tc.name, func(t *testing.T) { 342 keys, err := ParseKeys(tc.in) 343 if !test.DeepEqual(tc.expectedError, err) { 344 t.Fatalf("[%s] expected err %#v, got %#v", tc.name, tc.expectedError, err) 345 } 346 if !test.DeepEqual(tc.keys, keys) { 347 t.Fatalf("[%s] expected output %#v, got %#v", tc.name, tc.keys, keys) 348 } 349 }) 350 } 351 } 352 353 func strToPath(pathStr string) *pb.Path { 354 splitPath := SplitPath(pathStr) 355 path, _ := ParseGNMIElements(splitPath) 356 path.Element = nil 357 return path 358 } 359 360 func strsToPaths(pathStrs []string) []*pb.Path { 361 var paths []*pb.Path 362 for _, splitPath := range SplitPaths(pathStrs) { 363 path, _ := ParseGNMIElements(splitPath) 364 path.Element = nil 365 paths = append(paths, path) 366 } 367 return paths 368 } 369 370 func TestJoinPath(t *testing.T) { 371 cases := []struct { 372 paths []*pb.Path 373 exp string 374 }{{ 375 paths: strsToPaths([]string{"/foo/bar", "/baz/qux"}), 376 exp: "/foo/bar/baz/qux", 377 }, 378 { 379 paths: strsToPaths([]string{ 380 "/foo/bar[somekey=someval][otherkey=otherval]", "/baz/qux"}), 381 exp: "/foo/bar[otherkey=otherval][somekey=someval]/baz/qux", 382 }, 383 { 384 paths: strsToPaths([]string{ 385 "/foo/bar[somekey=someval][otherkey=otherval]", 386 "/baz/qux[somekey=someval][otherkey=otherval]"}), 387 exp: "/foo/bar[otherkey=otherval][somekey=someval]/" + 388 "baz/qux[otherkey=otherval][somekey=someval]", 389 }, 390 { 391 paths: []*pb.Path{ 392 {Element: []string{"foo", "bar[somekey=someval][otherkey=otherval]"}}, 393 {Element: []string{"baz", "qux[somekey=someval][otherkey=otherval]"}}}, 394 exp: "/foo/bar[somekey=someval][otherkey=otherval]/" + 395 "baz/qux[somekey=someval][otherkey=otherval]", 396 }, 397 { 398 paths: []*pb.Path{ 399 nil, 400 {Element: []string{"baz", "qux[somekey=someval][otherkey=otherval]"}}, 401 nil, 402 {Element: []string{"foo", "bar[somekey=someval][otherkey=otherval]"}}, 403 nil}, 404 exp: "baz/qux[otherkey=otherval][somekey=someval]/" + 405 "foo/bar[somekey=someval][otherkey=otherval]", 406 }, 407 } 408 409 for _, tc := range cases { 410 got := JoinPaths(tc.paths...) 411 exp := strToPath(tc.exp) 412 exp.Element = nil 413 if !test.DeepEqual(got, exp) { 414 t.Fatalf("ERROR!\n Got: %s,\n Want %s\n", got, exp) 415 } 416 } 417 } 418 419 func BenchmarkPathElementToSigleElementName(b *testing.B) { 420 for i := 0; i < b.N; i++ { 421 _, _, _ = parseElement("hello") 422 } 423 } 424 425 func BenchmarkPathElementTwoKeys(b *testing.B) { 426 for i := 0; i < b.N; i++ { 427 _, _, _ = parseElement("hello[hello=world][bye=moon]") 428 } 429 } 430 431 func BenchmarkPathElementBadKeys(b *testing.B) { 432 for i := 0; i < b.N; i++ { 433 _, _, _ = parseElement("hello[hello=world][byemoon]") 434 } 435 } 436 437 func BenchmarkPathElementMaxKeys(b *testing.B) { 438 for i := 0; i < b.N; i++ { 439 _, _, _ = parseElement("hello[name=firstName][name=secondName][name=thirdName]" + 440 "[name=fourthName][name=fifthName][name=sixthName]") 441 } 442 } 443 444 func BenchmarkKeyToString(b *testing.B) { 445 for _, bench := range []struct { 446 name string 447 key map[string]string 448 }{{ 449 name: "singlekey", 450 key: map[string]string{"abcdefghijkm": "nopqrstuvwxyz"}, 451 }, { 452 name: "fivekeys", 453 key: map[string]string{ 454 "one": "one", 455 "two": "two", 456 "three": "three", 457 "four": "four", 458 "five": "five", 459 }, 460 }, { 461 name: "escaped", 462 key: map[string]string{ 463 "foo=": "bar", 464 "foo": "ba]r]", 465 }, 466 }} { 467 b.Run(bench.name, func(b *testing.B) { 468 b.ReportAllocs() 469 for i := 0; i < b.N; i++ { 470 KeyToString(bench.key) 471 } 472 }) 473 } 474 }