| /* |
| Copyright 2015 The Kubernetes Authors. |
| |
| 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 jsonpath |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "reflect" |
| "sort" |
| "strings" |
| "testing" |
| ) |
| |
| type jsonpathTest struct { |
| name string |
| template string |
| input interface{} |
| expect string |
| expectError bool |
| } |
| |
| func testJSONPath(tests []jsonpathTest, allowMissingKeys bool, t *testing.T) { |
| for _, test := range tests { |
| j := New(test.name) |
| j.AllowMissingKeys(allowMissingKeys) |
| err := j.Parse(test.template) |
| if err != nil { |
| t.Errorf("in %s, parse %s error %v", test.name, test.template, err) |
| } |
| buf := new(bytes.Buffer) |
| err = j.Execute(buf, test.input) |
| if test.expectError { |
| if test.expectError && err == nil { |
| t.Errorf("in %s, expected execute error", test.name) |
| } |
| continue |
| } else if err != nil { |
| t.Errorf("in %s, execute error %v", test.name, err) |
| } |
| out := buf.String() |
| if out != test.expect { |
| t.Errorf(`in %s, expect to get "%s", got "%s"`, test.name, test.expect, out) |
| } |
| } |
| } |
| |
| // testJSONPathSortOutput test cases related to map, the results may print in random order |
| func testJSONPathSortOutput(tests []jsonpathTest, t *testing.T) { |
| for _, test := range tests { |
| j := New(test.name) |
| err := j.Parse(test.template) |
| if err != nil { |
| t.Errorf("in %s, parse %s error %v", test.name, test.template, err) |
| } |
| buf := new(bytes.Buffer) |
| err = j.Execute(buf, test.input) |
| if err != nil { |
| t.Errorf("in %s, execute error %v", test.name, err) |
| } |
| out := buf.String() |
| //since map is visited in random order, we need to sort the results. |
| sortedOut := strings.Fields(out) |
| sort.Strings(sortedOut) |
| sortedExpect := strings.Fields(test.expect) |
| sort.Strings(sortedExpect) |
| if !reflect.DeepEqual(sortedOut, sortedExpect) { |
| t.Errorf(`in %s, expect to get "%s", got "%s"`, test.name, test.expect, out) |
| } |
| } |
| } |
| |
| func testFailJSONPath(tests []jsonpathTest, t *testing.T) { |
| for _, test := range tests { |
| j := New(test.name) |
| err := j.Parse(test.template) |
| if err != nil { |
| t.Errorf("in %s, parse %s error %v", test.name, test.template, err) |
| } |
| buf := new(bytes.Buffer) |
| err = j.Execute(buf, test.input) |
| var out string |
| if err == nil { |
| out = "nil" |
| } else { |
| out = err.Error() |
| } |
| if out != test.expect { |
| t.Errorf("in %s, expect to get error %q, got %q", test.name, test.expect, out) |
| } |
| } |
| } |
| |
| type book struct { |
| Category string |
| Author string |
| Title string |
| Price float32 |
| } |
| |
| func (b book) String() string { |
| return fmt.Sprintf("{Category: %s, Author: %s, Title: %s, Price: %v}", b.Category, b.Author, b.Title, b.Price) |
| } |
| |
| type bicycle struct { |
| Color string |
| Price float32 |
| IsNew bool |
| } |
| |
| type empName string |
| type job string |
| type store struct { |
| Book []book |
| Bicycle []bicycle |
| Name string |
| Labels map[string]int |
| Employees map[empName]job |
| } |
| |
| func TestStructInput(t *testing.T) { |
| |
| storeData := store{ |
| Name: "jsonpath", |
| Book: []book{ |
| {"reference", "Nigel Rees", "Sayings of the Centurey", 8.95}, |
| {"fiction", "Evelyn Waugh", "Sword of Honour", 12.99}, |
| {"fiction", "Herman Melville", "Moby Dick", 8.99}, |
| }, |
| Bicycle: []bicycle{ |
| {"red", 19.95, true}, |
| {"green", 20.01, false}, |
| }, |
| Labels: map[string]int{ |
| "engieer": 10, |
| "web/html": 15, |
| "k8s-app": 20, |
| }, |
| Employees: map[empName]job{ |
| "jason": "manager", |
| "dan": "clerk", |
| }, |
| } |
| |
| storeTests := []jsonpathTest{ |
| {"plain", "hello jsonpath", nil, "hello jsonpath", false}, |
| {"recursive", "{..}", []int{1, 2, 3}, "[1 2 3]", false}, |
| {"filter", "{[?(@<5)]}", []int{2, 6, 3, 7}, "2 3", false}, |
| {"quote", `{"{"}`, nil, "{", false}, |
| {"union", "{[1,3,4]}", []int{0, 1, 2, 3, 4}, "1 3 4", false}, |
| {"array", "{[0:2]}", []string{"Monday", "Tudesday"}, "Monday Tudesday", false}, |
| {"variable", "hello {.Name}", storeData, "hello jsonpath", false}, |
| {"dict/", "{$.Labels.web/html}", storeData, "15", false}, |
| {"dict/", "{$.Employees.jason}", storeData, "manager", false}, |
| {"dict/", "{$.Employees.dan}", storeData, "clerk", false}, |
| {"dict-", "{.Labels.k8s-app}", storeData, "20", false}, |
| {"nest", "{.Bicycle[*].Color}", storeData, "red green", false}, |
| {"allarray", "{.Book[*].Author}", storeData, "Nigel Rees Evelyn Waugh Herman Melville", false}, |
| {"allfileds", "{.Bicycle.*}", storeData, "{red 19.95 true} {green 20.01 false}", false}, |
| {"recurfileds", "{..Price}", storeData, "8.95 12.99 8.99 19.95 20.01", false}, |
| {"lastarray", "{.Book[-1:]}", storeData, |
| "{Category: fiction, Author: Herman Melville, Title: Moby Dick, Price: 8.99}", false}, |
| {"recurarray", "{..Book[2]}", storeData, |
| "{Category: fiction, Author: Herman Melville, Title: Moby Dick, Price: 8.99}", false}, |
| {"bool", "{.Bicycle[?(@.IsNew==true)]}", storeData, "{red 19.95 true}", false}, |
| } |
| testJSONPath(storeTests, false, t) |
| |
| missingKeyTests := []jsonpathTest{ |
| {"nonexistent field", "{.hello}", storeData, "", false}, |
| } |
| testJSONPath(missingKeyTests, true, t) |
| |
| failStoreTests := []jsonpathTest{ |
| {"invalid identifier", "{hello}", storeData, "unrecognized identifier hello", false}, |
| {"nonexistent field", "{.hello}", storeData, "hello is not found", false}, |
| {"invalid array", "{.Labels[0]}", storeData, "map[string]int is not array or slice", false}, |
| {"invalid filter operator", "{.Book[?(@.Price<>10)]}", storeData, "unrecognized filter operator <>", false}, |
| {"redundant end", "{range .Labels.*}{@}{end}{end}", storeData, "not in range, nothing to end", false}, |
| } |
| testFailJSONPath(failStoreTests, t) |
| } |
| |
| func TestJSONInput(t *testing.T) { |
| var pointsJSON = []byte(`[ |
| {"id": "i1", "x":4, "y":-5}, |
| {"id": "i2", "x":-2, "y":-5, "z":1}, |
| {"id": "i3", "x": 8, "y": 3 }, |
| {"id": "i4", "x": -6, "y": -1 }, |
| {"id": "i5", "x": 0, "y": 2, "z": 1 }, |
| {"id": "i6", "x": 1, "y": 4 } |
| ]`) |
| var pointsData interface{} |
| err := json.Unmarshal(pointsJSON, &pointsData) |
| if err != nil { |
| t.Error(err) |
| } |
| pointsTests := []jsonpathTest{ |
| {"exists filter", "{[?(@.z)].id}", pointsData, "i2 i5", false}, |
| {"bracket key", "{[0]['id']}", pointsData, "i1", false}, |
| } |
| testJSONPath(pointsTests, false, t) |
| } |
| |
| // TestKubernetes tests some use cases from kubernetes |
| func TestKubernetes(t *testing.T) { |
| var input = []byte(`{ |
| "kind": "List", |
| "items":[ |
| { |
| "kind":"None", |
| "metadata":{ |
| "name":"127.0.0.1", |
| "labels":{ |
| "kubernetes.io/hostname":"127.0.0.1" |
| } |
| }, |
| "status":{ |
| "capacity":{"cpu":"4"}, |
| "ready": true, |
| "addresses":[{"type": "LegacyHostIP", "address":"127.0.0.1"}] |
| } |
| }, |
| { |
| "kind":"None", |
| "metadata":{ |
| "name":"127.0.0.2", |
| "labels":{ |
| "kubernetes.io/hostname":"127.0.0.2" |
| } |
| }, |
| "status":{ |
| "capacity":{"cpu":"8"}, |
| "ready": false, |
| "addresses":[ |
| {"type": "LegacyHostIP", "address":"127.0.0.2"}, |
| {"type": "another", "address":"127.0.0.3"} |
| ] |
| } |
| } |
| ], |
| "users":[ |
| { |
| "name": "myself", |
| "user": {} |
| }, |
| { |
| "name": "e2e", |
| "user": {"username": "admin", "password": "secret"} |
| } |
| ] |
| }`) |
| var nodesData interface{} |
| err := json.Unmarshal(input, &nodesData) |
| if err != nil { |
| t.Error(err) |
| } |
| |
| nodesTests := []jsonpathTest{ |
| {"range item", `{range .items[*]}{.metadata.name}, {end}{.kind}`, nodesData, "127.0.0.1, 127.0.0.2, List", false}, |
| {"range item with quote", `{range .items[*]}{.metadata.name}{"\t"}{end}`, nodesData, "127.0.0.1\t127.0.0.2\t", false}, |
| {"range addresss", `{.items[*].status.addresses[*].address}`, nodesData, |
| "127.0.0.1 127.0.0.2 127.0.0.3", false}, |
| {"double range", `{range .items[*]}{range .status.addresses[*]}{.address}, {end}{end}`, nodesData, |
| "127.0.0.1, 127.0.0.2, 127.0.0.3, ", false}, |
| {"item name", `{.items[*].metadata.name}`, nodesData, "127.0.0.1 127.0.0.2", false}, |
| {"union nodes capacity", `{.items[*]['metadata.name', 'status.capacity']}`, nodesData, |
| "127.0.0.1 127.0.0.2 map[cpu:4] map[cpu:8]", false}, |
| {"range nodes capacity", `{range .items[*]}[{.metadata.name}, {.status.capacity}] {end}`, nodesData, |
| "[127.0.0.1, map[cpu:4]] [127.0.0.2, map[cpu:8]] ", false}, |
| {"user password", `{.users[?(@.name=="e2e")].user.password}`, &nodesData, "secret", false}, |
| {"hostname", `{.items[0].metadata.labels.kubernetes\.io/hostname}`, &nodesData, "127.0.0.1", false}, |
| {"hostname filter", `{.items[?(@.metadata.labels.kubernetes\.io/hostname=="127.0.0.1")].kind}`, &nodesData, "None", false}, |
| {"bool item", `{.items[?(@..ready==true)].metadata.name}`, &nodesData, "127.0.0.1", false}, |
| } |
| testJSONPath(nodesTests, false, t) |
| |
| randomPrintOrderTests := []jsonpathTest{ |
| {"recursive name", "{..name}", nodesData, `127.0.0.1 127.0.0.2 myself e2e`, false}, |
| } |
| testJSONPathSortOutput(randomPrintOrderTests, t) |
| } |
| |
| func TestFilterPartialMatchesSometimesMissingAnnotations(t *testing.T) { |
| // for https://issues.k8s.io/45546 |
| var input = []byte(`{ |
| "kind": "List", |
| "items": [ |
| { |
| "kind": "Pod", |
| "metadata": { |
| "name": "pod1", |
| "annotations": { |
| "color": "blue" |
| } |
| } |
| }, |
| { |
| "kind": "Pod", |
| "metadata": { |
| "name": "pod2" |
| } |
| }, |
| { |
| "kind": "Pod", |
| "metadata": { |
| "name": "pod3", |
| "annotations": { |
| "color": "green" |
| } |
| } |
| }, |
| { |
| "kind": "Pod", |
| "metadata": { |
| "name": "pod4", |
| "annotations": { |
| "color": "blue" |
| } |
| } |
| } |
| ] |
| }`) |
| var data interface{} |
| err := json.Unmarshal(input, &data) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| testJSONPath( |
| []jsonpathTest{ |
| { |
| "filter, should only match a subset, some items don't have annotations, tolerate missing items", |
| `{.items[?(@.metadata.annotations.color=="blue")].metadata.name}`, |
| data, |
| "pod1 pod4", |
| false, // expect no error |
| }, |
| }, |
| true, // allow missing keys |
| t, |
| ) |
| |
| testJSONPath( |
| []jsonpathTest{ |
| { |
| "filter, should only match a subset, some items don't have annotations, error on missing items", |
| `{.items[?(@.metadata.annotations.color=="blue")].metadata.name}`, |
| data, |
| "", |
| true, // expect an error |
| }, |
| }, |
| false, // don't allow missing keys |
| t, |
| ) |
| } |