Skip to content

Commit feceee7

Browse files
committed
Constraint(s): introduce Equals() and sort.Interface
1 parent c2de231 commit feceee7

File tree

2 files changed

+188
-11
lines changed

2 files changed

+188
-11
lines changed

constraint.go

+87-11
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))
@@ -77,6 +88,56 @@ func (cs Constraints) Check(v *Version) bool {
7788
return true
7889
}
7990

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

171+
cop := constraintOperators[matches[1]]
172+
110173
return &Constraint{
111-
f: constraintOperators[matches[1]],
174+
f: cop.f,
175+
op: cop.op,
112176
check: check,
113177
original: v,
114178
}, nil
@@ -138,6 +202,18 @@ func prereleaseCheck(v, c *Version) bool {
138202
// Constraint functions
139203
//-------------------------------------------------------------------
140204

205+
type operator rune
206+
207+
const (
208+
equal operator = '='
209+
notEqual operator = '≠'
210+
greaterThan operator = '>'
211+
lessThan operator = '<'
212+
greaterThanEqual operator = '≥'
213+
lessThanEqual operator = '≤'
214+
pessimistic operator = '~'
215+
)
216+
141217
func constraintEqual(v, c *Version) bool {
142218
return v.Equal(c)
143219
}

constraint_test.go

+101
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)