From 3f55712d7e59bf758d93f49308ebdce54d83cc27 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 12 Feb 2019 09:56:10 +0000 Subject: [PATCH 01/21] Enable go modules --- go.mod | 1 + 1 file changed, 1 insertion(+) create mode 100644 go.mod diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d008305 --- /dev/null +++ b/go.mod @@ -0,0 +1 @@ +module github.com/blang/semver From fd2916946f4ac790747edb21d1aeb3011cea5531 Mon Sep 17 00:00:00 2001 From: Hanzei Date: Tue, 2 Oct 2018 16:24:54 +0200 Subject: [PATCH 02/21] Add go 1.11 to travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 8222047..7bbec87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ matrix: - go: 1.8.x - go: 1.9.x - go: 1.10.x + - go: 1.11.x - go: tip allow_failures: - go: tip From ee9e2238648e3906c8b7bed54ddcfef81798990a Mon Sep 17 00:00:00 2001 From: Hanzei Date: Tue, 2 Oct 2018 16:25:23 +0200 Subject: [PATCH 03/21] Fix go vet issues --- examples/main.go | 3 ++- semver_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/main.go b/examples/main.go index f36c983..7bd1c55 100644 --- a/examples/main.go +++ b/examples/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/blang/semver" ) @@ -33,7 +34,7 @@ func main() { } // Make == Parse (Value), New for Pointer - v001, err := semver.Make("0.0.1") + v001, _ := semver.Make("0.0.1") fmt.Println("\nUse Version.Compare for comparisons (-1, 0, 1):") fmt.Printf("%q is greater than %q: Compare == %d\n", v001, v, v001.Compare(v)) diff --git a/semver_test.go b/semver_test.go index b3e1fd4..a59579b 100644 --- a/semver_test.go +++ b/semver_test.go @@ -366,7 +366,7 @@ func BenchmarkStringSimple(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - v.String() + _ = v.String() } } @@ -376,7 +376,7 @@ func BenchmarkStringLarger(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - v.String() + _ = v.String() } } @@ -386,7 +386,7 @@ func BenchmarkStringComplex(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - v.String() + _ = v.String() } } @@ -395,7 +395,7 @@ func BenchmarkStringAverage(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - formatTests[n%l].v.String() + _ = formatTests[n%l].v.String() } } From 395bcbfe039d234fc836425fc66ce3907160e1bd Mon Sep 17 00:00:00 2001 From: Hanzei Date: Tue, 2 Oct 2018 16:27:09 +0200 Subject: [PATCH 04/21] Fix golint issue --- sql.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql.go b/sql.go index eb4d802..db95813 100644 --- a/sql.go +++ b/sql.go @@ -14,7 +14,7 @@ func (v *Version) Scan(src interface{}) (err error) { case []byte: str = string(src) default: - return fmt.Errorf("Version.Scan: cannot convert %T to string.", src) + return fmt.Errorf("version.Scan: cannot convert %T to string", src) } if t, err := Parse(str); err == nil { From 860ac3234ec0503afcca03402b917485e84a37ee Mon Sep 17 00:00:00 2001 From: Hanzei Date: Tue, 2 Oct 2018 16:31:06 +0200 Subject: [PATCH 05/21] Fix travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7bbec87..7319bde 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ install: - go get github.com/mattn/goveralls script: - echo "Test and track coverage" ; $HOME/gopath/bin/goveralls -package "." -service=travis-ci - -repotoken $COVERALLS_TOKEN + -repotoken=$COVERALLS_TOKEN - echo "Build examples" ; cd examples && go build - echo "Check if gofmt'd" ; diff -u <(echo -n) <(gofmt -d -s .) env: From b3fa6b39d15c42b03b5d7b5830d6f6c0fa5d75f9 Mon Sep 17 00:00:00 2001 From: matfax Date: Sat, 12 Jan 2019 21:50:57 +0100 Subject: [PATCH 06/21] feat: let ParseTolerant remove leading zeros from version digits Various vendors use versions with leading zeros (e.g., Docker, 18.09). Let semver accept them in its tolerant parser. --- semver.go | 13 ++++++++++--- semver_test.go | 2 ++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/semver.go b/semver.go index ec26aa0..c1f6e73 100644 --- a/semver.go +++ b/semver.go @@ -202,14 +202,21 @@ func Make(s string) (Version, error) { // ParseTolerant allows for certain version specifications that do not strictly adhere to semver // specs to be parsed by this library. It does so by normalizing versions before passing them to -// Parse(). It currently trims spaces, removes a "v" prefix, and adds a 0 patch number to versions -// with only major and minor components specified +// Parse(). It currently trims spaces, removes a "v" prefix, adds a 0 patch number to versions +// with only major and minor components specified, and removes leading 0s. func ParseTolerant(s string) (Version, error) { s = strings.TrimSpace(s) s = strings.TrimPrefix(s, "v") // Split into major.minor.(patch+pr+meta) parts := strings.SplitN(s, ".", 3) + // Remove leading zeros. + for i, p := range parts { + if len(p) > 1 { + parts[i] = strings.TrimPrefix(p, "0") + } + } + // Fill up shortened versions. if len(parts) < 3 { if strings.ContainsAny(parts[len(parts)-1], "+-") { return Version{}, errors.New("Short version cannot contain PreRelease/Build meta data") @@ -217,8 +224,8 @@ func ParseTolerant(s string) (Version, error) { for len(parts) < 3 { parts = append(parts, "0") } - s = strings.Join(parts, ".") } + s = strings.Join(parts, ".") return Parse(s) } diff --git a/semver_test.go b/semver_test.go index a59579b..419e9b4 100644 --- a/semver_test.go +++ b/semver_test.go @@ -33,6 +33,8 @@ var formatTests = []formatTest{ var tolerantFormatTests = []formatTest{ {Version{1, 2, 3, nil, nil}, "v1.2.3"}, {Version{1, 2, 3, nil, nil}, " 1.2.3 "}, + {Version{1, 2, 3, nil, nil}, "01.02.03"}, + {Version{0, 0, 3, nil, nil}, "00.0.03"}, {Version{1, 2, 0, nil, nil}, "1.2"}, {Version{1, 0, 0, nil, nil}, "1"}, } From 84f3627a28711974cc87258fe9fbb29690318376 Mon Sep 17 00:00:00 2001 From: Eduardo Lezcano Date: Thu, 3 May 2018 09:59:58 +0200 Subject: [PATCH 07/21] Increment methods for major, minor and patch version according to the specs. Signed-off-by: Eduardo Lezcano --- semver.go | 30 ++++++++++++++++++++++++++ semver_test.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/semver.go b/semver.go index c1f6e73..4165bc7 100644 --- a/semver.go +++ b/semver.go @@ -161,6 +161,36 @@ func (v Version) Compare(o Version) int { } +// IncrementPatch increments the patch version +func (v *Version) IncrementPatch() error { + if v.Major == 0 { + return fmt.Errorf("Patch version can not be incremented for %q", v.String()) + } + v.Patch += 1 + return nil +} + +// IncrementMinor increments the minor version +func (v *Version) IncrementMinor() error { + if v.Major == 0 { + return fmt.Errorf("Minor version can not be incremented for %q", v.String()) + } + v.Minor += 1 + v.Patch = 0 + return nil +} + +// IncrementMajor increments the major version +func (v *Version) IncrementMajor() error { + if v.Major == 0 { + return fmt.Errorf("Major version can not be incremented for %q", v.String()) + } + v.Major += 1 + v.Minor = 0 + v.Patch = 0 + return nil +} + // Validate validates v and returns error in case func (v Version) Validate() error { // Major, Minor, Patch already validated using uint64 diff --git a/semver_test.go b/semver_test.go index 419e9b4..8e5d80c 100644 --- a/semver_test.go +++ b/semver_test.go @@ -256,6 +256,64 @@ func TestCompareHelper(t *testing.T) { } } +const ( + MAJOR = iota + MINOR + PATCH +) + +type incrementTest struct { + version Version + incrementType int + expectingError bool + expectedVersion Version +} + +var incrementTests = []incrementTest{ + {Version{1, 2, 3, nil, nil}, PATCH, false, Version{1, 2, 4, nil, nil}}, + {Version{1, 2, 3, nil, nil}, MINOR, false, Version{1, 3, 0, nil, nil}}, + {Version{1, 2, 3, nil, nil}, MAJOR, false, Version{2, 0, 0, nil, nil}}, + {Version{0, 1, 2, nil, nil}, PATCH, true, Version{}}, + {Version{0, 1, 2, nil, nil}, MINOR, true, Version{}}, + {Version{0, 1, 2, nil, nil}, MAJOR, true, Version{}}, +} + +func TestIncrements(t *testing.T) { + for _, test := range incrementTests { + var originalVersion = Version{ + test.version.Major, + test.version.Minor, + test.version.Patch, + test.version.Pre, + test.version.Build, + } + var err error + switch test.incrementType { + case PATCH: + err = test.version.IncrementPatch() + case MINOR: + err = test.version.IncrementMinor() + case MAJOR: + err = test.version.IncrementMajor() + } + if test.expectingError { + if err == nil { + t.Errorf("Increment version %q, expecting error, got %q", test.version, err) + } + if test.version.NE(originalVersion) { + t.Errorf("Increment version, expecting %q, got %q", test.expectedVersion, test.version) + } + } else { + if (err != nil) && !test.expectingError { + t.Errorf("Increment version %q, not expecting error, got %q", test.version, err) + } + if test.version.NE(test.expectedVersion) { + t.Errorf("Increment version, expecting %q, got %q", test.expectedVersion, test.version) + } + } + } +} + func TestPreReleaseVersions(t *testing.T) { p1, err := NewPRVersion("123") if !p1.IsNumeric() { From 0c4b8eafe97e4f42c4ed0a3a86b6e622392f0882 Mon Sep 17 00:00:00 2001 From: Sven SAULEAU Date: Tue, 26 Dec 2017 17:55:43 +0100 Subject: [PATCH 08/21] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6af4dde..e05f986 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,8 @@ Range usage: ``` v, err := semver.Parse("1.2.3") -range, err := semver.ParseRange(">1.0.0 <2.0.0 || >=3.0.0") -if range(v) { +expectedRange, err := semver.ParseRange(">1.0.0 <2.0.0 || >=3.0.0") +if expectedRange(v) { //valid } From ba2c2ddd89069b46a7011d4106f6868f17ee1705 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Sun, 14 Apr 2019 12:29:17 +0200 Subject: [PATCH 09/21] Simplify string.Index in range --- range.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/range.go b/range.go index fca406d..95f7139 100644 --- a/range.go +++ b/range.go @@ -327,7 +327,7 @@ func expandWildcardVersion(parts [][]string) ([][]string, error) { for _, p := range parts { var newParts []string for _, ap := range p { - if strings.Index(ap, "x") != -1 { + if strings.Contains(ap, "x") { opStr, vStr, err := splitComparatorVersion(ap) if err != nil { return nil, err From e3f131b368b4eb2a86c2e777374148c6396d4913 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Sun, 14 Apr 2019 18:04:19 +0200 Subject: [PATCH 10/21] gx publish 3.6.1 --- .gx/lastpubver | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gx/lastpubver b/.gx/lastpubver index 27ed267..54339b2 100644 --- a/.gx/lastpubver +++ b/.gx/lastpubver @@ -1 +1 @@ -3.5.1: QmYRGECuvQnRX73fcvPnGbYijBcGN2HbKZQ7jh26qmLiHG +3.6.1: Qmc2TuykruVht9jiXbBXdGenbQhdsUMWCn61B8QFakH2Aw diff --git a/package.json b/package.json index 1cf8ebd..78e27c5 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,6 @@ "license": "MIT", "name": "semver", "releaseCmd": "git commit -a -m \"gx publish $VERSION\"", - "version": "3.5.1" + "version": "3.6.1" } From 1a9109f8c4a1d669a78442f567dfe8eedf982793 Mon Sep 17 00:00:00 2001 From: Hanzei Date: Sun, 14 Apr 2019 18:59:03 +0200 Subject: [PATCH 11/21] Add test for go 1.12 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 7319bde..a85d269 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ matrix: - go: 1.9.x - go: 1.10.x - go: 1.11.x + - go: 1.12.x - go: tip allow_failures: - go: tip From 086cfa096a727aaa4debf1564bba7a56161a8769 Mon Sep 17 00:00:00 2001 From: Hanzei Date: Mon, 15 Apr 2019 12:20:34 +0200 Subject: [PATCH 12/21] Use idiomatic ++ --- semver.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/semver.go b/semver.go index 4165bc7..2474f05 100644 --- a/semver.go +++ b/semver.go @@ -166,7 +166,7 @@ func (v *Version) IncrementPatch() error { if v.Major == 0 { return fmt.Errorf("Patch version can not be incremented for %q", v.String()) } - v.Patch += 1 + v.Patch++ return nil } @@ -175,7 +175,7 @@ func (v *Version) IncrementMinor() error { if v.Major == 0 { return fmt.Errorf("Minor version can not be incremented for %q", v.String()) } - v.Minor += 1 + v.Minor++ v.Patch = 0 return nil } @@ -185,7 +185,7 @@ func (v *Version) IncrementMajor() error { if v.Major == 0 { return fmt.Errorf("Major version can not be incremented for %q", v.String()) } - v.Major += 1 + v.Major++ v.Minor = 0 v.Patch = 0 return nil From d3feafb656afb1654476e8e0ddb1c1655c14cf5b Mon Sep 17 00:00:00 2001 From: Hanzei Date: Mon, 15 Apr 2019 12:21:13 +0200 Subject: [PATCH 13/21] Explicitly ignore errors in benchmarks --- range_test.go | 6 +++--- semver_test.go | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/range_test.go b/range_test.go index fc11670..2f44de4 100644 --- a/range_test.go +++ b/range_test.go @@ -525,7 +525,7 @@ func BenchmarkRangeParseSimple(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - ParseRange(VERSION) + _, _ = ParseRange(VERSION) } } @@ -534,7 +534,7 @@ func BenchmarkRangeParseAverage(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - ParseRange(VERSION) + _, _ = ParseRange(VERSION) } } @@ -543,7 +543,7 @@ func BenchmarkRangeParseComplex(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - ParseRange(VERSION) + _, _ = ParseRange(VERSION) } } diff --git a/semver_test.go b/semver_test.go index 8e5d80c..96a7101 100644 --- a/semver_test.go +++ b/semver_test.go @@ -389,7 +389,7 @@ func BenchmarkParseSimple(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - Parse(VERSION) + _, _ = Parse(VERSION) } } @@ -398,7 +398,7 @@ func BenchmarkParseComplex(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - Parse(VERSION) + _, _ = Parse(VERSION) } } @@ -407,7 +407,7 @@ func BenchmarkParseAverage(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - Parse(formatTests[n%l].result) + _, _ = Parse(formatTests[n%l].result) } } @@ -416,7 +416,7 @@ func BenchmarkParseTolerantAverage(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - ParseTolerant(tolerantFormatTests[n%l].result) + _, _ = ParseTolerant(tolerantFormatTests[n%l].result) } } @@ -465,7 +465,7 @@ func BenchmarkValidateSimple(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - v.Validate() + _ = v.Validate() } } @@ -475,7 +475,7 @@ func BenchmarkValidateComplex(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - v.Validate() + _ = v.Validate() } } @@ -484,7 +484,7 @@ func BenchmarkValidateAverage(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - formatTests[n%l].v.Validate() + _ = formatTests[n%l].v.Validate() } } From ad15b66e93a460a01298fe1f58e28c33e9de34a7 Mon Sep 17 00:00:00 2001 From: Songmu Date: Tue, 28 May 2019 12:26:04 +0900 Subject: [PATCH 14/21] Fix ParseTolerant when zeros are continuous --- semver.go | 6 +++++- semver_test.go | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/semver.go b/semver.go index 2474f05..26ece43 100644 --- a/semver.go +++ b/semver.go @@ -243,7 +243,11 @@ func ParseTolerant(s string) (Version, error) { // Remove leading zeros. for i, p := range parts { if len(p) > 1 { - parts[i] = strings.TrimPrefix(p, "0") + p = strings.TrimLeft(p, "0") + if len(p) == 0 || !strings.ContainsAny(p[0:1], "0123456789") { + p = "0" + p + } + parts[i] = p } } // Fill up shortened versions. diff --git a/semver_test.go b/semver_test.go index 96a7101..d7ddb64 100644 --- a/semver_test.go +++ b/semver_test.go @@ -32,9 +32,12 @@ var formatTests = []formatTest{ var tolerantFormatTests = []formatTest{ {Version{1, 2, 3, nil, nil}, "v1.2.3"}, + {Version{1, 2, 0, []PRVersion{prstr("alpha")}, nil}, "1.2.0-alpha"}, + {Version{1, 2, 0, nil, nil}, "1.2.00"}, {Version{1, 2, 3, nil, nil}, " 1.2.3 "}, {Version{1, 2, 3, nil, nil}, "01.02.03"}, {Version{0, 0, 3, nil, nil}, "00.0.03"}, + {Version{0, 0, 3, nil, nil}, "000.0.03"}, {Version{1, 2, 0, nil, nil}, "1.2"}, {Version{1, 0, 0, nil, nil}, "1"}, } From a64359ff1cd6c92bd6587f45395b5a18d2c5f752 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Tue, 6 Aug 2019 13:47:14 +0200 Subject: [PATCH 15/21] remove pre-release increment constraint --- semver.go | 9 --------- semver_test.go | 14 +++++++------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/semver.go b/semver.go index 26ece43..f7efe6b 100644 --- a/semver.go +++ b/semver.go @@ -163,18 +163,12 @@ func (v Version) Compare(o Version) int { // IncrementPatch increments the patch version func (v *Version) IncrementPatch() error { - if v.Major == 0 { - return fmt.Errorf("Patch version can not be incremented for %q", v.String()) - } v.Patch++ return nil } // IncrementMinor increments the minor version func (v *Version) IncrementMinor() error { - if v.Major == 0 { - return fmt.Errorf("Minor version can not be incremented for %q", v.String()) - } v.Minor++ v.Patch = 0 return nil @@ -182,9 +176,6 @@ func (v *Version) IncrementMinor() error { // IncrementMajor increments the major version func (v *Version) IncrementMajor() error { - if v.Major == 0 { - return fmt.Errorf("Major version can not be incremented for %q", v.String()) - } v.Major++ v.Minor = 0 v.Patch = 0 diff --git a/semver_test.go b/semver_test.go index d7ddb64..e5a8eeb 100644 --- a/semver_test.go +++ b/semver_test.go @@ -137,7 +137,7 @@ func TestCompare(t *testing.T) { if res := test.v1.Compare(test.v2); res != test.result { t.Errorf("Comparing %q : %q, expected %d but got %d", test.v1, test.v2, test.result, res) } - //Test counterpart + // Test counterpart if res := test.v2.Compare(test.v1); res != -test.result { t.Errorf("Comparing %q : %q, expected %d but got %d", test.v2, test.v1, -test.result, res) } @@ -276,9 +276,9 @@ var incrementTests = []incrementTest{ {Version{1, 2, 3, nil, nil}, PATCH, false, Version{1, 2, 4, nil, nil}}, {Version{1, 2, 3, nil, nil}, MINOR, false, Version{1, 3, 0, nil, nil}}, {Version{1, 2, 3, nil, nil}, MAJOR, false, Version{2, 0, 0, nil, nil}}, - {Version{0, 1, 2, nil, nil}, PATCH, true, Version{}}, - {Version{0, 1, 2, nil, nil}, MINOR, true, Version{}}, - {Version{0, 1, 2, nil, nil}, MAJOR, true, Version{}}, + {Version{0, 1, 2, nil, nil}, PATCH, false, Version{0, 1, 3, nil, nil}}, + {Version{0, 1, 2, nil, nil}, MINOR, false, Version{0, 2, 0, nil, nil}}, + {Version{0, 1, 2, nil, nil}, MAJOR, false, Version{1, 0, 0, nil, nil}}, } func TestIncrements(t *testing.T) { @@ -300,10 +300,10 @@ func TestIncrements(t *testing.T) { err = test.version.IncrementMajor() } if test.expectingError { - if err == nil { - t.Errorf("Increment version %q, expecting error, got %q", test.version, err) + if err != nil { + t.Errorf("Increment version, expecting %q, got error %q", test.expectedVersion, err) } - if test.version.NE(originalVersion) { + if test.version.EQ(originalVersion) { t.Errorf("Increment version, expecting %q, got %q", test.expectedVersion, test.version) } } else { From dedd31557083b002e8cc37a29143ed3a5c75c72f Mon Sep 17 00:00:00 2001 From: x899 Date: Sat, 10 Aug 2019 09:03:14 +0530 Subject: [PATCH 16/21] maintaining consistency with other func format --- semver.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/semver.go b/semver.go index f7efe6b..1a9a7ec 100644 --- a/semver.go +++ b/semver.go @@ -210,10 +210,10 @@ func (v Version) Validate() error { } // New is an alias for Parse and returns a pointer, parses version string and returns a validated Version or error -func New(s string) (vp *Version, err error) { +func New(s string) (*Version, error) { v, err := Parse(s) - vp = &v - return + vp := &v + return vp, err } // Make is an alias for Parse, parses version string and returns a validated Version or error From 262233612ee578d4df196755f2df6a2bf5828a4c Mon Sep 17 00:00:00 2001 From: x899 Date: Sat, 10 Aug 2019 11:08:03 +0530 Subject: [PATCH 17/21] finalize version feature --- semver.go | 26 ++++++++++++++++++++++++ semver_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/semver.go b/semver.go index 1a9a7ec..4dbc13c 100644 --- a/semver.go +++ b/semver.go @@ -61,6 +61,18 @@ func (v Version) String() string { return string(b) } +// FinalizeVersion discards prerelease and build number and only returns +// major, minor and patch number. +func (v Version) FinalizeVersion() string { + b := make([]byte, 0, 5) + b = strconv.AppendUint(b, v.Major, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Minor, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Patch, 10) + return string(b) +} + // Equals checks if v is equal to o. func (v Version) Equals(o Version) bool { return (v.Compare(o) == 0) @@ -448,3 +460,17 @@ func NewBuildVersion(s string) (string, error) { } return s, nil } + +// FinalizeVersion returns the major, minor and patch number only and discards +// prerelease and build number. +func FinalizeVersion(s string) (string, error) { + v, err := Parse(s) + if err != nil { + return "", err + } + v.Pre = nil + v.Build = nil + + finalVer := fmt.Sprintf("%s", v) + return finalVer, nil +} diff --git a/semver_test.go b/semver_test.go index e5a8eeb..baf10f8 100644 --- a/semver_test.go +++ b/semver_test.go @@ -95,6 +95,28 @@ func TestValidate(t *testing.T) { } } +var finalizeVersionMethod = []formatTest{ + {Version{1, 2, 3, nil, nil}, "1.2.3"}, + {Version{0, 0, 1, nil, nil}, "0.0.1"}, + {Version{0, 0, 1, []PRVersion{prstr("alpha"), prstr("preview")}, []string{"123", "456"}}, "0.0.1"}, + {Version{1, 2, 3, []PRVersion{prstr("alpha"), prnum(1)}, []string{"123", "456"}}, "1.2.3"}, + {Version{1, 2, 3, []PRVersion{prstr("alpha"), prnum(1)}, nil}, "1.2.3"}, + {Version{1, 2, 3, nil, []string{"123", "456"}}, "1.2.3"}, + // Prereleases and build metadata hyphens + {Version{1, 2, 3, []PRVersion{prstr("alpha"), prstr("b-eta")}, []string{"123", "b-uild"}}, "1.2.3"}, + {Version{1, 2, 3, nil, []string{"123", "b-uild"}}, "1.2.3"}, + {Version{1, 2, 3, []PRVersion{prstr("alpha"), prstr("b-eta")}, nil}, "1.2.3"}, +} + +func TestFinalizeVersionMethod(t *testing.T) { + for _, test := range finalizeVersionMethod { + out := test.v.FinalizeVersion() + if out != test.result { + t.Errorf("Finalized version error, expected %q but got %q", test.result, out) + } + } +} + type compareTest struct { v1 Version v2 Version @@ -387,6 +409,38 @@ func TestMakeHelper(t *testing.T) { } } +type finalizeTest struct { + input string + output string +} + +var finalizeTests = []finalizeTest{ + {"", ""}, + {"1.2.3", "1.2.3"}, + {"0.0.1", "0.0.1"}, + {"0.0.1-alpha.preview+123.456", "0.0.1"}, + {"1.2.3-alpha.1+123.456", "1.2.3"}, + {"1.2.3-alpha.1", "1.2.3"}, + {"1.2.3+123.456", "1.2.3"}, + {"1.2.3-alpha.b-eta+123.b-uild", "1.2.3"}, + {"1.2.3+123.b-uild", "1.2.3"}, + {"1.2.3-alpha.b-eta", "1.2.3"}, + {"1.2-alpha", ""}, +} + +func TestFinalizeVersion(t *testing.T) { + for _, test := range finalizeTests { + finalVer, err := FinalizeVersion(test.input) + if finalVer == "" { + if err == nil { + t.Errorf("Finalize Version error, expected error but got nil") + } + } else if finalVer != test.output && err != nil { + t.Errorf("Finalize Version error expected %q but got %q", test.output, finalVer) + } + } +} + func BenchmarkParseSimple(b *testing.B) { const VERSION = "0.0.1" b.ReportAllocs() From dff411dc0692cbc4d8eef4fb799a291f2d248645 Mon Sep 17 00:00:00 2001 From: x899 Date: Sat, 10 Aug 2019 11:22:29 +0530 Subject: [PATCH 18/21] gosimple use String() instead of fmt.Sprintf --- semver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/semver.go b/semver.go index 4dbc13c..307de61 100644 --- a/semver.go +++ b/semver.go @@ -471,6 +471,6 @@ func FinalizeVersion(s string) (string, error) { v.Pre = nil v.Build = nil - finalVer := fmt.Sprintf("%s", v) + finalVer := v.String() return finalVer, nil } From 19a06804ca573a89d063e03343eedc64e008c8bb Mon Sep 17 00:00:00 2001 From: Sascha Grunert Date: Wed, 29 May 2019 11:03:01 +0200 Subject: [PATCH 19/21] Update go.mod to stick to go 1.12 The go version should be pinned in order to avoid incompatibilities for the now and the future. Signed-off-by: Sascha Grunert --- go.mod | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.mod b/go.mod index d008305..dbaf4c2 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,3 @@ module github.com/blang/semver + +go 1.12 From af3461a9cbcf1f3f5889d21b83f5ef63880c33a8 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Sun, 24 May 2020 16:56:04 +0200 Subject: [PATCH 20/21] introduce version 4 as separate go module --- README.md | 16 +- v4/examples/main.go | 84 +++++++ v4/go.mod | 3 + v4/json.go | 23 ++ v4/json_test.go | 49 ++++ v4/range.go | 416 +++++++++++++++++++++++++++++++ v4/range_test.go | 581 ++++++++++++++++++++++++++++++++++++++++++++ v4/semver.go | 476 ++++++++++++++++++++++++++++++++++++ v4/semver_test.go | 575 +++++++++++++++++++++++++++++++++++++++++++ v4/sort.go | 28 +++ v4/sort_test.go | 30 +++ v4/sql.go | 30 +++ v4/sql_test.go | 38 +++ 13 files changed, 2344 insertions(+), 5 deletions(-) create mode 100644 v4/examples/main.go create mode 100644 v4/go.mod create mode 100644 v4/json.go create mode 100644 v4/json_test.go create mode 100644 v4/range.go create mode 100644 v4/range_test.go create mode 100644 v4/semver.go create mode 100644 v4/semver_test.go create mode 100644 v4/sort.go create mode 100644 v4/sort_test.go create mode 100644 v4/sql.go create mode 100644 v4/sql_test.go diff --git a/README.md b/README.md index e05f986..de07bae 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,29 @@ -semver for golang [![Build Status](https://travis-ci.org/blang/semver.svg?branch=master)](https://travis-ci.org/blang/semver) [![GoDoc](https://godoc.org/github.com/blang/semver?status.svg)](https://godoc.org/github.com/blang/semver) [![Coverage Status](https://img.shields.io/coveralls/blang/semver.svg)](https://coveralls.io/r/blang/semver?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/blang/semver)](https://goreportcard.com/report/github.com/blang/semver) +semver for golang [![Build Status](https://travis-ci.org/blang/semver.svg?branch=master)](https://travis-ci.org/blang/semver) [![GoDoc](https://godoc.org/github.com/blang/semver/v4?status.svg)](https://godoc.org/github.com/blang/semver/v4) [![Coverage Status](https://img.shields.io/coveralls/blang/semver.svg)](https://coveralls.io/r/blang/semver?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/blang/semver)](https://goreportcard.com/report/github.com/blang/semver) ====== semver is a [Semantic Versioning](http://semver.org/) library written in golang. It fully covers spec version `2.0.0`. +Versioning +---------- +Old v1-v3 versions exist in the root of the repository for compatiblity reasons and will only receive bug fixes. + +The current stable version is [*v4*](v4/). + Usage ----- ```bash -$ go get github.com/blang/semver +$ go get github.com/blang/semver/v4 ``` Note: Always vendor your dependencies or fix on a specific version tag. ```go -import github.com/blang/semver +import github.com/blang/semver/v4 v1, err := semver.Make("1.0.0-beta") v2, err := semver.Make("2.0.0-beta") v1.Compare(v2) ``` -Also check the [GoDocs](http://godoc.org/github.com/blang/semver). +Also check the [GoDocs](http://godoc.org/github.com/blang/semver/v4). Why should I use this lib? ----- @@ -96,7 +102,7 @@ Example Have a look at full examples in [examples/main.go](examples/main.go) ```go -import github.com/blang/semver +import github.com/blang/semver/v4 v, err := semver.Make("0.0.1-alpha.preview+123.github") fmt.Printf("Major: %d\n", v.Major) diff --git a/v4/examples/main.go b/v4/examples/main.go new file mode 100644 index 0000000..57a4b17 --- /dev/null +++ b/v4/examples/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + + "github.com/blang/semver/v4" +) + +func main() { + v, err := semver.Parse("0.0.1-alpha.preview.222+123.github") + if err != nil { + fmt.Printf("Error while parsing (not valid): %q", err) + } + fmt.Printf("Version to string: %q\n", v) + + fmt.Printf("Major: %d\n", v.Major) + fmt.Printf("Minor: %d\n", v.Minor) + fmt.Printf("Patch: %d\n", v.Patch) + + // Prerelease versions + if len(v.Pre) > 0 { + fmt.Println("Prerelease versions:") + for i, pre := range v.Pre { + fmt.Printf("%d: %q\n", i, pre) + } + } + + // Build meta data + if len(v.Build) > 0 { + fmt.Println("Build meta data:") + for i, build := range v.Build { + fmt.Printf("%d: %q\n", i, build) + } + } + + // Make == Parse (Value), New for Pointer + v001, _ := semver.Make("0.0.1") + + fmt.Println("\nUse Version.Compare for comparisons (-1, 0, 1):") + fmt.Printf("%q is greater than %q: Compare == %d\n", v001, v, v001.Compare(v)) + fmt.Printf("%q is less than %q: Compare == %d\n", v, v001, v.Compare(v001)) + fmt.Printf("%q is equal to %q: Compare == %d\n", v, v, v.Compare(v)) + + fmt.Println("\nUse comparison helpers returning booleans:") + fmt.Printf("%q is greater than %q: %t\n", v001, v, v001.GT(v)) + fmt.Printf("%q is greater than equal %q: %t\n", v001, v, v001.GTE(v)) + fmt.Printf("%q is greater than equal %q: %t\n", v, v, v.GTE(v)) + fmt.Printf("%q is less than %q: %t\n", v, v001, v.LT(v001)) + fmt.Printf("%q is less than equal %q: %t\n", v, v001, v.LTE(v001)) + fmt.Printf("%q is less than equal %q: %t\n", v, v, v.LTE(v)) + + fmt.Println("\nManipulate Version in place:") + v.Pre[0], err = semver.NewPRVersion("beta") + if err != nil { + fmt.Printf("Error parsing pre release version: %q", err) + } + fmt.Printf("Version to string: %q\n", v) + + fmt.Println("\nCompare Prerelease versions:") + pre1, _ := semver.NewPRVersion("123") + pre2, _ := semver.NewPRVersion("alpha") + pre3, _ := semver.NewPRVersion("124") + fmt.Printf("%q is less than %q: Compare == %d\n", pre1, pre2, pre1.Compare(pre2)) + fmt.Printf("%q is greater than %q: Compare == %d\n", pre3, pre1, pre3.Compare(pre1)) + fmt.Printf("%q is equal to %q: Compare == %d\n", pre1, pre1, pre1.Compare(pre1)) + + fmt.Println("\nValidate versions:") + v.Build[0] = "?" + + err = v.Validate() + if err != nil { + fmt.Printf("Validation failed: %s\n", err) + } + + fmt.Println("Create valid build meta data:") + b1, _ := semver.NewBuildVersion("build123") + v.Build[0] = b1 + fmt.Printf("Version with new build version %q\n", v) + + _, err = semver.NewBuildVersion("build?123") + if err != nil { + fmt.Printf("Create build version failed: %s\n", err) + } +} diff --git a/v4/go.mod b/v4/go.mod new file mode 100644 index 0000000..06d2622 --- /dev/null +++ b/v4/go.mod @@ -0,0 +1,3 @@ +module github.com/blang/semver/v4 + +go 1.14 diff --git a/v4/json.go b/v4/json.go new file mode 100644 index 0000000..a74bf7c --- /dev/null +++ b/v4/json.go @@ -0,0 +1,23 @@ +package semver + +import ( + "encoding/json" +) + +// MarshalJSON implements the encoding/json.Marshaler interface. +func (v Version) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + +// UnmarshalJSON implements the encoding/json.Unmarshaler interface. +func (v *Version) UnmarshalJSON(data []byte) (err error) { + var versionString string + + if err = json.Unmarshal(data, &versionString); err != nil { + return + } + + *v, err = Parse(versionString) + + return +} diff --git a/v4/json_test.go b/v4/json_test.go new file mode 100644 index 0000000..c635dea --- /dev/null +++ b/v4/json_test.go @@ -0,0 +1,49 @@ +package semver + +import ( + "encoding/json" + "strconv" + "testing" +) + +func TestJSONMarshal(t *testing.T) { + versionString := "3.1.4-alpha.1.5.9+build.2.6.5" + v, err := Parse(versionString) + if err != nil { + t.Fatal(err) + } + + versionJSON, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + + quotedVersionString := strconv.Quote(versionString) + + if string(versionJSON) != quotedVersionString { + t.Fatalf("JSON marshaled semantic version not equal: expected %q, got %q", quotedVersionString, string(versionJSON)) + } +} + +func TestJSONUnmarshal(t *testing.T) { + versionString := "3.1.4-alpha.1.5.9+build.2.6.5" + quotedVersionString := strconv.Quote(versionString) + + var v Version + if err := json.Unmarshal([]byte(quotedVersionString), &v); err != nil { + t.Fatal(err) + } + + if v.String() != versionString { + t.Fatalf("JSON unmarshaled semantic version not equal: expected %q, got %q", versionString, v.String()) + } + + badVersionString := strconv.Quote("3.1.4.1.5.9.2.6.5-other-digits-of-pi") + if err := json.Unmarshal([]byte(badVersionString), &v); err == nil { + t.Fatal("expected JSON unmarshal error, got nil") + } + + if err := json.Unmarshal([]byte("3.1"), &v); err == nil { + t.Fatal("expected JSON unmarshal error, got nil") + } +} diff --git a/v4/range.go b/v4/range.go new file mode 100644 index 0000000..95f7139 --- /dev/null +++ b/v4/range.go @@ -0,0 +1,416 @@ +package semver + +import ( + "fmt" + "strconv" + "strings" + "unicode" +) + +type wildcardType int + +const ( + noneWildcard wildcardType = iota + majorWildcard wildcardType = 1 + minorWildcard wildcardType = 2 + patchWildcard wildcardType = 3 +) + +func wildcardTypefromInt(i int) wildcardType { + switch i { + case 1: + return majorWildcard + case 2: + return minorWildcard + case 3: + return patchWildcard + default: + return noneWildcard + } +} + +type comparator func(Version, Version) bool + +var ( + compEQ comparator = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) == 0 + } + compNE = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) != 0 + } + compGT = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) == 1 + } + compGE = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) >= 0 + } + compLT = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) == -1 + } + compLE = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) <= 0 + } +) + +type versionRange struct { + v Version + c comparator +} + +// rangeFunc creates a Range from the given versionRange. +func (vr *versionRange) rangeFunc() Range { + return Range(func(v Version) bool { + return vr.c(v, vr.v) + }) +} + +// Range represents a range of versions. +// A Range can be used to check if a Version satisfies it: +// +// range, err := semver.ParseRange(">1.0.0 <2.0.0") +// range(semver.MustParse("1.1.1") // returns true +type Range func(Version) bool + +// OR combines the existing Range with another Range using logical OR. +func (rf Range) OR(f Range) Range { + return Range(func(v Version) bool { + return rf(v) || f(v) + }) +} + +// AND combines the existing Range with another Range using logical AND. +func (rf Range) AND(f Range) Range { + return Range(func(v Version) bool { + return rf(v) && f(v) + }) +} + +// ParseRange parses a range and returns a Range. +// If the range could not be parsed an error is returned. +// +// Valid ranges are: +// - "<1.0.0" +// - "<=1.0.0" +// - ">1.0.0" +// - ">=1.0.0" +// - "1.0.0", "=1.0.0", "==1.0.0" +// - "!1.0.0", "!=1.0.0" +// +// A Range can consist of multiple ranges separated by space: +// Ranges can be linked by logical AND: +// - ">1.0.0 <2.0.0" would match between both ranges, so "1.1.1" and "1.8.7" but not "1.0.0" or "2.0.0" +// - ">1.0.0 <3.0.0 !2.0.3-beta.2" would match every version between 1.0.0 and 3.0.0 except 2.0.3-beta.2 +// +// Ranges can also be linked by logical OR: +// - "<2.0.0 || >=3.0.0" would match "1.x.x" and "3.x.x" but not "2.x.x" +// +// AND has a higher precedence than OR. It's not possible to use brackets. +// +// Ranges can be combined by both AND and OR +// +// - `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`, but not `4.2.1`, `2.1.1` +func ParseRange(s string) (Range, error) { + parts := splitAndTrim(s) + orParts, err := splitORParts(parts) + if err != nil { + return nil, err + } + expandedParts, err := expandWildcardVersion(orParts) + if err != nil { + return nil, err + } + var orFn Range + for _, p := range expandedParts { + var andFn Range + for _, ap := range p { + opStr, vStr, err := splitComparatorVersion(ap) + if err != nil { + return nil, err + } + vr, err := buildVersionRange(opStr, vStr) + if err != nil { + return nil, fmt.Errorf("Could not parse Range %q: %s", ap, err) + } + rf := vr.rangeFunc() + + // Set function + if andFn == nil { + andFn = rf + } else { // Combine with existing function + andFn = andFn.AND(rf) + } + } + if orFn == nil { + orFn = andFn + } else { + orFn = orFn.OR(andFn) + } + + } + return orFn, nil +} + +// splitORParts splits the already cleaned parts by '||'. +// Checks for invalid positions of the operator and returns an +// error if found. +func splitORParts(parts []string) ([][]string, error) { + var ORparts [][]string + last := 0 + for i, p := range parts { + if p == "||" { + if i == 0 { + return nil, fmt.Errorf("First element in range is '||'") + } + ORparts = append(ORparts, parts[last:i]) + last = i + 1 + } + } + if last == len(parts) { + return nil, fmt.Errorf("Last element in range is '||'") + } + ORparts = append(ORparts, parts[last:]) + return ORparts, nil +} + +// buildVersionRange takes a slice of 2: operator and version +// and builds a versionRange, otherwise an error. +func buildVersionRange(opStr, vStr string) (*versionRange, error) { + c := parseComparator(opStr) + if c == nil { + return nil, fmt.Errorf("Could not parse comparator %q in %q", opStr, strings.Join([]string{opStr, vStr}, "")) + } + v, err := Parse(vStr) + if err != nil { + return nil, fmt.Errorf("Could not parse version %q in %q: %s", vStr, strings.Join([]string{opStr, vStr}, ""), err) + } + + return &versionRange{ + v: v, + c: c, + }, nil + +} + +// inArray checks if a byte is contained in an array of bytes +func inArray(s byte, list []byte) bool { + for _, el := range list { + if el == s { + return true + } + } + return false +} + +// splitAndTrim splits a range string by spaces and cleans whitespaces +func splitAndTrim(s string) (result []string) { + last := 0 + var lastChar byte + excludeFromSplit := []byte{'>', '<', '='} + for i := 0; i < len(s); i++ { + if s[i] == ' ' && !inArray(lastChar, excludeFromSplit) { + if last < i-1 { + result = append(result, s[last:i]) + } + last = i + 1 + } else if s[i] != ' ' { + lastChar = s[i] + } + } + if last < len(s)-1 { + result = append(result, s[last:]) + } + + for i, v := range result { + result[i] = strings.Replace(v, " ", "", -1) + } + + // parts := strings.Split(s, " ") + // for _, x := range parts { + // if s := strings.TrimSpace(x); len(s) != 0 { + // result = append(result, s) + // } + // } + return +} + +// splitComparatorVersion splits the comparator from the version. +// Input must be free of leading or trailing spaces. +func splitComparatorVersion(s string) (string, string, error) { + i := strings.IndexFunc(s, unicode.IsDigit) + if i == -1 { + return "", "", fmt.Errorf("Could not get version from string: %q", s) + } + return strings.TrimSpace(s[0:i]), s[i:], nil +} + +// getWildcardType will return the type of wildcard that the +// passed version contains +func getWildcardType(vStr string) wildcardType { + parts := strings.Split(vStr, ".") + nparts := len(parts) + wildcard := parts[nparts-1] + + possibleWildcardType := wildcardTypefromInt(nparts) + if wildcard == "x" { + return possibleWildcardType + } + + return noneWildcard +} + +// createVersionFromWildcard will convert a wildcard version +// into a regular version, replacing 'x's with '0's, handling +// special cases like '1.x.x' and '1.x' +func createVersionFromWildcard(vStr string) string { + // handle 1.x.x + vStr2 := strings.Replace(vStr, ".x.x", ".x", 1) + vStr2 = strings.Replace(vStr2, ".x", ".0", 1) + parts := strings.Split(vStr2, ".") + + // handle 1.x + if len(parts) == 2 { + return vStr2 + ".0" + } + + return vStr2 +} + +// incrementMajorVersion will increment the major version +// of the passed version +func incrementMajorVersion(vStr string) (string, error) { + parts := strings.Split(vStr, ".") + i, err := strconv.Atoi(parts[0]) + if err != nil { + return "", err + } + parts[0] = strconv.Itoa(i + 1) + + return strings.Join(parts, "."), nil +} + +// incrementMajorVersion will increment the minor version +// of the passed version +func incrementMinorVersion(vStr string) (string, error) { + parts := strings.Split(vStr, ".") + i, err := strconv.Atoi(parts[1]) + if err != nil { + return "", err + } + parts[1] = strconv.Itoa(i + 1) + + return strings.Join(parts, "."), nil +} + +// expandWildcardVersion will expand wildcards inside versions +// following these rules: +// +// * when dealing with patch wildcards: +// >= 1.2.x will become >= 1.2.0 +// <= 1.2.x will become < 1.3.0 +// > 1.2.x will become >= 1.3.0 +// < 1.2.x will become < 1.2.0 +// != 1.2.x will become < 1.2.0 >= 1.3.0 +// +// * when dealing with minor wildcards: +// >= 1.x will become >= 1.0.0 +// <= 1.x will become < 2.0.0 +// > 1.x will become >= 2.0.0 +// < 1.0 will become < 1.0.0 +// != 1.x will become < 1.0.0 >= 2.0.0 +// +// * when dealing with wildcards without +// version operator: +// 1.2.x will become >= 1.2.0 < 1.3.0 +// 1.x will become >= 1.0.0 < 2.0.0 +func expandWildcardVersion(parts [][]string) ([][]string, error) { + var expandedParts [][]string + for _, p := range parts { + var newParts []string + for _, ap := range p { + if strings.Contains(ap, "x") { + opStr, vStr, err := splitComparatorVersion(ap) + if err != nil { + return nil, err + } + + versionWildcardType := getWildcardType(vStr) + flatVersion := createVersionFromWildcard(vStr) + + var resultOperator string + var shouldIncrementVersion bool + switch opStr { + case ">": + resultOperator = ">=" + shouldIncrementVersion = true + case ">=": + resultOperator = ">=" + case "<": + resultOperator = "<" + case "<=": + resultOperator = "<" + shouldIncrementVersion = true + case "", "=", "==": + newParts = append(newParts, ">="+flatVersion) + resultOperator = "<" + shouldIncrementVersion = true + case "!=", "!": + newParts = append(newParts, "<"+flatVersion) + resultOperator = ">=" + shouldIncrementVersion = true + } + + var resultVersion string + if shouldIncrementVersion { + switch versionWildcardType { + case patchWildcard: + resultVersion, _ = incrementMinorVersion(flatVersion) + case minorWildcard: + resultVersion, _ = incrementMajorVersion(flatVersion) + } + } else { + resultVersion = flatVersion + } + + ap = resultOperator + resultVersion + } + newParts = append(newParts, ap) + } + expandedParts = append(expandedParts, newParts) + } + + return expandedParts, nil +} + +func parseComparator(s string) comparator { + switch s { + case "==": + fallthrough + case "": + fallthrough + case "=": + return compEQ + case ">": + return compGT + case ">=": + return compGE + case "<": + return compLT + case "<=": + return compLE + case "!": + fallthrough + case "!=": + return compNE + } + + return nil +} + +// MustParseRange is like ParseRange but panics if the range cannot be parsed. +func MustParseRange(s string) Range { + r, err := ParseRange(s) + if err != nil { + panic(`semver: ParseRange(` + s + `): ` + err.Error()) + } + return r +} diff --git a/v4/range_test.go b/v4/range_test.go new file mode 100644 index 0000000..2f44de4 --- /dev/null +++ b/v4/range_test.go @@ -0,0 +1,581 @@ +package semver + +import ( + "reflect" + "strings" + "testing" +) + +type wildcardTypeTest struct { + input string + wildcardType wildcardType +} + +type comparatorTest struct { + input string + comparator func(comparator) bool +} + +func TestParseComparator(t *testing.T) { + compatorTests := []comparatorTest{ + {">", testGT}, + {">=", testGE}, + {"<", testLT}, + {"<=", testLE}, + {"", testEQ}, + {"=", testEQ}, + {"==", testEQ}, + {"!=", testNE}, + {"!", testNE}, + {"-", nil}, + {"<==", nil}, + {"<<", nil}, + {">>", nil}, + } + + for _, tc := range compatorTests { + if c := parseComparator(tc.input); c == nil { + if tc.comparator != nil { + t.Errorf("Comparator nil for case %q\n", tc.input) + } + } else if !tc.comparator(c) { + t.Errorf("Invalid comparator for case %q\n", tc.input) + } + } +} + +var ( + v1 = MustParse("1.2.2") + v2 = MustParse("1.2.3") + v3 = MustParse("1.2.4") +) + +func testEQ(f comparator) bool { + return f(v1, v1) && !f(v1, v2) +} + +func testNE(f comparator) bool { + return !f(v1, v1) && f(v1, v2) +} + +func testGT(f comparator) bool { + return f(v2, v1) && f(v3, v2) && !f(v1, v2) && !f(v1, v1) +} + +func testGE(f comparator) bool { + return f(v2, v1) && f(v3, v2) && !f(v1, v2) +} + +func testLT(f comparator) bool { + return f(v1, v2) && f(v2, v3) && !f(v2, v1) && !f(v1, v1) +} + +func testLE(f comparator) bool { + return f(v1, v2) && f(v2, v3) && !f(v2, v1) +} + +func TestSplitAndTrim(t *testing.T) { + tests := []struct { + i string + s []string + }{ + {"1.2.3 1.2.3", []string{"1.2.3", "1.2.3"}}, + {" 1.2.3 1.2.3 ", []string{"1.2.3", "1.2.3"}}, // Spaces + {" >= 1.2.3 <= 1.2.3 ", []string{">=1.2.3", "<=1.2.3"}}, // Spaces between operator and version + {"1.2.3 || >=1.2.3 <1.2.3", []string{"1.2.3", "||", ">=1.2.3", "<1.2.3"}}, + {" 1.2.3 || >=1.2.3 <1.2.3 ", []string{"1.2.3", "||", ">=1.2.3", "<1.2.3"}}, + } + + for _, tc := range tests { + p := splitAndTrim(tc.i) + if !reflect.DeepEqual(p, tc.s) { + t.Errorf("Invalid for case %q: Expected %q, got: %q", tc.i, tc.s, p) + } + } +} + +func TestSplitComparatorVersion(t *testing.T) { + tests := []struct { + i string + p []string + }{ + {">1.2.3", []string{">", "1.2.3"}}, + {">=1.2.3", []string{">=", "1.2.3"}}, + {"<1.2.3", []string{"<", "1.2.3"}}, + {"<=1.2.3", []string{"<=", "1.2.3"}}, + {"1.2.3", []string{"", "1.2.3"}}, + {"=1.2.3", []string{"=", "1.2.3"}}, + {"==1.2.3", []string{"==", "1.2.3"}}, + {"!=1.2.3", []string{"!=", "1.2.3"}}, + {"!1.2.3", []string{"!", "1.2.3"}}, + {"error", nil}, + } + for _, tc := range tests { + if op, v, err := splitComparatorVersion(tc.i); err != nil { + if tc.p != nil { + t.Errorf("Invalid for case %q: Expected %q, got error %q", tc.i, tc.p, err) + } + } else if op != tc.p[0] { + t.Errorf("Invalid operator for case %q: Expected %q, got: %q", tc.i, tc.p[0], op) + } else if v != tc.p[1] { + t.Errorf("Invalid version for case %q: Expected %q, got: %q", tc.i, tc.p[1], v) + } + + } +} + +func TestBuildVersionRange(t *testing.T) { + tests := []struct { + opStr string + vStr string + c func(comparator) bool + v string + }{ + {">", "1.2.3", testGT, "1.2.3"}, + {">=", "1.2.3", testGE, "1.2.3"}, + {"<", "1.2.3", testLT, "1.2.3"}, + {"<=", "1.2.3", testLE, "1.2.3"}, + {"", "1.2.3", testEQ, "1.2.3"}, + {"=", "1.2.3", testEQ, "1.2.3"}, + {"==", "1.2.3", testEQ, "1.2.3"}, + {"!=", "1.2.3", testNE, "1.2.3"}, + {"!", "1.2.3", testNE, "1.2.3"}, + {">>", "1.2.3", nil, ""}, // Invalid comparator + {"=", "invalid", nil, ""}, // Invalid version + } + + for _, tc := range tests { + if r, err := buildVersionRange(tc.opStr, tc.vStr); err != nil { + if tc.c != nil { + t.Errorf("Invalid for case %q: Expected %q, got error %q", strings.Join([]string{tc.opStr, tc.vStr}, ""), tc.v, err) + } + } else if r == nil { + t.Errorf("Invalid for case %q: got nil", strings.Join([]string{tc.opStr, tc.vStr}, "")) + } else { + // test version + if tv := MustParse(tc.v); !r.v.EQ(tv) { + t.Errorf("Invalid for case %q: Expected version %q, got: %q", strings.Join([]string{tc.opStr, tc.vStr}, ""), tv, r.v) + } + // test comparator + if r.c == nil { + t.Errorf("Invalid for case %q: got nil comparator", strings.Join([]string{tc.opStr, tc.vStr}, "")) + continue + } + if !tc.c(r.c) { + t.Errorf("Invalid comparator for case %q\n", strings.Join([]string{tc.opStr, tc.vStr}, "")) + } + } + } + +} + +func TestSplitORParts(t *testing.T) { + tests := []struct { + i []string + o [][]string + }{ + {[]string{">1.2.3", "||", "<1.2.3", "||", "=1.2.3"}, [][]string{ + {">1.2.3"}, + {"<1.2.3"}, + {"=1.2.3"}, + }}, + {[]string{">1.2.3", "<1.2.3", "||", "=1.2.3"}, [][]string{ + {">1.2.3", "<1.2.3"}, + {"=1.2.3"}, + }}, + {[]string{">1.2.3", "||"}, nil}, + {[]string{"||", ">1.2.3"}, nil}, + } + for _, tc := range tests { + o, err := splitORParts(tc.i) + if err != nil && tc.o != nil { + t.Errorf("Unexpected error for case %q: %s", tc.i, err) + } + if !reflect.DeepEqual(tc.o, o) { + t.Errorf("Invalid for case %q: Expected %q, got: %q", tc.i, tc.o, o) + } + } +} + +func TestGetWildcardType(t *testing.T) { + wildcardTypeTests := []wildcardTypeTest{ + {"x", majorWildcard}, + {"1.x", minorWildcard}, + {"1.2.x", patchWildcard}, + {"fo.o.b.ar", noneWildcard}, + } + + for _, tc := range wildcardTypeTests { + o := getWildcardType(tc.input) + if o != tc.wildcardType { + t.Errorf("Invalid for case: %q: Expected %q, got: %q", tc.input, tc.wildcardType, o) + } + } +} + +func TestCreateVersionFromWildcard(t *testing.T) { + tests := []struct { + i string + s string + }{ + {"1.2.x", "1.2.0"}, + {"1.x", "1.0.0"}, + } + + for _, tc := range tests { + p := createVersionFromWildcard(tc.i) + if p != tc.s { + t.Errorf("Invalid for case %q: Expected %q, got: %q", tc.i, tc.s, p) + } + } +} + +func TestIncrementMajorVersion(t *testing.T) { + tests := []struct { + i string + s string + }{ + {"1.2.3", "2.2.3"}, + {"1.2", "2.2"}, + {"foo.bar", ""}, + } + + for _, tc := range tests { + p, _ := incrementMajorVersion(tc.i) + if p != tc.s { + t.Errorf("Invalid for case %q: Expected %q, got: %q", tc.i, tc.s, p) + } + } +} + +func TestIncrementMinorVersion(t *testing.T) { + tests := []struct { + i string + s string + }{ + {"1.2.3", "1.3.3"}, + {"1.2", "1.3"}, + {"foo.bar", ""}, + } + + for _, tc := range tests { + p, _ := incrementMinorVersion(tc.i) + if p != tc.s { + t.Errorf("Invalid for case %q: Expected %q, got: %q", tc.i, tc.s, p) + } + } +} + +func TestExpandWildcardVersion(t *testing.T) { + tests := []struct { + i [][]string + o [][]string + }{ + {[][]string{{"foox"}}, nil}, + {[][]string{{">=1.2.x"}}, [][]string{{">=1.2.0"}}}, + {[][]string{{"<=1.2.x"}}, [][]string{{"<1.3.0"}}}, + {[][]string{{">1.2.x"}}, [][]string{{">=1.3.0"}}}, + {[][]string{{"<1.2.x"}}, [][]string{{"<1.2.0"}}}, + {[][]string{{"!=1.2.x"}}, [][]string{{"<1.2.0", ">=1.3.0"}}}, + {[][]string{{">=1.x"}}, [][]string{{">=1.0.0"}}}, + {[][]string{{"<=1.x"}}, [][]string{{"<2.0.0"}}}, + {[][]string{{">1.x"}}, [][]string{{">=2.0.0"}}}, + {[][]string{{"<1.x"}}, [][]string{{"<1.0.0"}}}, + {[][]string{{"!=1.x"}}, [][]string{{"<1.0.0", ">=2.0.0"}}}, + {[][]string{{"1.2.x"}}, [][]string{{">=1.2.0", "<1.3.0"}}}, + {[][]string{{"1.x"}}, [][]string{{">=1.0.0", "<2.0.0"}}}, + } + + for _, tc := range tests { + o, _ := expandWildcardVersion(tc.i) + if !reflect.DeepEqual(tc.o, o) { + t.Errorf("Invalid for case %q: Expected %q, got: %q", tc.i, tc.o, o) + } + } +} + +func TestVersionRangeToRange(t *testing.T) { + vr := versionRange{ + v: MustParse("1.2.3"), + c: compLT, + } + rf := vr.rangeFunc() + if !rf(MustParse("1.2.2")) || rf(MustParse("1.2.3")) { + t.Errorf("Invalid conversion to range func") + } +} + +func TestRangeAND(t *testing.T) { + v := MustParse("1.2.2") + v1 := MustParse("1.2.1") + v2 := MustParse("1.2.3") + rf1 := Range(func(v Version) bool { + return v.GT(v1) + }) + rf2 := Range(func(v Version) bool { + return v.LT(v2) + }) + rf := rf1.AND(rf2) + if rf(v1) { + t.Errorf("Invalid rangefunc, accepted: %s", v1) + } + if rf(v2) { + t.Errorf("Invalid rangefunc, accepted: %s", v2) + } + if !rf(v) { + t.Errorf("Invalid rangefunc, did not accept: %s", v) + } +} + +func TestRangeOR(t *testing.T) { + tests := []struct { + v Version + b bool + }{ + {MustParse("1.2.0"), true}, + {MustParse("1.2.2"), false}, + {MustParse("1.2.4"), true}, + } + v1 := MustParse("1.2.1") + v2 := MustParse("1.2.3") + rf1 := Range(func(v Version) bool { + return v.LT(v1) + }) + rf2 := Range(func(v Version) bool { + return v.GT(v2) + }) + rf := rf1.OR(rf2) + for _, tc := range tests { + if r := rf(tc.v); r != tc.b { + t.Errorf("Invalid for case %q: Expected %t, got %t", tc.v, tc.b, r) + } + } +} + +func TestParseRange(t *testing.T) { + type tv struct { + v string + b bool + } + tests := []struct { + i string + t []tv + }{ + // Simple expressions + {">1.2.3", []tv{ + {"1.2.2", false}, + {"1.2.3", false}, + {"1.2.4", true}, + }}, + {">=1.2.3", []tv{ + {"1.2.3", true}, + {"1.2.4", true}, + {"1.2.2", false}, + }}, + {"<1.2.3", []tv{ + {"1.2.2", true}, + {"1.2.3", false}, + {"1.2.4", false}, + }}, + {"<=1.2.3", []tv{ + {"1.2.2", true}, + {"1.2.3", true}, + {"1.2.4", false}, + }}, + {"1.2.3", []tv{ + {"1.2.2", false}, + {"1.2.3", true}, + {"1.2.4", false}, + }}, + {"=1.2.3", []tv{ + {"1.2.2", false}, + {"1.2.3", true}, + {"1.2.4", false}, + }}, + {"==1.2.3", []tv{ + {"1.2.2", false}, + {"1.2.3", true}, + {"1.2.4", false}, + }}, + {"!=1.2.3", []tv{ + {"1.2.2", true}, + {"1.2.3", false}, + {"1.2.4", true}, + }}, + {"!1.2.3", []tv{ + {"1.2.2", true}, + {"1.2.3", false}, + {"1.2.4", true}, + }}, + // Simple Expression errors + {">>1.2.3", nil}, + {"!1.2.3", nil}, + {"1.0", nil}, + {"string", nil}, + {"", nil}, + {"fo.ob.ar.x", nil}, + // AND Expressions + {">1.2.2 <1.2.4", []tv{ + {"1.2.2", false}, + {"1.2.3", true}, + {"1.2.4", false}, + }}, + {"<1.2.2 <1.2.4", []tv{ + {"1.2.1", true}, + {"1.2.2", false}, + {"1.2.3", false}, + {"1.2.4", false}, + }}, + {">1.2.2 <1.2.5 !=1.2.4", []tv{ + {"1.2.2", false}, + {"1.2.3", true}, + {"1.2.4", false}, + {"1.2.5", false}, + }}, + {">1.2.2 <1.2.5 !1.2.4", []tv{ + {"1.2.2", false}, + {"1.2.3", true}, + {"1.2.4", false}, + {"1.2.5", false}, + }}, + // OR Expressions + {">1.2.2 || <1.2.4", []tv{ + {"1.2.2", true}, + {"1.2.3", true}, + {"1.2.4", true}, + }}, + {"<1.2.2 || >1.2.4", []tv{ + {"1.2.2", false}, + {"1.2.3", false}, + {"1.2.4", false}, + }}, + // Wildcard expressions + {">1.x", []tv{ + {"0.1.9", false}, + {"1.2.6", false}, + {"1.9.0", false}, + {"2.0.0", true}, + }}, + {">1.2.x", []tv{ + {"1.1.9", false}, + {"1.2.6", false}, + {"1.3.0", true}, + }}, + // Combined Expressions + {">1.2.2 <1.2.4 || >=2.0.0", []tv{ + {"1.2.2", false}, + {"1.2.3", true}, + {"1.2.4", false}, + {"2.0.0", true}, + {"2.0.1", true}, + }}, + {"1.x || >=2.0.x <2.2.x", []tv{ + {"0.9.2", false}, + {"1.2.2", true}, + {"2.0.0", true}, + {"2.1.8", true}, + {"2.2.0", false}, + }}, + {">1.2.2 <1.2.4 || >=2.0.0 <3.0.0", []tv{ + {"1.2.2", false}, + {"1.2.3", true}, + {"1.2.4", false}, + {"2.0.0", true}, + {"2.0.1", true}, + {"2.9.9", true}, + {"3.0.0", false}, + }}, + } + + for _, tc := range tests { + r, err := ParseRange(tc.i) + if err != nil && tc.t != nil { + t.Errorf("Error parsing range %q: %s", tc.i, err) + continue + } + for _, tvc := range tc.t { + v := MustParse(tvc.v) + if res := r(v); res != tvc.b { + t.Errorf("Invalid for case %q matching %q: Expected %t, got: %t", tc.i, tvc.v, tvc.b, res) + } + } + + } +} + +func TestMustParseRange(t *testing.T) { + testCase := ">1.2.2 <1.2.4 || >=2.0.0 <3.0.0" + r := MustParseRange(testCase) + if !r(MustParse("1.2.3")) { + t.Errorf("Unexpected range behavior on MustParseRange") + } +} + +func TestMustParseRange_panic(t *testing.T) { + defer func() { + if recover() == nil { + t.Errorf("Should have panicked") + } + }() + _ = MustParseRange("invalid version") +} + +func BenchmarkRangeParseSimple(b *testing.B) { + const VERSION = ">1.0.0" + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + _, _ = ParseRange(VERSION) + } +} + +func BenchmarkRangeParseAverage(b *testing.B) { + const VERSION = ">=1.0.0 <2.0.0" + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + _, _ = ParseRange(VERSION) + } +} + +func BenchmarkRangeParseComplex(b *testing.B) { + const VERSION = ">=1.0.0 <2.0.0 || >=3.0.1 <4.0.0 !=3.0.3 || >=5.0.0" + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + _, _ = ParseRange(VERSION) + } +} + +func BenchmarkRangeMatchSimple(b *testing.B) { + const VERSION = ">1.0.0" + r, _ := ParseRange(VERSION) + v := MustParse("2.0.0") + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + r(v) + } +} + +func BenchmarkRangeMatchAverage(b *testing.B) { + const VERSION = ">=1.0.0 <2.0.0" + r, _ := ParseRange(VERSION) + v := MustParse("1.2.3") + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + r(v) + } +} + +func BenchmarkRangeMatchComplex(b *testing.B) { + const VERSION = ">=1.0.0 <2.0.0 || >=3.0.1 <4.0.0 !=3.0.3 || >=5.0.0" + r, _ := ParseRange(VERSION) + v := MustParse("5.0.1") + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + r(v) + } +} diff --git a/v4/semver.go b/v4/semver.go new file mode 100644 index 0000000..307de61 --- /dev/null +++ b/v4/semver.go @@ -0,0 +1,476 @@ +package semver + +import ( + "errors" + "fmt" + "strconv" + "strings" +) + +const ( + numbers string = "0123456789" + alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" + alphanum = alphas + numbers +) + +// SpecVersion is the latest fully supported spec version of semver +var SpecVersion = Version{ + Major: 2, + Minor: 0, + Patch: 0, +} + +// Version represents a semver compatible version +type Version struct { + Major uint64 + Minor uint64 + Patch uint64 + Pre []PRVersion + Build []string //No Precedence +} + +// Version to string +func (v Version) String() string { + b := make([]byte, 0, 5) + b = strconv.AppendUint(b, v.Major, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Minor, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Patch, 10) + + if len(v.Pre) > 0 { + b = append(b, '-') + b = append(b, v.Pre[0].String()...) + + for _, pre := range v.Pre[1:] { + b = append(b, '.') + b = append(b, pre.String()...) + } + } + + if len(v.Build) > 0 { + b = append(b, '+') + b = append(b, v.Build[0]...) + + for _, build := range v.Build[1:] { + b = append(b, '.') + b = append(b, build...) + } + } + + return string(b) +} + +// FinalizeVersion discards prerelease and build number and only returns +// major, minor and patch number. +func (v Version) FinalizeVersion() string { + b := make([]byte, 0, 5) + b = strconv.AppendUint(b, v.Major, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Minor, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Patch, 10) + return string(b) +} + +// Equals checks if v is equal to o. +func (v Version) Equals(o Version) bool { + return (v.Compare(o) == 0) +} + +// EQ checks if v is equal to o. +func (v Version) EQ(o Version) bool { + return (v.Compare(o) == 0) +} + +// NE checks if v is not equal to o. +func (v Version) NE(o Version) bool { + return (v.Compare(o) != 0) +} + +// GT checks if v is greater than o. +func (v Version) GT(o Version) bool { + return (v.Compare(o) == 1) +} + +// GTE checks if v is greater than or equal to o. +func (v Version) GTE(o Version) bool { + return (v.Compare(o) >= 0) +} + +// GE checks if v is greater than or equal to o. +func (v Version) GE(o Version) bool { + return (v.Compare(o) >= 0) +} + +// LT checks if v is less than o. +func (v Version) LT(o Version) bool { + return (v.Compare(o) == -1) +} + +// LTE checks if v is less than or equal to o. +func (v Version) LTE(o Version) bool { + return (v.Compare(o) <= 0) +} + +// LE checks if v is less than or equal to o. +func (v Version) LE(o Version) bool { + return (v.Compare(o) <= 0) +} + +// Compare compares Versions v to o: +// -1 == v is less than o +// 0 == v is equal to o +// 1 == v is greater than o +func (v Version) Compare(o Version) int { + if v.Major != o.Major { + if v.Major > o.Major { + return 1 + } + return -1 + } + if v.Minor != o.Minor { + if v.Minor > o.Minor { + return 1 + } + return -1 + } + if v.Patch != o.Patch { + if v.Patch > o.Patch { + return 1 + } + return -1 + } + + // Quick comparison if a version has no prerelease versions + if len(v.Pre) == 0 && len(o.Pre) == 0 { + return 0 + } else if len(v.Pre) == 0 && len(o.Pre) > 0 { + return 1 + } else if len(v.Pre) > 0 && len(o.Pre) == 0 { + return -1 + } + + i := 0 + for ; i < len(v.Pre) && i < len(o.Pre); i++ { + if comp := v.Pre[i].Compare(o.Pre[i]); comp == 0 { + continue + } else if comp == 1 { + return 1 + } else { + return -1 + } + } + + // If all pr versions are the equal but one has further prversion, this one greater + if i == len(v.Pre) && i == len(o.Pre) { + return 0 + } else if i == len(v.Pre) && i < len(o.Pre) { + return -1 + } else { + return 1 + } + +} + +// IncrementPatch increments the patch version +func (v *Version) IncrementPatch() error { + v.Patch++ + return nil +} + +// IncrementMinor increments the minor version +func (v *Version) IncrementMinor() error { + v.Minor++ + v.Patch = 0 + return nil +} + +// IncrementMajor increments the major version +func (v *Version) IncrementMajor() error { + v.Major++ + v.Minor = 0 + v.Patch = 0 + return nil +} + +// Validate validates v and returns error in case +func (v Version) Validate() error { + // Major, Minor, Patch already validated using uint64 + + for _, pre := range v.Pre { + if !pre.IsNum { //Numeric prerelease versions already uint64 + if len(pre.VersionStr) == 0 { + return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr) + } + if !containsOnly(pre.VersionStr, alphanum) { + return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr) + } + } + } + + for _, build := range v.Build { + if len(build) == 0 { + return fmt.Errorf("Build meta data can not be empty %q", build) + } + if !containsOnly(build, alphanum) { + return fmt.Errorf("Invalid character(s) found in build meta data %q", build) + } + } + + return nil +} + +// New is an alias for Parse and returns a pointer, parses version string and returns a validated Version or error +func New(s string) (*Version, error) { + v, err := Parse(s) + vp := &v + return vp, err +} + +// Make is an alias for Parse, parses version string and returns a validated Version or error +func Make(s string) (Version, error) { + return Parse(s) +} + +// ParseTolerant allows for certain version specifications that do not strictly adhere to semver +// specs to be parsed by this library. It does so by normalizing versions before passing them to +// Parse(). It currently trims spaces, removes a "v" prefix, adds a 0 patch number to versions +// with only major and minor components specified, and removes leading 0s. +func ParseTolerant(s string) (Version, error) { + s = strings.TrimSpace(s) + s = strings.TrimPrefix(s, "v") + + // Split into major.minor.(patch+pr+meta) + parts := strings.SplitN(s, ".", 3) + // Remove leading zeros. + for i, p := range parts { + if len(p) > 1 { + p = strings.TrimLeft(p, "0") + if len(p) == 0 || !strings.ContainsAny(p[0:1], "0123456789") { + p = "0" + p + } + parts[i] = p + } + } + // Fill up shortened versions. + if len(parts) < 3 { + if strings.ContainsAny(parts[len(parts)-1], "+-") { + return Version{}, errors.New("Short version cannot contain PreRelease/Build meta data") + } + for len(parts) < 3 { + parts = append(parts, "0") + } + } + s = strings.Join(parts, ".") + + return Parse(s) +} + +// Parse parses version string and returns a validated Version or error +func Parse(s string) (Version, error) { + if len(s) == 0 { + return Version{}, errors.New("Version string empty") + } + + // Split into major.minor.(patch+pr+meta) + parts := strings.SplitN(s, ".", 3) + if len(parts) != 3 { + return Version{}, errors.New("No Major.Minor.Patch elements found") + } + + // Major + if !containsOnly(parts[0], numbers) { + return Version{}, fmt.Errorf("Invalid character(s) found in major number %q", parts[0]) + } + if hasLeadingZeroes(parts[0]) { + return Version{}, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0]) + } + major, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return Version{}, err + } + + // Minor + if !containsOnly(parts[1], numbers) { + return Version{}, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1]) + } + if hasLeadingZeroes(parts[1]) { + return Version{}, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1]) + } + minor, err := strconv.ParseUint(parts[1], 10, 64) + if err != nil { + return Version{}, err + } + + v := Version{} + v.Major = major + v.Minor = minor + + var build, prerelease []string + patchStr := parts[2] + + if buildIndex := strings.IndexRune(patchStr, '+'); buildIndex != -1 { + build = strings.Split(patchStr[buildIndex+1:], ".") + patchStr = patchStr[:buildIndex] + } + + if preIndex := strings.IndexRune(patchStr, '-'); preIndex != -1 { + prerelease = strings.Split(patchStr[preIndex+1:], ".") + patchStr = patchStr[:preIndex] + } + + if !containsOnly(patchStr, numbers) { + return Version{}, fmt.Errorf("Invalid character(s) found in patch number %q", patchStr) + } + if hasLeadingZeroes(patchStr) { + return Version{}, fmt.Errorf("Patch number must not contain leading zeroes %q", patchStr) + } + patch, err := strconv.ParseUint(patchStr, 10, 64) + if err != nil { + return Version{}, err + } + + v.Patch = patch + + // Prerelease + for _, prstr := range prerelease { + parsedPR, err := NewPRVersion(prstr) + if err != nil { + return Version{}, err + } + v.Pre = append(v.Pre, parsedPR) + } + + // Build meta data + for _, str := range build { + if len(str) == 0 { + return Version{}, errors.New("Build meta data is empty") + } + if !containsOnly(str, alphanum) { + return Version{}, fmt.Errorf("Invalid character(s) found in build meta data %q", str) + } + v.Build = append(v.Build, str) + } + + return v, nil +} + +// MustParse is like Parse but panics if the version cannot be parsed. +func MustParse(s string) Version { + v, err := Parse(s) + if err != nil { + panic(`semver: Parse(` + s + `): ` + err.Error()) + } + return v +} + +// PRVersion represents a PreRelease Version +type PRVersion struct { + VersionStr string + VersionNum uint64 + IsNum bool +} + +// NewPRVersion creates a new valid prerelease version +func NewPRVersion(s string) (PRVersion, error) { + if len(s) == 0 { + return PRVersion{}, errors.New("Prerelease is empty") + } + v := PRVersion{} + if containsOnly(s, numbers) { + if hasLeadingZeroes(s) { + return PRVersion{}, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s) + } + num, err := strconv.ParseUint(s, 10, 64) + + // Might never be hit, but just in case + if err != nil { + return PRVersion{}, err + } + v.VersionNum = num + v.IsNum = true + } else if containsOnly(s, alphanum) { + v.VersionStr = s + v.IsNum = false + } else { + return PRVersion{}, fmt.Errorf("Invalid character(s) found in prerelease %q", s) + } + return v, nil +} + +// IsNumeric checks if prerelease-version is numeric +func (v PRVersion) IsNumeric() bool { + return v.IsNum +} + +// Compare compares two PreRelease Versions v and o: +// -1 == v is less than o +// 0 == v is equal to o +// 1 == v is greater than o +func (v PRVersion) Compare(o PRVersion) int { + if v.IsNum && !o.IsNum { + return -1 + } else if !v.IsNum && o.IsNum { + return 1 + } else if v.IsNum && o.IsNum { + if v.VersionNum == o.VersionNum { + return 0 + } else if v.VersionNum > o.VersionNum { + return 1 + } else { + return -1 + } + } else { // both are Alphas + if v.VersionStr == o.VersionStr { + return 0 + } else if v.VersionStr > o.VersionStr { + return 1 + } else { + return -1 + } + } +} + +// PreRelease version to string +func (v PRVersion) String() string { + if v.IsNum { + return strconv.FormatUint(v.VersionNum, 10) + } + return v.VersionStr +} + +func containsOnly(s string, set string) bool { + return strings.IndexFunc(s, func(r rune) bool { + return !strings.ContainsRune(set, r) + }) == -1 +} + +func hasLeadingZeroes(s string) bool { + return len(s) > 1 && s[0] == '0' +} + +// NewBuildVersion creates a new valid build version +func NewBuildVersion(s string) (string, error) { + if len(s) == 0 { + return "", errors.New("Buildversion is empty") + } + if !containsOnly(s, alphanum) { + return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s) + } + return s, nil +} + +// FinalizeVersion returns the major, minor and patch number only and discards +// prerelease and build number. +func FinalizeVersion(s string) (string, error) { + v, err := Parse(s) + if err != nil { + return "", err + } + v.Pre = nil + v.Build = nil + + finalVer := v.String() + return finalVer, nil +} diff --git a/v4/semver_test.go b/v4/semver_test.go new file mode 100644 index 0000000..baf10f8 --- /dev/null +++ b/v4/semver_test.go @@ -0,0 +1,575 @@ +package semver + +import ( + "testing" +) + +func prstr(s string) PRVersion { + return PRVersion{s, 0, false} +} + +func prnum(i uint64) PRVersion { + return PRVersion{"", i, true} +} + +type formatTest struct { + v Version + result string +} + +var formatTests = []formatTest{ + {Version{1, 2, 3, nil, nil}, "1.2.3"}, + {Version{0, 0, 1, nil, nil}, "0.0.1"}, + {Version{0, 0, 1, []PRVersion{prstr("alpha"), prstr("preview")}, []string{"123", "456"}}, "0.0.1-alpha.preview+123.456"}, + {Version{1, 2, 3, []PRVersion{prstr("alpha"), prnum(1)}, []string{"123", "456"}}, "1.2.3-alpha.1+123.456"}, + {Version{1, 2, 3, []PRVersion{prstr("alpha"), prnum(1)}, nil}, "1.2.3-alpha.1"}, + {Version{1, 2, 3, nil, []string{"123", "456"}}, "1.2.3+123.456"}, + // Prereleases and build metadata hyphens + {Version{1, 2, 3, []PRVersion{prstr("alpha"), prstr("b-eta")}, []string{"123", "b-uild"}}, "1.2.3-alpha.b-eta+123.b-uild"}, + {Version{1, 2, 3, nil, []string{"123", "b-uild"}}, "1.2.3+123.b-uild"}, + {Version{1, 2, 3, []PRVersion{prstr("alpha"), prstr("b-eta")}, nil}, "1.2.3-alpha.b-eta"}, +} + +var tolerantFormatTests = []formatTest{ + {Version{1, 2, 3, nil, nil}, "v1.2.3"}, + {Version{1, 2, 0, []PRVersion{prstr("alpha")}, nil}, "1.2.0-alpha"}, + {Version{1, 2, 0, nil, nil}, "1.2.00"}, + {Version{1, 2, 3, nil, nil}, " 1.2.3 "}, + {Version{1, 2, 3, nil, nil}, "01.02.03"}, + {Version{0, 0, 3, nil, nil}, "00.0.03"}, + {Version{0, 0, 3, nil, nil}, "000.0.03"}, + {Version{1, 2, 0, nil, nil}, "1.2"}, + {Version{1, 0, 0, nil, nil}, "1"}, +} + +func TestStringer(t *testing.T) { + for _, test := range formatTests { + if res := test.v.String(); res != test.result { + t.Errorf("Stringer, expected %q but got %q", test.result, res) + } + } +} + +func TestParse(t *testing.T) { + for _, test := range formatTests { + if v, err := Parse(test.result); err != nil { + t.Errorf("Error parsing %q: %q", test.result, err) + } else if comp := v.Compare(test.v); comp != 0 { + t.Errorf("Parsing, expected %q but got %q, comp: %d ", test.v, v, comp) + } else if err := v.Validate(); err != nil { + t.Errorf("Error validating parsed version %q: %q", test.v, err) + } + } +} + +func TestParseTolerant(t *testing.T) { + for _, test := range tolerantFormatTests { + if v, err := ParseTolerant(test.result); err != nil { + t.Errorf("Error parsing %q: %q", test.result, err) + } else if comp := v.Compare(test.v); comp != 0 { + t.Errorf("Parsing, expected %q but got %q, comp: %d ", test.v, v, comp) + } else if err := v.Validate(); err != nil { + t.Errorf("Error validating parsed version %q: %q", test.v, err) + } + } +} + +func TestMustParse(t *testing.T) { + _ = MustParse("32.2.1-alpha") +} + +func TestMustParse_panic(t *testing.T) { + defer func() { + if recover() == nil { + t.Errorf("Should have panicked") + } + }() + _ = MustParse("invalid version") +} + +func TestValidate(t *testing.T) { + for _, test := range formatTests { + if err := test.v.Validate(); err != nil { + t.Errorf("Error validating %q: %q", test.v, err) + } + } +} + +var finalizeVersionMethod = []formatTest{ + {Version{1, 2, 3, nil, nil}, "1.2.3"}, + {Version{0, 0, 1, nil, nil}, "0.0.1"}, + {Version{0, 0, 1, []PRVersion{prstr("alpha"), prstr("preview")}, []string{"123", "456"}}, "0.0.1"}, + {Version{1, 2, 3, []PRVersion{prstr("alpha"), prnum(1)}, []string{"123", "456"}}, "1.2.3"}, + {Version{1, 2, 3, []PRVersion{prstr("alpha"), prnum(1)}, nil}, "1.2.3"}, + {Version{1, 2, 3, nil, []string{"123", "456"}}, "1.2.3"}, + // Prereleases and build metadata hyphens + {Version{1, 2, 3, []PRVersion{prstr("alpha"), prstr("b-eta")}, []string{"123", "b-uild"}}, "1.2.3"}, + {Version{1, 2, 3, nil, []string{"123", "b-uild"}}, "1.2.3"}, + {Version{1, 2, 3, []PRVersion{prstr("alpha"), prstr("b-eta")}, nil}, "1.2.3"}, +} + +func TestFinalizeVersionMethod(t *testing.T) { + for _, test := range finalizeVersionMethod { + out := test.v.FinalizeVersion() + if out != test.result { + t.Errorf("Finalized version error, expected %q but got %q", test.result, out) + } + } +} + +type compareTest struct { + v1 Version + v2 Version + result int +} + +var compareTests = []compareTest{ + {Version{1, 0, 0, nil, nil}, Version{1, 0, 0, nil, nil}, 0}, + {Version{2, 0, 0, nil, nil}, Version{1, 0, 0, nil, nil}, 1}, + {Version{0, 1, 0, nil, nil}, Version{0, 1, 0, nil, nil}, 0}, + {Version{0, 2, 0, nil, nil}, Version{0, 1, 0, nil, nil}, 1}, + {Version{0, 0, 1, nil, nil}, Version{0, 0, 1, nil, nil}, 0}, + {Version{0, 0, 2, nil, nil}, Version{0, 0, 1, nil, nil}, 1}, + {Version{1, 2, 3, nil, nil}, Version{1, 2, 3, nil, nil}, 0}, + {Version{2, 2, 4, nil, nil}, Version{1, 2, 4, nil, nil}, 1}, + {Version{1, 3, 3, nil, nil}, Version{1, 2, 3, nil, nil}, 1}, + {Version{1, 2, 4, nil, nil}, Version{1, 2, 3, nil, nil}, 1}, + + // Spec Examples #11 + {Version{1, 0, 0, nil, nil}, Version{2, 0, 0, nil, nil}, -1}, + {Version{2, 0, 0, nil, nil}, Version{2, 1, 0, nil, nil}, -1}, + {Version{2, 1, 0, nil, nil}, Version{2, 1, 1, nil, nil}, -1}, + + // Spec Examples #9 + {Version{1, 0, 0, nil, nil}, Version{1, 0, 0, []PRVersion{prstr("alpha")}, nil}, 1}, + {Version{1, 0, 0, []PRVersion{prstr("alpha")}, nil}, Version{1, 0, 0, []PRVersion{prstr("alpha"), prnum(1)}, nil}, -1}, + {Version{1, 0, 0, []PRVersion{prstr("alpha"), prnum(1)}, nil}, Version{1, 0, 0, []PRVersion{prstr("alpha"), prstr("beta")}, nil}, -1}, + {Version{1, 0, 0, []PRVersion{prstr("alpha"), prstr("beta")}, nil}, Version{1, 0, 0, []PRVersion{prstr("beta")}, nil}, -1}, + {Version{1, 0, 0, []PRVersion{prstr("beta")}, nil}, Version{1, 0, 0, []PRVersion{prstr("beta"), prnum(2)}, nil}, -1}, + {Version{1, 0, 0, []PRVersion{prstr("beta"), prnum(2)}, nil}, Version{1, 0, 0, []PRVersion{prstr("beta"), prnum(11)}, nil}, -1}, + {Version{1, 0, 0, []PRVersion{prstr("beta"), prnum(11)}, nil}, Version{1, 0, 0, []PRVersion{prstr("rc"), prnum(1)}, nil}, -1}, + {Version{1, 0, 0, []PRVersion{prstr("rc"), prnum(1)}, nil}, Version{1, 0, 0, nil, nil}, -1}, + + // Ignore Build metadata + {Version{1, 0, 0, nil, []string{"1", "2", "3"}}, Version{1, 0, 0, nil, nil}, 0}, +} + +func TestCompare(t *testing.T) { + for _, test := range compareTests { + if res := test.v1.Compare(test.v2); res != test.result { + t.Errorf("Comparing %q : %q, expected %d but got %d", test.v1, test.v2, test.result, res) + } + // Test counterpart + if res := test.v2.Compare(test.v1); res != -test.result { + t.Errorf("Comparing %q : %q, expected %d but got %d", test.v2, test.v1, -test.result, res) + } + } +} + +type wrongformatTest struct { + v *Version + str string +} + +var wrongformatTests = []wrongformatTest{ + {nil, ""}, + {nil, "."}, + {nil, "1."}, + {nil, ".1"}, + {nil, "a.b.c"}, + {nil, "1.a.b"}, + {nil, "1.1.a"}, + {nil, "1.a.1"}, + {nil, "a.1.1"}, + {nil, ".."}, + {nil, "1.."}, + {nil, "1.1."}, + {nil, "1..1"}, + {nil, "1.1.+123"}, + {nil, "1.1.-beta"}, + {nil, "-1.1.1"}, + {nil, "1.-1.1"}, + {nil, "1.1.-1"}, + // giant numbers + {nil, "20000000000000000000.1.1"}, + {nil, "1.20000000000000000000.1"}, + {nil, "1.1.20000000000000000000"}, + {nil, "1.1.1-20000000000000000000"}, + // Leading zeroes + {nil, "01.1.1"}, + {nil, "001.1.1"}, + {nil, "1.01.1"}, + {nil, "1.001.1"}, + {nil, "1.1.01"}, + {nil, "1.1.001"}, + {nil, "1.1.1-01"}, + {nil, "1.1.1-001"}, + {nil, "1.1.1-beta.01"}, + {nil, "1.1.1-beta.001"}, + {&Version{0, 0, 0, []PRVersion{prstr("!")}, nil}, "0.0.0-!"}, + {&Version{0, 0, 0, nil, []string{"!"}}, "0.0.0+!"}, + // empty prversion + {&Version{0, 0, 0, []PRVersion{prstr(""), prstr("alpha")}, nil}, "0.0.0-.alpha"}, + // empty build meta data + {&Version{0, 0, 0, []PRVersion{prstr("alpha")}, []string{""}}, "0.0.0-alpha+"}, + {&Version{0, 0, 0, []PRVersion{prstr("alpha")}, []string{"test", ""}}, "0.0.0-alpha+test."}, +} + +func TestWrongFormat(t *testing.T) { + for _, test := range wrongformatTests { + + if res, err := Parse(test.str); err == nil { + t.Errorf("Parsing wrong format version %q, expected error but got %q", test.str, res) + } + + if test.v != nil { + if err := test.v.Validate(); err == nil { + t.Errorf("Validating wrong format version %q (%q), expected error", test.v, test.str) + } + } + } +} + +var wrongTolerantFormatTests = []wrongformatTest{ + {nil, "1.0+abc"}, + {nil, "1.0-rc.1"}, +} + +func TestWrongTolerantFormat(t *testing.T) { + for _, test := range wrongTolerantFormatTests { + if res, err := ParseTolerant(test.str); err == nil { + t.Errorf("Parsing wrong format version %q, expected error but got %q", test.str, res) + } + } +} + +func TestCompareHelper(t *testing.T) { + v := Version{1, 0, 0, []PRVersion{prstr("alpha")}, nil} + v1 := Version{1, 0, 0, nil, nil} + if !v.EQ(v) { + t.Errorf("%q should be equal to %q", v, v) + } + if !v.Equals(v) { + t.Errorf("%q should be equal to %q", v, v) + } + if !v1.NE(v) { + t.Errorf("%q should not be equal to %q", v1, v) + } + if !v.GTE(v) { + t.Errorf("%q should be greater than or equal to %q", v, v) + } + if !v.LTE(v) { + t.Errorf("%q should be less than or equal to %q", v, v) + } + if !v.LT(v1) { + t.Errorf("%q should be less than %q", v, v1) + } + if !v.LTE(v1) { + t.Errorf("%q should be less than or equal %q", v, v1) + } + if !v.LE(v1) { + t.Errorf("%q should be less than or equal %q", v, v1) + } + if !v1.GT(v) { + t.Errorf("%q should be greater than %q", v1, v) + } + if !v1.GTE(v) { + t.Errorf("%q should be greater than or equal %q", v1, v) + } + if !v1.GE(v) { + t.Errorf("%q should be greater than or equal %q", v1, v) + } +} + +const ( + MAJOR = iota + MINOR + PATCH +) + +type incrementTest struct { + version Version + incrementType int + expectingError bool + expectedVersion Version +} + +var incrementTests = []incrementTest{ + {Version{1, 2, 3, nil, nil}, PATCH, false, Version{1, 2, 4, nil, nil}}, + {Version{1, 2, 3, nil, nil}, MINOR, false, Version{1, 3, 0, nil, nil}}, + {Version{1, 2, 3, nil, nil}, MAJOR, false, Version{2, 0, 0, nil, nil}}, + {Version{0, 1, 2, nil, nil}, PATCH, false, Version{0, 1, 3, nil, nil}}, + {Version{0, 1, 2, nil, nil}, MINOR, false, Version{0, 2, 0, nil, nil}}, + {Version{0, 1, 2, nil, nil}, MAJOR, false, Version{1, 0, 0, nil, nil}}, +} + +func TestIncrements(t *testing.T) { + for _, test := range incrementTests { + var originalVersion = Version{ + test.version.Major, + test.version.Minor, + test.version.Patch, + test.version.Pre, + test.version.Build, + } + var err error + switch test.incrementType { + case PATCH: + err = test.version.IncrementPatch() + case MINOR: + err = test.version.IncrementMinor() + case MAJOR: + err = test.version.IncrementMajor() + } + if test.expectingError { + if err != nil { + t.Errorf("Increment version, expecting %q, got error %q", test.expectedVersion, err) + } + if test.version.EQ(originalVersion) { + t.Errorf("Increment version, expecting %q, got %q", test.expectedVersion, test.version) + } + } else { + if (err != nil) && !test.expectingError { + t.Errorf("Increment version %q, not expecting error, got %q", test.version, err) + } + if test.version.NE(test.expectedVersion) { + t.Errorf("Increment version, expecting %q, got %q", test.expectedVersion, test.version) + } + } + } +} + +func TestPreReleaseVersions(t *testing.T) { + p1, err := NewPRVersion("123") + if !p1.IsNumeric() { + t.Errorf("Expected numeric prversion, got %q", p1) + } + if p1.VersionNum != 123 { + t.Error("Wrong prversion number") + } + if err != nil { + t.Errorf("Not expected error %q", err) + } + p2, err := NewPRVersion("alpha") + if p2.IsNumeric() { + t.Errorf("Expected non-numeric prversion, got %q", p2) + } + if p2.VersionStr != "alpha" { + t.Error("Wrong prversion string") + } + if err != nil { + t.Errorf("Not expected error %q", err) + } +} + +func TestBuildMetaDataVersions(t *testing.T) { + _, err := NewBuildVersion("123") + if err != nil { + t.Errorf("Unexpected error %q", err) + } + + _, err = NewBuildVersion("build") + if err != nil { + t.Errorf("Unexpected error %q", err) + } + + _, err = NewBuildVersion("test?") + if err == nil { + t.Error("Expected error, got none") + } + + _, err = NewBuildVersion("") + if err == nil { + t.Error("Expected error, got none") + } +} + +func TestNewHelper(t *testing.T) { + v, err := New("1.2.3") + if err != nil { + t.Fatalf("Unexpected error %q", err) + } + + // New returns pointer + if v == nil { + t.Fatal("Version is nil") + } + if v.Compare(Version{1, 2, 3, nil, nil}) != 0 { + t.Fatal("Unexpected comparison problem") + } +} + +func TestMakeHelper(t *testing.T) { + v, err := Make("1.2.3") + if err != nil { + t.Fatalf("Unexpected error %q", err) + } + if v.Compare(Version{1, 2, 3, nil, nil}) != 0 { + t.Fatal("Unexpected comparison problem") + } +} + +type finalizeTest struct { + input string + output string +} + +var finalizeTests = []finalizeTest{ + {"", ""}, + {"1.2.3", "1.2.3"}, + {"0.0.1", "0.0.1"}, + {"0.0.1-alpha.preview+123.456", "0.0.1"}, + {"1.2.3-alpha.1+123.456", "1.2.3"}, + {"1.2.3-alpha.1", "1.2.3"}, + {"1.2.3+123.456", "1.2.3"}, + {"1.2.3-alpha.b-eta+123.b-uild", "1.2.3"}, + {"1.2.3+123.b-uild", "1.2.3"}, + {"1.2.3-alpha.b-eta", "1.2.3"}, + {"1.2-alpha", ""}, +} + +func TestFinalizeVersion(t *testing.T) { + for _, test := range finalizeTests { + finalVer, err := FinalizeVersion(test.input) + if finalVer == "" { + if err == nil { + t.Errorf("Finalize Version error, expected error but got nil") + } + } else if finalVer != test.output && err != nil { + t.Errorf("Finalize Version error expected %q but got %q", test.output, finalVer) + } + } +} + +func BenchmarkParseSimple(b *testing.B) { + const VERSION = "0.0.1" + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + _, _ = Parse(VERSION) + } +} + +func BenchmarkParseComplex(b *testing.B) { + const VERSION = "0.0.1-alpha.preview+123.456" + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + _, _ = Parse(VERSION) + } +} + +func BenchmarkParseAverage(b *testing.B) { + l := len(formatTests) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + _, _ = Parse(formatTests[n%l].result) + } +} + +func BenchmarkParseTolerantAverage(b *testing.B) { + l := len(tolerantFormatTests) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + _, _ = ParseTolerant(tolerantFormatTests[n%l].result) + } +} + +func BenchmarkStringSimple(b *testing.B) { + const VERSION = "0.0.1" + v, _ := Parse(VERSION) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + _ = v.String() + } +} + +func BenchmarkStringLarger(b *testing.B) { + const VERSION = "11.15.2012" + v, _ := Parse(VERSION) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + _ = v.String() + } +} + +func BenchmarkStringComplex(b *testing.B) { + const VERSION = "0.0.1-alpha.preview+123.456" + v, _ := Parse(VERSION) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + _ = v.String() + } +} + +func BenchmarkStringAverage(b *testing.B) { + l := len(formatTests) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + _ = formatTests[n%l].v.String() + } +} + +func BenchmarkValidateSimple(b *testing.B) { + const VERSION = "0.0.1" + v, _ := Parse(VERSION) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + _ = v.Validate() + } +} + +func BenchmarkValidateComplex(b *testing.B) { + const VERSION = "0.0.1-alpha.preview+123.456" + v, _ := Parse(VERSION) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + _ = v.Validate() + } +} + +func BenchmarkValidateAverage(b *testing.B) { + l := len(formatTests) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + _ = formatTests[n%l].v.Validate() + } +} + +func BenchmarkCompareSimple(b *testing.B) { + const VERSION = "0.0.1" + v, _ := Parse(VERSION) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + v.Compare(v) + } +} + +func BenchmarkCompareComplex(b *testing.B) { + const VERSION = "0.0.1-alpha.preview+123.456" + v, _ := Parse(VERSION) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + v.Compare(v) + } +} + +func BenchmarkCompareAverage(b *testing.B) { + l := len(compareTests) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + compareTests[n%l].v1.Compare((compareTests[n%l].v2)) + } +} diff --git a/v4/sort.go b/v4/sort.go new file mode 100644 index 0000000..e18f880 --- /dev/null +++ b/v4/sort.go @@ -0,0 +1,28 @@ +package semver + +import ( + "sort" +) + +// Versions represents multiple versions. +type Versions []Version + +// Len returns length of version collection +func (s Versions) Len() int { + return len(s) +} + +// Swap swaps two versions inside the collection by its indices +func (s Versions) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less checks if version at index i is less than version at index j +func (s Versions) Less(i, j int) bool { + return s[i].LT(s[j]) +} + +// Sort sorts a slice of versions +func Sort(versions []Version) { + sort.Sort(Versions(versions)) +} diff --git a/v4/sort_test.go b/v4/sort_test.go new file mode 100644 index 0000000..6889397 --- /dev/null +++ b/v4/sort_test.go @@ -0,0 +1,30 @@ +package semver + +import ( + "reflect" + "testing" +) + +func TestSort(t *testing.T) { + v100, _ := Parse("1.0.0") + v010, _ := Parse("0.1.0") + v001, _ := Parse("0.0.1") + versions := []Version{v010, v100, v001} + Sort(versions) + + correct := []Version{v001, v010, v100} + if !reflect.DeepEqual(versions, correct) { + t.Fatalf("Sort returned wrong order: %s", versions) + } +} + +func BenchmarkSort(b *testing.B) { + v100, _ := Parse("1.0.0") + v010, _ := Parse("0.1.0") + v001, _ := Parse("0.0.1") + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + Sort([]Version{v010, v100, v001}) + } +} diff --git a/v4/sql.go b/v4/sql.go new file mode 100644 index 0000000..db95813 --- /dev/null +++ b/v4/sql.go @@ -0,0 +1,30 @@ +package semver + +import ( + "database/sql/driver" + "fmt" +) + +// Scan implements the database/sql.Scanner interface. +func (v *Version) Scan(src interface{}) (err error) { + var str string + switch src := src.(type) { + case string: + str = src + case []byte: + str = string(src) + default: + return fmt.Errorf("version.Scan: cannot convert %T to string", src) + } + + if t, err := Parse(str); err == nil { + *v = t + } + + return +} + +// Value implements the database/sql/driver.Valuer interface. +func (v Version) Value() (driver.Value, error) { + return v.String(), nil +} diff --git a/v4/sql_test.go b/v4/sql_test.go new file mode 100644 index 0000000..ebf48b5 --- /dev/null +++ b/v4/sql_test.go @@ -0,0 +1,38 @@ +package semver + +import ( + "testing" +) + +type scanTest struct { + val interface{} + shouldError bool + expected string +} + +var scanTests = []scanTest{ + {"1.2.3", false, "1.2.3"}, + {[]byte("1.2.3"), false, "1.2.3"}, + {7, true, ""}, + {7e4, true, ""}, + {true, true, ""}, +} + +func TestScanString(t *testing.T) { + for _, tc := range scanTests { + s := &Version{} + err := s.Scan(tc.val) + if tc.shouldError { + if err == nil { + t.Fatalf("Scan did not return an error on %v (%T)", tc.val, tc.val) + } + } else { + if err != nil { + t.Fatalf("Scan returned an unexpected error: %s (%T) on %v (%T)", tc.val, tc.val, tc.val, tc.val) + } + if val, _ := s.Value(); val != tc.expected { + t.Errorf("Wrong Value returned, expected %q, got %q", tc.expected, val) + } + } + } +} From 4487282d78122a245e413d7515e7c516b70c33fd Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Sun, 24 May 2020 17:35:40 +0200 Subject: [PATCH 21/21] docs: add instructions for version v4 --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index de07bae..dade530 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,14 @@ Versioning ---------- Old v1-v3 versions exist in the root of the repository for compatiblity reasons and will only receive bug fixes. -The current stable version is [*v4*](v4/). +The current stable version is [*v4*](v4/) and is fully go-mod compatible. Usage ----- ```bash $ go get github.com/blang/semver/v4 +# Or use fixed versions +$ go get github.com/blang/semver/v4@v4.0.0 ``` Note: Always vendor your dependencies or fix on a specific version tag. @@ -99,7 +101,7 @@ if expectedRange(v) { Example ----- -Have a look at full examples in [examples/main.go](examples/main.go) +Have a look at full examples in [v4/examples/main.go](v4/examples/main.go) ```go import github.com/blang/semver/v4