github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/gnmi/operation_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 "bytes" 9 "encoding/json" 10 "io/ioutil" 11 "os" 12 "testing" 13 14 "github.com/aristanetworks/goarista/test" 15 "google.golang.org/protobuf/proto" 16 "google.golang.org/protobuf/types/known/anypb" 17 18 pb "github.com/openconfig/gnmi/proto/gnmi" 19 ) 20 21 func TestNewSetRequest(t *testing.T) { 22 pathFoo := &pb.Path{ 23 Element: []string{"foo"}, 24 Elem: []*pb.PathElem{{Name: "foo"}}, 25 } 26 pathCli := &pb.Path{ 27 Origin: "cli", 28 } 29 pathP4 := &pb.Path{ 30 Origin: "p4_config", 31 } 32 pathOC := &pb.Path{ 33 Origin: "openconfig", 34 } 35 36 fileData := []struct { 37 name string 38 fileName string 39 content string 40 }{{ 41 name: "p4_config", 42 fileName: "p4TestFile", 43 content: "p4_config test", 44 }, { 45 name: "originCLIFileData", 46 fileName: "originCLIFile", 47 content: `enable 48 configure 49 hostname new`, 50 }} 51 52 fileNames := make([]string, 2, 2) 53 for i, data := range fileData { 54 f, err := ioutil.TempFile("", data.name) 55 if err != nil { 56 t.Errorf("cannot create test file for %s", data.name) 57 } 58 filename := f.Name() 59 defer os.Remove(filename) 60 fileNames[i] = filename 61 if _, err := f.WriteString(data.content); err != nil { 62 t.Errorf("cannot write test file for %s", data.name) 63 } 64 f.Close() 65 } 66 67 testCases := map[string]struct { 68 setOps []*Operation 69 exp *pb.SetRequest 70 }{ 71 "delete": { 72 setOps: []*Operation{{Type: "delete", Path: []string{"foo"}}}, 73 exp: &pb.SetRequest{Delete: []*pb.Path{pathFoo}}, 74 }, 75 "update": { 76 setOps: []*Operation{{Type: "update", Path: []string{"foo"}, Val: "true"}}, 77 exp: &pb.SetRequest{ 78 Update: []*pb.Update{{ 79 Path: pathFoo, 80 Val: &pb.TypedValue{ 81 Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("true")}}, 82 }}, 83 }, 84 }, 85 "replace": { 86 setOps: []*Operation{{Type: "replace", Path: []string{"foo"}, Val: "true"}}, 87 exp: &pb.SetRequest{ 88 Replace: []*pb.Update{{ 89 Path: pathFoo, 90 Val: &pb.TypedValue{ 91 Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("true")}}, 92 }}, 93 }, 94 }, 95 "cli-replace": { 96 setOps: []*Operation{{Type: "replace", Origin: "cli", 97 Val: "hostname foo\nip routing"}}, 98 exp: &pb.SetRequest{ 99 Replace: []*pb.Update{{ 100 Path: pathCli, 101 Val: &pb.TypedValue{ 102 Value: &pb.TypedValue_AsciiVal{AsciiVal: "hostname foo\nip routing"}}, 103 }}, 104 }, 105 }, 106 "p4_config": { 107 setOps: []*Operation{{Type: "replace", Origin: "p4_config", 108 Val: fileNames[0]}}, 109 exp: &pb.SetRequest{ 110 Replace: []*pb.Update{{ 111 Path: pathP4, 112 Val: &pb.TypedValue{ 113 Value: &pb.TypedValue_ProtoBytes{ProtoBytes: []byte(fileData[0].content)}}, 114 }}, 115 }, 116 }, 117 "target": { 118 setOps: []*Operation{{Type: "replace", Target: "JPE1234567", 119 Path: []string{"foo"}, Val: "true"}}, 120 exp: &pb.SetRequest{ 121 Prefix: &pb.Path{Target: "JPE1234567"}, 122 Replace: []*pb.Update{{ 123 Path: pathFoo, 124 Val: &pb.TypedValue{ 125 Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("true")}}, 126 }}, 127 }, 128 }, 129 "openconfig origin": { 130 setOps: []*Operation{{Type: "replace", Origin: "openconfig", 131 Val: "true"}}, 132 exp: &pb.SetRequest{ 133 Replace: []*pb.Update{{ 134 Path: pathOC, 135 Val: &pb.TypedValue{ 136 Value: &pb.TypedValue_JsonIetfVal{ 137 JsonIetfVal: []byte("true"), 138 }, 139 }, 140 }}, 141 }, 142 }, 143 "originCLI file": { 144 setOps: []*Operation{{Type: "update", Origin: "cli", 145 Val: fileNames[1]}}, 146 exp: &pb.SetRequest{ 147 Update: []*pb.Update{{ 148 Path: pathCli, 149 Val: &pb.TypedValue{ 150 Value: &pb.TypedValue_AsciiVal{AsciiVal: fileData[1].content}}, 151 }}, 152 }, 153 }, 154 "union_replace": { 155 setOps: []*Operation{{Type: "union_replace", Path: []string{"foo"}, Val: "true"}}, 156 exp: &pb.SetRequest{ 157 UnionReplace: []*pb.Update{{ 158 Path: pathFoo, 159 Val: &pb.TypedValue{ 160 Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("true")}}, 161 }}, 162 }, 163 }, 164 "union_replace openconfig and cli origin": { 165 setOps: []*Operation{{Type: "union_replace", Origin: "openconfig", Val: "true"}, 166 {Type: "union_replace", Origin: "cli", Val: fileNames[1]}}, 167 exp: &pb.SetRequest{ 168 UnionReplace: []*pb.Update{{ 169 Path: pathOC, 170 Val: &pb.TypedValue{ 171 Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("true")}}, 172 }, { 173 Path: pathCli, 174 Val: &pb.TypedValue{ 175 Value: &pb.TypedValue_AsciiVal{AsciiVal: fileData[1].content}}, 176 }}, 177 }, 178 }, 179 } 180 181 for name, tc := range testCases { 182 t.Run(name, func(t *testing.T) { 183 got, err := newSetRequest(tc.setOps) 184 if err != nil { 185 t.Fatal(err) 186 } 187 if !proto.Equal(tc.exp, got) { 188 t.Errorf("Exp: %v Got: %v", tc.exp, got) 189 } 190 }) 191 } 192 } 193 194 func TestStrUpdateVal(t *testing.T) { 195 anyBytes, err := proto.Marshal(&pb.ModelData{Name: "foobar"}) 196 if err != nil { 197 t.Fatal(err) 198 } 199 anyMessage := &anypb.Any{TypeUrl: "gnmi/ModelData", Value: anyBytes} 200 201 for name, tc := range map[string]struct { 202 update *pb.Update 203 exp string 204 }{ 205 "JSON Value": { 206 update: &pb.Update{ 207 Value: &pb.Value{ 208 Value: []byte(`{"foo":"bar"}`), 209 Type: pb.Encoding_JSON}}, 210 exp: `{"foo":"bar"}`, 211 }, 212 "JSON_IETF Value": { 213 update: &pb.Update{ 214 Value: &pb.Value{ 215 Value: []byte(`{"foo":"bar"}`), 216 Type: pb.Encoding_JSON_IETF}}, 217 exp: `{"foo":"bar"}`, 218 }, 219 "BYTES Value": { 220 update: &pb.Update{ 221 Value: &pb.Value{ 222 Value: []byte{0xde, 0xad}, 223 Type: pb.Encoding_BYTES}}, 224 exp: "3q0=", 225 }, 226 "PROTO Value": { 227 update: &pb.Update{ 228 Value: &pb.Value{ 229 Value: []byte{0xde, 0xad}, 230 Type: pb.Encoding_PROTO}}, 231 exp: "3q0=", 232 }, 233 "ASCII Value": { 234 update: &pb.Update{ 235 Value: &pb.Value{ 236 Value: []byte("foobar"), 237 Type: pb.Encoding_ASCII}}, 238 exp: "foobar", 239 }, 240 "INVALID Value": { 241 update: &pb.Update{ 242 Value: &pb.Value{ 243 Value: []byte("foobar"), 244 Type: pb.Encoding(42)}}, 245 exp: "foobar", 246 }, 247 "StringVal": { 248 update: &pb.Update{Val: &pb.TypedValue{ 249 Value: &pb.TypedValue_StringVal{StringVal: "foobar"}}}, 250 exp: "foobar", 251 }, 252 "IntVal": { 253 update: &pb.Update{Val: &pb.TypedValue{ 254 Value: &pb.TypedValue_IntVal{IntVal: -42}}}, 255 exp: "-42", 256 }, 257 "UintVal": { 258 update: &pb.Update{Val: &pb.TypedValue{ 259 Value: &pb.TypedValue_UintVal{UintVal: 42}}}, 260 exp: "42", 261 }, 262 "BoolVal": { 263 update: &pb.Update{Val: &pb.TypedValue{ 264 Value: &pb.TypedValue_BoolVal{BoolVal: true}}}, 265 exp: "true", 266 }, 267 "BytesVal": { 268 update: &pb.Update{Val: &pb.TypedValue{ 269 Value: &pb.TypedValue_BytesVal{BytesVal: []byte{0xde, 0xad}}}}, 270 exp: "3q0=", 271 }, 272 "FloatVal": { 273 update: &pb.Update{Val: &pb.TypedValue{ 274 Value: &pb.TypedValue_FloatVal{FloatVal: 3.14}}}, 275 exp: "3.14", 276 }, 277 "DoubleVal": { 278 update: &pb.Update{Val: &pb.TypedValue{ 279 Value: &pb.TypedValue_DoubleVal{DoubleVal: 3.14}}}, 280 exp: "3.14", 281 }, 282 "DecimalVal": { 283 update: &pb.Update{Val: &pb.TypedValue{ 284 Value: &pb.TypedValue_DecimalVal{ 285 DecimalVal: &pb.Decimal64{Digits: 3014, Precision: 3}, 286 }}}, 287 exp: "3.014", 288 }, 289 "DecimalValWithLeadingZeros": { 290 update: &pb.Update{Val: &pb.TypedValue{ 291 Value: &pb.TypedValue_DecimalVal{ 292 DecimalVal: &pb.Decimal64{Digits: 314, Precision: 6}, 293 }}}, 294 exp: "0.000314", 295 }, 296 "DecimalValWithLeadingZerosInFraction": { 297 update: &pb.Update{Val: &pb.TypedValue{ 298 Value: &pb.TypedValue_DecimalVal{ 299 DecimalVal: &pb.Decimal64{Digits: 3000141, Precision: 6}, 300 }}}, 301 exp: "3.000141", 302 }, 303 "DecimalValWithZeroPrecision": { 304 update: &pb.Update{Val: &pb.TypedValue{ 305 Value: &pb.TypedValue_DecimalVal{ 306 DecimalVal: &pb.Decimal64{Digits: 314, Precision: 0}, 307 }}}, 308 exp: "314.0", 309 }, 310 "DecimalValWithNegativeFraction": { 311 update: &pb.Update{Val: &pb.TypedValue{ 312 Value: &pb.TypedValue_DecimalVal{ 313 DecimalVal: &pb.Decimal64{Digits: -314, Precision: 3}, 314 }}}, 315 exp: "-0.314", 316 }, 317 "LeafListVal": { 318 update: &pb.Update{Val: &pb.TypedValue{ 319 Value: &pb.TypedValue_LeaflistVal{ 320 LeaflistVal: &pb.ScalarArray{Element: []*pb.TypedValue{ 321 &pb.TypedValue{Value: &pb.TypedValue_BoolVal{BoolVal: true}}, 322 &pb.TypedValue{Value: &pb.TypedValue_AsciiVal{AsciiVal: "foobar"}}, 323 }}, 324 }}}, 325 exp: "[true, foobar]", 326 }, 327 "AnyVal": { 328 update: &pb.Update{Val: &pb.TypedValue{ 329 Value: &pb.TypedValue_AnyVal{AnyVal: anyMessage}}}, 330 exp: anyMessage.String(), 331 }, 332 "JsonVal": { 333 update: &pb.Update{Val: &pb.TypedValue{ 334 Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`{"foo":"bar"}`)}}}, 335 exp: `{"foo":"bar"}`, 336 }, 337 "JsonVal_complex": { 338 update: &pb.Update{Val: &pb.TypedValue{ 339 Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`{"foo":"bar","baz":"qux"}`)}}}, 340 exp: `{ 341 "foo": "bar", 342 "baz": "qux" 343 }`, 344 }, 345 "JsonIetfVal": { 346 update: &pb.Update{Val: &pb.TypedValue{ 347 Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(`{"foo":"bar"}`)}}}, 348 exp: `{"foo":"bar"}`, 349 }, 350 "AsciiVal": { 351 update: &pb.Update{Val: &pb.TypedValue{ 352 Value: &pb.TypedValue_AsciiVal{AsciiVal: "foobar"}}}, 353 exp: "foobar", 354 }, 355 "ProtoBytes": { 356 update: &pb.Update{Val: &pb.TypedValue{ 357 Value: &pb.TypedValue_ProtoBytes{ProtoBytes: anyBytes}}}, 358 exp: "CgZmb29iYXI=", 359 }, 360 } { 361 t.Run(name, func(t *testing.T) { 362 got := StrUpdateVal(tc.update) 363 if got != tc.exp { 364 t.Errorf("Expected: %q Got: %q", tc.exp, got) 365 } 366 }) 367 } 368 } 369 370 func TestTypedValue(t *testing.T) { 371 for tname, tcase := range map[string]struct { 372 in interface{} 373 exp *pb.TypedValue 374 }{ 375 "string": { 376 in: "foo", 377 exp: &pb.TypedValue{Value: &pb.TypedValue_StringVal{StringVal: "foo"}}, 378 }, 379 "int": { 380 in: 42, 381 exp: &pb.TypedValue{Value: &pb.TypedValue_IntVal{IntVal: 42}}, 382 }, 383 "int64": { 384 in: int64(42), 385 exp: &pb.TypedValue{Value: &pb.TypedValue_IntVal{IntVal: 42}}, 386 }, 387 "uint": { 388 in: uint(42), 389 exp: &pb.TypedValue{Value: &pb.TypedValue_UintVal{UintVal: 42}}, 390 }, 391 "float32": { 392 in: float32(42.234123), 393 exp: &pb.TypedValue{Value: &pb.TypedValue_FloatVal{FloatVal: 42.234123}}, 394 }, 395 "float64": { 396 in: float64(42.234124222222), 397 exp: &pb.TypedValue{Value: &pb.TypedValue_DoubleVal{DoubleVal: 42.234124222222}}, 398 }, 399 "bool": { 400 in: true, 401 exp: &pb.TypedValue{Value: &pb.TypedValue_BoolVal{BoolVal: true}}, 402 }, 403 "slice": { 404 in: []interface{}{"foo", 1, uint(2), true}, 405 exp: &pb.TypedValue{Value: &pb.TypedValue_LeaflistVal{LeaflistVal: &pb.ScalarArray{ 406 Element: []*pb.TypedValue{ 407 &pb.TypedValue{Value: &pb.TypedValue_StringVal{StringVal: "foo"}}, 408 &pb.TypedValue{Value: &pb.TypedValue_IntVal{IntVal: 1}}, 409 &pb.TypedValue{Value: &pb.TypedValue_UintVal{UintVal: 2}}, 410 &pb.TypedValue{Value: &pb.TypedValue_BoolVal{BoolVal: true}}, 411 }}}}, 412 }, 413 "bytes": { 414 in: []byte("foo"), 415 exp: &pb.TypedValue{Value: &pb.TypedValue_BytesVal{BytesVal: []byte("foo")}}, 416 }, 417 "typed val": { 418 in: &pb.TypedValue{Value: &pb.TypedValue_StringVal{StringVal: "foo"}}, 419 exp: &pb.TypedValue{Value: &pb.TypedValue_StringVal{StringVal: "foo"}}, 420 }, 421 } { 422 t.Run(tname, func(t *testing.T) { 423 if got := TypedValue(tcase.in); !test.DeepEqual(got, tcase.exp) { 424 t.Errorf("Expected: %q Got: %q", tcase.exp, got) 425 } 426 }) 427 } 428 } 429 430 func TestExtractJSON(t *testing.T) { 431 jsonFile, err := ioutil.TempFile("", "extractContent") 432 if err != nil { 433 t.Fatal(err) 434 } 435 defer os.Remove(jsonFile.Name()) 436 if _, err := jsonFile.Write([]byte(`"jsonFile"`)); err != nil { 437 jsonFile.Close() 438 t.Fatal(err) 439 } 440 if err := jsonFile.Close(); err != nil { 441 t.Fatal(err) 442 } 443 444 for val, exp := range map[string][]byte{ 445 jsonFile.Name(): []byte(`"jsonFile"`), 446 "foobar": []byte(`"foobar"`), 447 `"foobar"`: []byte(`"foobar"`), 448 "Val: true": []byte(`"Val: true"`), 449 "host42": []byte(`"host42"`), 450 "42": []byte("42"), 451 "-123.43": []byte("-123.43"), 452 "0xFFFF": []byte("0xFFFF"), 453 // Int larger than can fit in 32 bits should be quoted 454 "0x8000000000": []byte(`"0x8000000000"`), 455 "-0x8000000000": []byte(`"-0x8000000000"`), 456 "true": []byte("true"), 457 "false": []byte("false"), 458 "null": []byte("null"), 459 "{true: 42}": []byte("{true: 42}"), 460 "[]": []byte("[]"), 461 } { 462 t.Run(val, func(t *testing.T) { 463 got := extractContent(val, "") 464 if !bytes.Equal(exp, got) { 465 t.Errorf("Unexpected diff. Expected: %q Got: %q", exp, got) 466 } 467 }) 468 } 469 } 470 471 func TestExtractValue(t *testing.T) { 472 cases := []struct { 473 in *pb.Update 474 exp interface{} 475 }{{ 476 in: &pb.Update{Val: &pb.TypedValue{ 477 Value: &pb.TypedValue_StringVal{StringVal: "foo"}}}, 478 exp: "foo", 479 }, { 480 in: &pb.Update{Val: &pb.TypedValue{ 481 Value: &pb.TypedValue_IntVal{IntVal: 123}}}, 482 exp: int64(123), 483 }, { 484 in: &pb.Update{Val: &pb.TypedValue{ 485 Value: &pb.TypedValue_UintVal{UintVal: 123}}}, 486 exp: uint64(123), 487 }, { 488 in: &pb.Update{Val: &pb.TypedValue{ 489 Value: &pb.TypedValue_BoolVal{BoolVal: true}}}, 490 exp: true, 491 }, { 492 in: &pb.Update{Val: &pb.TypedValue{ 493 Value: &pb.TypedValue_BytesVal{BytesVal: []byte{0xde, 0xad}}}}, 494 exp: []byte{0xde, 0xad}, 495 }, { 496 in: &pb.Update{Val: &pb.TypedValue{ 497 Value: &pb.TypedValue_FloatVal{FloatVal: -12.34}}}, 498 exp: float32(-12.34), 499 }, { 500 in: &pb.Update{Val: &pb.TypedValue{ 501 Value: &pb.TypedValue_DoubleVal{DoubleVal: -12.34}}}, 502 exp: float64(-12.34), 503 }, { 504 in: &pb.Update{Val: &pb.TypedValue{ 505 Value: &pb.TypedValue_DecimalVal{DecimalVal: &pb.Decimal64{ 506 Digits: -1234, Precision: 2}}}}, 507 exp: &pb.Decimal64{Digits: -1234, Precision: 2}, 508 }, { 509 in: &pb.Update{Val: &pb.TypedValue{ 510 Value: &pb.TypedValue_LeaflistVal{LeaflistVal: &pb.ScalarArray{ 511 Element: []*pb.TypedValue{ 512 &pb.TypedValue{Value: &pb.TypedValue_StringVal{StringVal: "foo"}}, 513 &pb.TypedValue{Value: &pb.TypedValue_IntVal{IntVal: 123}}}}}}}, 514 exp: []interface{}{"foo", int64(123)}, 515 }, { 516 in: &pb.Update{Val: &pb.TypedValue{ 517 Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`12.34`)}}}, 518 exp: json.Number("12.34"), 519 }, { 520 in: &pb.Update{Val: &pb.TypedValue{ 521 Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`[12.34, 123, "foo"]`)}}}, 522 exp: []interface{}{json.Number("12.34"), json.Number("123"), "foo"}, 523 }, { 524 in: &pb.Update{Val: &pb.TypedValue{ 525 Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`{"foo":"bar"}`)}}}, 526 exp: map[string]interface{}{"foo": "bar"}, 527 }, { 528 in: &pb.Update{Val: &pb.TypedValue{ 529 Value: &pb.TypedValue_JsonVal{JsonVal: []byte(`{"foo":45.67}`)}}}, 530 exp: map[string]interface{}{"foo": json.Number("45.67")}, 531 }, { 532 in: &pb.Update{Val: &pb.TypedValue{ 533 Value: &pb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(`{"foo":"bar"}`)}}}, 534 exp: map[string]interface{}{"foo": "bar"}, 535 }} 536 for _, tc := range cases { 537 out, err := ExtractValue(tc.in) 538 if err != nil { 539 t.Errorf(err.Error()) 540 } 541 if !test.DeepEqual(tc.exp, out) { 542 t.Errorf("Extracted value is incorrect. Expected %+v, got %+v", tc.exp, out) 543 } 544 } 545 }