github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/k8s/jsonpath/jsonpath_test.go (about) 1 /* 2 Copyright 2015 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package jsonpath 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "reflect" 24 "sort" 25 "strings" 26 "testing" 27 ) 28 29 type jsonpathTest struct { 30 name string 31 template string 32 input interface{} 33 expect string 34 expectError bool 35 } 36 37 func testJSONPath(tests []jsonpathTest, allowMissingKeys bool, t *testing.T) { 38 for _, test := range tests { 39 j := New(test.name) 40 j.AllowMissingKeys(allowMissingKeys) 41 err := j.Parse(test.template) 42 if err != nil { 43 if !test.expectError { 44 t.Errorf("in %s, parse %s error %v", test.name, test.template, err) 45 } 46 continue 47 } 48 buf := new(bytes.Buffer) 49 err = j.Execute(buf, test.input) 50 if test.expectError { 51 if err == nil { 52 t.Errorf("in %s, expected execute error", test.name) 53 } 54 continue 55 } else if err != nil { 56 t.Errorf("in %s, execute error %v", test.name, err) 57 } 58 out := buf.String() 59 if out != test.expect { 60 t.Errorf(`in %s, expect to get "%s", got "%s"`, test.name, test.expect, out) 61 } 62 } 63 } 64 65 // testJSONPathSortOutput test cases related to map, the results may print in random order 66 func testJSONPathSortOutput(tests []jsonpathTest, t *testing.T) { 67 for _, test := range tests { 68 j := New(test.name) 69 err := j.Parse(test.template) 70 if err != nil { 71 t.Errorf("in %s, parse %s error %v", test.name, test.template, err) 72 } 73 buf := new(bytes.Buffer) 74 err = j.Execute(buf, test.input) 75 if err != nil { 76 t.Errorf("in %s, execute error %v", test.name, err) 77 } 78 out := buf.String() 79 //since map is visited in random order, we need to sort the results. 80 sortedOut := strings.Fields(out) 81 sort.Strings(sortedOut) 82 sortedExpect := strings.Fields(test.expect) 83 sort.Strings(sortedExpect) 84 if !reflect.DeepEqual(sortedOut, sortedExpect) { 85 t.Errorf(`in %s, expect to get "%s", got "%s"`, test.name, test.expect, out) 86 } 87 } 88 } 89 90 func testFailJSONPath(tests []jsonpathTest, t *testing.T) { 91 for _, test := range tests { 92 j := New(test.name) 93 err := j.Parse(test.template) 94 if err != nil { 95 t.Errorf("in %s, parse %s error %v", test.name, test.template, err) 96 } 97 buf := new(bytes.Buffer) 98 err = j.Execute(buf, test.input) 99 var out string 100 if err == nil { 101 out = "nil" 102 } else { 103 out = err.Error() 104 } 105 if out != test.expect { 106 t.Errorf("in %s, expect to get error %q, got %q", test.name, test.expect, out) 107 } 108 } 109 } 110 111 type book struct { 112 Category string 113 Author string 114 Title string 115 Price float32 116 } 117 118 func (b book) String() string { 119 return fmt.Sprintf("{Category: %s, Author: %s, Title: %s, Price: %v}", b.Category, b.Author, b.Title, b.Price) 120 } 121 122 type bicycle struct { 123 Color string 124 Price float32 125 IsNew bool 126 } 127 128 type empName string 129 type job string 130 type store struct { 131 Book []book 132 Bicycle []bicycle 133 Name string 134 Labels map[string]int 135 Employees map[empName]job 136 } 137 138 func TestStructInput(t *testing.T) { 139 140 storeData := store{ 141 Name: "jsonpath", 142 Book: []book{ 143 {"reference", "Nigel Rees", "Sayings of the Centurey", 8.95}, 144 {"fiction", "Evelyn Waugh", "Sword of Honor", 12.99}, 145 {"fiction", "Herman Melville", "Moby Dick", 8.99}, 146 }, 147 Bicycle: []bicycle{ 148 {"red", 19.95, true}, 149 {"green", 20.01, false}, 150 }, 151 Labels: map[string]int{ 152 "engineer": 10, 153 "web/html": 15, 154 "k8s-app": 20, 155 }, 156 Employees: map[empName]job{ 157 "jason": "manager", 158 "dan": "clerk", 159 }, 160 } 161 162 storeTests := []jsonpathTest{ 163 {"plain", "hello jsonpath", nil, "hello jsonpath", false}, 164 {"recursive", "{..}", []int{1, 2, 3}, "[1 2 3]", false}, 165 {"filter", "{[?(@<5)]}", []int{2, 6, 3, 7}, "2 3", false}, 166 {"quote", `{"{"}`, nil, "{", false}, 167 {"union", "{[1,3,4]}", []int{0, 1, 2, 3, 4}, "1 3 4", false}, 168 {"array", "{[0:2]}", []string{"Monday", "Tudesday"}, "Monday Tudesday", false}, 169 {"variable", "hello {.Name}", storeData, "hello jsonpath", false}, 170 {"dict/", "{$.Labels.web/html}", storeData, "15", false}, 171 {"dict/", "{$.Employees.jason}", storeData, "manager", false}, 172 {"dict/", "{$.Employees.dan}", storeData, "clerk", false}, 173 {"dict-", "{.Labels.k8s-app}", storeData, "20", false}, 174 {"nest", "{.Bicycle[*].Color}", storeData, "red green", false}, 175 {"allarray", "{.Book[*].Author}", storeData, "Nigel Rees Evelyn Waugh Herman Melville", false}, 176 {"allfileds", "{.Bicycle.*}", storeData, "{red 19.95 true} {green 20.01 false}", false}, 177 {"recurfileds", "{..Price}", storeData, "8.95 12.99 8.99 19.95 20.01", false}, 178 {"lastarray", "{.Book[-1:]}", storeData, 179 "{Category: fiction, Author: Herman Melville, Title: Moby Dick, Price: 8.99}", false}, 180 {"recurarray", "{..Book[2]}", storeData, 181 "{Category: fiction, Author: Herman Melville, Title: Moby Dick, Price: 8.99}", false}, 182 {"bool", "{.Bicycle[?(@.IsNew==true)]}", storeData, "{red 19.95 true}", false}, 183 } 184 testJSONPath(storeTests, false, t) 185 186 missingKeyTests := []jsonpathTest{ 187 {"nonexistent field", "{.hello}", storeData, "", false}, 188 } 189 testJSONPath(missingKeyTests, true, t) 190 191 failStoreTests := []jsonpathTest{ 192 {"invalid identifier", "{hello}", storeData, "unrecognized identifier hello", false}, 193 {"nonexistent field", "{.hello}", storeData, "hello is not found", false}, 194 {"invalid array", "{.Labels[0]}", storeData, "map[string]int is not array or slice", false}, 195 {"invalid filter operator", "{.Book[?(@.Price<>10)]}", storeData, "unrecognized filter operator <>", false}, 196 {"redundant end", "{range .Labels.*}{@}{end}{end}", storeData, "not in range, nothing to end", false}, 197 } 198 testFailJSONPath(failStoreTests, t) 199 } 200 201 func TestJSONInput(t *testing.T) { 202 var pointsJSON = []byte(`[ 203 {"id": "i1", "x":4, "y":-5}, 204 {"id": "i2", "x":-2, "y":-5, "z":1}, 205 {"id": "i3", "x": 8, "y": 3 }, 206 {"id": "i4", "x": -6, "y": -1 }, 207 {"id": "i5", "x": 0, "y": 2, "z": 1 }, 208 {"id": "i6", "x": 1, "y": 4 } 209 ]`) 210 var pointsData interface{} 211 err := json.Unmarshal(pointsJSON, &pointsData) 212 if err != nil { 213 t.Error(err) 214 } 215 pointsTests := []jsonpathTest{ 216 {"exists filter", "{[?(@.z)].id}", pointsData, "i2 i5", false}, 217 {"bracket key", "{[0]['id']}", pointsData, "i1", false}, 218 } 219 testJSONPath(pointsTests, false, t) 220 } 221 222 // TestKubernetes tests some use cases from kubernetes 223 func TestKubernetes(t *testing.T) { 224 var input = []byte(`{ 225 "kind": "List", 226 "items":[ 227 { 228 "kind":"None", 229 "metadata":{ 230 "name":"127.0.0.1", 231 "labels":{ 232 "kubernetes.io/hostname":"127.0.0.1" 233 } 234 }, 235 "status":{ 236 "capacity":{"cpu":"4"}, 237 "ready": true, 238 "addresses":[{"type": "LegacyHostIP", "address":"127.0.0.1"}] 239 } 240 }, 241 { 242 "kind":"None", 243 "metadata":{ 244 "name":"127.0.0.2", 245 "labels":{ 246 "kubernetes.io/hostname":"127.0.0.2" 247 } 248 }, 249 "status":{ 250 "capacity":{"cpu":"8"}, 251 "ready": false, 252 "addresses":[ 253 {"type": "LegacyHostIP", "address":"127.0.0.2"}, 254 {"type": "another", "address":"127.0.0.3"} 255 ] 256 } 257 } 258 ], 259 "users":[ 260 { 261 "name": "myself", 262 "user": {} 263 }, 264 { 265 "name": "e2e", 266 "user": {"username": "admin", "password": "secret"} 267 } 268 ] 269 }`) 270 var nodesData interface{} 271 err := json.Unmarshal(input, &nodesData) 272 if err != nil { 273 t.Error(err) 274 } 275 276 nodesTests := []jsonpathTest{ 277 {"range item", `{range .items[*]}{.metadata.name}, {end}{.kind}`, nodesData, "127.0.0.1, 127.0.0.2, List", false}, 278 {"range item with quote", `{range .items[*]}{.metadata.name}{"\t"}{end}`, nodesData, "127.0.0.1\t127.0.0.2\t", false}, 279 {"range address", `{.items[*].status.addresses[*].address}`, nodesData, 280 "127.0.0.1 127.0.0.2 127.0.0.3", false}, 281 {"double range", `{range .items[*]}{range .status.addresses[*]}{.address}, {end}{end}`, nodesData, 282 "127.0.0.1, 127.0.0.2, 127.0.0.3, ", false}, 283 {"item name", `{.items[*].metadata.name}`, nodesData, "127.0.0.1 127.0.0.2", false}, 284 {"union nodes capacity", `{.items[*]['metadata.name', 'status.capacity']}`, nodesData, 285 "127.0.0.1 127.0.0.2 map[cpu:4] map[cpu:8]", false}, 286 {"range nodes capacity", `{range .items[*]}[{.metadata.name}, {.status.capacity}] {end}`, nodesData, 287 "[127.0.0.1, map[cpu:4]] [127.0.0.2, map[cpu:8]] ", false}, 288 {"user password", `{.users[?(@.name=="e2e")].user.password}`, &nodesData, "secret", false}, 289 {"hostname", `{.items[0].metadata.labels.kubernetes\.io/hostname}`, &nodesData, "127.0.0.1", false}, 290 {"hostname filter", `{.items[?(@.metadata.labels.kubernetes\.io/hostname=="127.0.0.1")].kind}`, &nodesData, "None", false}, 291 {"bool item", `{.items[?(@..ready==true)].metadata.name}`, &nodesData, "127.0.0.1", false}, 292 } 293 testJSONPath(nodesTests, false, t) 294 295 randomPrintOrderTests := []jsonpathTest{ 296 {"recursive name", "{..name}", nodesData, `127.0.0.1 127.0.0.2 myself e2e`, false}, 297 } 298 testJSONPathSortOutput(randomPrintOrderTests, t) 299 } 300 301 func TestFilterPartialMatchesSometimesMissingAnnotations(t *testing.T) { 302 // for https://issues.k8s.io/45546 303 var input = []byte(`{ 304 "kind": "List", 305 "items": [ 306 { 307 "kind": "Pod", 308 "metadata": { 309 "name": "pod1", 310 "annotations": { 311 "color": "blue" 312 } 313 } 314 }, 315 { 316 "kind": "Pod", 317 "metadata": { 318 "name": "pod2" 319 } 320 }, 321 { 322 "kind": "Pod", 323 "metadata": { 324 "name": "pod3", 325 "annotations": { 326 "color": "green" 327 } 328 } 329 }, 330 { 331 "kind": "Pod", 332 "metadata": { 333 "name": "pod4", 334 "annotations": { 335 "color": "blue" 336 } 337 } 338 } 339 ] 340 }`) 341 var data interface{} 342 err := json.Unmarshal(input, &data) 343 if err != nil { 344 t.Fatal(err) 345 } 346 347 testJSONPath( 348 []jsonpathTest{ 349 { 350 "filter, should only match a subset, some items don't have annotations, tolerate missing items", 351 `{.items[?(@.metadata.annotations.color=="blue")].metadata.name}`, 352 data, 353 "pod1 pod4", 354 false, // expect no error 355 }, 356 }, 357 true, // allow missing keys 358 t, 359 ) 360 361 testJSONPath( 362 []jsonpathTest{ 363 { 364 "filter, should only match a subset, some items don't have annotations, error on missing items", 365 `{.items[?(@.metadata.annotations.color=="blue")].metadata.name}`, 366 data, 367 "", 368 true, // expect an error 369 }, 370 }, 371 false, // don't allow missing keys 372 t, 373 ) 374 } 375 376 func TestNegativeIndex(t *testing.T) { 377 var input = []byte( 378 `{ 379 "apiVersion": "v1", 380 "kind": "Pod", 381 "spec": { 382 "containers": [ 383 { 384 "image": "radial/busyboxplus:curl", 385 "name": "fake0" 386 }, 387 { 388 "image": "radial/busyboxplus:curl", 389 "name": "fake1" 390 }, 391 { 392 "image": "radial/busyboxplus:curl", 393 "name": "fake2" 394 }, 395 { 396 "image": "radial/busyboxplus:curl", 397 "name": "fake3" 398 }]}}`) 399 400 var data interface{} 401 err := json.Unmarshal(input, &data) 402 if err != nil { 403 t.Fatal(err) 404 } 405 406 testJSONPath( 407 []jsonpathTest{ 408 { 409 "test containers[0], it equals containers[0]", 410 `{.spec.containers[0].name}`, 411 data, 412 "fake0", 413 false, 414 }, 415 { 416 "test containers[0:0], it equals the empty set", 417 `{.spec.containers[0:0].name}`, 418 data, 419 "", 420 false, 421 }, 422 { 423 "test containers[0:-1], it equals containers[0:3]", 424 `{.spec.containers[0:-1].name}`, 425 data, 426 "fake0 fake1 fake2", 427 false, 428 }, 429 { 430 "test containers[-1:0], expect error", 431 `{.spec.containers[-1:0].name}`, 432 data, 433 "", 434 true, 435 }, 436 { 437 "test containers[-1], it equals containers[3]", 438 `{.spec.containers[-1].name}`, 439 data, 440 "fake3", 441 false, 442 }, 443 { 444 "test containers[-1:], it equals containers[3:]", 445 `{.spec.containers[-1:].name}`, 446 data, 447 "fake3", 448 false, 449 }, 450 { 451 "test containers[-2], it equals containers[2]", 452 `{.spec.containers[-2].name}`, 453 data, 454 "fake2", 455 false, 456 }, 457 { 458 "test containers[-2:], it equals containers[2:]", 459 `{.spec.containers[-2:].name}`, 460 data, 461 "fake2 fake3", 462 false, 463 }, 464 { 465 "test containers[-3], it equals containers[1]", 466 `{.spec.containers[-3].name}`, 467 data, 468 "fake1", 469 false, 470 }, 471 { 472 "test containers[-4], it equals containers[0]", 473 `{.spec.containers[-4].name}`, 474 data, 475 "fake0", 476 false, 477 }, 478 { 479 "test containers[-4:], it equals containers[0:]", 480 `{.spec.containers[-4:].name}`, 481 data, 482 "fake0 fake1 fake2 fake3", 483 false, 484 }, 485 { 486 "test containers[-5], expect a error cause it out of bounds", 487 `{.spec.containers[-5].name}`, 488 data, 489 "", 490 true, // expect error 491 }, 492 { 493 "test containers[5:5], expect empty set", 494 `{.spec.containers[5:5].name}`, 495 data, 496 "", 497 false, 498 }, 499 { 500 "test containers[-5:-5], expect empty set", 501 `{.spec.containers[-5:-5].name}`, 502 data, 503 "", 504 false, 505 }, 506 { 507 "test containers[3:1], expect a error cause start index is greater than end index", 508 `{.spec.containers[3:1].name}`, 509 data, 510 "", 511 true, 512 }, 513 { 514 "test containers[-1:-2], it equals containers[3:2], expect a error cause start index is greater than end index", 515 `{.spec.containers[-1:-2].name}`, 516 data, 517 "", 518 true, 519 }, 520 }, 521 false, 522 t, 523 ) 524 } 525 526 func TestStep(t *testing.T) { 527 var input = []byte( 528 `{ 529 "apiVersion": "v1", 530 "kind": "Pod", 531 "spec": { 532 "containers": [ 533 { 534 "image": "radial/busyboxplus:curl", 535 "name": "fake0" 536 }, 537 { 538 "image": "radial/busyboxplus:curl", 539 "name": "fake1" 540 }, 541 { 542 "image": "radial/busyboxplus:curl", 543 "name": "fake2" 544 }, 545 { 546 "image": "radial/busyboxplus:curl", 547 "name": "fake3" 548 }, 549 { 550 "image": "radial/busyboxplus:curl", 551 "name": "fake4" 552 }, 553 { 554 "image": "radial/busyboxplus:curl", 555 "name": "fake5" 556 }]}}`) 557 558 var data interface{} 559 err := json.Unmarshal(input, &data) 560 if err != nil { 561 t.Fatal(err) 562 } 563 564 testJSONPath( 565 []jsonpathTest{ 566 { 567 "test containers[0:], it equals containers[0:6:1]", 568 `{.spec.containers[0:].name}`, 569 data, 570 "fake0 fake1 fake2 fake3 fake4 fake5", 571 false, 572 }, 573 { 574 "test containers[0:6:], it equals containers[0:6:1]", 575 `{.spec.containers[0:6:].name}`, 576 data, 577 "fake0 fake1 fake2 fake3 fake4 fake5", 578 false, 579 }, 580 { 581 "test containers[0:6:1]", 582 `{.spec.containers[0:6:1].name}`, 583 data, 584 "fake0 fake1 fake2 fake3 fake4 fake5", 585 false, 586 }, 587 { 588 "test containers[0:6:0], it errors", 589 `{.spec.containers[0:6:0].name}`, 590 data, 591 "", 592 true, 593 }, 594 { 595 "test containers[0:6:-1], it errors", 596 `{.spec.containers[0:6:-1].name}`, 597 data, 598 "", 599 true, 600 }, 601 { 602 "test containers[1:4:2]", 603 `{.spec.containers[1:4:2].name}`, 604 data, 605 "fake1 fake3", 606 false, 607 }, 608 { 609 "test containers[1:4:3]", 610 `{.spec.containers[1:4:3].name}`, 611 data, 612 "fake1", 613 false, 614 }, 615 { 616 "test containers[1:4:4]", 617 `{.spec.containers[1:4:4].name}`, 618 data, 619 "fake1", 620 false, 621 }, 622 { 623 "test containers[0:6:2]", 624 `{.spec.containers[0:6:2].name}`, 625 data, 626 "fake0 fake2 fake4", 627 false, 628 }, 629 { 630 "test containers[0:6:3]", 631 `{.spec.containers[0:6:3].name}`, 632 data, 633 "fake0 fake3", 634 false, 635 }, 636 { 637 "test containers[0:6:5]", 638 `{.spec.containers[0:6:5].name}`, 639 data, 640 "fake0 fake5", 641 false, 642 }, 643 { 644 "test containers[0:6:6]", 645 `{.spec.containers[0:6:6].name}`, 646 data, 647 "fake0", 648 false, 649 }, 650 }, 651 false, 652 t, 653 ) 654 }