diff --git a/.gitignore b/.gitignore index bcdbd6a5..a55b4baf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ edit +.vscode CMakeLists.txt.user /msbuild.log /*std*.log diff --git a/include/uibase/exceptions.h b/include/uibase/exceptions.h index 433b9be7..c89a4833 100644 --- a/include/uibase/exceptions.h +++ b/include/uibase/exceptions.h @@ -3,6 +3,7 @@ #include +#include #include #include "dllimport.h" diff --git a/include/uibase/imoinfo.h b/include/uibase/imoinfo.h index e4f072a8..bf6588d5 100644 --- a/include/uibase/imoinfo.h +++ b/include/uibase/imoinfo.h @@ -35,6 +35,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include "imodlist.h" #include "iprofile.h" #include "versioninfo.h" +#include "versioning.h" namespace MOBase { @@ -121,7 +122,12 @@ class QDLLEXPORT IOrganizer : public QObject /** * @return the running version of Mod Organizer */ - virtual VersionInfo appVersion() const = 0; + [[deprecated]] virtual VersionInfo appVersion() const = 0; + + /** + * @return the running version of Mod Organizer + */ + virtual Version version() const = 0; /** * @brief create a new mod with the specified name diff --git a/include/uibase/versioning.h b/include/uibase/versioning.h new file mode 100644 index 00000000..c4f95419 --- /dev/null +++ b/include/uibase/versioning.h @@ -0,0 +1,181 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include "dllimport.h" +#include "exceptions.h" + +namespace MOBase +{ + +class InvalidVersionException : public Exception +{ +public: + using Exception::Exception; +}; + +// class representing a Version object +// +// valid versions are an "extension" of SemVer (see https://semver.org/) with the +// following tweaks: +// - version can have a sub-patch, i.e., x.y.z.p, which are normally not allowed by +// SemVer +// - non-integer pre-release identifiers are limited to dev, alpha (a), beta (b) and rc, +// and dev is lower than alpha (according to SemVer, the pre-release should be +// ordered alphabetically) +// - the '-' between version and pre-release can be made optional, and also the '.' +// between pre-releases segment +// +// the extension from SemVer are only meant to be used by MO2 and USVFS versioning, +// plugins and extensions should follow SemVer standard (and not use dev), this is +// mainly +// - for back-compatibility purposes, because USVFS versioning contains sub-patches and +// there are old MO2 releases with sub-patch +// - because MO2 is not going to become MO3, so having an extra level make sense +// +// unlike VersionInfo, this class is immutable and only hold valid versions +// +class QDLLEXPORT Version +{ +public: + enum class ParseMode + { + // official semver parsing with pre-release limited to dev, alpha/a, beta/b and rc + // + SemVer, + + // MO2 parsing, e.g., 2.5.1rc1 - this either parse a string with no pre-release + // information (e.g. 2.5.1) or with a single pre-release + a version (e.g., 2.5.1a1 + // or 2.5.2rc1) + // + // this mode can parse sub-patch (SemVer mode cannot) + // + MO2 + }; + + enum class FormatMode + { + // show subpatch even if subpatch is 0 + // + ForceSubPatch = 0b0001, + + // do not add separators between version and pre-release (-) or between pre-release + // segments (.) + // + NoSeparator = 0b0010, + + // uses short form for alpha and beta (a/b instead of alpha/beta) + // + ShortAlphaBeta = 0b0100, + + // do not add metadata even if present + // + NoMetadata = 0b1000 + }; + Q_DECLARE_FLAGS(FormatModes, FormatMode); + + // condensed format, no separator, short alpha/beta and no metadata + // + static constexpr auto FormatCondensed = FormatModes{ + FormatMode::NoSeparator, FormatMode::ShortAlphaBeta, FormatMode::NoMetadata}; + + enum class ReleaseType + { + Development, // -dev + Alpha, // -alpha, -a + Beta, // -beta, -b + ReleaseCandidate, // -rc + }; + using enum ReleaseType; + +public: // parsing + // parse version from the given string, throw InvalidVersionException if the string + // cannot be parsed + // + static Version parse(QString const& value, ParseMode mode = ParseMode::SemVer); + +public: // constructors + Version(int major, int minor, int patch, QString metadata = {}); + Version(int major, int minor, int patch, int subpatch, QString metadata = {}); + + Version(int major, int minor, int patch, ReleaseType type, QString metadata = {}); + Version(int major, int minor, int patch, int subpatch, ReleaseType type, + QString metadata = {}); + + Version(int major, int minor, int patch, ReleaseType type, int prerelease, + QString metadata = {}); + Version(int major, int minor, int patch, int subpatch, ReleaseType type, + int prerelease, QString metadata = {}); + + Version(int major, int minor, int patch, int subpatch, + std::vector> prereleases, + QString metadata = {}); + +public: // special member functions + Version(const Version&) = default; + Version(Version&&) = default; + + Version& operator=(const Version&) = default; + Version& operator=(Version&&) = default; + +public: + // check if this version corresponds to a pre-release version (dev, alpha, beta, etc.) + // + bool isPreRelease() const { return !m_PreReleases.empty(); } + + // retrieve major, minor, patch and sub-patch of this version + // + int major() const { return m_Major; } + int minor() const { return m_Minor; } + int patch() const { return m_Patch; } + int subpatch() const { return m_SubPatch; } + + // retrieve pre-releases information for this version + // + const auto& preReleases() const { return m_PreReleases; } + + // retrieve build metadata, if any, otherwise return an empty string + // + const auto& buildMetadata() const { return m_BuildMetadata; } + + // convert this version to a string + // + QString string(const FormatModes& modes = {}) const; + +private: + // major.minor.patch + int m_Major, m_Minor, m_Patch, m_SubPatch; + + // pre-release information + std::vector> m_PreReleases; + + // metadata + QString m_BuildMetadata; +}; + +QDLLEXPORT std::strong_ordering operator<=>(const Version& lhs, const Version& rhs); + +inline bool operator==(const Version& lhs, const Version& rhs) +{ + return (lhs <=> rhs) == 0; +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(Version::FormatModes); + +} // namespace MOBase + +template +struct std::formatter : std::formatter +{ + template + FmtContext::iterator format(const MOBase::Version& v, FmtContext& ctx) const + { + return std::formatter::format(v.string(), ctx); + } +}; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5b096a67..fa1d0a66 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -30,6 +30,7 @@ set(root_headers ../include/uibase/steamutility.h ../include/uibase/strings.h ../include/uibase/utility.h + ../include/uibase/versioning.h ../include/uibase/versioninfo.h ) set(interface_headers @@ -115,7 +116,6 @@ mo2_target_sources(uibase nxmurl.cpp pch.cpp pluginrequirements.cpp - pluginsetting.cpp registry.cpp report.cpp safewritefile.cpp @@ -123,6 +123,7 @@ mo2_target_sources(uibase steamutility.cpp strings.cpp utility.cpp + versioning.cpp versioninfo.cpp ) @@ -133,7 +134,6 @@ mo2_target_sources(uibase ifiletree.cpp imodrepositorybridge.cpp imoinfo.cpp - iplugininstaller.cpp ) mo2_target_sources(uibase diff --git a/src/iplugininstaller.cpp b/src/iplugininstaller.cpp deleted file mode 100644 index 79e62472..00000000 --- a/src/iplugininstaller.cpp +++ /dev/null @@ -1,18 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License -along with Mod Organizer. If not, see . -*/ diff --git a/src/pluginsetting.cpp b/src/pluginsetting.cpp deleted file mode 100644 index cce84eb6..00000000 --- a/src/pluginsetting.cpp +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License -along with Mod Organizer. If not, see . -*/ - -#include "pluginsetting.h" - -namespace MOBase -{ -} // namespace MOBase diff --git a/src/versioning.cpp b/src/versioning.cpp new file mode 100644 index 00000000..ddc0ed6a --- /dev/null +++ b/src/versioning.cpp @@ -0,0 +1,278 @@ +#include "versioning.h" + +#include +#include +#include + +#include "formatters.h" + +// official semver regex +static const QRegularExpression s_SemVerStrictRegEx{ + R"(^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$)"}; + +// for MO2, to match stuff like 1.2.3rc1 or v1.2.3a1+XXX +static const QRegularExpression s_SemVerMO2RegEx{ + R"(^v?(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:\.(?P0|[1-9]\d*))?(?:(?Pdev|a|alpha|b|beta|rc)(?P0|[1-9](?:[.0-9])*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$)"}; + +// match from value to release type +static const std::unordered_map + s_StringToRelease{{"dev", MOBase::Version::Development}, + {"alpha", MOBase::Version::Alpha}, + {"a", MOBase::Version::Alpha}, + {"beta", MOBase::Version::Beta}, + {"b", MOBase::Version::Beta}, + {"rc", MOBase::Version::ReleaseCandidate}}; + +namespace MOBase +{ + +namespace +{ + + Version parseVersionSemVer(QString const& value) + { + const auto match = s_SemVerStrictRegEx.match(value); + + if (!match.hasMatch()) { + throw InvalidVersionException( + QString::fromStdString(std::format("invalid version string: '{}'", value))); + } + + const auto major = match.captured("major").toInt(); + const auto minor = match.captured("minor").toInt(); + const auto patch = match.captured("patch").toInt(); + + std::vector> prereleases; + for (auto& part : match.captured("prerelease") + .split(".", Qt::SplitBehaviorFlags::SkipEmptyParts)) { + // try to extract an int + bool ok = true; + const auto intValue = part.toInt(&ok); + if (ok) { + prereleases.push_back(intValue); + continue; + } + + // check if we have a valid prerelease type + const auto it = s_StringToRelease.find(part.toLower()); + if (it == s_StringToRelease.end()) { + throw InvalidVersionException( + QString::fromStdString(std::format("invalid prerelease type: '{}'", part))); + } + + prereleases.push_back(it->second); + } + + const auto buildMetadata = match.captured("buildmetadata").trimmed(); + + return Version(major, minor, patch, 0, prereleases, buildMetadata); + } + + Version parseVersionMO2(QString const& value) + { + const auto match = s_SemVerMO2RegEx.match(value); + + if (!match.hasMatch()) { + throw InvalidVersionException( + QString::fromStdString(std::format("invalid version string: '{}'", value))); + } + + const auto major = match.captured("major").toInt(); + const auto minor = match.captured("minor").toInt(); + const auto patch = match.captured("patch").toInt(); + + const auto subpatch = match.captured("subpatch").toInt(); + + // unlike semver, the regex will only match valid values + std::vector> prereleases; + if (match.hasCaptured("type")) { + prereleases.push_back(s_StringToRelease.at(match.captured("type"))); + + // for version with decimal point, e.g., 2.4.1rc1.1, we split the components into + // pre-release components to get {rc, 1, 1} - this works fine since {rc, 1} < {rc, + // 1, 1} + // + for (const auto& preVersion : + match.captured("prerelease").split(".", Qt::SkipEmptyParts)) { + prereleases.push_back(preVersion.toInt()); + } + } + + const auto buildMetadata = match.captured("buildmetadata").trimmed(); + + return Version(major, minor, patch, subpatch, prereleases, buildMetadata); + } + +} // namespace + +Version Version::parse(QString const& value, ParseMode mode) +{ + return mode == ParseMode::SemVer ? parseVersionSemVer(value) : parseVersionMO2(value); +} + +// constructors + +Version::Version(int major, int minor, int patch, QString metadata) + : Version(major, minor, patch, 0, std::move(metadata)) +{} +Version::Version(int major, int minor, int patch, int subpatch, QString metadata) + : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch}, + m_PreReleases{}, m_BuildMetadata{std::move(metadata)} +{} + +Version::Version(int major, int minor, int patch, ReleaseType type, QString metadata) + : Version(major, minor, patch, 0, type, std::move(metadata)) +{} +Version::Version(int major, int minor, int patch, int subpatch, ReleaseType type, + QString metadata) + : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch}, + m_PreReleases{type}, m_BuildMetadata{std::move(metadata)} +{} + +Version::Version(int major, int minor, int patch, ReleaseType type, int prerelease, + QString metadata) + : Version(major, minor, patch, 0, type, prerelease, std::move(metadata)) +{} +Version::Version(int major, int minor, int patch, int subpatch, ReleaseType type, + int prerelease, QString metadata) + : Version(major, minor, patch, subpatch, {type, prerelease}, std::move(metadata)) +{} + +Version::Version(int major, int minor, int patch, int subpatch, + std::vector> prereleases, + QString metadata) + : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch}, + m_PreReleases{std::move(prereleases)}, m_BuildMetadata{std::move(metadata)} +{} + +// string + +QString Version::string(const FormatModes& modes) const +{ + const bool noSeparator = modes.testFlag(FormatMode::NoSeparator); + const bool shortAlphaBeta = modes.testFlag(FormatMode::ShortAlphaBeta); + auto value = std::format("{}.{}.{}", m_Major, m_Minor, m_Patch); + + if (m_SubPatch || modes.testFlag(FormatMode::ForceSubPatch)) { + value += std::format(".{}", m_SubPatch); + } + + if (!m_PreReleases.empty()) { + if (!noSeparator) { + value += "-"; + } + for (std::size_t i = 0; i < m_PreReleases.size(); ++i) { + value += std::visit( + [shortAlphaBeta](auto const& pre) -> std::string { + if constexpr (std::is_same_v) { + switch (pre) { + case Development: + return "dev"; + case Alpha: + return shortAlphaBeta ? "a" : "alpha"; + case Beta: + return shortAlphaBeta ? "b" : "beta"; + case ReleaseCandidate: + return "rc"; + } + return ""; + } else { + return std::to_string(pre); + } + }, + m_PreReleases[i]); + if (!noSeparator && i < m_PreReleases.size() - 1) { + value += "."; + } + } + } + + if (!modes.testFlag(FormatMode::NoMetadata) && !m_BuildMetadata.isEmpty()) { + value += "+" + m_BuildMetadata.toStdString(); + } + + return QString::fromStdString(value); +} + +namespace +{ + // consume the given iterator until the given end iterator or until a non-zero value + // is found + // + template + It consumePreReleaseZeros(It it, It end) + { + for (; it != end; ++it) { + if (!std::holds_alternative(*it) != 0 || std::get(*it) != 0) { + break; + } + } + return it; + }; +} // namespace + +std::strong_ordering operator<=>(const Version& lhs, const Version& rhs) +{ + auto mmp_cmp = + std::forward_as_tuple(lhs.major(), lhs.minor(), lhs.patch(), lhs.subpatch()) <=> + std::forward_as_tuple(rhs.major(), rhs.minor(), rhs.patch(), rhs.subpatch()); + + // major.minor.patch have precedence over everything else + if (mmp_cmp != std::strong_ordering::equal) { + return mmp_cmp; + } + + // handle cases were one is a pre-release and not the other - the pre-release is + // "less" than the release + if (lhs.isPreRelease() && !rhs.isPreRelease()) { + return std::strong_ordering::less; + } + + if (!lhs.isPreRelease() && rhs.isPreRelease()) { + return std::strong_ordering::greater; + } + + // compare pre-release fields + auto lhsIt = lhs.preReleases().begin(), rhsIt = rhs.preReleases().begin(); + for (; lhsIt != lhs.preReleases().end() && rhsIt != rhs.preReleases().end(); + ++lhsIt, ++rhsIt) { + + const auto &lhsPre = *lhsIt, rhsPre = *rhsIt; + + // if one is alpha/beta/etc. and the other is numeric, the alpha/beta/etc. is lower + // than the numeric one, which matches the index + auto pre_cmp = lhsPre.index() <=> rhsPre.index(); + if (pre_cmp != std::strong_ordering::equal) { + return pre_cmp; + } + + // compare the actual values + pre_cmp = lhsPre <=> rhsPre; + if (pre_cmp != std::strong_ordering::equal) { + return pre_cmp; + } + } + + // the code below does not follow semver 100% (I think) - basically, this makes stuff + // like 2.4.1rc1.0 equals to 2.4.1rc1, which according to semver is probably not right + // but is probably best for us + // + + // if we land here, we have consumed one of the pre-release, we skip all the 0 in the + // remaining one + lhsIt = consumePreReleaseZeros(lhsIt, lhs.preReleases().end()); + rhsIt = consumePreReleaseZeros(rhsIt, rhs.preReleases().end()); + + const auto lhsConsumed = lhsIt == lhs.preReleases().end(), + rhsConsumed = rhsIt == rhs.preReleases().end(); + + if (lhsConsumed && rhsConsumed) { + return std::strong_ordering::equal; + } else if (!lhsConsumed) { + return std::strong_ordering::greater; + } else { + return std::strong_ordering::less; + } +} + +} // namespace MOBase diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 54f02ead..ac17234a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,6 +6,7 @@ target_sources(uibase-tests test_formatters.cpp test_ifiletree.cpp test_strings.cpp + test_versioning.cpp ) mo2_configure_tests(uibase-tests NO_SOURCES WARNINGS 4) target_link_libraries(uibase-tests PRIVATE uibase) diff --git a/tests/cmake/CMakeLists.txt b/tests/cmake/CMakeLists.txt index 2a8e34a7..e1d636f7 100644 --- a/tests/cmake/CMakeLists.txt +++ b/tests/cmake/CMakeLists.txt @@ -7,3 +7,4 @@ find_package(mo2-uibase CONFIG REQUIRED) add_library(plugin SHARED) target_sources(plugin PRIVATE plugin.cpp) target_link_libraries(plugin PRIVATE mo2::uibase) +set_target_properties(plugin PROPERTIES CXX_STANDARD 20) diff --git a/tests/test_versioning.cpp b/tests/test_versioning.cpp new file mode 100644 index 00000000..08a346fd --- /dev/null +++ b/tests/test_versioning.cpp @@ -0,0 +1,90 @@ +#pragma warning(push) +#pragma warning(disable : 4668) +#include +#pragma warning(pop) + +#include +#include + +#include + +#include + +using namespace MOBase; + +using enum Version::ReleaseType; +using ParseMode = Version::ParseMode; + +TEST(VersioningTest, VersionParse) +{ + // TODO: add exceptions test + + // semver + ASSERT_EQ(Version(1, 0, 0), Version::parse("1.0.0")); + ASSERT_EQ(Version(1, 0, 0, Development, 1), Version::parse("1.0.0-dev.1")); + ASSERT_EQ(Version(1, 0, 0, Development, 2), Version::parse("1.0.0-dev.2")); + ASSERT_EQ(Version(1, 0, 0, Alpha), Version::parse("1.0.0-a")); + ASSERT_EQ(Version(1, 0, 0, Alpha), Version::parse("1.0.0-alpha")); + ASSERT_EQ(Version(1, 0, 0, 0, {Alpha, 1, Beta}), Version::parse("1.0.0-alpha.1.b")); + ASSERT_EQ(Version(1, 0, 0, Beta, 2), Version::parse("1.0.0-beta.2")); + ASSERT_EQ(Version(2, 5, 2, ReleaseCandidate, 1), Version::parse("2.5.2-rc.1")); + + // mo2 + ASSERT_EQ(Version(1, 0, 0), Version::parse("1.0.0", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Development, 1), + Version::parse("1.0.0dev1", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Development, 2), + Version::parse("1.0.0dev2", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Alpha, 1), Version::parse("1.0.0a1", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Alpha, 1), Version::parse("1.0.0alpha1", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Beta, 2), Version::parse("1.0.0beta2", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Beta, 2), Version::parse("1.0.0beta2", ParseMode::MO2)); + ASSERT_EQ(Version(2, 4, 1, 0, {ReleaseCandidate, 1, 1}), + Version::parse("2.4.1rc1.1", ParseMode::MO2)); + ASSERT_EQ(Version(2, 2, 2, 1, Beta, 2), + Version::parse("2.2.2.1beta2", ParseMode::MO2)); + ASSERT_EQ(Version(2, 5, 2, ReleaseCandidate, 1), + Version::parse("v2.5.2rc1", ParseMode::MO2)); + ASSERT_EQ(Version(2, 5, 2, ReleaseCandidate, 2), + Version::parse("2.5.2rc2", ParseMode::MO2)); +} + +TEST(VersioningTest, VersionString) +{ + ASSERT_EQ("1.0.0", Version(1, 0, 0).string()); + ASSERT_EQ("1.0.0-dev.1", Version(1, 0, 0, Development, 1).string()); + ASSERT_EQ("1.0.0-dev.2", Version(1, 0, 0, Development, 2).string()); + ASSERT_EQ("1.0.0-alpha", Version(1, 0, 0, Alpha).string()); + ASSERT_EQ("1.0.0-alpha.1.beta", Version(1, 0, 0, 0, {Alpha, 1, Beta}).string()); + ASSERT_EQ("1.0.0-beta.2", Version(1, 0, 0, Beta, 2).string()); + ASSERT_EQ("2.5.2-rc.1", Version(2, 5, 2, ReleaseCandidate, 1).string()); + ASSERT_EQ("2.5.2rc1", + Version(2, 5, 2, ReleaseCandidate, 1).string(Version::FormatCondensed)); +} + +TEST(VersioningTest, VersionCompare) +{ + // shortcut + using v = Version; + + // test from https://semver.org/ + ASSERT_TRUE(v(1, 0, 0) < v(2, 0, 0)); + ASSERT_TRUE(v(2, 0, 0) < v(2, 1, 0)); + ASSERT_TRUE(v(2, 1, 0) < v(2, 1, 1)); + + ASSERT_TRUE(v(1, 0, 0, Alpha) < v(1, 0, 0, Alpha, 1)); + ASSERT_TRUE(v(1, 0, 0, Alpha, 1) < v(1, 0, 0, 0, {Alpha, Beta})); + ASSERT_TRUE(v(1, 0, 0, 0, {Alpha, Beta}) < v(1, 0, 0, 1)); + ASSERT_TRUE(v(1, 0, 0, Beta) < v(1, 0, 0, Beta, 2)); + ASSERT_TRUE(v(1, 0, 0, Beta, 2) < v(1, 0, 0, Beta, 11)); + ASSERT_TRUE(v(1, 0, 0, Beta, 11) < v(1, 0, 0, ReleaseCandidate, 1)); + ASSERT_TRUE(v(1, 0, 0, ReleaseCandidate, 0) < v(1, 0, 0)); + + ASSERT_TRUE(v(2, 4, 1, 0, {ReleaseCandidate, 1, 0}) == + v(2, 4, 1, ReleaseCandidate, 1)); + ASSERT_TRUE(v(2, 4, 1, 0, {ReleaseCandidate, 1, 0}) < + v(2, 4, 1, 0, {ReleaseCandidate, 1, 1})); + ASSERT_TRUE(v(2, 4, 1, ReleaseCandidate, 1) < + v(2, 4, 1, 0, {ReleaseCandidate, 1, 1})); + ASSERT_TRUE(v(1, 0, 0) < v(2, 0, 0, Alpha)); +}