Kaynağa Gözat

version: add an AtLeast helper to compare versions.

Signed-off-by: David Anderson <[email protected]>
David Anderson 5 yıl önce
ebeveyn
işleme
03aa319762
2 değiştirilmiş dosya ile 213 ekleme ve 0 silme
  1. 142 0
      version/cmp.go
  2. 71 0
      version/cmp_test.go

+ 142 - 0
version/cmp.go

@@ -0,0 +1,142 @@
+// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package version
+
+import (
+	"log"
+	"strconv"
+	"strings"
+)
+
+// AtLeast returns whether version is at least the specified minimum
+// version.
+//
+// Version comparison in Tailscale is a little complex, because we
+// switched "styles" a few times, and additionally have a completely
+// separate track of version numbers for OSS-only builds.
+//
+// AtLeast acts conservatively, returning true only if it's certain
+// that version is at least minimum. As a result, it can produce false
+// negatives, for example when an OSS build supports a given feature,
+// but AtLeast is called with an official release number as the
+// minimum
+//
+// version and minimum can both be either an official Tailscale
+// version numbers (major.minor.patch-extracommits-extrastring), or an
+// OSS build datestamp (date.YYYYMMDD). For Tailscale version numbers,
+// AtLeast also accepts a prefix of a full version, in which case all
+// missing fields are assumed to be zero.
+func AtLeast(version string, minimum string) bool {
+	v, ok := parse(version)
+	if !ok {
+		return false
+	}
+	m, ok := parse(minimum)
+	if !ok {
+		return false
+	}
+
+	log.Print(v, m)
+	switch {
+	case v.Datestamp != 0 && m.Datestamp == 0:
+		// OSS version vs. Tailscale version
+		return false
+	case v.Datestamp == 0 && m.Datestamp != 0:
+		// Tailscale version vs. OSS version
+		return false
+	case v.Datestamp != 0:
+		// OSS version vs. OSS version
+		return v.Datestamp >= m.Datestamp
+	case v.Major == m.Major && v.Minor == m.Minor && v.Patch == m.Patch && v.ExtraCommits == m.ExtraCommits:
+		// Exactly equal Tailscale versions
+		return true
+	case v.Major != m.Major:
+		return v.Major > m.Major
+	case v.Minor != m.Minor:
+		return v.Minor > m.Minor
+	case v.Patch != m.Patch:
+		return v.Patch > m.Patch
+	default:
+		return v.ExtraCommits > m.ExtraCommits
+	}
+}
+
+type parsed struct {
+	Major, Minor, Patch, ExtraCommits int // for Tailscale version e.g. e.g. "0.99.1-20"
+	Datestamp                         int // for OSS version e.g. "date.20200612"
+}
+
+func parse(version string) (parsed, bool) {
+	if strings.HasPrefix(version, "date.") {
+		stamp, err := strconv.Atoi(version[5:])
+		if err != nil {
+			return parsed{}, false
+		}
+		return parsed{Datestamp: stamp}, true
+	}
+
+	var ret parsed
+
+	major, rest, err := splitNumericPrefix(version)
+	if err != nil {
+		return parsed{}, false
+	}
+	ret.Major = major
+	if len(rest) == 0 {
+		return ret, true
+	}
+
+	ret.Minor, rest, err = splitNumericPrefix(rest[1:])
+	if err != nil {
+		return parsed{}, false
+	}
+	if len(rest) == 0 {
+		return ret, true
+	}
+
+	// Optional patch version, if the next separator is a dot.
+	if rest[0] == '.' {
+		ret.Patch, rest, err = splitNumericPrefix(rest[1:])
+		if err != nil {
+			return parsed{}, false
+		}
+		if len(rest) == 0 {
+			return ret, true
+		}
+	}
+
+	// Optional extraCommits, if the next bit can be completely
+	// consumed as an integer.
+	if rest[0] != '-' {
+		return parsed{}, false
+	}
+
+	var trailer string
+	ret.ExtraCommits, trailer, err = splitNumericPrefix(rest[1:])
+	if err != nil || (len(trailer) > 0 && trailer[0] != '-') {
+		// rest was probably the string trailer, ignore it.
+		ret.ExtraCommits = 0
+	}
+	return ret, true
+}
+
+func splitNumericPrefix(s string) (int, string, error) {
+	for i, r := range s {
+		if r >= '0' && r <= '9' {
+			continue
+		}
+		ret, err := strconv.Atoi(s[:i])
+		if err != nil {
+			return 0, "", err
+		}
+		return ret, s[i:], nil
+	}
+
+	ret, err := strconv.Atoi(s)
+	if err != nil {
+		return 0, "", err
+	}
+	return ret, "", nil
+}

+ 71 - 0
version/cmp_test.go

@@ -0,0 +1,71 @@
+// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package version
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestParse(t *testing.T) {
+	tests := []struct {
+		version string
+		parsed  parsed
+		want    bool
+	}{
+		{"1", parsed{Major: 1}, true},
+		{"1.2", parsed{Major: 1, Minor: 2}, true},
+		{"1.2.3", parsed{Major: 1, Minor: 2, Patch: 3}, true},
+		{"1.2.3-4", parsed{Major: 1, Minor: 2, Patch: 3, ExtraCommits: 4}, true},
+		{"1.2-4", parsed{Major: 1, Minor: 2, ExtraCommits: 4}, true},
+		{"1.2.3-4-extra", parsed{Major: 1, Minor: 2, Patch: 3, ExtraCommits: 4}, true},
+		{"1.2.3-4a-test", parsed{Major: 1, Minor: 2, Patch: 3}, true},
+		{"1.2-extra", parsed{Major: 1, Minor: 2}, true},
+		{"1.2.3-extra", parsed{Major: 1, Minor: 2, Patch: 3}, true},
+		{"date.20200612", parsed{Datestamp: 20200612}, true},
+		{"borkbork", parsed{}, false},
+		{"1a.2.3", parsed{}, false},
+	}
+
+	for _, test := range tests {
+		gotParsed, got := parse(test.version)
+		if got != test.want {
+			t.Errorf("version(%q) = %v, want %v", test.version, got, test.want)
+		}
+		if diff := cmp.Diff(gotParsed, test.parsed); diff != "" {
+			t.Errorf("parse(%q) diff (-got+want):\n%s", test.version, diff)
+		}
+	}
+}
+
+func TestAtLeast(t *testing.T) {
+	tests := []struct {
+		v, m string
+		want bool
+	}{
+		{"1", "1", true},
+		{"1.2", "1", true},
+		{"1.2.3", "1", true},
+		{"1.2.3-4", "1", true},
+		{"0.98-0", "0.98", true},
+		{"0.97.1-216", "0.98", false},
+		{"0.94", "0.98", false},
+		{"0.98", "0.98", true},
+		{"0.98.0-0", "0.98", true},
+		{"1.2.3-4", "1.2.4-4", false},
+		{"1.2.3-4", "1.2.3-4", true},
+		{"date.20200612", "date.20200612", true},
+		{"date.20200701", "date.20200612", true},
+		{"date.20200501", "date.20200612", false},
+	}
+
+	for _, test := range tests {
+		got := AtLeast(test.v, test.m)
+		if got != test.want {
+			t.Errorf("AtLeast(%q, %q) = %v, want %v", test.v, test.m, got, test.want)
+		}
+	}
+}