Skip to content

Commit

Permalink
Merge pull request #88 from hashicorp/constraints-equality
Browse files Browse the repository at this point in the history
Constraint(s): introduce `Equals()` and `sort.Interface`
  • Loading branch information
radeksimko committed Nov 22, 2021
2 parents ac9bfc9 + feceee7 commit d7f6c9b
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 11 deletions.
98 changes: 87 additions & 11 deletions constraint.go
Expand Up @@ -4,37 +4,48 @@ import (
"fmt"
"reflect"
"regexp"
"sort"
"strings"
)

// Constraint represents a single constraint for a version, such as
// ">= 1.0".
type Constraint struct {
f constraintFunc
op operator
check *Version
original string
}

func (c *Constraint) Equals(con *Constraint) bool {
return c.op == con.op && c.check.Equal(con.check)
}

// Constraints is a slice of constraints. We make a custom type so that
// we can add methods to it.
type Constraints []*Constraint

type constraintFunc func(v, c *Version) bool

var constraintOperators map[string]constraintFunc
var constraintOperators map[string]constraintOperation

type constraintOperation struct {
op operator
f constraintFunc
}

var constraintRegexp *regexp.Regexp

func init() {
constraintOperators = map[string]constraintFunc{
"": constraintEqual,
"=": constraintEqual,
"!=": constraintNotEqual,
">": constraintGreaterThan,
"<": constraintLessThan,
">=": constraintGreaterThanEqual,
"<=": constraintLessThanEqual,
"~>": constraintPessimistic,
constraintOperators = map[string]constraintOperation{
"": {op: equal, f: constraintEqual},
"=": {op: equal, f: constraintEqual},
"!=": {op: notEqual, f: constraintNotEqual},
">": {op: greaterThan, f: constraintGreaterThan},
"<": {op: lessThan, f: constraintLessThan},
">=": {op: greaterThanEqual, f: constraintGreaterThanEqual},
"<=": {op: lessThanEqual, f: constraintLessThanEqual},
"~>": {op: pessimistic, f: constraintPessimistic},
}

ops := make([]string, 0, len(constraintOperators))
Expand Down Expand Up @@ -87,6 +98,56 @@ func (cs Constraints) Check(v *Version) bool {
return true
}

// Equals compares Constraints with other Constraints
// for equality. This may not represent logical equivalence
// of compared constraints.
// e.g. even though '>0.1,>0.2' is logically equivalent
// to '>0.2' it is *NOT* treated as equal.
//
// Missing operator is treated as equal to '=', whitespaces
// are ignored and constraints are sorted before comaparison.
func (cs Constraints) Equals(c Constraints) bool {
if len(cs) != len(c) {
return false
}

// make copies to retain order of the original slices
left := make(Constraints, len(cs))
copy(left, cs)
sort.Stable(left)
right := make(Constraints, len(c))
copy(right, c)
sort.Stable(right)

// compare sorted slices
for i, con := range left {
if !con.Equals(right[i]) {
return false
}
}

return true
}

func (cs Constraints) Len() int {
return len(cs)
}

func (cs Constraints) Less(i, j int) bool {
if cs[i].op < cs[j].op {
return true
}
if cs[i].op > cs[j].op {
return false
}

return cs[i].check.LessThan(cs[j].check)
}

func (cs Constraints) Swap(i, j int) {
cs[i], cs[j] = cs[j], cs[i]
}

// Returns the string format of the constraints
func (cs Constraints) String() string {
csStr := make([]string, len(cs))
Expand Down Expand Up @@ -117,8 +178,11 @@ func parseSingle(v string) (*Constraint, error) {
return nil, err
}

cop := constraintOperators[matches[1]]

return &Constraint{
f: constraintOperators[matches[1]],
f: cop.f,
op: cop.op,
check: check,
original: v,
}, nil
Expand Down Expand Up @@ -148,6 +212,18 @@ func prereleaseCheck(v, c *Version) bool {
// Constraint functions
//-------------------------------------------------------------------

type operator rune

const (
equal operator = '='
notEqual operator = '≠'
greaterThan operator = '>'
lessThan operator = '<'
greaterThanEqual operator = '≥'
lessThanEqual operator = '≤'
pessimistic operator = '~'
)

func constraintEqual(v, c *Version) bool {
return v.Equal(c)
}
Expand Down
101 changes: 101 additions & 0 deletions constraint_test.go
@@ -1,6 +1,9 @@
package version

import (
"fmt"
"reflect"
"sort"
"testing"
)

Expand Down Expand Up @@ -97,6 +100,104 @@ func TestConstraintCheck(t *testing.T) {
}
}

func TestConstraintEqual(t *testing.T) {
cases := []struct {
leftConstraint string
rightConstraint string
expectedEqual bool
}{
{
"0.0.1",
"0.0.1",
true,
},
{ // whitespaces
" 0.0.1 ",
"0.0.1",
true,
},
{ // equal op implied
"=0.0.1 ",
"0.0.1",
true,
},
{ // version difference
"=0.0.1",
"=0.0.2",
false,
},
{ // operator difference
">0.0.1",
"=0.0.1",
false,
},
{ // different order
">0.1.0, <=1.0.0",
"<=1.0.0, >0.1.0",
true,
},
}

for _, tc := range cases {
leftCon, err := NewConstraint(tc.leftConstraint)
if err != nil {
t.Fatalf("err: %s", err)
}
rightCon, err := NewConstraint(tc.rightConstraint)
if err != nil {
t.Fatalf("err: %s", err)
}

actual := leftCon.Equals(rightCon)
if actual != tc.expectedEqual {
t.Fatalf("Constraints: %s vs %s\nExpected: %t\nActual: %t",
tc.leftConstraint, tc.rightConstraint, tc.expectedEqual, actual)
}
}
}

func TestConstraint_sort(t *testing.T) {
cases := []struct {
constraint string
expectedConstraints string
}{
{
">= 0.1.0,< 1.12",
"< 1.12,>= 0.1.0",
},
{
"< 1.12,>= 0.1.0",
"< 1.12,>= 0.1.0",
},
{
"< 1.12,>= 0.1.0,0.2.0",
"< 1.12,0.2.0,>= 0.1.0",
},
{
">1.0,>0.1.0,>0.3.0,>0.2.0",
">0.1.0,>0.2.0,>0.3.0,>1.0",
},
}

for i, tc := range cases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
c, err := NewConstraint(tc.constraint)
if err != nil {
t.Fatalf("err: %s", err)
}

sort.Sort(c)

actual := c.String()

if !reflect.DeepEqual(actual, tc.expectedConstraints) {
t.Fatalf("unexpected order\nexpected: %#v\nactual: %#v",
tc.expectedConstraints, actual)
}
})
}
}

func TestConstraintsString(t *testing.T) {
cases := []struct {
constraint string
Expand Down

0 comments on commit d7f6c9b

Please sign in to comment.