diff --git a/go.mod b/go.mod index bc9fe71..cc62859 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( ) require ( + github.com/Masterminds/semver/v3 v3.2.1 github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect diff --git a/go.sum b/go.sum index aa265da..d4d6cda 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/version/error.go b/version/error.go new file mode 100644 index 0000000..9307310 --- /dev/null +++ b/version/error.go @@ -0,0 +1,14 @@ +package version + +import "errors" + +var ( + ErrInvalidVersion = errors.New("invalid version string") + ErrIncomparableVersion = errors.New("incomparable: dev version") + + ErrInvalidHarvesterVersion = errors.New("invalid harvester version") + ErrMinUpgradeRequirement = errors.New("current version does not meet minimum upgrade requirement") + ErrDowngrade = errors.New("downgrading is prohibited") + ErrDevUpgrade = errors.New("upgrading from dev versions to non-dev versions is prohibited") + ErrPrereleaseCrossVersionUpgrade = errors.New("cross-version upgrades from/to any prerelease version are prohibited") +) diff --git a/version/upgrade.go b/version/upgrade.go new file mode 100644 index 0000000..8e74cb8 --- /dev/null +++ b/version/upgrade.go @@ -0,0 +1,97 @@ +package version + +type HarvesterUpgradeVersion struct { + currentVersion *HarvesterVersion + upgradeVersion *HarvesterVersion + minUpgradableVersion *HarvesterVersion +} + +func NewHarvesterUpgradeVersion(cv, uv, mv *HarvesterVersion) *HarvesterUpgradeVersion { + return &HarvesterUpgradeVersion{ + currentVersion: cv, + upgradeVersion: uv, + minUpgradableVersion: mv, + } +} + +func (u *HarvesterUpgradeVersion) IsUpgrade() error { + if u.currentVersion == nil || u.upgradeVersion == nil { + return ErrInvalidHarvesterVersion + } + + isDowngrade, err := u.currentVersion.IsNewer(u.upgradeVersion) + if err != nil { + return err + } + + if isDowngrade { + return ErrDowngrade + } + + return nil +} + +// IsUpgradable checks that whether the current version satisfies the minimum upgrade requirement. +func (u *HarvesterUpgradeVersion) IsUpgradable() error { + // If minUpgradableVersion is nil means there's no restriction. + if u.minUpgradableVersion == nil { + return nil + } + + isNotUpgradable, err := u.currentVersion.IsOlder(u.minUpgradableVersion) + if err != nil { + return err + } + + if isNotUpgradable { + return ErrMinUpgradeRequirement + } + + return nil +} + +func (u *HarvesterUpgradeVersion) CheckUpgradeEligibility(strictMode bool) error { + // Upgrading to dev versions is always allowed + if u.upgradeVersion.isDev { + return nil + } + + // Upgrading from dev versions is restricted if strict mode is enabled + if u.currentVersion.isDev { + if strictMode { + return ErrDevUpgrade + } + return nil + } + + // Same-version upgrade is always allowed + isSameVersion, err := u.currentVersion.IsEqual(u.upgradeVersion) + if err != nil { + return err + } + if isSameVersion { + return nil + } + + // General cases + // Check if it's effectively a downgrade + if err := u.IsUpgrade(); err != nil { + return err + } + + // Check the minimum upgradable version + if err := u.IsUpgradable(); err != nil { + return err + } + + // Check if it's a prerelease cross-version upgrade + if u.currentVersion.isPrerelease { + currentStableVersion := u.currentVersion.GetStableVersion() + upgradeStableVersion := u.upgradeVersion.GetStableVersion() + if isSameStableVersion, _ := currentStableVersion.IsEqual(upgradeStableVersion); !isSameStableVersion { + return ErrPrereleaseCrossVersionUpgrade + } + } + + return nil +} diff --git a/version/upgrade_test.go b/version/upgrade_test.go new file mode 100644 index 0000000..7bea91e --- /dev/null +++ b/version/upgrade_test.go @@ -0,0 +1,147 @@ +package version + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHarvesterUpgradeVersion_IsUpgrade(t *testing.T) { + var testCases = []struct { + name string + currentVersion string + upgradeVersion string + expectedErr error + }{ + {"upgrade from a newer release version to an older one", "v1.3.0", "v1.2.1", ErrDowngrade}, + {"upgrade from an older release version to a newer one", "v1.2.2", "v1.3.0", nil}, + + {"upgrade from a newer prerelease version to an older one", "v1.3.0-rc1", "v1.2.1-rc1", ErrDowngrade}, + {"upgrade from a newer prerelease version to an older one", "v1.2.1-rc2", "v1.2.1-rc1", ErrDowngrade}, + {"upgrade from an older prerelease version to a newer one", "v1.2.1-rc1", "v1.3.0-rc1", nil}, + {"upgrade from an older prerelease version to a newer one", "v1.2.1-rc1", "v1.2.1-rc2", nil}, + + {"upgrade among two dev versions", "11223344", "aabbccdd", ErrIncomparableVersion}, + + {"upgrade from a newer prerelease version to an older release version", "v1.2.2-rc2", "v1.2.1", ErrDowngrade}, + {"upgrade from an older prerelease version to a newer release version", "v1.2.1-rc2", "v1.2.2", nil}, + {"upgrade from a newer release version to an older prerelease version", "v1.2.2", "v1.2.2-rc2", ErrDowngrade}, + {"upgrade from a newer release version to an older prerelease version", "v1.2.2", "v1.2.1-rc2", ErrDowngrade}, + {"upgrade from an older release version to a newer prerelease version", "v1.2.1", "v1.2.2-rc1", nil}, + + {"upgrade from a release version to a dev version", "v1.2.1", "aabbccdd", ErrIncomparableVersion}, + {"upgrade from a dev version to a release version", "aabbccdd", "v1.2.1", ErrIncomparableVersion}, + {"upgrade from a prerelease version to a dev version", "v1.2.2-rc1", "aabbccdd", ErrIncomparableVersion}, + {"upgrade from a dev version to a prerelease version", "aabbccdd", "v1.2.2-rc1", ErrIncomparableVersion}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cv, _ := NewHarvesterVersion(tc.currentVersion) + uv, _ := NewHarvesterVersion(tc.upgradeVersion) + + huv := NewHarvesterUpgradeVersion(cv, uv, nil) + actualErr := huv.IsUpgrade() + if tc.expectedErr != nil { + assert.Equal(t, tc.expectedErr, actualErr, tc.name) + } else { + assert.Nil(t, actualErr, tc.name) + } + }) + } +} + +func TestHarvesterUpgradeVersion_IsUpgradable(t *testing.T) { + var testCases = []struct { + name string + currentVersion string + minUpgradableVersion string + expectedErr error + }{ + {"upgrade from a release version above the minimal requirement", "v1.2.1", "v1.2.0", nil}, + {"upgrade from a release version lower than the minimal requirement", "v1.2.1", "v1.2.2", ErrMinUpgradeRequirement}, + {"upgrade from the exact same release version of the minimal requirement", "v1.2.1", "v1.2.1", nil}, + + {"upgrade from a prerelease version lower than the minimal requirement", "v1.2.1-rc1", "v1.2.1", ErrMinUpgradeRequirement}, + {"upgrade from a prerelease version above the minimal requirement (rc minUpgradableVersion)", "v1.2.1-rc2", "v1.2.1-rc1", nil}, + {"upgrade from a prerelease version lower than the minimal requirement (rc minUpgradableVersion)", "v1.2.1-rc1", "v1.2.1-rc2", ErrMinUpgradeRequirement}, + {"upgrade from a prerelease version lower than the minimal requirement (rc minUpgradableVersion)", "v1.2.0-rc1", "v1.2.1-rc2", ErrMinUpgradeRequirement}, + {"upgrade from the exact same prerelease version of the minimal requirement (rc minUpgradableVersion)", "v1.2.1-rc1", "v1.2.1-rc1", nil}, + + {"upgrade from dev versions", "11223344", "v1.2.1", ErrIncomparableVersion}, + {"upgrade from dev versions", "aabbccdd", "v1.2.1", ErrIncomparableVersion}, + + {"upgrade from a release version without any minimum requirement specified", "v1.2.1", "", nil}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cv, _ := NewHarvesterVersion(tc.currentVersion) + mv, _ := NewHarvesterVersion(tc.minUpgradableVersion) + + huv := NewHarvesterUpgradeVersion(cv, nil, mv) + actualErr := huv.IsUpgradable() + if tc.expectedErr != nil { + assert.Equal(t, tc.expectedErr, actualErr, tc.name) + } else { + assert.Nil(t, actualErr, tc.name) + } + }) + } +} + +func TestHarvesterUpgradeVersion_CheckUpgradeEligibility(t *testing.T) { + var testCases = []struct { + name string + currentVersion string + upgradeVersion string + minUpgradableVersion string + strictMode bool + expectedErr error + }{ + {"upgrade to same release version", "v1.2.1", "v1.2.1", "v1.1.2", true, nil}, + {"upgrade from an old release version same as the minimal requirement of a new release version", "v1.1.2", "v1.2.1", "v1.1.2", true, nil}, + {"upgrade from an old release version above the minimal requirement of a new release version", "v1.2.0", "v1.2.1", "v1.1.2", true, nil}, + {"upgrade from an old release version below the minimal requirement of a new release version", "v1.1.1", "v1.2.1", "v1.1.2", true, ErrMinUpgradeRequirement}, + {"upgrade from a new release version above the minimal requirement of an old release version", "v1.2.1", "v1.2.0", "v1.1.2", true, ErrDowngrade}, + {"upgrade from an old release version same the minimal requirement of a new prerelease version", "v1.1.2", "v1.2.1-rc1", "v1.1.2", true, nil}, + {"upgrade from an old release version above the minimal requirement of a new prerelease version", "v1.2.0", "v1.2.1-rc1", "v1.1.2", true, nil}, + {"upgrade from an old release version below the minimal requirement of a new prerelease version", "v1.1.1", "v1.2.1-rc1", "v1.1.2", true, ErrMinUpgradeRequirement}, + {"upgrade from a new release version above the minimal requirement of an old prerelease version", "v1.2.1", "v1.2.1-rc1", "v1.1.2", true, ErrDowngrade}, + {"upgrade from a release version to a dev version", "v1.2.1", "v1.2-ab12cd34", "", true, nil}, + + {"upgrade to same prerelease version", "v1.2.2-rc1", "v1.2.2-rc1", "v1.2.1", true, nil}, + {"upgrade from an old prerelease version below the minimal requirement of a new release version", "v1.1.2-rc1", "v1.2.1", "v1.1.2", true, ErrMinUpgradeRequirement}, + {"upgrade from an old prerelease version above the minimal requirement of a new release version", "v1.2.0-rc1", "v1.2.1", "v1.1.2", true, ErrPrereleaseCrossVersionUpgrade}, + {"upgrade from an old prerelease version below the minimal requirement of a new release version", "v1.1.1-rc1", "v1.2.1", "v1.1.2", true, ErrMinUpgradeRequirement}, + {"upgrade from an old prerelease version above the minimal requirement of a new prerelease version", "v1.2.2-rc1", "v1.2.2-rc2", "v1.2.1", true, nil}, + {"upgrade from an old prerelease version below the minimal requirement of a new prerelease version", "v1.2.1-rc1", "v1.2.2-rc2", "v1.2.1", true, ErrMinUpgradeRequirement}, + {"upgrade from an old prerelease version above the minimal requirement of a new prerelease version", "v1.2.0-rc1", "v1.2.1-rc2", "v1.1.2", true, ErrPrereleaseCrossVersionUpgrade}, + {"upgrade from a prerelease version to a dev version", "v1.2.2-rc1", "v1.2-ab12cd34", "", true, nil}, + + {"upgrade to same dev version", "v1.2-ab12cd34", "v1.2-ab12cd34", "", true, nil}, + {"upgrade among two dev versions", "v1.2-ab12cd34", "v1.2-1234567", "", true, nil}, + {"upgrade from a dev version to a release version (strict mode)", "v1.2-ab12cd34", "v1.2.1", "v1.1.2", true, ErrDevUpgrade}, + {"upgrade from a dev version to a release version (loose mode)", "v1.2-ab12cd34", "v1.2.1", "v1.1.2", false, nil}, + {"upgrade from a dev version to a prerelease version (strict mode)", "v1.2-ab12cd34", "v1.2.2-rc1", "v1.2.1", true, ErrDevUpgrade}, + {"upgrade from a dev version to a prerelease version (loose mode)", "v1.2-ab12cd34", "v1.2.2-rc1", "v1.2.1", false, nil}, + + {"upgrade from an old release version to a new release version without any minimum requirement specified", "v1.2.1", "v1.3.0", "", true, nil}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cv, _ := NewHarvesterVersion(tc.currentVersion) + uv, _ := NewHarvesterVersion(tc.upgradeVersion) + mv, _ := NewHarvesterVersion(tc.minUpgradableVersion) + + huv := NewHarvesterUpgradeVersion(cv, uv, mv) + actualErr := huv.CheckUpgradeEligibility(tc.strictMode) + if tc.expectedErr != nil { + assert.Equal(t, tc.expectedErr, actualErr, tc.name) + } else { + assert.Nil(t, actualErr, tc.name) + } + }) + } +} diff --git a/version/version.go b/version/version.go new file mode 100644 index 0000000..cc7fc7f --- /dev/null +++ b/version/version.go @@ -0,0 +1,142 @@ +package version + +import ( + "fmt" + "strings" + + semverv3 "github.com/Masterminds/semver/v3" +) + +type HarvesterVersion struct { + version *semverv3.Version + rawVersion string + isDev bool + isPrerelease bool +} + +func NewHarvesterVersion(versionStr string) (*HarvesterVersion, error) { + var ( + v *semverv3.Version + isDev, isReleaseCandidate bool + ) + + trimmedVersionStr := strings.TrimPrefix(versionStr, "v") + + // HarvesterVersion accepts any form of version strings except the empty string. + if trimmedVersionStr == "" { + return nil, ErrInvalidVersion + } + + // Version strings other than semantic versions will be treated as dev versions. + // For example, v1.2-head, f024f49a, etc. are dev versions. + v, err := semverv3.StrictNewVersion(trimmedVersionStr) + if err != nil { + isDev = true + } + + if !isDev && v.Prerelease() != "" { + isReleaseCandidate = true + } + + return &HarvesterVersion{ + version: v, + rawVersion: versionStr, + isDev: isDev, + isPrerelease: isReleaseCandidate, + }, nil +} + +// GetStableVersion returns the Version object without the suffix. For example, given "v1.2.2-rc1", it returns "v1.2.2" +func (v *HarvesterVersion) GetStableVersion() *HarvesterVersion { + sv := semverv3.New(v.version.Major(), v.version.Minor(), v.version.Patch(), "", "") + return &HarvesterVersion{ + version: sv, + rawVersion: sv.Original(), + isDev: false, + isPrerelease: false, + } +} + +func (v *HarvesterVersion) IsNewer(version *HarvesterVersion) (bool, error) { + if version == nil { + return false, ErrInvalidHarvesterVersion + } + + if v.isDev || version.isDev { + return false, ErrIncomparableVersion + } + + var constraint string + + if v.isPrerelease && !version.isPrerelease { + constraint = fmt.Sprintf("> %s-z", version.rawVersion) + } else { + constraint = fmt.Sprintf("> %s", version.rawVersion) + } + + c, err := semverv3.NewConstraint(constraint) + if err != nil { + return false, err + } + + if v.version == nil { + return false, ErrInvalidVersion + } + + return c.Check(v.version), nil +} + +func (v *HarvesterVersion) IsEqual(version *HarvesterVersion) (bool, error) { + if version == nil { + return false, ErrInvalidHarvesterVersion + } + + if v.isDev || version.isDev { + if v.rawVersion == version.rawVersion { + return true, nil + } + return false, ErrIncomparableVersion + } + + constraint := version.rawVersion + + c, err := semverv3.NewConstraint(constraint) + if err != nil { + return false, err + } + + if v.version == nil { + return false, ErrInvalidVersion + } + + return c.Check(v.version), nil +} + +func (v *HarvesterVersion) IsOlder(version *HarvesterVersion) (bool, error) { + if version == nil { + return false, ErrInvalidHarvesterVersion + } + + if v.isDev || version.isDev { + return false, ErrIncomparableVersion + } + + isNewer, err := v.IsNewer(version) + if err != nil { + return false, err + } + isEqual, err := v.IsEqual(version) + if err != nil { + return false, err + } + + return !(isNewer || isEqual), nil +} + +func (v *HarvesterVersion) String() string { + if v.isDev { + return v.rawVersion + } + + return fmt.Sprintf("v%s", v.version.String()) +} diff --git a/version/version_test.go b/version/version_test.go new file mode 100644 index 0000000..bfe1e69 --- /dev/null +++ b/version/version_test.go @@ -0,0 +1,239 @@ +package version + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewHarvesterVersion(t *testing.T) { + var testCases = []struct { + name string + versionStr string + isDev bool + isPrerelease bool + expectedErr error + }{ + {"formal release version string", "v1.2.2", false, false, nil}, + {"prerelease version string", "v1.2.2-rc1", false, true, nil}, + {"dev version string", "f024f49a", true, false, nil}, + {"empty version string", "", false, false, ErrInvalidVersion}, + } + + for _, tc := range testCases { + hv, err := NewHarvesterVersion(tc.versionStr) + assert.Equal(t, tc.expectedErr, err, tc.name) + if tc.expectedErr == nil { + assert.Equal(t, tc.isDev, hv.isDev, tc.name) + assert.Equal(t, tc.isPrerelease, hv.isPrerelease, tc.name) + } + } +} + +func TestHarvesterVersion_IsNewer(t *testing.T) { + var testCases = []struct { + name string + version1 string + version2 string + isNewer bool + expectedErr error + }{ + {"same formal release versions", "v1.2.1", "v1.2.1", false, nil}, + {"same prerelease versions", "v1.2.2-rc1", "v1.2.2-rc1", false, nil}, + + {"same dev versions", "aabbccdd", "aabbccdd", false, ErrIncomparableVersion}, + {"same dev versions (dirty build)", "aabbccdd-dirty", "aabbccdd-dirty", false, ErrIncomparableVersion}, + + {"compare between formal release versions", "v1.2.2", "v1.2.1", true, nil}, + {"compare between formal release versions", "v1.2.1", "v1.2.2", false, nil}, + + {"compare between prerelease versions", "v1.2.2-rc1", "v1.2.1-rc1", true, nil}, + {"compare between prerelease versions", "v1.2.1-rc1", "v1.2.2-rc1", false, nil}, + {"compare between prerelease versions", "v1.2.2-rc1", "v1.2.1-rc2", true, nil}, + {"compare between prerelease versions", "v1.2.1-rc2", "v1.2.2-rc1", false, nil}, + {"compare between prerelease versions", "v1.2.1-rc2", "v1.2.2-rc1", false, nil}, + {"compare between prerelease versions", "v1.2.1-rc3", "v1.2.1-rc2", true, nil}, + {"compare between prerelease versions", "v1.2.1-rc2", "v1.2.1-rc3", false, nil}, + + {"compare between dev versions", "aabbccdd", "11223344", false, ErrIncomparableVersion}, + {"compare between dev versions (dirty build)", "aabbccdd-dirty", "11223344-dirty", false, ErrIncomparableVersion}, + + {"compare between formal release and prerelease versions", "v1.2.2-rc1", "v1.2.1", true, nil}, + {"compare between formal release and prerelease versions", "v1.2.1", "v1.2.2-rc1", false, nil}, + {"compare between formal release and prerelease versions", "v1.2.2", "v1.2.2-rc1", true, nil}, + {"compare between formal release and prerelease versions", "v1.2.2-rc1", "v1.2.2", false, nil}, + {"compare between formal release and prerelease versions", "v1.2.2-rc1", "v1.3.0", false, nil}, + {"compare between formal release and prerelease versions", "v1.10.0", "v1.2.0-rc10", true, nil}, + {"compare between formal release and prerelease versions", "v1.10.0-rc1", "v1.2.0", true, nil}, + + {"compare between formal release and dev version", "v1.2.1", "11223344", false, ErrIncomparableVersion}, + {"compare between formal release and dev version", "11223344", "v1.2.1", false, ErrIncomparableVersion}, + {"compare between prerelease and dev version", "v1.2.1-rc1", "11223344", false, ErrIncomparableVersion}, + {"compare between prerelease and dev version", "11223344", "v1.2.1-rc1", false, ErrIncomparableVersion}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + v1, err := NewHarvesterVersion(tc.version1) + assert.Nil(t, err, tc.name) + + v2, err := NewHarvesterVersion(tc.version2) + assert.Nil(t, err, tc.name) + + isNewer, err := v1.IsNewer(v2) + if tc.expectedErr != nil { + assert.Equal(t, tc.expectedErr, err, tc.name) + } else { + assert.Nil(t, err, tc.name) + } + assert.Equal(t, tc.isNewer, isNewer, tc.name) + }) + } +} + +func TestHarvesterVersion_IsEqual(t *testing.T) { + var testCases = []struct { + name string + version1 string + version2 string + isEqual bool + expectedErr error + }{ + {"same formal release versions", "v1.2.1", "v1.2.1", true, nil}, + {"same prerelease versions", "v1.2.2-rc1", "v1.2.2-rc1", true, nil}, + + {"same dev versions", "aabbccdd", "aabbccdd", true, nil}, + {"same dev versions (dirty build)", "aabbccdd-dirty", "aabbccdd-dirty", true, nil}, + + {"compare between formal release versions", "v1.2.2", "v1.2.1", false, nil}, + {"compare between formal release versions", "v1.2.1", "v1.2.2", false, nil}, + + {"compare between prerelease versions", "v1.2.2-rc1", "v1.2.1-rc1", false, nil}, + {"compare between prerelease versions", "v1.2.1-rc1", "v1.2.2-rc1", false, nil}, + {"compare between prerelease versions", "v1.2.2-rc1", "v1.2.1-rc2", false, nil}, + {"compare between prerelease versions", "v1.2.1-rc2", "v1.2.2-rc1", false, nil}, + {"compare between prerelease versions", "v1.2.1-rc2", "v1.2.2-rc1", false, nil}, + + {"compare between dev versions", "aabbccdd", "11223344", false, ErrIncomparableVersion}, + {"compare between dev versions (dirty build)", "aabbccdd-dirty", "11223344-dirty", false, ErrIncomparableVersion}, + + {"compare between formal release and prerelease versions", "v1.2.2-rc1", "v1.2.1", false, nil}, + {"compare between formal release and prerelease versions", "v1.2.1", "v1.2.2-rc1", false, nil}, + {"compare between formal release and prerelease versions", "v1.2.2", "v1.2.2-rc1", false, nil}, + {"compare between formal release and prerelease versions", "v1.2.2-rc1", "v1.2.2", false, nil}, + {"compare between formal release and prerelease versions", "v1.2.2-rc1", "v1.3.0", false, nil}, + {"compare between formal release and prerelease versions", "v1.10.0", "v1.2.0-rc10", false, nil}, + {"compare between formal release and prerelease versions", "v1.10.0-rc1", "v1.2.0", false, nil}, + + {"compare between formal release and dev version", "v1.2.1", "11223344", false, ErrIncomparableVersion}, + {"compare between formal release and dev version", "11223344", "v1.2.1", false, ErrIncomparableVersion}, + {"compare between prerelease and dev version", "v1.2.1-rc1", "11223344", false, ErrIncomparableVersion}, + {"compare between prerelease and dev version", "11223344", "v1.2.1-rc1", false, ErrIncomparableVersion}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + v1, err := NewHarvesterVersion(tc.version1) + assert.Nil(t, err, tc.name) + + v2, err := NewHarvesterVersion(tc.version2) + assert.Nil(t, err, tc.name) + + isEqual, err := v1.IsEqual(v2) + if tc.expectedErr != nil { + assert.Equal(t, tc.expectedErr, err, tc.name) + } else { + assert.Nil(t, err, tc.name) + } + assert.Equal(t, tc.isEqual, isEqual, tc.name) + }) + } +} + +func TestHarvesterVersion_IsOlder(t *testing.T) { + var testCases = []struct { + name string + version1 string + version2 string + isOlder bool + expectedErr error + }{ + {"same formal release versions", "v1.2.1", "v1.2.1", false, nil}, + {"same prerelease versions", "v1.2.2-rc1", "v1.2.2-rc1", false, nil}, + + {"same dev versions", "aabbccdd", "aabbccdd", false, ErrIncomparableVersion}, + {"same dev versions (dirty build)", "aabbccdd-dirty", "aabbccdd-dirty", false, ErrIncomparableVersion}, + + {"compare between formal release versions", "v1.2.2", "v1.2.1", false, nil}, + {"compare between formal release versions", "v1.2.1", "v1.2.2", true, nil}, + + {"compare between prerelease versions", "v1.2.2-rc1", "v1.2.1-rc1", false, nil}, + {"compare between prerelease versions", "v1.2.1-rc1", "v1.2.2-rc1", true, nil}, + {"compare between prerelease versions", "v1.2.2-rc1", "v1.2.1-rc2", false, nil}, + {"compare between prerelease versions", "v1.2.1-rc2", "v1.2.2-rc1", true, nil}, + {"compare between prerelease versions", "v1.2.1-rc2", "v1.2.2-rc1", true, nil}, + + {"compare between dev versions", "aabbccdd", "11223344", false, ErrIncomparableVersion}, + {"compare between dev versions (dirty build)", "aabbccdd-dirty", "11223344-dirty", false, ErrIncomparableVersion}, + + {"compare between formal release and prerelease versions", "v1.2.2-rc1", "v1.2.1", false, nil}, + {"compare between formal release and prerelease versions", "v1.2.1", "v1.2.2-rc1", true, nil}, + {"compare between formal release and prerelease versions", "v1.2.2", "v1.2.2-rc1", false, nil}, + {"compare between formal release and prerelease versions", "v1.2.2-rc1", "v1.2.2", true, nil}, + {"compare between formal release and prerelease versions", "v1.2.2-rc1", "v1.3.0", true, nil}, + {"compare between formal release and prerelease versions", "v1.10.0", "v1.2.0-rc10", false, nil}, + {"compare between formal release and prerelease versions", "v1.10.0-rc1", "v1.2.0", false, nil}, + + {"compare between formal release and dev version", "v1.2.1", "11223344", false, ErrIncomparableVersion}, + {"compare between formal release and dev version", "11223344", "v1.2.1", false, ErrIncomparableVersion}, + {"compare between prerelease and dev version", "v1.2.1-rc1", "11223344", false, ErrIncomparableVersion}, + {"compare between prerelease and dev version", "11223344", "v1.2.1-rc1", false, ErrIncomparableVersion}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + v1, err := NewHarvesterVersion(tc.version1) + assert.Nil(t, err, tc.name) + + v2, err := NewHarvesterVersion(tc.version2) + assert.Nil(t, err, tc.name) + + isOlder, err := v1.IsOlder(v2) + if tc.expectedErr != nil { + assert.Equal(t, tc.expectedErr, err, tc.name) + } else { + assert.Nil(t, err, tc.name) + } + assert.Equal(t, tc.isOlder, isOlder, tc.name) + }) + } +} + +func TestHarvesterVersion_String(t *testing.T) { + var testCases = []struct { + name string + input string + output string + }{ + {"formal release version", "v1.2.1", "v1.2.1"}, + {"prerelease version", "v1.2.2-rc1", "v1.2.2-rc1"}, + {"dev version", "aabbccdd", "aabbccdd"}, + {"dev version (dirty build)", "aabbccdd-dirty", "aabbccdd-dirty"}, + + {"formal release version without prefix", "1.2.1", "v1.2.1"}, + {"prerelease version", "1.2.2-rc1", "v1.2.2-rc1"}, + {"dev version", "aabbccdd", "aabbccdd"}, + {"dev version (dirty build)", "aabbccdd-dirty", "aabbccdd-dirty"}, + {"dev version", "v1.2-11223344", "v1.2-11223344"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + v, err := NewHarvesterVersion(tc.input) + assert.Nil(t, err, tc.name) + + actual := v.String() + assert.Equal(t, tc.output, actual, tc.name) + }) + } +}