core: add semver constraint

The existing version constraint uses logic optimized for package
managers, not schedulers, when checking prereleases:

- 1.3.0-beta1 will *not* satisfy ">= 0.6.1"
- 1.7.0-rc1 will *not* satisfy ">= 1.6.0-beta1"

This is due to package managers wishing to favor final releases over
prereleases.

In a scheduler versions more often represent the earliest release all
required features/APIs are available in a system. Whether the constraint
or the version being evaluated are prereleases has no impact on
ordering.

This commit adds a new constraint - `semver` - which will use Semver
v2.0 ordering when evaluating constraints. Given the above examples:

- 1.3.0-beta1 satisfies ">= 0.6.1" using `semver`
- 1.7.0-rc1 satisfies ">= 1.6.0-beta1" using `semver`

Since existing jobspecs may rely on the old behavior, a new constraint
was added and the implicit Consul Connect and Vault constraints were
updated to use it.
This commit is contained in:
Michael Schurter 2019-11-13 15:36:15 -08:00
parent 98321f9729
commit 796758b8a5
15 changed files with 603 additions and 37 deletions

View File

@ -5,6 +5,7 @@ const (
ConstraintDistinctHosts = "distinct_hosts"
ConstraintRegex = "regexp"
ConstraintVersion = "version"
ConstraintSemver = "semver"
ConstraintSetContains = "set_contains"
ConstraintSetContainsAll = "set_contains_all"
ConstraintSetContainsAny = "set_contains_any"

View File

@ -0,0 +1,147 @@
// semver is a Semver Constraints package copied from
// github.com/hashicorp/go-version @ 2046c9d0f0b03c779670f5186a2a4b2c85493a71
//
// Unlike Constraints in go-version, Semver constraints use Semver 2.0 ordering
// rules and only accept properly formatted Semver versions.
package semver
import (
"fmt"
"regexp"
"strings"
"github.com/hashicorp/go-version"
)
// Constraint represents a single constraint for a version, such as ">=
// 1.0".
type Constraint struct {
f constraintFunc
check *version.Version
original string
}
// 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.Version) bool
var constraintOperators map[string]constraintFunc
var constraintRegexp *regexp.Regexp
func init() {
constraintOperators = map[string]constraintFunc{
"": constraintEqual,
"=": constraintEqual,
"!=": constraintNotEqual,
">": constraintGreaterThan,
"<": constraintLessThan,
">=": constraintGreaterThanEqual,
"<=": constraintLessThanEqual,
}
ops := make([]string, 0, len(constraintOperators))
for k := range constraintOperators {
ops = append(ops, regexp.QuoteMeta(k))
}
constraintRegexp = regexp.MustCompile(fmt.Sprintf(
`^\s*(%s)\s*(%s)\s*$`,
strings.Join(ops, "|"),
version.SemverRegexpRaw))
}
// NewConstraint will parse one or more constraints from the given
// constraint string. The string must be a comma-separated list of constraints.
func NewConstraint(v string) (Constraints, error) {
vs := strings.Split(v, ",")
result := make([]*Constraint, len(vs))
for i, single := range vs {
c, err := parseSingle(single)
if err != nil {
return nil, err
}
result[i] = c
}
return Constraints(result), nil
}
// Check tests if a version satisfies all the constraints.
func (cs Constraints) Check(v *version.Version) bool {
for _, c := range cs {
if !c.Check(v) {
return false
}
}
return true
}
// Returns the string format of the constraints
func (cs Constraints) String() string {
csStr := make([]string, len(cs))
for i, c := range cs {
csStr[i] = c.String()
}
return strings.Join(csStr, ",")
}
// Check tests if a constraint is validated by the given version.
func (c *Constraint) Check(v *version.Version) bool {
return c.f(v, c.check)
}
func (c *Constraint) String() string {
return c.original
}
func parseSingle(v string) (*Constraint, error) {
matches := constraintRegexp.FindStringSubmatch(v)
if matches == nil {
return nil, fmt.Errorf("Malformed constraint: %s", v)
}
check, err := version.NewSemver(matches[2])
if err != nil {
return nil, err
}
return &Constraint{
f: constraintOperators[matches[1]],
check: check,
original: v,
}, nil
}
//-------------------------------------------------------------------
// Constraint functions
//-------------------------------------------------------------------
func constraintEqual(v, c *version.Version) bool {
return v.Equal(c)
}
func constraintNotEqual(v, c *version.Version) bool {
return !v.Equal(c)
}
func constraintGreaterThan(v, c *version.Version) bool {
return v.Compare(c) == 1
}
func constraintLessThan(v, c *version.Version) bool {
return v.Compare(c) == -1
}
func constraintGreaterThanEqual(v, c *version.Version) bool {
return v.Compare(c) >= 0
}
func constraintLessThanEqual(v, c *version.Version) bool {
return v.Compare(c) <= 0
}

View File

@ -0,0 +1,125 @@
package semver
import (
"testing"
"github.com/hashicorp/go-version"
)
// This file is a copy of github.com/hashicorp/go-version/constraint_test.go
// with minimal changes to demonstrate differences. Diffing the files should
// illustrate behavior differences in Constraint and version.Constraint.
func TestNewConstraint(t *testing.T) {
cases := []struct {
input string
count int
err bool
}{
{">= 1.2", 1, false},
{"1.0", 1, false},
{">= 1.x", 0, true},
{">= 1.2, < 1.0", 2, false},
// Out of bounds
{"11387778780781445675529500000000000000000", 0, true},
// Semver only
{">= 1.0beta1", 0, true},
// No pessimistic operator
{"~> 1.0", 0, true},
}
for _, tc := range cases {
v, err := NewConstraint(tc.input)
if tc.err && err == nil {
t.Fatalf("expected error for input: %s", tc.input)
} else if !tc.err && err != nil {
t.Fatalf("error for input %s: %s", tc.input, err)
}
if len(v) != tc.count {
t.Fatalf("input: %s\nexpected len: %d\nactual: %d",
tc.input, tc.count, len(v))
}
}
}
func TestConstraintCheck(t *testing.T) {
cases := []struct {
constraint string
version string
check bool
}{
{">= 1.0, < 1.2", "1.1.5", true},
{"< 1.0, < 1.2", "1.1.5", false},
{"= 1.0", "1.1.5", false},
{"= 1.0", "1.0.0", true},
{"1.0", "1.0.0", true},
// Pre-releases are ordered according to Semver v2
{"> 2.0", "2.1.0-beta", true},
{"> 2.1.0-a", "2.1.0-beta", true},
{"> 2.1.0-a", "2.1.1-beta", true},
{"> 2.0.0", "2.1.0-beta", true},
{"> 2.1.0-a", "2.1.1", true},
{"> 2.1.0-a", "2.1.1-beta", true},
{"> 2.1.0-a", "2.1.0", true},
{"<= 2.1.0-a", "2.0.0", true},
{">= 0.6.1", "1.3.0-beta1", true},
{"> 1.0-beta1", "1.0-rc1", true},
// Meta components are ignored according to Semver v2
{">= 0.6.1", "1.3.0-beta1+ent", true},
{">= 1.3.0-beta1", "1.3.0-beta1+ent", true},
{"> 1.3.0-beta1+cgo", "1.3.0-beta1+ent", false},
{"= 1.3.0-beta1+cgo", "1.3.0-beta1+ent", true},
}
for _, tc := range cases {
c, err := NewConstraint(tc.constraint)
if err != nil {
t.Fatalf("err: %s", err)
}
v, err := version.NewSemver(tc.version)
if err != nil {
t.Fatalf("err: %s", err)
}
actual := c.Check(v)
expected := tc.check
if actual != expected {
t.Fatalf("Version: %s\nConstraint: %s\nExpected: %#v",
tc.version, tc.constraint, expected)
}
}
}
func TestConstraintsString(t *testing.T) {
cases := []struct {
constraint string
result string
}{
{">= 1.0, < 1.2", ""},
}
for _, tc := range cases {
c, err := NewConstraint(tc.constraint)
if err != nil {
t.Fatalf("err: %s", err)
}
actual := c.String()
expected := tc.result
if expected == "" {
expected = tc.constraint
}
if actual != expected {
t.Fatalf("Constraint: %s\nExpected: %#v\nActual: %s",
tc.constraint, expected, actual)
}
}
}

View File

@ -138,6 +138,7 @@ func parseConstraints(result *[]*api.Constraint, list *ast.ObjectList) error {
"set_contains",
"value",
"version",
"semver",
}
if err := helper.CheckHCLKeys(o.Val, valid); err != nil {
return err
@ -159,6 +160,13 @@ func parseConstraints(result *[]*api.Constraint, list *ast.ObjectList) error {
m["RTarget"] = constraint
}
// If "semver" is provided, set the operand
// to "semver" and the value to the "RTarget"
if constraint, ok := m[api.ConstraintSemver]; ok {
m["Operand"] = api.ConstraintSemver
m["RTarget"] = constraint
}
// If "regexp" is provided, set the operand
// to "regexp" and the value to the "RTarget"
if constraint, ok := m[api.ConstraintRegex]; ok {
@ -219,6 +227,7 @@ func parseAffinities(result *[]*api.Affinity, list *ast.ObjectList) error {
"set_contains_all",
"value",
"version",
"semver",
"weight",
}
if err := helper.CheckHCLKeys(o.Val, valid); err != nil {
@ -241,6 +250,13 @@ func parseAffinities(result *[]*api.Affinity, list *ast.ObjectList) error {
m["RTarget"] = affinity
}
// If "semver" is provided, set the operand
// to "semver" and the value to the "RTarget"
if affinity, ok := m[api.ConstraintSemver]; ok {
m["Operand"] = api.ConstraintSemver
m["RTarget"] = affinity
}
// If "regexp" is provided, set the operand
// to "regexp" and the value to the "RTarget"
if affinity, ok := m[api.ConstraintRegex]; ok {

View File

@ -43,6 +43,11 @@ func TestParse(t *testing.T) {
RTarget: "windows",
Operand: "=",
},
{
LTarget: "${attr.vault.version}",
RTarget: ">= 0.6.1",
Operand: "semver",
},
},
Affinities: []*api.Affinity{

View File

@ -16,6 +16,12 @@ job "binstore-storagelocker" {
value = "windows"
}
constraint {
attribute = "${attr.vault.version}"
value = ">= 0.6.1"
operator = "semver"
}
affinity {
attribute = "${meta.team}"
value = "mobile"

View File

@ -951,6 +951,12 @@ func (j *Job) Allocations(args *structs.JobSpecificRequest,
return structs.ErrPermissionDenied
}
// Ensure JobID is set otherwise everything works and never returns
// allocations which can hide bugs in request code.
if args.JobID == "" {
return fmt.Errorf("missing job ID")
}
// Setup the blocking query
opts := blockingOptions{
queryOpts: &args.QueryOptions,

View File

@ -35,8 +35,8 @@ var (
connectVersionConstraint = func() *structs.Constraint {
return &structs.Constraint{
LTarget: "${attr.consul.version}",
RTarget: ">= 1.6.0beta1",
Operand: "version",
RTarget: ">= 1.6.0-beta1",
Operand: structs.ConstraintSemver,
}
}
)

View File

@ -21,7 +21,7 @@ var (
vaultConstraint = &structs.Constraint{
LTarget: vaultConstraintLTarget,
RTarget: ">= 0.6.1",
Operand: structs.ConstraintVersion,
Operand: structs.ConstraintSemver,
}
)

View File

@ -718,6 +718,7 @@ func TestJobEndpoint_Register_Dispatched(t *testing.T) {
require.Error(err)
require.Contains(err.Error(), "job can't be submitted with 'Dispatched'")
}
func TestJobEndpoint_Register_EnforceIndex(t *testing.T) {
t.Parallel()
s1 := TestServer(t, func(c *Config) {
@ -1178,6 +1179,87 @@ func TestJobEndpoint_Register_Vault_Policies(t *testing.T) {
}
}
// TestJobEndpoint_Register_SemverConstraint asserts that semver ordering is
// used when evaluating semver constraints.
func TestJobEndpoint_Register_SemverConstraint(t *testing.T) {
t.Parallel()
s1 := TestServer(t, nil)
defer s1.Shutdown()
codec := rpcClient(t, s1)
testutil.WaitForLeader(t, s1.RPC)
state := s1.State()
// Create a job with a semver constraint
job := mock.Job()
job.Constraints = []*structs.Constraint{
{
LTarget: "${attr.vault.version}",
RTarget: ">= 0.6.1",
Operand: structs.ConstraintSemver,
},
}
job.TaskGroups[0].Count = 1
// Insert 2 Nodes, 1 matching the constraint, 1 not
node1 := mock.Node()
node1.Attributes["vault.version"] = "1.3.0-beta1+ent"
node1.ComputeClass()
require.NoError(t, state.UpsertNode(1, node1))
node2 := mock.Node()
delete(node2.Attributes, "vault.version")
node2.ComputeClass()
require.NoError(t, state.UpsertNode(2, node2))
// Create the register request
req := &structs.JobRegisterRequest{
Job: job,
WriteRequest: structs.WriteRequest{
Region: "global",
Namespace: job.Namespace,
},
}
// Fetch the response
var resp structs.JobRegisterResponse
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Job.Register", req, &resp))
require.NotZero(t, resp.Index)
// Wait for placements
allocReq := &structs.JobSpecificRequest{
JobID: job.ID,
QueryOptions: structs.QueryOptions{
Region: "global",
Namespace: structs.DefaultNamespace,
},
}
testutil.WaitForResult(func() (bool, error) {
resp := structs.JobAllocationsResponse{}
err := msgpackrpc.CallWithCodec(codec, "Job.Allocations", allocReq, &resp)
if err != nil {
return false, err
}
if n := len(resp.Allocations); n != 1 {
return false, fmt.Errorf("expected 1 alloc, found %d", n)
}
alloc := resp.Allocations[0]
if alloc.NodeID != node1.ID {
return false, fmt.Errorf("expected alloc to be one node=%q but found node=%q",
node1.ID, alloc.NodeID)
}
return true, nil
}, func(waitErr error) {
evals, err := state.EvalsByJob(nil, structs.DefaultNamespace, job.ID)
require.NoError(t, err)
for i, e := range evals {
t.Logf("%d Eval: %s", i, pretty.Sprint(e))
}
require.NoError(t, waitErr)
})
}
func TestJobEndpoint_Revert(t *testing.T) {
t.Parallel()
s1 := TestServer(t, func(c *Config) {
@ -3712,6 +3794,29 @@ func TestJobEndpoint_Allocations_Blocking(t *testing.T) {
}
}
// TestJobEndpoint_Allocations_NoJobID asserts not setting a JobID in the
// request returns an error.
func TestJobEndpoint_Allocations_NoJobID(t *testing.T) {
t.Parallel()
s1 := TestServer(t, nil)
defer s1.Shutdown()
codec := rpcClient(t, s1)
testutil.WaitForLeader(t, s1.RPC)
get := &structs.JobSpecificRequest{
JobID: "",
QueryOptions: structs.QueryOptions{
Region: "global",
Namespace: structs.DefaultNamespace,
},
}
var resp structs.JobAllocationsResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Allocations", get, &resp)
require.Error(t, err)
require.Contains(t, err.Error(), "missing job ID")
}
func TestJobEndpoint_Evaluations(t *testing.T) {
t.Parallel()
s1 := TestServer(t, nil)

View File

@ -30,6 +30,7 @@ import (
"github.com/hashicorp/nomad/acl"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/helper/args"
"github.com/hashicorp/nomad/helper/constraints/semver"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/lib/kheap"
psstructs "github.com/hashicorp/nomad/plugins/shared/structs"
@ -6619,6 +6620,7 @@ const (
ConstraintDistinctHosts = "distinct_hosts"
ConstraintRegex = "regexp"
ConstraintVersion = "version"
ConstraintSemver = "semver"
ConstraintSetContains = "set_contains"
ConstraintSetContainsAll = "set_contains_all"
ConstraintSetContainsAny = "set_contains_any"
@ -6689,6 +6691,10 @@ func (c *Constraint) Validate() error {
if _, err := version.NewConstraint(c.RTarget); err != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Version constraint is invalid: %v", err))
}
case ConstraintSemver:
if _, err := semver.NewConstraint(c.RTarget); err != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Semver constraint is invalid: %v", err))
}
case ConstraintDistinctProperty:
// If a count is set, make sure it is convertible to a uint64
if c.RTarget != "" {
@ -6803,6 +6809,10 @@ func (a *Affinity) Validate() error {
if _, err := version.NewConstraint(a.RTarget); err != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Version affinity is invalid: %v", err))
}
case ConstraintSemver:
if _, err := semver.NewConstraint(a.RTarget); err != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Semver affinity is invalid: %v", err))
}
case "=", "==", "is", "!=", "not", "<", "<=", ">", ">=":
if a.RTarget == "" {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Operator %q requires an RTarget", a.Operand))

View File

@ -1827,9 +1827,7 @@ func TestConstraint_Validate(t *testing.T) {
Operand: "=",
}
err = c.Validate()
if err != nil {
t.Fatalf("err: %v", err)
}
require.NoError(t, err)
// Perform additional regexp validation
c.Operand = ConstraintRegex
@ -1849,6 +1847,15 @@ func TestConstraint_Validate(t *testing.T) {
t.Fatalf("err: %s", err)
}
// Perform semver validation
c.Operand = ConstraintSemver
err = c.Validate()
require.Error(t, err)
require.Contains(t, err.Error(), "Malformed constraint")
c.RTarget = ">= 0.6.1"
require.NoError(t, c.Validate())
// Perform distinct_property validation
c.Operand = ConstraintDistinctProperty
c.RTarget = "0"

View File

@ -5,7 +5,6 @@ import (
log "github.com/hashicorp/go-hclog"
memdb "github.com/hashicorp/go-memdb"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/nomad/nomad/structs"
)
@ -35,7 +34,10 @@ type Context interface {
RegexpCache() map[string]*regexp.Regexp
// VersionConstraintCache is a cache of version constraints
VersionConstraintCache() map[string]version.Constraints
VersionConstraintCache() map[string]VerConstraints
// SemverConstraintCache is a cache of semver constraints
SemverConstraintCache() map[string]VerConstraints
// Eligibility returns a tracker for node eligibility in the context of the
// eval.
@ -44,8 +46,9 @@ type Context interface {
// EvalCache is used to cache certain things during an evaluation
type EvalCache struct {
reCache map[string]*regexp.Regexp
constraintCache map[string]version.Constraints
reCache map[string]*regexp.Regexp
versionCache map[string]VerConstraints
semverCache map[string]VerConstraints
}
func (e *EvalCache) RegexpCache() map[string]*regexp.Regexp {
@ -55,11 +58,18 @@ func (e *EvalCache) RegexpCache() map[string]*regexp.Regexp {
return e.reCache
}
func (e *EvalCache) VersionConstraintCache() map[string]version.Constraints {
if e.constraintCache == nil {
e.constraintCache = make(map[string]version.Constraints)
func (e *EvalCache) VersionConstraintCache() map[string]VerConstraints {
if e.versionCache == nil {
e.versionCache = make(map[string]VerConstraints)
}
return e.constraintCache
return e.versionCache
}
func (e *EvalCache) SemverConstraintCache() map[string]VerConstraints {
if e.semverCache == nil {
e.semverCache = make(map[string]VerConstraints)
}
return e.semverCache
}
// EvalContext is a Context used during an Evaluation

View File

@ -8,6 +8,7 @@ import (
"strings"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/nomad/helper/constraints/semver"
"github.com/hashicorp/nomad/nomad/structs"
psstructs "github.com/hashicorp/nomad/plugins/shared/structs"
)
@ -551,7 +552,11 @@ func checkConstraint(ctx Context, operand string, lVal, rVal interface{}, lFound
case structs.ConstraintAttributeIsNotSet:
return !lFound
case structs.ConstraintVersion:
return lFound && rFound && checkVersionMatch(ctx, lVal, rVal)
parser := newVersionConstraintParser(ctx)
return lFound && rFound && checkVersionMatch(ctx, parser, lVal, rVal)
case structs.ConstraintSemver:
parser := newSemverConstraintParser(ctx)
return lFound && rFound && checkVersionMatch(ctx, parser, lVal, rVal)
case structs.ConstraintRegex:
return lFound && rFound && checkRegexpMatch(ctx, lVal, rVal)
case structs.ConstraintSetContains, structs.ConstraintSetContainsAll:
@ -601,7 +606,7 @@ func checkLexicalOrder(op string, lVal, rVal interface{}) bool {
// checkVersionMatch is used to compare a version on the
// left hand side with a set of constraints on the right hand side
func checkVersionMatch(ctx Context, lVal, rVal interface{}) bool {
func checkVersionMatch(ctx Context, parse verConstraintParser, lVal, rVal interface{}) bool {
// Parse the version
var versionStr string
switch v := lVal.(type) {
@ -625,17 +630,10 @@ func checkVersionMatch(ctx Context, lVal, rVal interface{}) bool {
return false
}
// Check the cache for a match
cache := ctx.VersionConstraintCache()
constraints := cache[constraintStr]
// Parse the constraints
constraints := parse(constraintStr)
if constraints == nil {
constraints, err = version.NewConstraint(constraintStr)
if err != nil {
return false
}
cache[constraintStr] = constraints
return false
}
// Check the constraints against the version
@ -644,7 +642,7 @@ func checkVersionMatch(ctx Context, lVal, rVal interface{}) bool {
// checkAttributeVersionMatch is used to compare a version on the
// left hand side with a set of constraints on the right hand side
func checkAttributeVersionMatch(ctx Context, lVal, rVal *psstructs.Attribute) bool {
func checkAttributeVersionMatch(ctx Context, parse verConstraintParser, lVal, rVal *psstructs.Attribute) bool {
// Parse the version
var versionStr string
if s, ok := lVal.GetString(); ok {
@ -667,17 +665,10 @@ func checkAttributeVersionMatch(ctx Context, lVal, rVal *psstructs.Attribute) bo
return false
}
// Check the cache for a match
cache := ctx.VersionConstraintCache()
constraints := cache[constraintStr]
// Parse the constraints
constraints := parse(constraintStr)
if constraints == nil {
constraints, err = version.NewConstraint(constraintStr)
if err != nil {
return false
}
cache[constraintStr] = constraints
return false
}
// Check the constraints against the version
@ -1119,7 +1110,17 @@ func checkAttributeConstraint(ctx Context, operand string, lVal, rVal *psstructs
return false
}
return checkAttributeVersionMatch(ctx, lVal, rVal)
parser := newVersionConstraintParser(ctx)
return checkAttributeVersionMatch(ctx, parser, lVal, rVal)
case structs.ConstraintSemver:
if !(lFound && rFound) {
return false
}
parser := newSemverConstraintParser(ctx)
return checkAttributeVersionMatch(ctx, parser, lVal, rVal)
case structs.ConstraintRegex:
if !(lFound && rFound) {
return false
@ -1164,3 +1165,50 @@ func checkAttributeConstraint(ctx Context, operand string, lVal, rVal *psstructs
}
}
// VerConstraints is the interface implemented by both go-verson constraints
// and semver constraints.
type VerConstraints interface {
Check(v *version.Version) bool
String() string
}
// verConstraintParser returns a version constraints implementation (go-version
// or semver).
type verConstraintParser func(verConstraint string) VerConstraints
func newVersionConstraintParser(ctx Context) verConstraintParser {
cache := ctx.VersionConstraintCache()
return func(cstr string) VerConstraints {
if c := cache[cstr]; c != nil {
return c
}
constraints, err := version.NewConstraint(cstr)
if err != nil {
return nil
}
cache[cstr] = constraints
return constraints
}
}
func newSemverConstraintParser(ctx Context) verConstraintParser {
cache := ctx.SemverConstraintCache()
return func(cstr string) VerConstraints {
if c := cache[cstr]; c != nil {
return c
}
constraints, err := semver.NewConstraint(cstr)
if err != nil {
return nil
}
cache[cstr] = constraints
return constraints
}
}

View File

@ -708,6 +708,8 @@ func TestCheckLexicalOrder(t *testing.T) {
}
func TestCheckVersionConstraint(t *testing.T) {
t.Parallel()
type tcase struct {
lVal, rVal interface{}
result bool
@ -733,15 +735,93 @@ func TestCheckVersionConstraint(t *testing.T) {
lVal: 1, rVal: "~> 1.0",
result: true,
},
{
// Prereleases are never > final releases
lVal: "1.3.0-beta1", rVal: ">= 0.6.1",
result: false,
},
{
// Prerelease X.Y.Z must match
lVal: "1.7.0-alpha1", rVal: ">= 1.6.0-beta1",
result: false,
},
{
// Meta is ignored
lVal: "1.3.0-beta1+ent", rVal: "= 1.3.0-beta1",
result: true,
},
}
for _, tc := range cases {
_, ctx := testContext(t)
if res := checkVersionMatch(ctx, tc.lVal, tc.rVal); res != tc.result {
p := newVersionConstraintParser(ctx)
if res := checkVersionMatch(ctx, p, tc.lVal, tc.rVal); res != tc.result {
t.Fatalf("TC: %#v, Result: %v", tc, res)
}
}
}
func TestCheckSemverConstraint(t *testing.T) {
t.Parallel()
type tcase struct {
name string
lVal, rVal interface{}
result bool
}
cases := []tcase{
{
name: "Pessimistic operator always fails 1",
lVal: "1.2.3", rVal: "~> 1.0",
result: false,
},
{
name: "1.2.3 does satisfy >= 1.0, < 1.4",
lVal: "1.2.3", rVal: ">= 1.0, < 1.4",
result: true,
},
{
name: "Pessimistic operator always fails 2",
lVal: "2.0.1", rVal: "~> 1.0",
result: false,
},
{
name: "1.4 does not satisfy >= 1.0, < 1.4",
lVal: "1.4", rVal: ">= 1.0, < 1.4",
result: false,
},
{
name: "Pessimistic operator always fails 3",
lVal: 1, rVal: "~> 1.0",
result: false,
},
{
name: "Prereleases are handled according to semver 1",
lVal: "1.3.0-beta1", rVal: ">= 0.6.1",
result: true,
},
{
name: "Prereleases are handled according to semver 2",
lVal: "1.7.0-alpha1", rVal: ">= 1.6.0-beta1",
result: true,
},
{
name: "Meta is ignored according to semver",
lVal: "1.3.0-beta1+ent", rVal: "= 1.3.0-beta1",
result: true,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
_, ctx := testContext(t)
p := newSemverConstraintParser(ctx)
actual := checkVersionMatch(ctx, p, tc.lVal, tc.rVal)
require.Equal(t, tc.result, actual)
})
}
}
func TestCheckRegexpConstraint(t *testing.T) {
type tcase struct {
lVal, rVal interface{}