Skip to content

Commit d7f6c9b

Browse files
authored
Merge pull request #88 from hashicorp/constraints-equality
Constraint(s): introduce `Equals()` and `sort.Interface`
2 parents ac9bfc9 + feceee7 commit d7f6c9b

File tree

2 files changed

+188
-11
lines changed

2 files changed

+188
-11
lines changed

constraint.go

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,48 @@ import (
44
"fmt"
55
"reflect"
66
"regexp"
7+
"sort"
78
"strings"
89
)
910

1011
// Constraint represents a single constraint for a version, such as
1112
// ">= 1.0".
1213
type Constraint struct {
1314
f constraintFunc
15+
op operator
1416
check *Version
1517
original string
1618
}
1719

20+
func (c *Constraint) Equals(con *Constraint) bool {
21+
return c.op == con.op && c.check.Equal(con.check)
22+
}
23+
1824
// Constraints is a slice of constraints. We make a custom type so that
1925
// we can add methods to it.
2026
type Constraints []*Constraint
2127

2228
type constraintFunc func(v, c *Version) bool
2329

24-
var constraintOperators map[string]constraintFunc
30+
var constraintOperators map[string]constraintOperation
31+
32+
type constraintOperation struct {
33+
op operator
34+
f constraintFunc
35+
}
2536

2637
var constraintRegexp *regexp.Regexp
2738

2839
func init() {
29-
constraintOperators = map[string]constraintFunc{
30-
"": constraintEqual,
31-
"=": constraintEqual,
32-
"!=": constraintNotEqual,
33-
">": constraintGreaterThan,
34-
"<": constraintLessThan,
35-
">=": constraintGreaterThanEqual,
36-
"<=": constraintLessThanEqual,
37-
"~>": constraintPessimistic,
40+
constraintOperators = map[string]constraintOperation{
41+
"": {op: equal, f: constraintEqual},
42+
"=": {op: equal, f: constraintEqual},
43+
"!=": {op: notEqual, f: constraintNotEqual},
44+
">": {op: greaterThan, f: constraintGreaterThan},
45+
"<": {op: lessThan, f: constraintLessThan},
46+
">=": {op: greaterThanEqual, f: constraintGreaterThanEqual},
47+
"<=": {op: lessThanEqual, f: constraintLessThanEqual},
48+
"~>": {op: pessimistic, f: constraintPessimistic},
3849
}
3950

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

101+
// Equals compares Constraints with other Constraints
102+
// for equality. This may not represent logical equivalence
103+
// of compared constraints.
104+
// e.g. even though '>0.1,>0.2' is logically equivalent
105+
// to '>0.2' it is *NOT* treated as equal.
106+
//
107+
// Missing operator is treated as equal to '=', whitespaces
108+
// are ignored and constraints are sorted before comaparison.
109+
func (cs Constraints) Equals(c Constraints) bool {
110+
if len(cs) != len(c) {
111+
return false
112+
}
113+
114+
// make copies to retain order of the original slices
115+
left := make(Constraints, len(cs))
116+
copy(left, cs)
117+
sort.Stable(left)
118+
right := make(Constraints, len(c))
119+
copy(right, c)
120+
sort.Stable(right)
121+
122+
// compare sorted slices
123+
for i, con := range left {
124+
if !con.Equals(right[i]) {
125+
return false
126+
}
127+
}
128+
129+
return true
130+
}
131+
132+
func (cs Constraints) Len() int {
133+
return len(cs)
134+
}
135+
136+
func (cs Constraints) Less(i, j int) bool {
137+
if cs[i].op < cs[j].op {
138+
return true
139+
}
140+
if cs[i].op > cs[j].op {
141+
return false
142+
}
143+
144+
return cs[i].check.LessThan(cs[j].check)
145+
}
146+
147+
func (cs Constraints) Swap(i, j int) {
148+
cs[i], cs[j] = cs[j], cs[i]
149+
}
150+
90151
// Returns the string format of the constraints
91152
func (cs Constraints) String() string {
92153
csStr := make([]string, len(cs))
@@ -117,8 +178,11 @@ func parseSingle(v string) (*Constraint, error) {
117178
return nil, err
118179
}
119180

181+
cop := constraintOperators[matches[1]]
182+
120183
return &Constraint{
121-
f: constraintOperators[matches[1]],
184+
f: cop.f,
185+
op: cop.op,
122186
check: check,
123187
original: v,
124188
}, nil
@@ -148,6 +212,18 @@ func prereleaseCheck(v, c *Version) bool {
148212
// Constraint functions
149213
//-------------------------------------------------------------------
150214

215+
type operator rune
216+
217+
const (
218+
equal operator = '='
219+
notEqual operator = '≠'
220+
greaterThan operator = '>'
221+
lessThan operator = '<'
222+
greaterThanEqual operator = '≥'
223+
lessThanEqual operator = '≤'
224+
pessimistic operator = '~'
225+
)
226+
151227
func constraintEqual(v, c *Version) bool {
152228
return v.Equal(c)
153229
}

constraint_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package version
22

33
import (
4+
"fmt"
5+
"reflect"
6+
"sort"
47
"testing"
58
)
69

@@ -97,6 +100,104 @@ func TestConstraintCheck(t *testing.T) {
97100
}
98101
}
99102

103+
func TestConstraintEqual(t *testing.T) {
104+
cases := []struct {
105+
leftConstraint string
106+
rightConstraint string
107+
expectedEqual bool
108+
}{
109+
{
110+
"0.0.1",
111+
"0.0.1",
112+
true,
113+
},
114+
{ // whitespaces
115+
" 0.0.1 ",
116+
"0.0.1",
117+
true,
118+
},
119+
{ // equal op implied
120+
"=0.0.1 ",
121+
"0.0.1",
122+
true,
123+
},
124+
{ // version difference
125+
"=0.0.1",
126+
"=0.0.2",
127+
false,
128+
},
129+
{ // operator difference
130+
">0.0.1",
131+
"=0.0.1",
132+
false,
133+
},
134+
{ // different order
135+
">0.1.0, <=1.0.0",
136+
"<=1.0.0, >0.1.0",
137+
true,
138+
},
139+
}
140+
141+
for _, tc := range cases {
142+
leftCon, err := NewConstraint(tc.leftConstraint)
143+
if err != nil {
144+
t.Fatalf("err: %s", err)
145+
}
146+
rightCon, err := NewConstraint(tc.rightConstraint)
147+
if err != nil {
148+
t.Fatalf("err: %s", err)
149+
}
150+
151+
actual := leftCon.Equals(rightCon)
152+
if actual != tc.expectedEqual {
153+
t.Fatalf("Constraints: %s vs %s\nExpected: %t\nActual: %t",
154+
tc.leftConstraint, tc.rightConstraint, tc.expectedEqual, actual)
155+
}
156+
}
157+
}
158+
159+
func TestConstraint_sort(t *testing.T) {
160+
cases := []struct {
161+
constraint string
162+
expectedConstraints string
163+
}{
164+
{
165+
">= 0.1.0,< 1.12",
166+
"< 1.12,>= 0.1.0",
167+
},
168+
{
169+
"< 1.12,>= 0.1.0",
170+
"< 1.12,>= 0.1.0",
171+
},
172+
{
173+
"< 1.12,>= 0.1.0,0.2.0",
174+
"< 1.12,0.2.0,>= 0.1.0",
175+
},
176+
{
177+
">1.0,>0.1.0,>0.3.0,>0.2.0",
178+
">0.1.0,>0.2.0,>0.3.0,>1.0",
179+
},
180+
}
181+
182+
for i, tc := range cases {
183+
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
184+
c, err := NewConstraint(tc.constraint)
185+
if err != nil {
186+
t.Fatalf("err: %s", err)
187+
}
188+
189+
sort.Sort(c)
190+
191+
actual := c.String()
192+
193+
if !reflect.DeepEqual(actual, tc.expectedConstraints) {
194+
t.Fatalf("unexpected order\nexpected: %#v\nactual: %#v",
195+
tc.expectedConstraints, actual)
196+
}
197+
})
198+
}
199+
}
200+
100201
func TestConstraintsString(t *testing.T) {
101202
cases := []struct {
102203
constraint string

0 commit comments

Comments
 (0)