You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mesos.apache.org by ne...@apache.org on 2017/05/05 23:54:44 UTC

[2/6] mesos git commit: Enhanced stout's Version to support prerelease and build labels.

Enhanced stout's Version to support prerelease and build labels.

Previously, Stout's `Version` abstraction only supported a subset of
Semver: version numbers with three numeric components (an optional
trailing "label" with a leading hyphen was supported but ignored).

This commit adds support for SemVer 2.0.0, which defines two additional
optional fields: a "prerelease label" and a "build metadata label",
e.g., "1.2.3-alpha.1+foo". Both labels consist of a series of
dot-separated identifiers.

Review: https://reviews.apache.org/r/58707


Project: http://git-wip-us.apache.org/repos/asf/mesos/repo
Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/405891d2
Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/405891d2
Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/405891d2

Branch: refs/heads/1.2.x
Commit: 405891d207482c860ff020d2cceba136c91aaf6c
Parents: 15b8ee2
Author: Neil Conway <ne...@gmail.com>
Authored: Fri Apr 21 09:03:10 2017 -0700
Committer: Neil Conway <ne...@gmail.com>
Committed: Fri May 5 15:14:02 2017 -0700

----------------------------------------------------------------------
 3rdparty/stout/include/stout/version.hpp | 346 ++++++++++++++++++++++----
 3rdparty/stout/tests/version_tests.cpp   | 120 +++++++--
 2 files changed, 399 insertions(+), 67 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mesos/blob/405891d2/3rdparty/stout/include/stout/version.hpp
----------------------------------------------------------------------
diff --git a/3rdparty/stout/include/stout/version.hpp b/3rdparty/stout/include/stout/version.hpp
index 7717c85..5e2bd2e 100644
--- a/3rdparty/stout/include/stout/version.hpp
+++ b/3rdparty/stout/include/stout/version.hpp
@@ -13,73 +13,161 @@
 #ifndef __STOUT_VERSION_HPP__
 #define __STOUT_VERSION_HPP__
 
+#include <algorithm>
+#include <cctype>
 #include <ostream>
 #include <string>
 #include <vector>
 
+#include <stout/check.hpp>
 #include <stout/error.hpp>
 #include <stout/numify.hpp>
+#include <stout/option.hpp>
 #include <stout/stringify.hpp>
 #include <stout/strings.hpp>
 #include <stout/try.hpp>
 
-// This class provides convenience routines for version checks.
+// This class provides convenience routines for working with version
+// numbers.  We support the SemVer 2.0.0 (http://semver.org) format,
+// with minor extensions.
 //
-// Ideally, the components would be called simply major, minor and
-// patch. However, GNU libstdc++ already defines these as macros for
-// compatibility reasons (man 3 makedev for more information) implicitly
-// included in every compilation.
-//
-// TODO(karya): Consider adding support for more than 3 components, and
-// compatibility operators.
-// TODO(karya): Add support for labels and build metadata. Consider
-// semantic versioning (http://semvar.org/) for specs.
+// Ideally, the version components would be called simply "major",
+// "minor", and "patch". However, GNU libstdc++ already defines these
+// as macros for compatibility reasons (man 3 makedev for more
+// information) implicitly included in every compilation.
 struct Version
 {
   // Expect the string in the following format:
-  //   <major>[.<minor>[.<patch>]]
-  // Missing components are treated as zero.
-  static Try<Version> parse(const std::string& s)
+  //   <major>[.<minor>[.<patch>]][-prerelease][+build]
+  //
+  // Missing `minor` or `patch` components are treated as zero.
+  // Allowing some version numbers to be omitted is an extension to
+  // SemVer, albeit one that several other SemVer libraries implement.
+  //
+  // TODO(neilc): Consider providing a "strict" variant that does not
+  // allow version numbers to be omitted.
+  //
+  // `prerelease` is a prerelease label (e.g., "beta", "rc.1");
+  // `build` is a build metadata label. Both `prerelease` and `build`
+  // consist of one or more dot-separated identifiers. An identifier
+  // is a non-empty string containing ASCII alphanumeric characters or
+  // hyphens.
+  static Try<Version> parse(const std::string& input)
   {
-    const size_t maxComponents = 3;
-
-    // Use only the part before '-', i.e. strip and discard the tags
-    // and labels.
-    // TODO(karya): Once we have support for labels and tags, we
-    // should not discard the remaining string.
-    std::vector<std::string> split =
-      strings::split(strings::split(s, "-")[0], ".");
-
-    if (split.size() > maxComponents) {
-      return Error("Version string has " + stringify(split.size()) +
-                   " components; maximum " + stringify(maxComponents) +
+    // The input string consists of the numeric components, optionally
+    // followed by the prerelease label (prefixed with '-') and/or the
+    // build label (prefixed with '+'). We parse the string from right
+    // to left: build label (if any), prerelease label (if any), and
+    // finally numeric components.
+
+    std::vector<std::string> buildLabel;
+
+    std::vector<std::string> buildParts = strings::split(input, "+", 2);
+    CHECK(buildParts.size() == 1 || buildParts.size() == 2);
+
+    if (buildParts.size() == 2) {
+      const std::string& buildString = buildParts.back();
+
+      // NOTE: Build metadata identifiers can contain leading zeros
+      // (unlike numeric prerelease identifiers; see below).
+      Try<std::vector<std::string>> parsed = parseLabel(buildString, false);
+      if (parsed.isError()) {
+        return Error("Invalid build label: " + parsed.error());
+      }
+
+      buildLabel = parsed.get();
+    }
+
+    std::string remainder = buildParts.front();
+
+    // Parse the prerelease label, if any. Note that the prerelease
+    // label might itself contain hyphens.
+    std::vector<std::string> prereleaseLabel;
+
+    std::vector<std::string> prereleaseParts =
+      strings::split(remainder, "-", 2);
+    CHECK(prereleaseParts.size() == 1 || prereleaseParts.size() == 2);
+
+    if (prereleaseParts.size() == 2) {
+      const std::string& prereleaseString = prereleaseParts.back();
+
+      // Prerelease identifiers cannot contain leading zeros.
+      Try<std::vector<std::string>> parsed = parseLabel(prereleaseString, true);
+      if (parsed.isError()) {
+        return Error("Invalid prerelease label: " + parsed.error());
+      }
+
+      prereleaseLabel = parsed.get();
+    }
+
+    remainder = prereleaseParts.front();
+
+    constexpr size_t maxNumericComponents = 3;
+    std::vector<std::string> numericComponents = strings::split(remainder, ".");
+
+    if (numericComponents.size() > maxNumericComponents) {
+      return Error("Version has " + stringify(numericComponents.size()) +
+                   " components; maximum " + stringify(maxNumericComponents) +
                    " components allowed");
     }
 
-    int components[maxComponents] = {0};
+    int versionNumbers[maxNumericComponents] = {0};
 
-    for (size_t i = 0; i < split.size(); i++) {
-      Try<int> result = numify<int>(split[i]);
+    for (size_t i = 0; i < numericComponents.size(); i++) {
+      Try<int> result = parseNumericIdentifier(numericComponents[i]);
       if (result.isError()) {
-        return Error("Invalid version component '" + split[i] + "': " +
-                     result.error());
+        return Error("Invalid version component '" + numericComponents[i] + "'"
+                     ": " + result.error());
+      }
+
+      if (hasLeadingZero(numericComponents[i])) {
+        return Error("Invalid version component '" + numericComponents[i] + "'"
+                     ": cannot contain leading zero");
       }
-      components[i] = result.get();
+
+      versionNumbers[i] = result.get();
     }
 
-    return Version(components[0], components[1], components[2]);
+    return Version(versionNumbers[0],
+                   versionNumbers[1],
+                   versionNumbers[2],
+                   prereleaseLabel,
+                   buildLabel);
   }
 
-  Version(int _majorVersion, int _minorVersion, int _patchVersion)
+  // Construct a new Version. The `_prerelease` and `_build` arguments
+  // contain lists of prerelease and build identifiers, respectively.
+  Version(int _majorVersion,
+          int _minorVersion,
+          int _patchVersion,
+          const std::vector<std::string>& _prerelease = {},
+          const std::vector<std::string>& _build = {})
     : majorVersion(_majorVersion),
       minorVersion(_minorVersion),
-      patchVersion(_patchVersion) {}
+      patchVersion(_patchVersion),
+      prerelease(_prerelease),
+      build(_build)
+      {
+        // As a sanity check, ensure that the caller has provided
+        // valid prerelease and build identifiers.
+
+        foreach (const std::string& identifier, prerelease) {
+          CHECK_NONE(validateIdentifier(identifier, true));
+        }
+
+        foreach (const std::string& identifier, build) {
+          CHECK_NONE(validateIdentifier(identifier, false));
+        }
+      }
 
   bool operator==(const Version& other) const
   {
+    // NOTE: The `build` field is ignored when comparing two versions
+    // for equality, per SemVer spec.
     return majorVersion == other.majorVersion &&
         minorVersion == other.minorVersion &&
-        patchVersion == other.patchVersion;
+        patchVersion == other.patchVersion &&
+        prerelease == other.prerelease;
   }
 
   bool operator!=(const Version& other) const
@@ -87,28 +175,94 @@ struct Version
     return !(*this == other);
   }
 
+  // SemVer 2.0.0 defines version precedence (ordering) like so:
+  //
+  //   Precedence MUST be calculated by separating the version into
+  //   major, minor, patch and pre-release identifiers in that order
+  //   (Build metadata does not figure into precedence). Precedence is
+  //   determined by the first difference when comparing each of these
+  //   identifiers from left to right as follows: Major, minor, and
+  //   patch versions are always compared numerically. Example: 1.0.0
+  //   < 2.0.0 < 2.1.0 < 2.1.1. When major, minor, and patch are
+  //   equal, a pre-release version has lower precedence than a normal
+  //   version. Example: 1.0.0-alpha < 1.0.0. Precedence for two
+  //   pre-release versions with the same major, minor, and patch
+  //   version MUST be determined by comparing each dot separated
+  //   identifier from left to right until a difference is found as
+  //   follows: identifiers consisting of only digits are compared
+  //   numerically and identifiers with letters or hyphens are
+  //   compared lexically in ASCII sort order. Numeric identifiers
+  //   always have lower precedence than non-numeric identifiers. A
+  //   larger set of pre-release fields has a higher precedence than a
+  //   smaller set, if all of the preceding identifiers are equal.
+  //   Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta <
+  //   1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0.
+  //
+  // NOTE: The `build` field is ignored when comparing two versions
+  // for precedence, per the SemVer spec text above.
   bool operator<(const Version& other) const
   {
-    // Lexicographic ordering.
+    // Compare version numbers numerically.
     if (majorVersion != other.majorVersion) {
       return majorVersion < other.majorVersion;
-    } else if (minorVersion != other.minorVersion) {
+    }
+    if (minorVersion != other.minorVersion) {
       return minorVersion < other.minorVersion;
-    } else {
+    }
+    if (patchVersion != other.patchVersion) {
       return patchVersion < other.patchVersion;
     }
+
+    // If one version has a prerelease label and the other does not,
+    // the prerelease version has lower precedence.
+    if (!prerelease.empty() && other.prerelease.empty()) {
+      return true;
+    }
+    if (prerelease.empty() && !other.prerelease.empty()) {
+      return false;
+    }
+
+    // Compare two versions with prerelease labels by proceeding from
+    // left to right.
+    size_t minPrereleaseSize = std::min(
+        prerelease.size(), other.prerelease.size());
+
+    for (size_t i = 0; i < minPrereleaseSize; i++) {
+      // Check whether the two prerelease identifiers can be converted
+      // to numbers.
+      Try<int> identifier = parseNumericIdentifier(prerelease.at(i));
+      Try<int> otherIdentifier = parseNumericIdentifier(other.prerelease.at(i));
+
+      if (identifier.isSome() && otherIdentifier.isSome()) {
+        // Both identifiers are numeric.
+        if (identifier.get() != otherIdentifier.get()) {
+          return identifier.get() < otherIdentifier.get();
+        }
+      } else if (identifier.isSome()) {
+        // `identifier` is numeric but `otherIdentifier` is not, so
+        // `identifier` comes first.
+        return true;
+      } else if (otherIdentifier.isSome()) {
+        // `otherIdentifier` is numeric but `identifier` is not, so
+        // `otherIdentifier` comes first.
+        return false;
+      } else {
+        // Neither identifier is numeric, so compare via ASCII sort.
+        if (prerelease.at(i) != other.prerelease.at(i)) {
+          return prerelease.at(i) < other.prerelease.at(i);
+        }
+      }
+    }
+
+    // If two versions have different numbers of prerelease labels but
+    // they match on the common prefix, the version with the smaller
+    // set of labels comes first.
+    return prerelease.size() < other.prerelease.size();
   }
 
   bool operator>(const Version& other) const
   {
-    // Lexicographic ordering.
-    if (majorVersion != other.majorVersion) {
-      return majorVersion > other.majorVersion;
-    } else if (minorVersion != other.minorVersion) {
-      return minorVersion > other.minorVersion;
-    } else {
-      return patchVersion > other.patchVersion;
-    }
+    return other < *this;
   }
 
   bool operator<=(const Version& other) const
@@ -128,6 +282,90 @@ struct Version
   const int majorVersion;
   const int minorVersion;
   const int patchVersion;
+  const std::vector<std::string> prerelease;
+  const std::vector<std::string> build;
+
+private:
+  // Check that a string contains a valid identifier. An identifier is
+  // a non-empty string; each character must be an ASCII alphanumeric
+  // or hyphen.
+  static Option<Error> validateIdentifier(
+      const std::string& identifier,
+      bool rejectLeadingZero)
+  {
+    if (identifier.empty()) {
+      return Error("Empty identifier");
+    }
+
+    auto alphaNumericOrHyphen = [](char c) -> bool {
+      return std::isalnum(c) || c == '-';
+    };
+
+    auto firstInvalid = std::find_if_not(
+        identifier.begin(), identifier.end(), alphaNumericOrHyphen);
+
+    if (firstInvalid != identifier.end()) {
+      return Error("Identifier contains illegal character: "
+                   "'" + stringify(*firstInvalid) + "'");
+    }
+
+    // If requested, disallow identifiers that contain a leading
+    // zero. Note that this only applies to numeric identifiers, and
+    // that zero-valued identifiers are allowed.
+    if (rejectLeadingZero && hasLeadingZero(identifier)) {
+      return Error("Identifier contains leading zero");
+    }
+
+    return None();
+  }
+
+  // Parse a string containing a series of dot-separated identifiers
+  // into a vector of strings; each element of the vector contains a
+  // single identifier. If `rejectLeadingZeros` is true, we reject any
+  // numeric identifier in the label that contains a leading zero.
+  static Try<std::vector<std::string>> parseLabel(
+      const std::string& label,
+      bool rejectLeadingZeros)
+  {
+    if (label.empty()) {
+      return Error("Empty label");
+    }
+
+    std::vector<std::string> identifiers = strings::split(label, ".");
+
+    foreach (const std::string& identifier, identifiers) {
+      Option<Error> error = validateIdentifier(identifier, rejectLeadingZeros);
+      if (error.isSome()) {
+        return error.get();
+      }
+    }
+
+    return identifiers;
+  }
+
+  // Checks whether the input string is numeric and contains a leading
+  // zero. Note that "0" by itself is not considered a "leading zero".
+  static bool hasLeadingZero(const std::string& identifier) {
+    Try<int> numericIdentifier = parseNumericIdentifier(identifier);
+
+    return numericIdentifier.isSome() &&
+      numericIdentifier.get() != 0 &&
+      strings::startsWith(identifier, '0');
+  }
+
+  // Attempt to parse the given string as a numeric identifier.
+  // According to the SemVer spec, identifiers that begin with hyphens
+  // are considered non-numeric.
+  //
+  // TODO(neilc): Consider adding a variant of `numify<T>` that only
+  // supports non-negative inputs.
+  static Try<int> parseNumericIdentifier(const std::string& identifier) {
+    if (strings::startsWith(identifier, '-')) {
+      return Error("Contains leading hyphen");
+    }
+
+    return numify<int>(identifier);
+  }
 };
 
 
@@ -135,9 +373,19 @@ inline std::ostream& operator<<(
     std::ostream& stream,
     const Version& version)
 {
-  return stream << version.majorVersion << "."
-                << version.minorVersion << "."
-                << version.patchVersion;
+  stream << version.majorVersion << "."
+         << version.minorVersion << "."
+         << version.patchVersion;
+
+  if (!version.prerelease.empty()) {
+    stream << "-" << strings::join(".", version.prerelease);
+  }
+
+  if (!version.build.empty()) {
+    stream << "+" << strings::join(".", version.build);
+  }
+
+  return stream;
 }
 
 #endif // __STOUT_VERSION_HPP__

http://git-wip-us.apache.org/repos/asf/mesos/blob/405891d2/3rdparty/stout/tests/version_tests.cpp
----------------------------------------------------------------------
diff --git a/3rdparty/stout/tests/version_tests.cpp b/3rdparty/stout/tests/version_tests.cpp
index 925f383..bce185e 100644
--- a/3rdparty/stout/tests/version_tests.cpp
+++ b/3rdparty/stout/tests/version_tests.cpp
@@ -30,20 +30,64 @@ using std::vector;
 // Verify version comparison operations.
 TEST(VersionTest, Comparison)
 {
-  Version version1(0, 10, 4);
-  Version version2(0, 20, 3);
-  Try<Version> version3 = Version::parse("0.20.3");
-
-  EXPECT_EQ(version2, version3.get());
-  EXPECT_NE(version1, version2);
-  EXPECT_LT(version1, version2);
-  EXPECT_LE(version1, version2);
-  EXPECT_LE(version2, version3.get());
-  EXPECT_GT(version2, version1);
-  EXPECT_GE(version2, version1);
-  EXPECT_GE(version3.get(), version1);
-
-  EXPECT_EQ(stringify(version2), "0.20.3");
+  const vector<string> inputs = {
+    "0.0.0",
+    "0.2.3",
+    "0.9.9",
+    "0.10.4",
+    "0.20.3",
+    "1.0.0-alpha",
+    "1.0.0-alpha.1",
+    "1.0.0-alpha.-02",
+    "1.0.0-alpha.-1",
+    "1.0.0-alpha.1-1",
+    "1.0.0-alpha.beta",
+    "1.0.0-beta",
+    "1.0.0-beta.2",
+    "1.0.0-beta.11",
+    "1.0.0-rc.1",
+    "1.0.0-rc.1.2",
+    "1.0.0",
+    "1.0.1",
+    "2.0.0"
+  };
+
+  vector<Version> versions;
+
+  foreach (const string& input, inputs) {
+    Try<Version> version = Version::parse(input);
+    ASSERT_SOME(version);
+
+    versions.push_back(version.get());
+  }
+
+  // Check that `versions` is in ascending order.
+  for (size_t i = 0; i < versions.size(); i++) {
+    EXPECT_FALSE(versions[i] < versions[i])
+      << "Expected " << versions[i] << " < " << versions[i] << " to be false";
+
+    for (size_t j = i + 1; j < versions.size(); j++) {
+      EXPECT_TRUE(versions[i] < versions[j])
+        << "Expected " << versions[i] << " < " << versions[j];
+
+      EXPECT_FALSE(versions[j] < versions[i])
+        << "Expected " << versions[i] << " < " << versions[j] << " to be false";
+    }
+  }
+}
+
+
+// Verify that build metadata labels are ignored when determining
+// equality and ordering between versions.
+TEST(VersionTest, BuildMetadataComparison)
+{
+  Version plain = Version(1, 2, 3);
+  Version buildMetadata = Version(1, 2, 3, {}, {"abc"});
+
+  EXPECT_TRUE(plain == buildMetadata);
+  EXPECT_FALSE(plain != buildMetadata);
+  EXPECT_FALSE(plain < buildMetadata);
+  EXPECT_FALSE(plain > buildMetadata);
 }
 
 
@@ -57,11 +101,30 @@ TEST(VersionTest, ParseValid)
   typedef pair<Version, string> ExpectedValue;
 
   const map<string, ExpectedValue> testCases = {
-    // Prerelease labels are currently accepted but ignored.
-    {"1.20.3-rc1", {Version(1, 20, 3), "1.20.3"}},
     {"1.20.3", {Version(1, 20, 3), "1.20.3"}},
     {"1.20", {Version(1, 20, 0), "1.20.0"}},
-    {"1", {Version(1, 0, 0), "1.0.0"}}
+    {"1", {Version(1, 0, 0), "1.0.0"}},
+    {"1.20.3-rc1", {Version(1, 20, 3, {"rc1"}), "1.20.3-rc1"}},
+    {"1.20.3--", {Version(1, 20, 3, {"-"}), "1.20.3--"}},
+    {"1.20.3+-.-", {Version(1, 20, 3, {}, {"-", "-"}), "1.20.3+-.-"}},
+    {"1.0.0-alpha.1", {Version(1, 0, 0, {"alpha", "1"}), "1.0.0-alpha.1"}},
+    {"1.0.0-alpha+001",
+     {Version(1, 0, 0, {"alpha"}, {"001"}), "1.0.0-alpha+001"}},
+    {"1.0.0-alpha.-123",
+     {Version(1, 0, 0, {"alpha", "-123"}), "1.0.0-alpha.-123"}},
+    {"1+20130313144700",
+     {Version(1, 0, 0, {}, {"20130313144700"}), "1.0.0+20130313144700"}},
+    {"1.0.0-beta+exp.sha.5114f8",
+     {Version(1, 0, 0, {"beta"}, {"exp", "sha", "5114f8"}),
+      "1.0.0-beta+exp.sha.5114f8"}},
+    {"1.0.0--1", {Version(1, 0, 0, {"-1"}), "1.0.0--1"}},
+    {"1.0.0-----1", {Version(1, 0, 0, {"----1"}), "1.0.0-----1"}},
+    {"1-2-3+4-5",
+     {Version(1, 0, 0, {"2-3"}, {"4-5"}), "1.0.0-2-3+4-5"}},
+    {"1-2-3.4+5.6-7",
+     {Version(1, 0, 0, {"2-3", "4"}, {"5", "6-7"}), "1.0.0-2-3.4+5.6-7"}},
+    {"1-2.-3+4.-5",
+     {Version(1, 0, 0, {"2", "-3"}, {"4", "-5"}), "1.0.0-2.-3+4.-5"}}
   };
 
   foreachpair (const string& input, const ExpectedValue& expected, testCases) {
@@ -87,8 +150,29 @@ TEST(VersionTest, ParseInvalid)
     "a",
     "1.",
     ".1.2",
+    "0.1.-2",
+    "0.-1.2",
     "0.1.2.3",
-    "-1.1.2"
+    "-1.1.2",
+    "01.2.3",
+    "1.02.3",
+    "1.2.03",
+    "1.1.2-",
+    "1.1.2+",
+    "1.1.2-+",
+    "1.1.2-.",
+    "1.1.2+.",
+    "1.1.2-foo..",
+    "1.1.2-.foo",
+    "1.1.2+",
+    "1.1.2+foo..",
+    "1.1.2+.foo",
+    "1.1.2-al^pha",
+    "1.1.2+exp;",
+    "1.1.2-alpha.001",
+    "-foo",
+    "+foo",
+    u8"1.0.0-b\u00e9ta"
   };
 
   foreach (const string& input, inputs) {