diff --git a/constraints.go b/constraints.go index 8b7a10f..6568871 100644 --- a/constraints.go +++ b/constraints.go @@ -68,6 +68,14 @@ func NewConstraint(c string) (*Constraints, error) { return o, nil } +func MustParseConstraint(c string) *Constraints { + sc, err := NewConstraint(c) + if err != nil { + panic(err) + } + return sc +} + // Check tests if a version satisfies the constraints. func (cs Constraints) Check(v *Version) bool { // TODO(mattfarina): For v4 of this library consolidate the Check and Validate diff --git a/intersection.go b/intersection.go new file mode 100644 index 0000000..e374986 --- /dev/null +++ b/intersection.go @@ -0,0 +1,297 @@ +package semver + +import ( + "cmp" + "slices" + "strings" +) + +// Intersection returns a Constraints struct satisfied by all versions that satisfy a and b (a ∩ b). +// Returns nil if either input is nil. +func Intersection(a, b *Constraints) *Constraints { + if a == nil || b == nil { + return nil + } + + // We include prereleases if any of the constraints has IncludePrerelease=true. + includePre := a.IncludePrerelease || b.IncludePrerelease + + ca, cb := canonicalise(a), canonicalise(b) + var out [][]*constraint + for _, ga := range ca.constraints { + for _, gb := range cb.constraints { + g := intersect(ga, gb, includePre) + out = append(out, g) + } + } + + if len(out) == 0 { + return &Constraints{} + } + + return &Constraints{ + constraints: canonicalise(&Constraints{constraints: out}).constraints, + IncludePrerelease: includePre, + } +} + +// IsSubset returns true if every version satisfying sub also satisfies sup (sub ⊆ sup). +// Returns false if either input is nil. +func IsSubset(sub, sup *Constraints) bool { + return sub != nil && sup != nil && + Intersection(sub, sup).String() == canonicalise(sub).String() +} + +func intersect(a, b []*constraint, incPre bool) []*constraint { + ea, ra := splitExact(a) + eb, rb := splitExact(b) + + switch { + case len(ra) == 0 && len(rb) == 0: + return exactIntersection(ea, eb) + case len(ra) == 0: + return filterExact(ea, b, incPre) + case len(rb) == 0: + return filterExact(eb, a, incPre) + default: + return simplify(append(append([]*constraint{}, a...), b...)) + } +} + +func splitExact(cs []*constraint) (exact, ranges []*constraint) { + for _, c := range cs { + if c.origfunc == "" || c.origfunc == "=" { + exact = append(exact, c) + } else { + ranges = append(ranges, c) + } + } + return exact, ranges +} + +func exactIntersection(a, b []*constraint) (res []*constraint) { + for _, ea := range a { + for _, eb := range b { + if ea.con.Equal(eb.con) { + res = append(res, ea) + } + } + } + return res +} + +func filterExact(exact, cs []*constraint, incPre bool) (res []*constraint) { + for _, e := range exact { + if satisfiesAll(e.con, cs, incPre) { + res = append(res, e) + } + } + return res +} + +func satisfiesAll(v *Version, cs []*constraint, incPre bool) bool { + if !incPre { + for _, c := range cs { + if c.con.Prerelease() != "" { + incPre = true + break + } + } + } + + for _, c := range cs { + if v.Prerelease() != "" && !incPre { + return false + } + + compare := v.Compare(c.con) + switch c.origfunc { + case ">": + if compare <= 0 { + return false + } + case ">=": + if compare < 0 { + return false + } + case "<": + if compare >= 0 { + return false + } + case "<=": + if compare > 0 { + return false + } + } + } + return true +} + +func canonicalise(c *Constraints) *Constraints { + if c == nil { + return nil + } + + seen := make(map[string]struct{}) + var groups [][]*constraint + for _, g := range c.constraints { + clean := simplify(expand(g)) + if isValid(clean) { + k := groupKey(clean) + _, ok := seen[k] + if !ok { + seen[k] = struct{}{} + groups = append(groups, clean) + } + } + } + slices.SortFunc(groups, func(a, b []*constraint) int { + return cmp.Compare(groupKey(a), groupKey(b)) + }) + + return &Constraints{constraints: groups} +} + +func expand(cs []*constraint) (res []*constraint) { + for _, c := range cs { + res = append(res, expandConstraint(c)...) + } + return res +} + +func expandConstraint(c *constraint) []*constraint { + switch c.origfunc { + case "^": + return createRange(c, func() Version { + if c.con.Major() > 0 { + return c.con.IncMajor() + } + return c.con.IncMinor() + }) + case "~", "~>": + return createRange(c, func() Version { + if c.minorDirty { + return c.con.IncMajor() + } + return c.con.IncMinor() + }) + case "", "=": + if c.dirty { + return expandWildcard(c) + } + case "<=": + if c.dirty { + var hi Version + if c.minorDirty { + hi = c.con.IncMajor() + } else { + hi = c.con.IncMinor() + } + return []*constraint{upperConstraint(hi)} + } + } + + return []*constraint{c} +} + +func createRange(c *constraint, upper func() Version) []*constraint { + return []*constraint{clone(c, ">="), upperConstraint(upper())} +} + +func expandWildcard(c *constraint) []*constraint { + lo := clone(c, ">=") + var hi Version + switch { + case c.minorDirty: + hi = c.con.IncMajor() + case c.patchDirty: + hi = c.con.IncMinor() + default: + return []*constraint{lo} + } + + return []*constraint{lo, upperConstraint(hi)} +} + +func simplify(cs []*constraint) (res []*constraint) { + if len(cs) <= 1 { + return cs + } + lo, hi := bounds(cs) + if lo != nil { + res = append(res, lo) + } + if hi != nil { + res = append(res, hi) + } + + return res +} + +func better(cur, cand *constraint, dir int) bool { + if cand == nil { + return false + } + if cur == nil { + return true + } + diff := cand.con.Compare(cur.con) + if diff != 0 { + return diff*dir > 0 + } + if dir > 0 { + return cur.origfunc == ">=" && cand.origfunc == ">" + } + + return cur.origfunc == "<=" && cand.origfunc == "<" +} + +func clone(c *constraint, op string) *constraint { + return &constraint{con: c.con, orig: c.con.String(), origfunc: op} +} + +func upperConstraint(v Version) *constraint { + return &constraint{con: &v, orig: v.String(), origfunc: "<"} +} + +func groupKey(cs []*constraint) string { + var sb strings.Builder + for _, c := range cs { + sb.WriteString(c.string()) + sb.WriteByte(' ') + } + return sb.String() +} + +func isValid(cs []*constraint) bool { + if len(cs) == 0 { + return false + } + + lo, hi := bounds(cs) + if lo == nil || hi == nil { + return true + } + + compare := lo.con.Compare(hi.con) + if compare > 0 || (compare == 0 && (lo.origfunc != ">=" || hi.origfunc != "<=")) { + return false + } + return true +} + +func bounds(cs []*constraint) (lo, hi *constraint) { + for _, c := range cs { + switch c.origfunc { + case ">", ">=": + if better(lo, c, 1) { + lo = c + } + case "<", "<=": + if better(hi, c, -1) { + hi = c + } + } + } + return lo, hi +} diff --git a/intersection_test.go b/intersection_test.go new file mode 100644 index 0000000..2f447de --- /dev/null +++ b/intersection_test.go @@ -0,0 +1,366 @@ +package semver + +import ( + "fmt" + "strconv" + "testing" +) + +func TestIntersection_NilSafety(t *testing.T) { + c := MustParseConstraint(">=0.0.0") + if Intersection(nil, c) != nil { + t.Fatal("Intersection(nil, c) should return nil") + } + if Intersection(c, nil) != nil { + t.Fatal("Intersection(c, nil) should return nil") + } + if Intersection(nil, nil) != nil { + t.Fatal("Intersection(nil, nil) should return nil") + } +} + +func TestIntersection(t *testing.T) { + cases := []struct { + a, b, want string + }{ + {"^1", ">=1.4.0", ">=1.4.0 <2.0.0"}, + {"~1.2", "<1.2.5", ">=1.2.0 <1.2.5"}, + {"^0.2.3", ">=0.2.4", ">=0.2.4 <0.3.0"}, + {"~1", "<1.5.0", ">=1.0.0 <1.5.0"}, + {">=1.0.0 <2.0.0", ">=1.5.0 <3.0.0", ">=1.5.0 <2.0.0"}, + {"~1.2.0", ">=1.2.3 <1.3.0", ">=1.2.3 <1.3.0"}, + {"^1.2.0", ">=1.5.0 <2.0.0", ">=1.5.0 <2.0.0"}, + {"1.0.0 || 2.0.0", ">=1.0.0 <=2.0.0", "1.0.0 || 2.0.0"}, + {"^1.0.0 || ~2.1.0", ">=1.5.0 <2.2.0", ">=1.5.0 <2.0.0 || >=2.1.0 <2.2.0"}, + {">=1.0.0 <2.0.0", ">=3.0.0 <4.0.0", ""}, + {"1.2.3 || 1.2.4", ">=1.2.3 <=1.2.5", "1.2.3 || 1.2.4"}, + {"^2.0.0 || ~1.5.0", ">=1.5.2 <2.1.0", ">=1.5.2 <1.6.0 || >=2.0.0 <2.1.0"}, + {">=1.0.0 <2.0.0 || >=3.0.0 <4.0.0", ">=1.5.0 <3.5.0", ">=1.5.0 <2.0.0 || >=3.0.0 <3.5.0"}, + {">=1.0.0-alpha <1.0.0", ">=1.0.0-beta <1.0.0-gamma", ">=1.0.0-beta <1.0.0-gamma"}, + {">=1.0.0", ">=1.0.0", ">=1.0.0"}, + {">=1.0.0-alpha.1 <1.0.0-beta", ">=1.0.0-alpha.2 <1.0.0-alpha.10", ">=1.0.0-alpha.2 <1.0.0-alpha.10"}, + {">=1.0.0-1 <1.0.0-10", ">=1.0.0-2 <1.0.0-5", ">=1.0.0-2 <1.0.0-5"}, + {">=1.0.0-alpha+build1", ">=1.0.0-alpha+build2", ">=1.0.0-alpha+build1"}, + {">=1.0.0-alpha <2.0.0", ">=1.0.0 <1.5.0", ">=1.0.0 <1.5.0"}, + {">=1.0.0 <=2.0.0", ">2.0.0 <3.0.0", ""}, + {">=1.0.0 <=2.0.0", ">=2.0.0 <3.0.0", ">=2.0.0 <=2.0.0"}, + {">=0.0.0 <0.1.0", ">=0.0.1 <1.0.0", ">=0.0.1 <0.1.0"}, + {">=999999.999999.999999", ">=1000000.0.0 <2000000.0.0", ">=1000000.0.0 <2000000.0.0"}, + {">1.0.0 <1.0.1", ">=1.0.0 <=1.0.0", ""}, + {"1.0.0 || 3.0.0 || 5.0.0", "2.0.0 || 4.0.0 || 6.0.0", ""}, + {">=1.0.0 <2.0.0 || >=4.0.0 <5.0.0", ">=1.5.0 <3.0.0 || >=4.5.0 <6.0.0", ">=1.5.0 <2.0.0 || >=4.5.0 <5.0.0"}, + {">=4.0.0 <5.0.0 || >=1.0.0 <2.0.0", ">=4.5.0 <6.0.0 || >=1.5.0 <3.0.0", ">=1.5.0 <2.0.0 || >=4.5.0 <5.0.0"}, + {"1.0.0 || 1.1.0 || 1.2.0 || 1.3.0", ">=1.1.0 <=1.2.0", "1.1.0 || 1.2.0"}, + {"1.0.0 || >=2.0.0 <3.0.0", ">=0.9.0 <=1.0.0 || 2.5.0", "1.0.0 || 2.5.0"}, + {">=1.0.0 >=1.2.0", ">=1.1.0", ">=1.2.0"}, + {"<2.0.0 <1.8.0", "<1.9.0", "<1.8.0"}, + {">1.0.0 >=1.0.0", "<=2.0.0 <2.0.0", ">1.0.0 <2.0.0"}, + {">=2.0.0", "<1.0.0", ""}, + {"1.2.3 || 1.4.0", ">=1.0.0 <1.3.0", "1.2.3"}, + {"1.2.3", "=1.2.3", "1.2.3"}, + {"1.2.3", "=1.24", ""}, + {"1", ">=1.4.0", ">=1.4.0 <2.0.0"}, + // * + {">=1.0.0 >=1.2.0", "*", ">=1.2.0"}, + {"<2.0.0 <1.8.0", "*", ">=0.0.0 <1.8.0"}, + {"1.x", "*", ">=1.0.0 <2.0.0"}, + {"1.x", "<1.5.0", ">=1.0.0 <1.5.0"}, + {">=1.2.0", "*", ">=1.2.0"}, + {"<2.0.0 <=1.8.0", "*", ">=0.0.0 <=1.8.0"}, + {">1.0.0 >=1.0.0", "*", ">1.0.0"}, + {">=1.0.0 >=1.2.0 <=2.0.0 <2.5.0", "*", ">=1.2.0 <=2.0.0"}, + {"1.2.x", ">=1.2.3", ">=1.2.3 <1.3.0"}, + {"1.2.x", "<1.2.1", ">=1.2.0 <1.2.1"}, + {"0.x.x", "<0.3.0", ">=0.0.0 <0.3.0"}, + {"1.x", ">=1.2.0 <1.4.0", ">=1.2.0 <1.4.0"}, + {"1.2.x", ">=1.2.3 <1.2.8", ">=1.2.3 <1.2.8"}, + {">=1.0.0-alpha <1.0.0-beta", ">=1.0.0-beta <1.0.0-rc", ""}, + {"=1.2.3", ">1.2.3", ""}, + {">=1 <=2", "~2", ">=2.0.0 <3.0.0"}, + {">=1.1.1-1", ">=1.1.1", ">=1.1.1"}, + {">=1.1.1-1", ">=1.1.1 <1.2.1-1", ">=1.1.1 <1.2.1-1"}, + + {"1.0.6-1", ">=1.0.3-0 <1.0.6", "1.0.6-1"}, + } + + for i, tc := range cases { + t.Run(fmt.Sprint("WithoutIncludePrerelease ", strconv.Itoa(i)), func(t *testing.T) { + got := Intersection(MustParseConstraint(tc.a), MustParseConstraint(tc.b)).String() + if got != tc.want { + t.Errorf("Intersection(%q, %q) = %q, want %q", tc.a, tc.b, got, tc.want) + } + }) + t.Run(fmt.Sprint("IncludePrerelease ", strconv.Itoa(i)), func(t *testing.T) { + a := MustParseConstraint(tc.a) + b := MustParseConstraint(tc.b) + a.IncludePrerelease = true + b.IncludePrerelease = true + got := Intersection(a, b).String() + if got != tc.want { + t.Errorf("Intersection(%q, %q) = %q, want %q", tc.a, tc.b, got, tc.want) + } + }) + } +} + +func TestIntersectionWithoutIncludePrerelease(t *testing.T) { + cases := []struct { + a, b, want string + }{ + {">=1.1", "4.1.0-beta", ""}, + {">1.1", "4.1.0-beta", ""}, + {"<=1.1", "0.1.0-alpha", ""}, + {"<1.1", "0.1.0-alpha", ""}, + {"^1.x", "1.1.1-beta1", ""}, + {"~1.1", "1.1.1-alpha", ""}, + {"*", "1.2.3-alpha", ""}, + {"= 2.0", "2.0.1-beta", ""}, + } + + for i, tc := range cases { + t.Run(strconv.Itoa(i), func(t *testing.T) { + got := Intersection(MustParseConstraint(tc.a), MustParseConstraint(tc.b)).String() + if got != tc.want { + t.Errorf("Intersection(%q, %q) = %q, want %q", tc.a, tc.b, got, tc.want) + } + }) + } +} + +func TestIntersectionIncludePrerelease(t *testing.T) { + cases := []struct { + a, b, want string + }{ + {">=1.1", "4.1.0-beta", "4.1.0-beta"}, + {">1.1", "4.1.0-beta", "4.1.0-beta"}, + {"<=1.1", "0.1.0-alpha", "0.1.0-alpha"}, + {"<1.1", "0.1.0-alpha", "0.1.0-alpha"}, + {"^1.x", "1.1.1-beta1", "1.1.1-beta1"}, + {"~1.1", "1.1.1-alpha", "1.1.1-alpha"}, + {"*", "1.2.3-alpha", "1.2.3-alpha"}, + {"= 2.0", "2.0.1-beta", "2.0.1-beta"}, + } + + for i, tc := range cases { + t.Run(strconv.Itoa(i), func(t *testing.T) { + a := MustParseConstraint(tc.a) + b := MustParseConstraint(tc.b) + a.IncludePrerelease = true + b.IncludePrerelease = true + got := Intersection(a, b).String() + if got != tc.want { + t.Errorf("Intersection(%q, %q) = %q, want %q", tc.a, tc.b, got, tc.want) + } + }) + } +} + +func TestIsSubset_NilSafety(t *testing.T) { + c := MustParseConstraint(">=1.2.3 <4") + if IsSubset(nil, c) { + t.Fatal("IsSubset(nil, c) should not be false") + } + if IsSubset(c, nil) { + t.Fatal("IsSubset(nil, c) should not be false") + } + if IsSubset(nil, nil) { + t.Fatal("IsSubset(nil, nil) should not be false") + } +} + +func TestIsSubset(t *testing.T) { + cases := []struct { + a, b string + want bool + }{ + {"~8", ">=8 <=17", true}, + {"~1.2.x", "^1.2.x", true}, + {"~1.2.3", "~>1.2.3", true}, + {"~>2.0", "^2", true}, + {"~>1.2.x", "~1.2.x", true}, + {"~1.x", "^1", true}, + {"~1.x", "^1.1", false}, + {">=1.4.0", "^1", false}, + {"^1", ">=1.4.0", false}, + {">1 <2", ">=1 <3", true}, + {">1 <=2", ">=0 <3", true}, + {">=1.5.0 <2.0.0", ">=1.0.0 <2.5.0", true}, + {">=1.0.0 <2.0.0 || >=3.0.0 <4.0.0", ">=0.5.0 <5.0.0", true}, + {">=1.0.0 <2.0.0", ">=0.5.0 <3.0.0", true}, + {">=1.0.0 <2.0.0 || >=4.0.0 <5.0.0", ">=1.0.0 <3.0.0", false}, + {">=1.0.0 <3.0.0", ">=1.0.0 <2.0.0 || >=4.0.0 <5.0.0", false}, + {"1.4.0", ">=1.5.0 <2.0.0 || >=3.0.0 <3.5.0", false}, + {"1.5.0", ">=1.5.0 <2.0.0 || >=3.0.0 <3.5.0", true}, + {"2.5.0", ">=1.5.0 <2.0.0 || >=3.0.0 <3.5.0", false}, + {"3.2.0", ">=1.5.0 <2.0.0 || >=3.0.0 <3.5.0", true}, + {"3.6.0", ">=1.5.0 <2.0.0 || >=3.0.0 <3.5.0", false}, + {">=3.1.0 <3.5.0 || >=1.7.0 <1.9.0", ">=3.0.0 <3.5.0 || >=1.5.0 <2.0.0", true}, + {">1 <2", ">2 <3", false}, + {">1 <2", ">1.5 <2.5", false}, + {">=1.0.0 <=2.0.0", ">=1.0.0 <=2.0.0", true}, + {">1", ">=1", true}, + {"<2", "<=2", true}, + {">1 <=2", ">1 <2.5", false}, + {">=1.0.0", ">=0.0.0", true}, + {">=1.0.0", ">=1.0.0 <2.0.0", false}, + {">=1.2.3 <4", ">=1.2.3 <4", true}, + {"^1", "^1", true}, + {"^1.2.3", "^1.2.3", true}, + {"~1", "~1", true}, + {"~1.2", "~1.2", true}, + {"^1.2.0", "^1", true}, + {"~1.2", "~1", true}, + {"^1", "^1.2.0", false}, + {"~1", "~2", false}, + {"^1", "^2", false}, + {"^0.2.3", "^0.2.4", false}, + {"~1.2", ">=1.2.5 <1.3.0", false}, + {"^1.2.3", ">=1.2.3 <2.0.0", true}, + {"^1.2.3", ">=1.3.0 <2.0.0", false}, + {"^0.2", ">=0.2.0 <0.3.0", true}, + {"^0.2", ">=0.2.5 <0.3.0", false}, + {"~1", ">=1.0.0 <2.0.0", true}, + {"~1", ">=1.5.0 <2.0.0", false}, + {"^2", ">=2.3.0 <3.0.0", false}, + {"~1.2", ">=1.2.0 <1.3.0", true}, + {"~1.2", ">=1.0.0 <2.0.0", true}, + {"~1", ">=1.4.0 <2.0.0", false}, + {"^1", "<2.0.0", true}, + {"^1", ">=1.4.0 <2.0.0", false}, + {">=1.2.0 <1.3.0", ">=1.0.0 <2.0.0", true}, + {"~1.2.0", ">=1.0.0 <2.0.0", true}, + {"^1.2.0", ">=1.0.0 <2.0.0", true}, + {">=1.0.0 <3.0.0", ">=1.0.0 <2.0.0", false}, + {">=0.5.0 <2.0.0", ">=1.0.0 <2.0.0", false}, + {"1.2.3", ">=1.0.0 <=2.0.0", true}, + {"1.2.3 || 1.2.4", ">=1.2.0 <1.3.0", true}, + {"~1.2.0 || ^1.5.0", ">=1.0.0 <2.0.0", true}, + {"~1.2.0 || ^2.0.0", ">=1.0.0 <2.0.0", false}, + {">=1.2.0 <1.3.0", "~1.2.0", true}, + {">=1.0.0-alpha <1.0.0-beta", ">=1.0.0-alpha <1.0.0", true}, + {">=1.5.0 <2.5.0", ">=1.0.0 <2.0.0", false}, + {">=3.0.0 <4.0.0", ">=1.0.0 <2.0.0", false}, + {">=1.0.0 <2.0.0", ">=1.0.0 <2.0.0", true}, + {"1.0.0 || 2.0.0 || 3.0.0", ">=1.0.0 <=2.0.0", false}, + {"1.0.0 || 1.5.0 || 2.0.0", ">=1.0.0 <=2.0.0", true}, + {"1.5.0", ">=2.0.0 <1.0.0", false}, + {">=1.0.0-alpha <1.0.0-beta", ">=1.0.0 <2.0.0", false}, + {"1.0.0+build1", "1.0.0+build2", true}, + {"1.0.0 || 3.0.0", ">=0.9.0 <=1.1.0", false}, + {"^1.2.3", ">=1.0.0 <2.0.0", true}, + {">=1.2.4 <1.3.0", "~1.2.0", true}, + {">=1.0.0-beta.1 <1.0.0", ">=1.0.0-alpha <1.0.0", true}, + {"1.2.3 || 1.2.4 || 1.2.5", "~1.2.0", true}, + {"1.2.3", "=1.24", false}, + {">=1.2.0 >=1.0.0", ">=1.1.0", true}, + {">=1.1.0", ">=1.2.0 >=1.0.0", false}, + {"<1.8.0 <2.0.0", "<2.0.0", true}, + {"<2.0.0", "<1.8.0 <2.0.0", false}, + {">=1.2.0 <1.5.0 >=1.0.0", ">=1.1.0 <2.0.0", true}, + {">=1.2.0 <1.5.0", ">=1.2.0 <=1.4.0", false}, + {">=1.0.0 <=2.0.0 >=1.0.0", ">=1.0.0 <=2.0.0", true}, + {"<=2.0.0 <2.0.0", "<=2.0.0", true}, + {"<=2.0.0", "<2.0.0 <=2.0.0", false}, + // x + {"1.x", "^1", true}, + {"^1", "1.x", true}, + {"1.2.x", "1.x", true}, + {"1.x", "1.2.x", false}, + {"1.2.x", "x.x.x", true}, + {"0.2.x", "0.x.x", true}, + {"^0.2.4", "0.x.x", true}, + {"~0.2.4", "0.x.x", true}, + {"=0.2.4", "=0.x.x", true}, + // * + {">=3.0.0 <2.0.0", "*", true}, + {"*", "*", true}, + {"*", "<2.0.0", false}, + {"0.x", "<1.0.0", true}, + {"0.x", ">=0.1.0 <0.5.0", false}, + {"~2", ">=1 <=2", true}, + + {"1.0.6-1", ">=1.0.3-0 <1.0.6", true}, + {"1.0.6-1", ">=1.0.3-0 <1.0.7", true}, + {"1.0.6-1", ">=1.0.3-0 <=1.0.6", true}, + } + + for i, tc := range cases { + t.Run(fmt.Sprint("WithoutIncludePrerelease ", strconv.Itoa(i)), + func(t *testing.T) { + got := IsSubset(MustParseConstraint(tc.a), MustParseConstraint(tc.b)) + if got != tc.want { + t.Errorf("IsSubset(%q, %q) = %v, want %v", tc.a, tc.b, got, tc.want) + } + }) + + t.Run(fmt.Sprint("IncludePrerelease ", strconv.Itoa(i)), func(t *testing.T) { + a := MustParseConstraint(tc.a) + b := MustParseConstraint(tc.b) + a.IncludePrerelease = true + b.IncludePrerelease = true + got := IsSubset(a, b) + if got != tc.want { + t.Errorf("IsSubset(%q, %q) = %v, want %v", tc.a, tc.b, got, tc.want) + } + }) + + } +} + +func TestIsSubsetWithoutIncludePrerelease(t *testing.T) { + cases := []struct { + a, b string + want bool + }{ + {"4.1.0-beta", ">=1.1", false}, + {"4.1.0-beta", ">1.1", false}, + {"0.1.0-alpha", "<=1.1", false}, + {"0.1.0-alpha", "<1.1", false}, + {"1.1.1-beta1", "^1.x", false}, + {"1.1.1-alpha", "~1.1", false}, + {"1.2.3-alpha", "*", false}, + {"2.0.1-beta", "= 2.0", false}, + } + + for i, tc := range cases { + t.Run(strconv.Itoa(i), + func(t *testing.T) { + got := IsSubset(MustParseConstraint(tc.a), MustParseConstraint(tc.b)) + if got != tc.want { + t.Errorf("IsSubset(%q, %q) = %v, want %v", tc.a, tc.b, got, tc.want) + } + }) + + } +} + +func TestIsSubsetIncludePrerelease(t *testing.T) { + cases := []struct { + a, b string + want bool + }{ + {"4.1.0-beta", ">=1.1", true}, + {"4.1.0-beta", ">1.1", true}, + {"0.1.0-alpha", "<=1.1", true}, + {"0.1.0-alpha", "<1.1", true}, + {"1.1.1-beta1", "^1.x", true}, + {"1.1.1-alpha", "~1.1", true}, + {"1.2.3-alpha", "*", true}, + {"2.0.1-beta", "= 2.0", true}, + } + + for i, tc := range cases { + t.Run(strconv.Itoa(i), + func(t *testing.T) { + a := MustParseConstraint(tc.a) + b := MustParseConstraint(tc.b) + a.IncludePrerelease = true + b.IncludePrerelease = true + got := IsSubset(a, b) + if got != tc.want { + t.Errorf("IsSubset(%q, %q) = %v, want %v", tc.a, tc.b, got, tc.want) + } + }) + + } +}