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) {