From 6bee9cf8c9807748b63e596285ba4fbf4ab256c8 Mon Sep 17 00:00:00 2001 From: Benedikt Lang Date: Wed, 2 Jul 2014 10:38:36 +0200 Subject: [PATCH] Add validation, further tests, compare helpers --- semver.go | 87 ++++++++++++++++++++++++++++++++--- semver_test.go | 120 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 194 insertions(+), 13 deletions(-) diff --git a/semver.go b/semver.go index fc392e9..ec2932d 100644 --- a/semver.go +++ b/semver.go @@ -8,7 +8,11 @@ import ( "strings" ) -var SEMVER_SPEC_VERSION = Version{ +const NUMBERS = "0123456789" +const ALPHAS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" + +// Latest fully supported spec version +var SPEC_VERSION = Version{ Major: 2, Minor: 0, Patch: 0, @@ -22,6 +26,7 @@ type Version struct { Build []string //No Precendence } +// Version to string func (v *Version) String() string { var buf bytes.Buffer var DOT = []byte(".") @@ -53,6 +58,30 @@ func (v *Version) String() string { return buf.String() } +// Checks if v is greater than o. +func (v *Version) GT(o *Version) bool { + return (v.Compare(o) == 1) +} + +// Checks if v is greater than or equal to o. +func (v *Version) GTE(o *Version) bool { + return (v.Compare(o) >= 0) +} + +// Checks if v is less than o. +func (v *Version) LT(o *Version) bool { + return (v.Compare(o) == -1) +} + +// Checks if v is less than or equal to o. +func (v *Version) LTE(o *Version) bool { + return (v.Compare(o) <= 0) +} + +// 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 { @@ -108,16 +137,50 @@ func (v *Version) Compare(o *Version) int { } } +// Validates v and returns error in case +func (v *Version) Validate() error { + // Major, Minor, Patch already validated using uint64 + + if len(v.Pre) > 0 { + 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, NUMBERS+ALPHAS) { + return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr) + } + } + } + } + + if len(v.Build) > 0 { + for _, build := range v.Build { + if len(build) == 0 { + return fmt.Errorf("Build meta data can not be empty %q", build) + } + if !containsOnly(build, ALPHAS+NUMBERS) { + return fmt.Errorf("Invalid character(s) found in build meta data %q", build) + } + } + } + + return nil +} + +// Parses a string to version func Parse(s string) (*Version, error) { if len(s) == 0 { return nil, errors.New("Version string empty") } + // Split into major.minor.(patch+pr+meta) parts := strings.SplitN(s, ".", 3) if len(parts) != 3 { return nil, errors.New("No Major.Minor.Patch elements found") } + // Major if !containsOnly(parts[0], NUMBERS) { return nil, fmt.Errorf("Invalid character(s) found in major number %q", parts[0]) } @@ -126,6 +189,7 @@ func Parse(s string) (*Version, error) { return nil, err } + // Minor if !containsOnly(parts[1], NUMBERS) { return nil, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1]) } @@ -137,6 +201,7 @@ func Parse(s string) (*Version, error) { preIndex := strings.Index(parts[2], "-") buildIndex := strings.Index(parts[2], "+") + // Determine last index of patch version (first of pre or build versions) var subVersionIndex int if preIndex != -1 && buildIndex == -1 { subVersionIndex = preIndex @@ -145,7 +210,7 @@ func Parse(s string) (*Version, error) { } else if preIndex == -1 && buildIndex == -1 { subVersionIndex = len(parts[2]) } else { - // if there is no actual preIndex but a hyphen inside the build meta data + // if there is no actual prversion but a hyphen inside the build meta data if buildIndex < preIndex { subVersionIndex = buildIndex preIndex = -1 // Build meta data before preIndex found implicates there are no prerelease versions @@ -189,6 +254,9 @@ func Parse(s string) (*Version, error) { buildStr := parts[2][buildIndex+1:] buildParts := strings.Split(buildStr, ".") for _, str := range buildParts { + if len(str) == 0 { + return nil, errors.New("Build meta data is empty") + } if !containsOnly(str, ALPHAS+NUMBERS) { return nil, fmt.Errorf("Invalid character(s) found in build meta data %q", str) } @@ -206,10 +274,16 @@ type PRVersion struct { IsNum bool } +// Creates a new valid prerelease version func NewPRVersion(s string) (*PRVersion, error) { + if len(s) == 0 { + return nil, errors.New("Prerelease is empty") + } v := &PRVersion{} if containsOnly(s, NUMBERS) { num, err := strconv.ParseUint(s, 10, 64) + + // Might never be hit, but just in case if err != nil { return nil, err } @@ -224,10 +298,15 @@ func NewPRVersion(s string) (*PRVersion, error) { return v, nil } +// Is pre release version numeric? func (v *PRVersion) IsNumeric() bool { return v.IsNum } +// Compares PreRelease Versions v to 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 @@ -252,6 +331,7 @@ func (v *PRVersion) Compare(o *PRVersion) int { } } +// PreRelease version to string func (v *PRVersion) String() string { if v.IsNum { return strconv.FormatUint(v.VersionNum, 10) @@ -259,9 +339,6 @@ func (v *PRVersion) String() string { return v.VersionStr } -const NUMBERS = "0123456789" -const ALPHAS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" - func containsOnly(s string, set string) bool { return strings.IndexFunc(s, func(r rune) bool { return !strings.ContainsRune(set, r) diff --git a/semver_test.go b/semver_test.go index 897d976..9707f51 100644 --- a/semver_test.go +++ b/semver_test.go @@ -6,6 +6,14 @@ 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 @@ -24,14 +32,6 @@ var formatTests = []formatTest{ {Version{1, 2, 3, []*PRVersion{prstr("alpha"), prstr("b-eta")}, nil}, "1.2.3-alpha.b-eta"}, } -func prstr(s string) *PRVersion { - return &PRVersion{s, 0, false} -} - -func prnum(i uint64) *PRVersion { - return &PRVersion{"", i, true} -} - func TestStringer(t *testing.T) { for _, test := range formatTests { if res := test.v.String(); res != test.result { @@ -46,6 +46,16 @@ func TestParse(t *testing.T) { 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 TestValidate(t *testing.T) { + for _, test := range formatTests { + if err := test.v.Validate(); err != nil { + t.Errorf("Error validating %q: %q", test.v, err) } } } @@ -96,3 +106,97 @@ func TestCompare(t *testing.T) { } } } + +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"}, + {&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) + } + } + } +} + +func TestCompareHelper(t *testing.T) { + v := &Version{1, 0, 0, []*PRVersion{prstr("alpha")}, nil} + v1 := &Version{1, 0, 0, nil, nil} + 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 greater 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 !v1.GT(v) { + t.Errorf("%q should be less than %q", v1, v) + } + if !v1.GTE(v) { + t.Errorf("%q should be less than or equal %q", v1, v) + } +} + +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) + } +}