internal: add custom version type (#1256)

* add custom version type

* extend functionality

* adapt to requested changes

* move to own package

* remove duplicate tests, rename package

* not handle err
This commit is contained in:
Moritz Sanft 2023-02-28 10:36:04 +01:00 committed by GitHub
parent 984f0589d2
commit 90ed470178
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 312 additions and 0 deletions

105
internal/semver/semver.go Normal file
View File

@ -0,0 +1,105 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
/*
Package semver provides functionality to parse and process semantic versions, as they are used in multiple components of Constellation.
*/
package semver
import (
"encoding/json"
"fmt"
"strings"
"github.com/edgelesssys/constellation/v2/internal/constants"
"golang.org/x/mod/semver"
)
// Semver represents a semantic version.
type Semver struct {
Major int
Minor int
Patch int
}
// NewSemver returns a Version from a string.
func NewSemver(version string) (Semver, error) {
// ensure that semver has "v" prefix
if !strings.HasPrefix(version, "v") {
version = "v" + version
}
if !semver.IsValid(version) {
return Semver{}, fmt.Errorf("invalid semver: %s", version)
}
version = semver.Canonical(version)
var major, minor, patch int
_, err := fmt.Sscanf(version, "v%d.%d.%d", &major, &minor, &patch)
if err != nil {
return Semver{}, fmt.Errorf("parsing semver parts: %w", err)
}
return Semver{
Major: major,
Minor: minor,
Patch: patch,
}, nil
}
// String returns the string representation of the version.
func (v Semver) String() string {
return fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Patch)
}
// Compare compares two versions. It relies on the semver.Compare function internally.
func (v Semver) Compare(other Semver) int {
return semver.Compare(v.String(), other.String())
}
// IsUpgradeTo returns if a version is an upgrade to another version.
// It checks if the version of v is greater than the version of other and allows a drift of at most one minor version.
func (v Semver) IsUpgradeTo(other Semver) bool {
return v.Compare(other) > 0 && v.Major == other.Major && v.Minor-other.Minor <= 1
}
// CompatibleWithBinary returns if a version is compatible version of the current built binary.
// It checks if the version of the binary is equal or greater than the current version and allows a drift of at most one minor version.
func (v Semver) CompatibleWithBinary() bool {
binaryVersion, err := NewSemver(constants.VersionInfo)
if err != nil {
return false
}
return v.Compare(binaryVersion) == 0 || binaryVersion.IsUpgradeTo(v)
}
// NextMinor returns the next minor version in the format "vMAJOR.MINOR".
func (v Semver) NextMinor() string {
return fmt.Sprintf("v%d.%d", v.Major, v.Minor+1)
}
// MarshalJSON implements the json.Marshaler interface.
func (v Semver) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%s"`, v.String())), nil
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (v *Semver) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
version, err := NewSemver(s)
if err != nil {
return err
}
*v = version
return nil
}

View File

@ -0,0 +1,207 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package semver
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
v1_18_0 = Semver{Major: 1, Minor: 18, Patch: 0}
v1_19_0 = Semver{Major: 1, Minor: 19, Patch: 0}
v1_18_1 = Semver{Major: 1, Minor: 18, Patch: 1}
v1_20_0 = Semver{Major: 1, Minor: 20, Patch: 0}
v2_0_0 = Semver{Major: 2, Minor: 0, Patch: 0}
)
func TestNewVersion(t *testing.T) {
testCases := map[string]struct {
version string
wantErr bool
}{
"valid version": {"v1.18.0", false},
"invalid version": {"v1.18. 0", true},
"add prefix": {"1.18.0", false},
"only major.minor": {"v1.18", false},
"only major": {"v1", false},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
_, err := NewSemver(tc.version)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}
func TestJSONMarshal(t *testing.T) {
testCases := map[string]struct {
version Semver
wantString string
wantErr bool
}{
"valid version": {
version: v1_18_0,
wantString: `"v1.18.0"`,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
b, err := tc.version.MarshalJSON()
if tc.wantErr {
require.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.wantString, string(b))
}
})
}
}
func TestJSONUnmarshal(t *testing.T) {
testCases := map[string]struct {
version string
wantString string
wantErr bool
}{
"valid version": {
version: `"v1.18.0"`,
wantString: "v1.18.0",
},
"invalid version": {
version: `"v1. 18.0"`,
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
var version Semver
err := version.UnmarshalJSON([]byte(tc.version))
if tc.wantErr {
require.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.wantString, version.String())
}
})
}
}
func TestComparison(t *testing.T) {
testCases := map[string]struct {
version1 Semver
version2 Semver
want int
}{
"equal": {
version1: v1_18_0,
version2: v1_18_0,
want: 0,
},
"less than": {
version1: v1_18_0,
version2: v1_18_1,
want: -1,
},
"greater than": {
version1: v1_18_1,
version2: v1_18_0,
want: 1,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(tc.want, tc.version1.Compare(tc.version2))
})
}
}
func TestCanUpgrade(t *testing.T) {
testCases := map[string]struct {
version1 Semver
version2 Semver
want bool
wantErr bool
}{
"equal": {
version1: v1_18_0,
version2: v1_18_0,
want: false,
},
"patch less than": {
version1: v1_18_0,
version2: v1_18_1,
want: true,
},
"minor less then": {
version1: v1_18_0,
version2: v1_19_0,
want: true,
},
"minor too big drift": {
version1: v1_18_0,
version2: v1_20_0,
want: false,
},
"major too big drift": {
version1: v1_18_0,
version2: v2_0_0,
want: false,
},
"greater than": {
version1: v1_18_1,
version2: v1_18_0,
want: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(tc.want, tc.version2.IsUpgradeTo(tc.version1))
})
}
}
func TestNextMinor(t *testing.T) {
testCases := map[string]struct {
version Semver
want string
}{
"valid version": {
version: v1_18_0,
want: "v1.19",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(tc.want, tc.version.NextMinor())
})
}
}