/* Copyright 2014 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. */ /* This file is derived from https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector_test.go */ package queryparser import ( "fmt" "reflect" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/rancher/steve/pkg/stores/sqlpartition/selection" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" ) var ( ignoreDetail = cmpopts.IgnoreFields(field.Error{}, "Detail") ) func TestSelectorParse(t *testing.T) { testGoodStrings := []string{ "x=a,y=b,z=c", "", "x!=a,y=b", "close ~ value", "notclose !~ value", "x>1", "x>1,z<5", "x gt 1,z lt 5", `y == def`, "metadata.labels.im-here", "!metadata.labels.im-not-here", "metadata.labels[im.here]", "!metadata.labels[im.not.here]", "metadata.labels[k8s.io/meta-stuff] ~ has-dashes_underscores.dots.only", } testBadStrings := []string{ "!no-label-absence-test", "no-label-presence-test", "x=a||y=b", "x==a==b", "!x=a", "x", GreaterThanToken}, {"<", LessThanToken}, //Note that Lex returns the longest valid token found {"!", DoesNotExistToken}, {"!=", NotEqualsToken}, {"(", OpenParToken}, {")", ClosedParToken}, {`'sq string''`, ErrorToken}, {`"dq string"`, ErrorToken}, {"~", PartialEqualsToken}, {"!~", NotPartialEqualsToken}, {"||", ErrorToken}, } for _, v := range testcases { l := &Lexer{s: v.s, pos: 0} token, lit := l.Lex() if token != v.t { t.Errorf("Got %d it should be %d for '%s'", token, v.t, v.s) } if v.t != ErrorToken && lit != v.s { t.Errorf("Got '%s' it should be '%s'", lit, v.s) } } } func min(l, r int) (m int) { m = r if l < r { m = l } return m } func TestLexerSequence(t *testing.T) { testcases := []struct { s string t []Token }{ {"key in ( value )", []Token{IdentifierToken, InToken, OpenParToken, IdentifierToken, ClosedParToken}}, {"key notin ( value )", []Token{IdentifierToken, NotInToken, OpenParToken, IdentifierToken, ClosedParToken}}, {"key in ( value1, value2 )", []Token{IdentifierToken, InToken, OpenParToken, IdentifierToken, CommaToken, IdentifierToken, ClosedParToken}}, {"key", []Token{IdentifierToken}}, {"!key", []Token{DoesNotExistToken, IdentifierToken}}, {"()", []Token{OpenParToken, ClosedParToken}}, {"x in (),y", []Token{IdentifierToken, InToken, OpenParToken, ClosedParToken, CommaToken, IdentifierToken}}, {"== != (), = notin", []Token{DoubleEqualsToken, NotEqualsToken, OpenParToken, ClosedParToken, CommaToken, EqualsToken, NotInToken}}, {"key>2", []Token{IdentifierToken, GreaterThanToken, IdentifierToken}}, {"key<1", []Token{IdentifierToken, LessThanToken, IdentifierToken}}, {"key gt 3", []Token{IdentifierToken, IdentifierToken, IdentifierToken}}, {"key lt 4", []Token{IdentifierToken, IdentifierToken, IdentifierToken}}, {"key=value", []Token{IdentifierToken, EqualsToken, IdentifierToken}}, {"key == value", []Token{IdentifierToken, DoubleEqualsToken, IdentifierToken}}, {"key ~ value", []Token{IdentifierToken, PartialEqualsToken, IdentifierToken}}, {"key~ value", []Token{IdentifierToken, PartialEqualsToken, IdentifierToken}}, {"key ~value", []Token{IdentifierToken, PartialEqualsToken, IdentifierToken}}, {"key~value", []Token{IdentifierToken, PartialEqualsToken, IdentifierToken}}, {"key !~ value", []Token{IdentifierToken, NotPartialEqualsToken, IdentifierToken}}, {"key!~ value", []Token{IdentifierToken, NotPartialEqualsToken, IdentifierToken}}, {"key !~value", []Token{IdentifierToken, NotPartialEqualsToken, IdentifierToken}}, {"key!~value", []Token{IdentifierToken, NotPartialEqualsToken, IdentifierToken}}, } for _, v := range testcases { var tokens []Token l := &Lexer{s: v.s, pos: 0} for { token, _ := l.Lex() if token == EndOfStringToken { break } tokens = append(tokens, token) } if len(tokens) != len(v.t) { t.Errorf("Bad number of tokens for '%s': got %d, wanted %d (got %v)", v.s, len(tokens), len(v.t), tokens) } for i := 0; i < min(len(tokens), len(v.t)); i++ { if tokens[i] != v.t[i] { t.Errorf("Test '%s': Mismatching in token type found '%v' it should be '%v'", v.s, tokens[i], v.t[i]) } } } } func TestParserLookahead(t *testing.T) { testcases := []struct { s string t []Token }{ {"key in ( value )", []Token{IdentifierToken, InToken, OpenParToken, IdentifierToken, ClosedParToken, EndOfStringToken}}, {"key notin ( value )", []Token{IdentifierToken, NotInToken, OpenParToken, IdentifierToken, ClosedParToken, EndOfStringToken}}, {"key in ( value1, value2 )", []Token{IdentifierToken, InToken, OpenParToken, IdentifierToken, CommaToken, IdentifierToken, ClosedParToken, EndOfStringToken}}, {"key", []Token{IdentifierToken, EndOfStringToken}}, {"!key", []Token{DoesNotExistToken, IdentifierToken, EndOfStringToken}}, {"()", []Token{OpenParToken, ClosedParToken, EndOfStringToken}}, {"", []Token{EndOfStringToken}}, {"x in (),y", []Token{IdentifierToken, InToken, OpenParToken, ClosedParToken, CommaToken, IdentifierToken, EndOfStringToken}}, {"== != (), = notin", []Token{DoubleEqualsToken, NotEqualsToken, OpenParToken, ClosedParToken, CommaToken, EqualsToken, NotInToken, EndOfStringToken}}, {"key>2", []Token{IdentifierToken, GreaterThanToken, IdentifierToken, EndOfStringToken}}, {"key<1", []Token{IdentifierToken, LessThanToken, IdentifierToken, EndOfStringToken}}, {"key gt 3", []Token{IdentifierToken, GreaterThanToken, IdentifierToken, EndOfStringToken}}, {"key lt 4", []Token{IdentifierToken, LessThanToken, IdentifierToken, EndOfStringToken}}, {`key = multi-word-string`, []Token{IdentifierToken, EqualsToken, QuotedStringToken, EndOfStringToken}}, } for _, v := range testcases { p := &Parser{l: &Lexer{s: v.s, pos: 0}, position: 0} p.scan() if len(p.scannedItems) != len(v.t) { t.Errorf("Expected %d items for test %s, found %d", len(v.t), v.s, len(p.scannedItems)) } for { token, lit := p.lookahead(KeyAndOperator) token2, lit2 := p.consume(KeyAndOperator) if token == EndOfStringToken { break } if token != token2 || lit != lit2 { t.Errorf("Bad values") } } } } func TestParseOperator(t *testing.T) { testcases := []struct { token string expectedError error }{ {"in", nil}, {"=", nil}, {"==", nil}, {"~", nil}, {">", nil}, {"<", nil}, {"lt", nil}, {"gt", nil}, {"notin", nil}, {"!=", nil}, {"!~", nil}, {"!", fmt.Errorf("found '%s', expected: %v", selection.DoesNotExist, strings.Join(binaryOperators, ", "))}, {"exists", fmt.Errorf("found '%s', expected: %v", selection.Exists, strings.Join(binaryOperators, ", "))}, {"(", fmt.Errorf("found '%s', expected: %v", "(", strings.Join(binaryOperators, ", "))}, } for _, testcase := range testcases { p := &Parser{l: &Lexer{s: testcase.token, pos: 0}, position: 0} p.scan() _, err := p.parseOperator() if ok := reflect.DeepEqual(testcase.expectedError, err); !ok { t.Errorf("\nexpect err [%v], \nactual err [%v]", testcase.expectedError, err) } } } // Some error fields are commented out here because this fork no longer // enforces k8s label expression lexical and length restrictions func TestRequirementConstructor(t *testing.T) { requirementConstructorTests := []struct { Key string Op selection.Operator Vals sets.String WantErr field.ErrorList }{ { Key: "x1", Op: selection.In, WantErr: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "values", BadValue: []string{}, }, }, }, { Key: "x2", Op: selection.NotIn, Vals: sets.NewString(), WantErr: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "values", BadValue: []string{}, }, }, }, { Key: "x3", Op: selection.In, Vals: sets.NewString("foo"), }, { Key: "x4", Op: selection.NotIn, Vals: sets.NewString("foo"), }, { Key: "x5", Op: selection.Equals, Vals: sets.NewString("foo", "bar"), WantErr: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "values", BadValue: []string{"bar", "foo"}, }, }, }, { Key: "x6", Op: selection.Exists, }, { Key: "x7", Op: selection.DoesNotExist, }, { Key: "x8", Op: selection.Exists, Vals: sets.NewString("foo"), WantErr: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "values", BadValue: []string{"foo"}, }, }, }, { Key: "x9", Op: selection.In, Vals: sets.NewString("bar"), }, { Key: "x10", Op: selection.In, Vals: sets.NewString("bar"), }, { Key: "x11", Op: selection.GreaterThan, Vals: sets.NewString("1"), }, { Key: "x12", Op: selection.LessThan, Vals: sets.NewString("6"), }, { Key: "x13", Op: selection.GreaterThan, WantErr: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "values", BadValue: []string{}, }, }, }, { Key: "x14", Op: selection.GreaterThan, Vals: sets.NewString("bar"), WantErr: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "values[0]", BadValue: "bar", }, }, }, { Key: "x15", Op: selection.LessThan, Vals: sets.NewString("bar"), WantErr: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "values[0]", BadValue: "bar", }, }, }, { Key: strings.Repeat("a", 254), //breaks DNS rule that len(key) <= 253 Op: selection.Exists, }, { Key: "x16", Op: selection.Equals, Vals: sets.NewString(strings.Repeat("a", 254)), }, { Key: "x17", Op: selection.Equals, Vals: sets.NewString("a b"), }, { Key: "x18", Op: "unsupportedOp", WantErr: field.ErrorList{ &field.Error{ Type: field.ErrorTypeNotSupported, Field: "operator", BadValue: selection.Operator("unsupportedOp"), }, }, }, } for _, rc := range requirementConstructorTests { _, err := NewRequirement(rc.Key, rc.Op, rc.Vals.List()) if diff := cmp.Diff(rc.WantErr.ToAggregate(), err, ignoreDetail); diff != "" { t.Errorf("NewRequirement test %v returned unexpected error (-want,+got):\n%s", rc.Key, diff) } } }