| #pragma once | |||||
| #include <semver/ident.hpp> | |||||
| namespace semver { | |||||
| class build_metadata { | |||||
| std::vector<ident> _ids; | |||||
| public: | |||||
| build_metadata() = default; | |||||
| [[nodiscard]] bool empty() const noexcept { return _ids.empty(); } | |||||
| void add_ident(ident id) noexcept { _ids.push_back(id); } | |||||
| void add_ident(std::string_view s) { add_ident(ident(s)); } | |||||
| auto& idents() const noexcept { return _ids; } | |||||
| static build_metadata parse(std::string_view s) { | |||||
| build_metadata ret; | |||||
| ret._ids = ident::parse_dotted_seq(s); | |||||
| return ret; | |||||
| } | |||||
| }; | |||||
| } // namespace semver |
| #include "./ident.hpp" | |||||
| #include <cassert> | |||||
| #include <charconv> | |||||
| using namespace semver; | |||||
| ident::ident(std::string_view str) { | |||||
| const auto str_begin = str.data(); | |||||
| auto ptr = str_begin; | |||||
| const auto str_end = str_begin + str.size(); | |||||
| bool any_alpha = false; | |||||
| if (ptr == str_end) { | |||||
| throw invalid_ident(std::string(str)); | |||||
| } | |||||
| for (; ptr != str_end; ++ptr) { | |||||
| any_alpha = any_alpha || (*ptr == '-' || std::isalpha(*ptr)); | |||||
| if (!std::isalnum(*ptr) && *ptr != '-') { | |||||
| break; | |||||
| } | |||||
| } | |||||
| if (ptr != str_end) { | |||||
| throw invalid_ident(std::string(str)); | |||||
| } | |||||
| _str.assign(str_begin, ptr); | |||||
| if (any_alpha) { | |||||
| _kind = ident_kind::alphanumeric; | |||||
| } else if (_str.size() > 1 && _str[0] == '0') { | |||||
| _kind = ident_kind::digits; | |||||
| } else { | |||||
| _kind = ident_kind::numeric; | |||||
| // Check that the integer is representable on this system | |||||
| std::uint64_t n; | |||||
| auto res = std::from_chars(str_begin, str_end, n); | |||||
| if (res.ptr != str_end) { | |||||
| throw invalid_ident(_str); | |||||
| } | |||||
| } | |||||
| } | |||||
| std::vector<ident> ident::parse_dotted_seq(const std::string_view s) { | |||||
| std::vector<ident> acc; | |||||
| auto remaining = s; | |||||
| while (!remaining.empty()) { | |||||
| auto next_dot = remaining.find('.'); | |||||
| auto id_sub = remaining.substr(0, next_dot); | |||||
| if (id_sub.empty()) { | |||||
| throw invalid_ident(std::string(s)); | |||||
| } | |||||
| acc.emplace_back(id_sub); | |||||
| if (next_dot == remaining.npos) { | |||||
| break; | |||||
| } | |||||
| remaining = remaining.substr(next_dot + 1); | |||||
| if (remaining.empty()) { | |||||
| throw invalid_ident(std::string(s)); | |||||
| } | |||||
| } | |||||
| if (acc.empty()) { | |||||
| throw invalid_ident(std::string(s)); | |||||
| } | |||||
| return acc; | |||||
| } | |||||
| order semver::compare(ident_kind lhs, ident_kind rhs) noexcept { | |||||
| assert(lhs != ident_kind::digits && "Ordering with digit identifiers is undefined"); | |||||
| assert(rhs != ident_kind::digits && "Ordering with digit identifiers is undefined"); | |||||
| if (lhs == rhs) { | |||||
| return order::equivalent; | |||||
| } else if (lhs == ident_kind::numeric) { | |||||
| // numeric is less than alnum | |||||
| return order::less; | |||||
| } else { | |||||
| return order::greater; | |||||
| } | |||||
| } | |||||
| order semver::compare(const ident& lhs, const ident& rhs) noexcept { | |||||
| auto ord = compare(lhs.kind(), rhs.kind()); | |||||
| if (ord != order::equivalent) { | |||||
| return ord; | |||||
| } | |||||
| assert(lhs.kind() == rhs.kind() && "[semver library bug]"); | |||||
| if (lhs.kind() == ident_kind::numeric) { | |||||
| auto as_int = [](auto& str) { | |||||
| std::uint64_t value; | |||||
| auto fc_res = std::from_chars(str.data(), str.data() + str.size(), value); | |||||
| assert(fc_res.ptr == str.data() + str.size()); | |||||
| return value; | |||||
| }; | |||||
| auto lhs_num = as_int(lhs.string()); | |||||
| auto rhs_num = as_int(rhs.string()); | |||||
| if (lhs_num == rhs_num) { | |||||
| return order::equivalent; | |||||
| } else if (lhs_num < rhs_num) { | |||||
| return order::less; | |||||
| } else { | |||||
| return order::greater; | |||||
| } | |||||
| } | |||||
| assert(lhs.kind() == ident_kind::alphanumeric && "[semver library bug]"); | |||||
| auto comp = lhs.string().compare(rhs.string()); | |||||
| if (comp == 0) { | |||||
| return order::equivalent; | |||||
| } else if (comp < 0) { | |||||
| return order::less; | |||||
| } else { | |||||
| return order::greater; | |||||
| } | |||||
| } |
| #pragma once | |||||
| #include <semver/order.hpp> | |||||
| #include <stdexcept> | |||||
| #include <string> | |||||
| #include <string_view> | |||||
| #include <vector> | |||||
| namespace semver { | |||||
| class invalid_ident : public std::runtime_error { | |||||
| std::string _str; | |||||
| public: | |||||
| invalid_ident(std::string s) | |||||
| : std::runtime_error::runtime_error("Invalid metadata identifier: " + s) | |||||
| , _str(s) {} | |||||
| auto& string() const noexcept { return _str; } | |||||
| }; | |||||
| enum class ident_kind { | |||||
| alphanumeric, | |||||
| numeric, | |||||
| digits, | |||||
| }; | |||||
| order compare(ident_kind lhs, ident_kind rhs) noexcept; | |||||
| class ident; | |||||
| order compare(const ident& lhs, const ident& rhs) noexcept; | |||||
| class ident { | |||||
| std::string _str; | |||||
| ident_kind _kind; | |||||
| public: | |||||
| explicit ident(std::string_view str); | |||||
| auto kind() const noexcept { return _kind; } | |||||
| const auto& string() const noexcept { return _str; } | |||||
| #define DEF_OP(op, expr) \ | |||||
| inline friend bool operator op(const ident& lhs, const ident& rhs) noexcept { \ | |||||
| auto o = compare(lhs, rhs); \ | |||||
| return (expr); \ | |||||
| } \ | |||||
| static_assert(true) | |||||
| DEF_OP(==, (o == order::equivalent)); | |||||
| DEF_OP(!=, (o != order::equivalent)); | |||||
| DEF_OP(<, (o == order::less)); | |||||
| DEF_OP(>, (o == order::greater)); | |||||
| DEF_OP(<=, (o == order::less || o == order::equivalent)); | |||||
| DEF_OP(>=, (o == order::greater || o == order::equivalent)); | |||||
| #undef DEF_OP | |||||
| static std::vector<ident> parse_dotted_seq(std::string_view s); | |||||
| }; | |||||
| } // namespace semver |
| #include <semver/ident.hpp> | |||||
| #include <catch2/catch.hpp> | |||||
| TEST_CASE("Parse identifiers") { | |||||
| struct case_ { | |||||
| std::string str; | |||||
| semver::ident_kind expect_kind; | |||||
| }; | |||||
| case_ cases[] = { | |||||
| {"foo", semver::ident_kind::alphanumeric}, | |||||
| {"12", semver::ident_kind::numeric}, | |||||
| {"03412", semver::ident_kind::digits}, | |||||
| {"0", semver::ident_kind::numeric}, | |||||
| {"00", semver::ident_kind::digits}, | |||||
| }; | |||||
| for (auto [str, expect] : cases) { | |||||
| INFO("Checking parsing of '" << str << "'"); | |||||
| auto id = semver::ident(str); | |||||
| CHECK(id.string() == str); | |||||
| CHECK(id.kind() == expect); | |||||
| } | |||||
| } | |||||
| TEST_CASE("Invalid identifiers") { | |||||
| #define BAD_IDENT(x) CHECK_THROWS_AS(semver::ident(x), semver::invalid_ident) | |||||
| BAD_IDENT(""); | |||||
| BAD_IDENT("="); | |||||
| BAD_IDENT("asdf-_"); | |||||
| BAD_IDENT("."); | |||||
| BAD_IDENT(" "); | |||||
| BAD_IDENT("124a["); | |||||
| } | |||||
| TEST_CASE("Ident comparison") { | |||||
| using semver::order; | |||||
| struct comp_test { | |||||
| std::string lhs; | |||||
| std::string rhs; | |||||
| order expect_ordering; | |||||
| }; | |||||
| comp_test comparisons[] = { | |||||
| {"foo", "bar", order::greater}, | |||||
| {"foo", "foo", order::equivalent}, | |||||
| {"bar", "foo", order::less}, | |||||
| {"12", "333", order::less}, | |||||
| {"fooood", "3", order::greater}, | |||||
| {"0", "0", order::equivalent}, | |||||
| {"34", "f", order::less}, | |||||
| {"aaaaaaaaaaa", "z", order::less}, | |||||
| }; | |||||
| for (auto [lhs, rhs, expect] : comparisons) { | |||||
| auto lhs_id = semver::ident(lhs); | |||||
| auto rhs_id = semver::ident(rhs); | |||||
| INFO("Comparing '" << lhs << "' to '" << rhs << "'"); | |||||
| auto result = semver::compare(lhs_id, rhs_id); | |||||
| CHECK(result == expect); | |||||
| } | |||||
| } | |||||
| TEST_CASE("Parse dotted sequence") { | |||||
| struct case_ { | |||||
| std::string str; | |||||
| std::vector<std::string> expected; | |||||
| }; | |||||
| case_ cases[] = { | |||||
| {"foo", {"foo"}}, | |||||
| {"foo.bar", {"foo", "bar"}}, | |||||
| }; | |||||
| for (auto& [str, expected] : cases) { | |||||
| INFO("Parsing dotted-ident-sequence '" << str << "'"); | |||||
| auto actual = semver::ident::parse_dotted_seq(str); | |||||
| REQUIRE(actual.size() == expected.size()); | |||||
| } | |||||
| } | |||||
| TEST_CASE("Invalid dotted sequence") { | |||||
| std::string_view bad_seqs[] = { | |||||
| "", | |||||
| ".", | |||||
| ".foo", | |||||
| "foo.bar.", | |||||
| "foo..bar", | |||||
| }; | |||||
| for (auto s : bad_seqs) { | |||||
| INFO("Checking bad ident sequence '" << s << "'"); | |||||
| CHECK_THROWS_AS(semver::ident::parse_dotted_seq(s), semver::invalid_ident); | |||||
| } | |||||
| } |
| #pragma once | |||||
| namespace semver { | |||||
| enum class order { | |||||
| less = -1, | |||||
| equivalent = 0, | |||||
| greater = 1, | |||||
| }; | |||||
| } // namespace semver |
| #include "./prerelease.hpp" | |||||
| using namespace semver; | |||||
| prerelease prerelease::parse(std::string_view s) { | |||||
| auto ids = ident::parse_dotted_seq(s); | |||||
| for (auto& id : ids) { | |||||
| if (id.kind() == ident_kind::digits) { | |||||
| throw invalid_ident("Prerelease strings may not have plain-digit identifiers: " | |||||
| + std::string(s)); | |||||
| } | |||||
| } | |||||
| prerelease ret; | |||||
| ret._ids = std::move(ids); | |||||
| return ret; | |||||
| } | |||||
| order semver::compare(const prerelease& lhs, const prerelease& rhs) noexcept { | |||||
| auto lhs_iter = lhs.idents().cbegin(); | |||||
| auto rhs_iter = rhs.idents().cbegin(); | |||||
| const auto lhs_end = lhs.idents().cend(); | |||||
| const auto rhs_end = rhs.idents().cend(); | |||||
| for (; lhs_iter != lhs_end && rhs_iter != rhs_end; ++lhs_iter, ++rhs_iter) { | |||||
| auto ord = compare(*lhs_iter, *rhs_iter); | |||||
| if (ord != order::equivalent) { | |||||
| return ord; | |||||
| } | |||||
| } | |||||
| if (lhs_iter != lhs_end) { | |||||
| // Left-hand is longer | |||||
| return order::greater; | |||||
| } else if (rhs_iter != rhs_end) { | |||||
| // Right-hand is longer | |||||
| return order::less; | |||||
| } else { | |||||
| // They are equivalent | |||||
| return order::equivalent; | |||||
| } | |||||
| } |
| #pragma once | |||||
| #include <semver/ident.hpp> | |||||
| #include <semver/order.hpp> | |||||
| #include <stdexcept> | |||||
| #include <string_view> | |||||
| #include <vector> | |||||
| namespace semver { | |||||
| class prerelease; | |||||
| order compare(const prerelease& lhs, const prerelease& rhs) noexcept; | |||||
| class prerelease { | |||||
| std::vector<ident> _ids; | |||||
| public: | |||||
| prerelease() = default; | |||||
| [[nodiscard]] bool empty() const noexcept { return _ids.empty(); } | |||||
| void add_ident(ident id); | |||||
| void add_ident(std::string_view str) { add_ident(ident(str)); } | |||||
| auto& idents() const noexcept { return _ids; } | |||||
| static prerelease parse(std::string_view str); | |||||
| #define DEF_OP(op, expr) \ | |||||
| inline friend bool operator op(const prerelease& lhs, const prerelease& rhs) noexcept { \ | |||||
| auto o = compare(lhs, rhs); \ | |||||
| return (expr); \ | |||||
| } \ | |||||
| static_assert(true) | |||||
| DEF_OP(==, (o == order::equivalent)); | |||||
| DEF_OP(!=, (o != order::equivalent)); | |||||
| DEF_OP(<, (o == order::less)); | |||||
| DEF_OP(>, (o == order::greater)); | |||||
| DEF_OP(<=, (o == order::less || o == order::equivalent)); | |||||
| DEF_OP(>=, (o == order::greater || o == order::equivalent)); | |||||
| #undef DEF_OP | |||||
| }; | |||||
| } // namespace semver |
| #include "./prerelease.hpp" | |||||
| #include <catch2/catch.hpp> | |||||
| TEST_CASE("Parse a prerelease") { | |||||
| auto pre = semver::prerelease::parse("foo.bar"); | |||||
| CHECK(pre.idents() == semver::ident::parse_dotted_seq("foo.bar")); | |||||
| pre = pre.parse("foo.0"); | |||||
| CHECK(pre.idents() == semver::ident::parse_dotted_seq("foo.0")); | |||||
| } | |||||
| TEST_CASE("Invalid prereleases") { | |||||
| std::string_view bad_tags[] = { | |||||
| "foo.", // Just plain bad | |||||
| "foo.122.03", // Leading zero | |||||
| }; | |||||
| for (auto bad : bad_tags) { | |||||
| CHECK_THROWS_AS(semver::prerelease::parse(bad), semver::invalid_ident); | |||||
| } | |||||
| } | |||||
| TEST_CASE("Compare prereleases") { | |||||
| using semver::order; | |||||
| struct case_ { | |||||
| std::string lhs; | |||||
| std::string rhs; | |||||
| order expected_ord; | |||||
| }; | |||||
| case_ cases[] = { | |||||
| {"foo.bar", "foo.bar", order::equivalent}, | |||||
| {"foo", "foo.bar", order::less}, | |||||
| {"foo.bar", "foo", order::greater}, | |||||
| {"foo", "foo", order::equivalent}, | |||||
| {"foo.1", "foo.bar", order::less}, // numeric is less | |||||
| {"foo.foo", "foo.bar", order::greater}, | |||||
| {"alpha", "beta", order::less}, | |||||
| {"alpha.1", "alpha.2", order::less}, | |||||
| }; | |||||
| for (auto& [lhs, rhs, exp] : cases) { | |||||
| INFO("Comparing prerelease '" << lhs << "' to '" << rhs << "'"); | |||||
| auto lhs_pre = semver::prerelease::parse(lhs); | |||||
| auto rhs_pre = semver::prerelease::parse(rhs); | |||||
| auto actual = semver::compare(lhs_pre, rhs_pre); | |||||
| CHECK(actual == exp); | |||||
| } | |||||
| } |
| version parse(const char* ptr, std::size_t size) { | version parse(const char* ptr, std::size_t size) { | ||||
| version ret; | version ret; | ||||
| // const auto str_begin = ptr; | // const auto str_begin = ptr; | ||||
| const auto str_end = ptr + size; | |||||
| const auto str_begin = ptr; | |||||
| const auto str_end = ptr + size; | |||||
| auto get_str = [=] { return std::string(str_begin, size); }; | |||||
| auto cur_off = [&] { return ptr - str_begin; }; | |||||
| std::from_chars_result fc_res; | std::from_chars_result fc_res; | ||||
| auto did_error = [&](int elem) { return fc_res.ec == std::errc::invalid_argument || elem < 0; }; | auto did_error = [&](int elem) { return fc_res.ec == std::errc::invalid_argument || elem < 0; }; | ||||
| // Parse major | // Parse major | ||||
| fc_res = std::from_chars(ptr, str_end, ret.major); | fc_res = std::from_chars(ptr, str_end, ret.major); | ||||
| ptr = fc_res.ptr; | |||||
| if (did_error(ret.major) || fc_res.ptr == str_end || *fc_res.ptr != '.') { | if (did_error(ret.major) || fc_res.ptr == str_end || *fc_res.ptr != '.') { | ||||
| throw invalid_version(0); | |||||
| throw invalid_version(get_str(), cur_off()); | |||||
| } | } | ||||
| // Parse minor | // Parse minor | ||||
| ptr = fc_res.ptr + 1; | ptr = fc_res.ptr + 1; | ||||
| fc_res = std::from_chars(ptr, str_end, ret.minor); | fc_res = std::from_chars(ptr, str_end, ret.minor); | ||||
| ptr = fc_res.ptr; | |||||
| if (did_error(ret.minor) || fc_res.ptr == str_end || *fc_res.ptr != '.') { | if (did_error(ret.minor) || fc_res.ptr == str_end || *fc_res.ptr != '.') { | ||||
| throw invalid_version(ptr - str_end); | |||||
| throw invalid_version(get_str(), cur_off()); | |||||
| } | } | ||||
| // Parse patch | // Parse patch | ||||
| ptr = fc_res.ptr + 1; | ptr = fc_res.ptr + 1; | ||||
| fc_res = std::from_chars(ptr, str_end, ret.patch); | fc_res = std::from_chars(ptr, str_end, ret.patch); | ||||
| ptr = fc_res.ptr; | |||||
| if (did_error(ret.patch)) { | if (did_error(ret.patch)) { | ||||
| throw invalid_version(ptr - str_end); | |||||
| throw invalid_version(get_str(), cur_off()); | |||||
| } | } | ||||
| if (fc_res.ptr != str_end) { | |||||
| assert(false && "More complex version numbers are not ready yet!"); | |||||
| throw invalid_version(-42); | |||||
| auto remaining = std::string_view(ptr, str_end - ptr); | |||||
| if (!remaining.empty() && remaining[0] == '-') { | |||||
| auto plus_pos = remaining.find('+'); | |||||
| auto prerelease_str = remaining.substr(1, plus_pos - 1); | |||||
| ret.prerelease = prerelease::parse(prerelease_str); | |||||
| remaining = remaining.substr(prerelease_str.size() + 1); | |||||
| } | |||||
| if (!remaining.empty() && remaining[0] == '+') { | |||||
| auto bmeta_str = remaining.substr(1); | |||||
| ret.build_metadata = build_metadata::parse(bmeta_str); | |||||
| remaining = remaining.substr(bmeta_str.size() + 1); | |||||
| } | |||||
| if (!remaining.empty()) { | |||||
| throw invalid_version(get_str(), remaining.data() - str_begin); | |||||
| } | } | ||||
| return ret; | return ret; | ||||
| } | |||||
| } // namespace | |||||
| } // namespace | } // namespace | ||||
| *buf_ptr++ = '.'; | *buf_ptr++ = '.'; | ||||
| conv_one(patch); | conv_one(patch); | ||||
| return std::string(buf_begin, (buf_ptr - buf_begin)); | |||||
| auto main_ver = std::string(buf_begin, (buf_ptr - buf_begin)); | |||||
| auto format_ids = [](auto& ids) { | |||||
| std::string acc; | |||||
| auto it = ids.cbegin(); | |||||
| auto stop = ids.cend(); | |||||
| while (it != stop) { | |||||
| acc += it->string(); | |||||
| ++it; | |||||
| if (it != stop) { | |||||
| acc += "."; | |||||
| } | |||||
| } | |||||
| return acc; | |||||
| }; | |||||
| if (!prerelease.empty()) { | |||||
| main_ver += "-" + format_ids(prerelease.idents()); | |||||
| } | |||||
| if (!build_metadata.empty()) { | |||||
| main_ver += "+" + format_ids(build_metadata.idents()); | |||||
| } | |||||
| return main_ver; | |||||
| } | |||||
| order semver::compare(const version& lhs, const version& rhs) noexcept { | |||||
| auto lhs_tup = std::tie(lhs.major, lhs.minor, lhs.patch); | |||||
| auto rhs_tup = std::tie(rhs.major, rhs.minor, rhs.patch); | |||||
| if (lhs_tup < rhs_tup) { | |||||
| return order::less; | |||||
| } else if (lhs_tup > rhs_tup) { | |||||
| return order::greater; | |||||
| } else { | |||||
| if (!lhs.is_prerelease() && rhs.is_prerelease()) { | |||||
| // No prerelease is greater than any prerelease | |||||
| return order::greater; | |||||
| } else if (lhs.is_prerelease() && !rhs.is_prerelease()) { | |||||
| // A prerelease version is lesser than any non-prerelease version | |||||
| return order::less; | |||||
| } else { | |||||
| // Compare the prerelease tags | |||||
| return compare(lhs.prerelease, rhs.prerelease); | |||||
| } | |||||
| } | |||||
| } | } |
| #pragma once | #pragma once | ||||
| #include <semver/build_metadata.hpp> | |||||
| #include <semver/prerelease.hpp> | |||||
| #include <stdexcept> | #include <stdexcept> | ||||
| #include <string> | #include <string> | ||||
| #include <string_view> | #include <string_view> | ||||
| namespace semver { | namespace semver { | ||||
| class invalid_version : public std::runtime_error { | class invalid_version : public std::runtime_error { | ||||
| std::string _string; | |||||
| std::ptrdiff_t _offset = 0; | std::ptrdiff_t _offset = 0; | ||||
| public: | public: | ||||
| invalid_version(std::ptrdiff_t n) | |||||
| : runtime_error("Invalid version number") | |||||
| invalid_version(std::string string, std::ptrdiff_t n) | |||||
| : runtime_error("Invalid semantic version: " + string) | |||||
| , _string(string) | |||||
| , _offset(n) {} | , _offset(n) {} | ||||
| auto offset() const noexcept { return _offset; } | |||||
| auto& string() const noexcept { return _string; } | |||||
| auto offset() const noexcept { return _offset; } | |||||
| }; | }; | ||||
| class version; | |||||
| order compare(const version& lhs, const version& rhs) noexcept; | |||||
| struct version { | struct version { | ||||
| int major = 0; | int major = 0; | ||||
| int minor = 0; | int minor = 0; | ||||
| int patch = 0; | int patch = 0; | ||||
| // Prerelease tag is optional: | |||||
| class prerelease prerelease = {}; | |||||
| // Build metadata is optional: | |||||
| class build_metadata build_metadata = {}; | |||||
| static version parse(std::string_view s); | static version parse(std::string_view s); | ||||
| std::string to_string() const noexcept; | std::string to_string() const noexcept; | ||||
| auto tie() const noexcept { return std::tie(major, minor, patch); } | |||||
| auto tie() noexcept { return std::tie(major, minor, patch); } | |||||
| bool is_prerelease() const noexcept { return !prerelease.empty(); } | |||||
| #define DEF_OP(op, expr) \ | |||||
| inline friend bool operator op(const version& lhs, const version& rhs) noexcept { \ | |||||
| auto o = compare(lhs, rhs); \ | |||||
| return (expr); \ | |||||
| } \ | |||||
| static_assert(true) | |||||
| DEF_OP(==, (o == order::equivalent)); | |||||
| DEF_OP(!=, (o != order::equivalent)); | |||||
| DEF_OP(<, (o == order::less)); | |||||
| DEF_OP(>, (o == order::greater)); | |||||
| DEF_OP(<=, (o == order::less || o == order::equivalent)); | |||||
| DEF_OP(>=, (o == order::greater || o == order::equivalent)); | |||||
| #undef DEF_OP | |||||
| friend inline std::string to_string(const version& ver) noexcept { return ver.to_string(); } | |||||
| }; | }; | ||||
| inline bool operator!=(const version& lhs, const version& rhs) noexcept { | |||||
| return lhs.tie() != rhs.tie(); | |||||
| } | |||||
| inline bool operator<(const version& lhs, const version& rhs) noexcept { | |||||
| return lhs.tie() < rhs.tie(); | |||||
| } | |||||
| inline std::string to_string(const version& ver) noexcept { return ver.to_string(); } | |||||
| } // namespace semver | } // namespace semver |
| CHECK(v1.to_string() == "1.2.55"); | CHECK(v1.to_string() == "1.2.55"); | ||||
| v1.major = 999999; | v1.major = 999999; | ||||
| CHECK(v1.to_string() == "999999.2.55"); | CHECK(v1.to_string() == "999999.2.55"); | ||||
| v1 = semver::version::parse("1.2.3-r1"); | |||||
| CHECK(v1.prerelease == semver::prerelease::parse("r1")); | |||||
| CHECK(v1.to_string() == "1.2.3-r1"); | |||||
| v1 = semver::version::parse("1.2.3-3"); | |||||
| CHECK(v1.prerelease == semver::prerelease::parse("3")); | |||||
| v1 = semver::version::parse("1.2.3-0"); | |||||
| CHECK(v1.prerelease == semver::prerelease::parse("0")); | |||||
| v1 = semver::version::parse("1.2.3--fasdf"); | |||||
| CHECK(v1.prerelease == semver::prerelease::parse("-fasdf")); | |||||
| v1 = semver::version::parse("1.2.3-foo.bar"); | |||||
| CHECK(v1.prerelease == semver::prerelease::parse("foo.bar")); | |||||
| v1 = semver::version::parse("1.2.3-foo.bar+cats"); | |||||
| CHECK(v1.prerelease == semver::prerelease::parse("foo.bar")); | |||||
| CHECK(v1.build_metadata.idents() == semver::ident::parse_dotted_seq("cats")); | |||||
| } | } | ||||
| TEST_CASE("Compare versions") { | |||||
| using semver::order; | |||||
| struct case_ { | |||||
| std::string_view lhs; | |||||
| std::string_view rhs; | |||||
| order expect_ord; | |||||
| }; | |||||
| case_ cases[] = { | |||||
| {"1.2.3", "1.2.3", order::equivalent}, | |||||
| {"1.2.3-alpha", "1.2.3-alpha", order::equivalent}, | |||||
| {"1.2.3-alpha", "1.2.3", order::less}, | |||||
| {"1.2.3-alpha.1", "1.2.3-beta", order::less}, | |||||
| {"1.2.3-alpha.1", "1.2.3-alpha.2", order::less}, | |||||
| {"1.2.3-alpha.4", "1.2.3-alpha.2", order::greater}, | |||||
| {"1.2.1", "1.2.2-alpha.2", order::less}, | |||||
| {"1.2.1", "1.2.1+foo", order::equivalent}, // Build metadata has no effect | |||||
| }; | |||||
| for (auto [lhs, rhs, exp] : cases) { | |||||
| INFO("Compare version '" << lhs << "' to '" << rhs << "'"); | |||||
| auto lhs_ver = semver::version::parse(lhs); | |||||
| auto rhs_ver = semver::version::parse(rhs); | |||||
| auto actual = semver::compare(lhs_ver, rhs_ver); | |||||
| CHECK(actual == exp); | |||||
| } | |||||
| } | |||||
| TEST_CASE("Invalid versions") { | |||||
| struct invalid_version { | |||||
| std::string str; | |||||
| int bad_offset; | |||||
| }; | |||||
| invalid_version versions[] = { | |||||
| {"", 0}, | |||||
| {"1.", 2}, | |||||
| {"1.2", 3}, | |||||
| {"1.2.", 4}, | |||||
| {"1.e", 2}, | |||||
| {"lol", 0}, | |||||
| {"1e.3.1", 1}, | |||||
| {"1.2.5-", 6}, | |||||
| {"1.2.5-02", 6}, | |||||
| {"1.2.3-1..3", 8}, | |||||
| }; | |||||
| for (auto&& [str, bad_offset] : versions) { | |||||
| INFO("Checking for failure while parsing bad version string '" << str << "'"); | |||||
| try { | |||||
| auto ver = semver::version::parse(str); | |||||
| FAIL_CHECK("Parsing didn't throw! Produced version: " << ver.to_string()); | |||||
| } catch (const semver::invalid_version& e) { | |||||
| CHECK(e.offset() == bad_offset); | |||||
| } catch (const semver::invalid_ident&) { | |||||
| // Nothing to check, but a valid error | |||||
| } | |||||
| } | |||||
| } |