Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

extend selector with EXISTS, new tests/some old tests use table-driven style #951

Merged
merged 1 commit into from
Oct 7, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions pkg/labels/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import (

// Labels allows you to present labels independently from their storage.
type Labels interface {
// Has returns whether the provided label exists.
Has(label string) (exists bool)

// Get returns the value for the provided label.
Get(label string) (value string)
}
Expand All @@ -42,6 +45,12 @@ func (ls Set) String() string {
return strings.Join(selector, ",")
}

// Has returns whether the provided label exists in the map.
func (ls Set) Has(label string) bool {
_, exists := ls[label]
return exists
}

// Get returns the value in the map for the provided label.
func (ls Set) Get(label string) string {
return ls[label]
Expand Down
17 changes: 17 additions & 0 deletions pkg/labels/labels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@ func TestSetString(t *testing.T) {
// with ",=!" characters in their names.
}

func TestLabelHas(t *testing.T) {
labelHasTests := []struct {
Ls Labels
Key string
Has bool
}{
{Set{"x": "y"}, "x", true},
{Set{"x": ""}, "x", true},
{Set{"x": "y"}, "foo", false},
}
for _, lh := range labelHasTests {
if has := lh.Ls.Has(lh.Key); has != lh.Has {
t.Errorf("%#v.Has(%#v) => %v, expected %v", lh.Ls, lh.Key, has, lh.Has)
}
}
}

func TestLabelGet(t *testing.T) {
ls := Set{"x": "y"}
if ls.Get("x") != "y" {
Expand Down
146 changes: 130 additions & 16 deletions pkg/labels/selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package labels

import (
"bytes"
"fmt"
"sort"
"strings"
Expand Down Expand Up @@ -138,46 +139,159 @@ func (t andTerm) String() string {

// Operator represents a key's relationship
// to a set of values in a Requirement.
// TODO: Should also represent key's existence.
type Operator int

const (
IN Operator = iota + 1
NOT_IN
In Operator = iota + 1
NotIn
Exists
)

// LabelSelector only not named 'Selector' due
// to name conflict until Selector is deprecated.
// LabelSelector contains a list of Requirements.
// LabelSelector is set-based and is distinguished from exact
// match-based selectors composed of key=value matching conjunctions.
// TODO: Remove previous sentence when exact match-based
// selectors are removed.
type LabelSelector struct {
Requirements []Requirement
}

// Requirement is a selector that contains values, a key
// and an operator that relates the key and values. The zero
// value of Requirement is invalid. See the NewRequirement
// constructor for creating a valid Requirement.
// Requirement is set-based and is distinguished from exact
// match-based selectors composed of key=value matching.
// TODO: Remove previous sentence when exact match-based
// selectors are removed.
type Requirement struct {
key string
operator Operator
strValues util.StringSet
}

func (r *Requirement) Matches(ls Labels) bool {
// NewRequirement is the constructor for a Requirement.
// If either of these rules is violated, an error is returned:
// (1) The operator can only be In, NotIn or Exists.
// (2) If the operator is In or NotIn, the values set must
// be non-empty.
//
// The empty string is a valid value in the input values set.
func NewRequirement(key string, op Operator, vals util.StringSet) (*Requirement, error) {
switch op {
case In, NotIn:
if len(vals) == 0 {
return nil, fmt.Errorf("for In,NotIn operators, values set can't be empty")
}
case Exists:
default:
return nil, fmt.Errorf("operator '%v' is not recognized", op)
}
return &Requirement{key: key, operator: op, strValues: vals}, nil
}

// Matches returns true if the Requirement matches the input Labels.
// There is a match in the following cases:
// (1) The operator is Exists and Labels has the Requirement's key.
// (2) The operator is In, Labels has the Requirement's key and Labels'
// value for that key is in Requirement's value set.
// (3) The operator is NotIn, Labels has the Requirement's key and
// Labels' value for that key is not in Requirement's value set.
// (4) The operator is NotIn and Labels does not have the
// Requirement's key.
//
// If called on an invalid Requirement, an error is returned. See
// NewRequirement for creating a valid Requirement.
func (r *Requirement) Matches(ls Labels) (bool, error) {
switch r.operator {
case IN:
return r.strValues.Has(ls.Get(r.key))
case NOT_IN:
return !r.strValues.Has(ls.Get(r.key))
case In:
if !ls.Has(r.key) {
return false, nil
}
return r.strValues.Has(ls.Get(r.key)), nil
case NotIn:
if !ls.Has(r.key) {
return true, nil
}
return !r.strValues.Has(ls.Get(r.key)), nil
case Exists:
return ls.Has(r.key), nil
default:
return false
return false, fmt.Errorf("requirement is not set: %+v", r)
}
}

func (sg *LabelSelector) Matches(ls Labels) bool {
for _, req := range sg.Requirements {
if !req.Matches(ls) {
return false
// String returns a human-readable string that represents this
// Requirement. If called on an invalid Requirement, an error is
// returned. See NewRequirement for creating a valid Requirement.
func (r *Requirement) String() (string, error) {
var buffer bytes.Buffer
buffer.WriteString(r.key)

switch r.operator {
case In:
buffer.WriteString(" in ")
case NotIn:
buffer.WriteString(" not in ")
case Exists:
return buffer.String(), nil
default:
return "", fmt.Errorf("requirement is not set: %+v", r)
}

buffer.WriteString("(")
if len(r.strValues) == 1 {
buffer.WriteString(r.strValues.List()[0])
} else { // only > 1 since == 0 prohibited by NewRequirement
buffer.WriteString(strings.Join(r.strValues.List(), ","))
}
buffer.WriteString(")")
return buffer.String(), nil
}

// Matches for a LabelSelector returns true if all
// its Requirements match the input Labels. If any
// Requirement does not match, false is returned.
// An error is returned if any match attempt between
// a Requirement and the input Labels returns an error.
func (lsel *LabelSelector) Matches(l Labels) (bool, error) {
for _, req := range lsel.Requirements {
if matches, err := req.Matches(l); err != nil {
return false, err
} else if !matches {
return false, nil
}
}
return true
return true, nil
}

// String returns a comma-separated string of all
// the LabelSelector Requirements' human-readable strings.
// An error is returned if any attempt to get a
// Requirement's human-readable string returns an error.
func (lsel *LabelSelector) String() (string, error) {
var reqs []string
for _, req := range lsel.Requirements {
if str, err := req.String(); err != nil {
return "", err
} else {
reqs = append(reqs, str)
}
}
return strings.Join(reqs, ","), nil
}

// TODO: Parse takes a string representing a selector and returns
// a selector, or an error. A well-formed input string follows
// the syntax of that which is returned by LabelSelector.String
// and therefore is largely controlled by that which is returned
// by Requirement.String. The returned selector object's type
// should be an interface implemented by LabelSelector. Note that
// this parsing function is different than ParseSelector since
// they parse different selectors with different syntaxes.
// See comments above for LabelSelector and Requirement struct
// definition for more details.

func try(selectorPiece, op string) (lhs, rhs string, ok bool) {
pieces := strings.Split(selectorPiece, op)
if len(pieces) == 2 {
Expand Down
128 changes: 86 additions & 42 deletions pkg/labels/selector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,56 +199,100 @@ func TestRequiresExactMatch(t *testing.T) {
}
}

func expectMatchRequirement(t *testing.T, req Requirement, ls Set) {
if !req.Matches(ls) {
t.Errorf("Wanted '%+v' to match '%s', but it did not.\n", req, ls)
}
}

func expectNoMatchRequirement(t *testing.T, req Requirement, ls Set) {
if req.Matches(ls) {
t.Errorf("Wanted '%+v' to not match '%s', but it did.", req, ls)
func TestRequirementConstructor(t *testing.T) {
requirementConstructorTests := []struct {
Key string
Op Operator
Vals util.StringSet
Success bool
}{
{"x", 8, util.NewStringSet("foo"), false},
{"x", In, nil, false},
{"x", NotIn, util.NewStringSet(), false},
{"x", In, util.NewStringSet("foo"), true},
{"x", NotIn, util.NewStringSet("foo"), true},
{"x", Exists, nil, true},
}
for _, rc := range requirementConstructorTests {
if _, err := NewRequirement(rc.Key, rc.Op, rc.Vals); err == nil && !rc.Success {
t.Errorf("expected error with key:%#v op:%v vals:%v, got no error", rc.Key, rc.Op, rc.Vals)
} else if err != nil && rc.Success {
t.Errorf("expected no error with key:%#v op:%v vals:%v, got:%v", rc.Key, rc.Op, rc.Vals, err)
}
}
}

func TestRequirementMatches(t *testing.T) {
s := Set{"x": "foo", "y": "baz"}
a := Requirement{key: "x", operator: IN, strValues: util.NewStringSet("foo")}
b := Requirement{key: "x", operator: NOT_IN, strValues: util.NewStringSet("beta")}
c := Requirement{key: "y", operator: IN, strValues: nil}
d := Requirement{key: "y", strValues: util.NewStringSet("foo")}
expectMatchRequirement(t, a, s)
expectMatchRequirement(t, b, s)
expectNoMatchRequirement(t, c, s)
expectNoMatchRequirement(t, d, s)
}

func expectMatchLabSelector(t *testing.T, lsel LabelSelector, s Set) {
if !lsel.Matches(s) {
t.Errorf("Wanted '%+v' to match '%s', but it did not.\n", lsel, s)
func TestToString(t *testing.T) {
var req Requirement
toStringTests := []struct {
In *LabelSelector
Out string
Valid bool
}{
{&LabelSelector{Requirements: []Requirement{
getRequirement("x", In, util.NewStringSet("abc", "def"), t),
getRequirement("y", NotIn, util.NewStringSet("jkl"), t),
getRequirement("z", Exists, nil, t),
}}, "x in (abc,def),y not in (jkl),z", true},
{&LabelSelector{Requirements: []Requirement{
getRequirement("x", In, util.NewStringSet("abc", "def"), t),
req,
}}, "", false},
{&LabelSelector{Requirements: []Requirement{
getRequirement("x", NotIn, util.NewStringSet("abc"), t),
getRequirement("y", In, util.NewStringSet("jkl", "mno"), t),
getRequirement("z", NotIn, util.NewStringSet(""), t),
}}, "x not in (abc),y in (jkl,mno),z not in ()", true},
}
for _, ts := range toStringTests {
if out, err := ts.In.String(); err != nil && ts.Valid {
t.Errorf("%+v.String() => %v, expected no error", ts.In, err)
} else if out != ts.Out {
t.Errorf("%+v.String() => %v, want %v", ts.In, out, ts.Out)
}
}
}

func expectNoMatchLabSelector(t *testing.T, lsel LabelSelector, s Set) {
if lsel.Matches(s) {
t.Errorf("Wanted '%+v' to not match '%s', but it did.\n", lsel, s)
func TestRequirementLabelSelectorMatching(t *testing.T) {
var req Requirement
labelSelectorMatchingTests := []struct {
Set Set
Sel *LabelSelector
Match bool
Valid bool
}{
{Set{"x": "foo", "y": "baz"}, &LabelSelector{Requirements: []Requirement{
req,
}}, false, false},
{Set{"x": "foo", "y": "baz"}, &LabelSelector{Requirements: []Requirement{
getRequirement("x", In, util.NewStringSet("foo"), t),
getRequirement("y", NotIn, util.NewStringSet("alpha"), t),
}}, true, true},
{Set{"x": "foo", "y": "baz"}, &LabelSelector{Requirements: []Requirement{
getRequirement("x", In, util.NewStringSet("foo"), t),
getRequirement("y", In, util.NewStringSet("alpha"), t),
}}, false, true},
{Set{"y": ""}, &LabelSelector{Requirements: []Requirement{
getRequirement("x", NotIn, util.NewStringSet(""), t),
getRequirement("y", Exists, nil, t),
}}, true, true},
{Set{"y": "baz"}, &LabelSelector{Requirements: []Requirement{
getRequirement("x", In, util.NewStringSet(""), t),
}}, false, true},
}
for _, lsm := range labelSelectorMatchingTests {
if match, err := lsm.Sel.Matches(lsm.Set); err != nil && lsm.Valid {
t.Errorf("%+v.Matches(%#v) => %v, expected no error", lsm.Sel, lsm.Set, err)
} else if match != lsm.Match {
t.Errorf("%+v.Matches(%#v) => %v, want %v", lsm.Sel, lsm.Set, match, lsm.Match)
}
}
}

func TestLabelSelectorMatches(t *testing.T) {
s := Set{"x": "foo", "y": "baz"}
allMatch := LabelSelector{
Requirements: []Requirement{
{key: "x", operator: IN, strValues: util.NewStringSet("foo")},
{key: "y", operator: NOT_IN, strValues: util.NewStringSet("alpha")},
},
}
singleNonMatch := LabelSelector{
Requirements: []Requirement{
{key: "x", operator: IN, strValues: util.NewStringSet("foo")},
{key: "y", operator: IN, strValues: util.NewStringSet("alpha")},
},
func getRequirement(key string, op Operator, vals util.StringSet, t *testing.T) Requirement {
req, err := NewRequirement(key, op, vals)
if err != nil {
t.Errorf("NewRequirement(%v, %v, %v) resulted in error:%v", key, op, vals, err)
}
expectMatchLabSelector(t, allMatch, s)
expectNoMatchLabSelector(t, singleNonMatch, s)
return *req
}