From 2f5981c7d5119f60fd43ec8bcc6b1eb6974c7571 Mon Sep 17 00:00:00 2001 From: Marko Zivanovic Date: Thu, 8 Oct 2015 16:00:22 +0200 Subject: [PATCH] Full semver 2.0.0 implementation and validation by a number of tests. --- include/version.h | 31 ++- src/CMakeLists.txt | 2 +- src/Semver200_comparator.cpp | 81 ++++++ src/{version.cpp => Semver200_parser.cpp} | 118 +++++---- test/semver200_comparator_tests.cpp | 136 +++++------ test/semver200_parser_tests.cpp | 285 ++++++---------------- test/semver200_parser_util.h | 98 ++++++++ 7 files changed, 409 insertions(+), 342 deletions(-) create mode 100644 src/Semver200_comparator.cpp rename src/{version.cpp => Semver200_parser.cpp} (52%) create mode 100644 test/semver200_parser_util.h diff --git a/include/version.h b/include/version.h index 3068a9e..17fa074 100644 --- a/include/version.h +++ b/include/version.h @@ -24,25 +24,25 @@ SOFTWARE. #pragma once -#include +#include #include +#include namespace version { + enum class Identifier_type { alnum, num }; + + using Prerelease_identifier = std::pair; + using Prerelease_identifiers = std::vector; + using Build_identifier = std::string; + using Build_identifiers = std::vector; + struct Version_data { int major; int minor; int patch; - std::string release; - std::string build; - }; - - struct Semver200_parser { - Version_data parse(const std::string&); - }; - - struct Semver200_comparator { - int compare(const Version_data&, const Version_data&); + Prerelease_identifiers prerelease_ids; + Build_identifiers build_ids; }; template @@ -63,9 +63,18 @@ namespace version { const Version_data ver_; }; + struct Semver200_parser { + Version_data parse(const std::string&); + }; + + struct Semver200_comparator { + int compare(const Version_data&, const Version_data&); + }; + class Semver200 : public Basic_version {}; class Parse_error : public std::runtime_error { using std::runtime_error::runtime_error; }; + } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9c4f51d..379d626 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,5 +3,5 @@ include_directories( ) add_library(semver - version.cpp + Semver200_parser.cpp Semver200_comparator.cpp ) diff --git a/src/Semver200_comparator.cpp b/src/Semver200_comparator.cpp new file mode 100644 index 0000000..3180fef --- /dev/null +++ b/src/Semver200_comparator.cpp @@ -0,0 +1,81 @@ +/* +The MIT License (MIT) + +Copyright (c) 2015 Marko Zivanovic + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "version.h" + +using namespace std; + +namespace { + + using namespace version; + + int compare_prerel_identifiers(const Prerelease_identifier& l, Prerelease_identifier& r) { + if (l.second == Identifier_type::alnum && r.second == Identifier_type::alnum) { + return l.first.compare(r.first); + } else if (l.second == Identifier_type::alnum && r.second == Identifier_type::num) { + return 1; + } else if (l.second == Identifier_type::num && r.second == Identifier_type::alnum) { + return -1; + } else if (l.second == Identifier_type::num && r.second == Identifier_type::num) { + int li = stoi(l.first); + int ri = stoi(r.first); + if (li == ri) return 0; + return li > ri ? 1 : -1; + } + throw runtime_error("unexpected identifier types: " + to_string(static_cast(l.second)) + ", " + + to_string(static_cast(r.second))); + } +} + +namespace version { + + int Semver200_comparator::compare(const Version_data& l, const Version_data& r) { + if (l.major > r.major) return 1; + if (l.major < r.major) return -1; + + if (l.minor > r.minor) return 1; + if (l.minor < r.minor) return -1; + + if (l.patch > r.patch) return 1; + if (l.patch < r.patch) return -1; + + // release version is always higher than prerelease + if (l.prerelease_ids.empty() && !r.prerelease_ids.empty()) return 1; + if (r.prerelease_ids.empty() && !l.prerelease_ids.empty()) return -1; + + // compare release by looking at each identifier: numeric ones are compared as numbers, alphanum as ASCII strings + auto shorter = l.prerelease_ids.size() <= r.prerelease_ids.size() ? l.prerelease_ids.size() : r.prerelease_ids.size(); + for (size_t i = 0; i < shorter; i++) { + auto lid = l.prerelease_ids[i]; + auto rid = r.prerelease_ids[i]; + int cmp = compare_prerel_identifiers(lid, rid); + if (cmp != 0) return cmp; + } + // prerels are the same, to the length of the shorter one; + // if they are the same length, then version are equal, otherwise, longer wins + if (l.prerelease_ids.size() == r.prerelease_ids.size()) return 0; + return l.prerelease_ids.size() > r.prerelease_ids.size() ? 1 : -1; + } + +} \ No newline at end of file diff --git a/src/version.cpp b/src/Semver200_parser.cpp similarity index 52% rename from src/version.cpp rename to src/Semver200_parser.cpp index eefe506..2f98496 100644 --- a/src/version.cpp +++ b/src/Semver200_parser.cpp @@ -33,18 +33,19 @@ namespace { major, minor, patch, release, build }; - using Phase_transition_hook = function; + using Phase_transition_hook = function; using Transition = tuple; using Validator = function; - inline Transition mkx(const char c, Parser_phase p, Phase_transition_hook px) { - return make_tuple(c, p, px); + inline Transition mkx(const char c, Parser_phase p, Phase_transition_hook pth) { + return make_tuple(c, p, pth); } - inline void process_char(const char c, Parser_phase& phase, const vector transitions, - string& target, Validator validate) { + inline void process_char(const char c, Parser_phase& phase, Parser_phase& prev_phase, + const vector transitions, string& target, Validator validate) { for (const auto& t : transitions) { if (c == get<0>(t)) { + prev_phase = phase; phase = get<1>(t); if (get<2>(t)) get<2>(t)(target); return; @@ -54,35 +55,24 @@ namespace { target.push_back(c); } - auto normal_version_validator = [](const string& tgt, const char c) { + inline void normal_version_validator(const string& tgt, const char c) { if (c < '0' || c > '9') throw version::Parse_error("invalid character encountered: " + string(1, c)); if (tgt.compare(0, 1, "0") == 0) throw version::Parse_error("leading 0 not allowed"); - }; + } - auto release_version_validator = [](const string& tgt, const char c) { + inline void release_version_validator(const string& tgt, const char c) { if ((c < '0' || c > '9') && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && c != '.' && c != '-') throw version::Parse_error("invalid character encountered: " + string(1, c)); - }; - - void validate_release_identifier(const string& id) { - if (id.find_first_not_of("0123456789") == string::npos - && id.compare(0, 1, "0") == 0) { - throw version::Parse_error("leading 0 not allowed"); - } } - void validate_release_identifiers(const string& release) { - string id; - for (const auto& c : release) { - if (c == '.') { - validate_release_identifier(id); - id.clear(); - } else { - id.push_back(c); - } - } - validate_release_identifier(id); + bool is_identifier_numeric(const string& id) { + return id.find_first_not_of("0123456789") == string::npos; } + + bool check_identifier_for_leading_0(const string& id) { + return id.length() > 1 && id[0] == '0'; + } + } namespace version { @@ -91,18 +81,32 @@ namespace version { string major; string minor; string patch; - string release; - string build; + string release_id; + string build_id; + Prerelease_identifiers release; + Build_identifiers build; + Parser_phase prev_phase; - auto process_release_component = [&](const string& t) { - if (t.empty()) throw Parse_error("version identifier cannot be empty"); - if (t.compare(t.length() - 1, 1, ".") == 0) throw Parse_error("version identifier cannot be empty"); - release.push_back('.');}; + auto release_hook = [&](string& id) { + if (id.empty()) throw version::Parse_error("version identifier cannot be empty"); + Identifier_type t = Identifier_type::alnum; + if (is_identifier_numeric(id)) { + t = Identifier_type::num; + if (check_identifier_for_leading_0(id)) { + throw Parse_error("numeric identifiers cannot have leading 0"); + } + } + release.push_back(Prerelease_identifier(id, t)); + id.clear(); + }; - auto process_build_component = [&](const string& t) { - if (t.empty()) throw Parse_error("version identifier cannot be empty"); - if (t.compare(t.length() - 1, 1, ".") == 0) throw Parse_error("version identifier cannot be empty"); - build.push_back('.');}; + auto build_hook = [&](string& id) { + // process last token left from parsing release data + if (prev_phase == Parser_phase::release) release_hook(release_id); + if (id.empty()) throw version::Parse_error("version identifier cannot be empty"); + build.push_back(id); + id.clear(); + }; auto major_xs = { mkx('.', Parser_phase::minor, {}) @@ -115,34 +119,41 @@ namespace version { mkx('+', Parser_phase::build, {}) }; auto release_xs = { - mkx('.', Parser_phase::release, process_release_component), + mkx('.', Parser_phase::release, release_hook), mkx('+', Parser_phase::build, {}) }; auto build_xs = { - mkx('.', Parser_phase::build, process_build_component) + mkx('.', Parser_phase::build, build_hook) }; Parser_phase phase = Parser_phase::major; for (const auto& c : s) { switch (phase) { case Parser_phase::major: - process_char(c, phase, major_xs, major, normal_version_validator); + process_char(c, phase, prev_phase, major_xs, major, normal_version_validator); break; case Parser_phase::minor: - process_char(c, phase, minor_xs, minor, normal_version_validator); + process_char(c, phase, prev_phase, minor_xs, minor, normal_version_validator); break; case Parser_phase::patch: - process_char(c, phase, patch_xs, patch, normal_version_validator); + process_char(c, phase, prev_phase, patch_xs, patch, normal_version_validator); break; case Parser_phase::release: - process_char(c, phase, release_xs, release, release_version_validator); + process_char(c, phase, prev_phase, release_xs, release_id, release_version_validator); break; case Parser_phase::build: - process_char(c, phase, build_xs, build, release_version_validator); + process_char(c, phase, prev_phase, build_xs, build_id, release_version_validator); break; } } - validate_release_identifiers(release); + // Trigger appropriate hooks in order to process last token, because no phase transition was + // triggered for it. + if (phase == Parser_phase::release) { + release_hook(release_id); + } + if (phase == Parser_phase::build) { + build_hook(build_id); + } try { return Version_data{ stoi(major), stoi(minor), stoi(patch), release, build }; @@ -151,8 +162,23 @@ namespace version { } } - int Semver200_comparator::compare(const Version_data& l, const Version_data& r) { - throw 1; + ostream& operator<<(ostream& os, const Prerelease_identifier& id) { + os << id.first << "<" << static_cast(id.second) << ">"; + return os; + } + + ostream& operator<<(ostream& os, const Prerelease_identifiers& ids) { + for (const auto& id : ids) { + os << id << ","; + } + return os; + } + + ostream& operator<<(ostream& os, const Build_identifiers& ids) { + for (const auto& id : ids) { + os << id << ","; + } + return os; } } diff --git a/test/semver200_comparator_tests.cpp b/test/semver200_comparator_tests.cpp index fc59078..7c2540a 100644 --- a/test/semver200_comparator_tests.cpp +++ b/test/semver200_comparator_tests.cpp @@ -29,99 +29,77 @@ SOFTWARE. using namespace version; -int compare(const std::string& l, const std::string& r) { - Semver200_comparator c; - Semver200_parser p; +Semver200_comparator c; +Semver200_parser p; + +inline int compare(const std::string& l, const std::string& r) { auto lv = p.parse(l); auto rv = p.parse(r); return c.compare(lv, rv); } -BOOST_AUTO_TEST_CASE(compare_normal_eq) { - BOOST_CHECK(compare("0.0.0", "0.0.0") == 0); +#define GT(L, R) BOOST_CHECK(compare(L, R) > 0) +#define GE(L, R) BOOST_CHECK(compare(L, R) >= 0) +#define LT(L, R) BOOST_CHECK(compare(L, R) < 0) +#define LE(L, R) BOOST_CHECK(compare(L, R) <= 0) +#define EQ(L, R) BOOST_CHECK(compare(L, R) == 0) + +// check normal precedence +BOOST_AUTO_TEST_CASE(compare_normal) { + EQ("1.2.3", "1.2.3"); + + GT("0.0.2", "0.0.1"); + GT("0.2.0", "0.0.3"); + GT("0.2.0", "0.1.3"); + GT("2.0.0", "0.0.1"); + GT("2.0.0", "0.3.1"); + GT("2.0.0", "1.3.1"); } -BOOST_AUTO_TEST_CASE(compare_normal_major_gt_1) { - BOOST_CHECK(compare("1.0.0", "0.0.0") == 1); +// normal and prerel precedence +BOOST_AUTO_TEST_CASE(compare_normal_prerel) { + GT("1.0.0", "1.0.0-alpha"); + GT("1.0.0", "1.0.0-99"); + GT("1.0.0", "1.0.0-ZZ"); } -BOOST_AUTO_TEST_CASE(compare_normal_major_gt_2) { - BOOST_CHECK(compare("1.0.0", "0.0.5") == 1); +// same prerels +BOOST_AUTO_TEST_CASE(compare_equal_prerels) { + EQ("1.0.0-alpha", "1.0.0-alpha"); + EQ("1.0.0-alpha.1", "1.0.0-alpha.1"); + EQ("1.0.0-1", "1.0.0-1"); } -BOOST_AUTO_TEST_CASE(compare_normal_major_gt_3) { - BOOST_CHECK(compare("1.0.0", "0.17.0") == 1); +// prerels precedence with numeric id +BOOST_AUTO_TEST_CASE(compare_numeric_prerels) { + GT("1.0.0-1", "1.0.0-0"); + GT("1.0.0-10", "1.0.0-1"); + GT("1.0.0-alpha.3", "1.0.0-alpha.1"); } -BOOST_AUTO_TEST_CASE(compare_normal_minor_gt_1) { - BOOST_CHECK(compare("1.1.0", "1.0.0") == 1); +// prerels precedence with alphanum id +BOOST_AUTO_TEST_CASE(compare_alphanum_prerels) { + GT("1.0.0-1", "1.0.0-0"); + GT("1.0.0-Z", "1.0.0-A"); + GT("1.0.0-Z", "1.0.0-1"); + GT("1.0.0-alpha-3", "1.0.0-alpha-1"); + GT("1.0.0-alpha-3", "1.0.0-alpha-100"); } -BOOST_AUTO_TEST_CASE(compare_normal_minor_gt_2) { - BOOST_CHECK(compare("1.1.0", "1.0.32") == 1); -} - -BOOST_AUTO_TEST_CASE(compare_normal_patch_gt) { - BOOST_CHECK(compare("1.1.3", "1.1.0") == 1); -} - -BOOST_AUTO_TEST_CASE(compare_normal_major_lt_1) { - BOOST_CHECK(compare("1.0.0", "2.0.0") == -1); -} - -BOOST_AUTO_TEST_CASE(compare_normal_major_lt_2) { - BOOST_CHECK(compare("1.0.1", "2.0.0") == -1); -} - -BOOST_AUTO_TEST_CASE(compare_normal_major_lt_3) { - BOOST_CHECK(compare("1.3.0", "2.0.0") == -1); -} - -BOOST_AUTO_TEST_CASE(compare_normal_minor_lt_1) { - BOOST_CHECK(compare("1.1.0", "1.2.0") == -1); -} - -BOOST_AUTO_TEST_CASE(compare_normal_minor_lt_2) { - BOOST_CHECK(compare("1.1.3", "1.2.0") == -1); -} - -BOOST_AUTO_TEST_CASE(compare_normal_patch_lt) { - BOOST_CHECK(compare("1.1.3", "1.1.5") == -1); -} - -BOOST_AUTO_TEST_CASE(compare_prerel_single_eq) { - BOOST_CHECK(compare("1.1.3-prerel", "1.1.3-prerel") == 1); -} - -BOOST_AUTO_TEST_CASE(compare_prerel_multiple_eq) { - BOOST_CHECK(compare("1.1.3-prerel.1.2.3", "1.1.3-prerel.1.2.3") == 1); -} - -BOOST_AUTO_TEST_CASE(compare_prerel_multiple_num_eq) { - BOOST_CHECK(compare("1.1.3-prerel.123", "1.1.3-prerel.123") == 1); -} - -BOOST_AUTO_TEST_CASE(compare_prerel_multiple_num_gt) { - BOOST_CHECK(compare("1.1.3-prerel.123", "1.1.3-prerel.120") == 1); -} - -BOOST_AUTO_TEST_CASE(compare_prerel_multiple_num_lt) { - BOOST_CHECK(compare("1.1.3-prerel.123", "1.1.3-prerel.125") == -1); -} - -BOOST_AUTO_TEST_CASE(compare_prerel_single_alpha_gt) { - BOOST_CHECK(compare("1.1.3-alpha", "1.1.3-beta") == 1); -} - -BOOST_AUTO_TEST_CASE(compare_prerel_single_alpha_lt) { - BOOST_CHECK(compare("1.1.3-beta", "1.1.3-gamma") == -1); -} - -BOOST_AUTO_TEST_CASE(compare_prerel_single_alpha_gt_2) { - BOOST_CHECK(compare("1.1.3-alpha5", "1.1.3-alpha4") == 1); -} - -BOOST_AUTO_TEST_CASE(compare_prerel_single_alpha_lt_2) { - BOOST_CHECK(compare("1.1.3-beta3", "1.1.3-beta5") == -1); +// prerels precedence misc +BOOST_AUTO_TEST_CASE(compare_misc_prerels) { + LT("1.0.0-alpha", "1.0.0-alpha.1"); + LT("1.0.0-alpha.1", "1.0.0-alpha.beta"); + LT("1.0.0-alpha.beta", "1.0.0-beta"); + LT("1.0.0-beta", "1.0.0-beta.2"); + LT("1.0.0-beta.2", "1.0.0-beta.11"); + LT("1.0.0-beta.11", "1.0.0-rc.1"); + LT("1.0.0-rc.1", "1.0.0"); } +// equal precedence based on build +BOOST_AUTO_TEST_CASE(compare_build) { + EQ("1.0.0", "1.0.0+build.1.2.3"); + EQ("1.0.0+ZZZ", "1.0.0+build.1.2.3"); + EQ("1.0.0+100", "1.0.0+200"); +} \ No newline at end of file diff --git a/test/semver200_parser_tests.cpp b/test/semver200_parser_tests.cpp index e8df487..98763a8 100644 --- a/test/semver200_parser_tests.cpp +++ b/test/semver200_parser_tests.cpp @@ -24,234 +24,109 @@ SOFTWARE. #define BOOST_TEST_MODULE semver200_parser_tests -#include -#include "version.h" +#include "semver200_parser_util.h" using namespace version; +using namespace std; -Version_data parse(const std::string& s) { - Semver200_parser p; - return p.parse(s); +// normal must have major, minor and patch version +BOOST_AUTO_TEST_CASE(parse_normal_ids) { + CHECK_NORMALS("0.0.0", 0, 0, 0); + CHECK_PARSE_ERROR("1"); + CHECK_PARSE_ERROR("1.1"); } -BOOST_AUTO_TEST_CASE(parse_normal) { - auto r = parse("1.0.0"); - BOOST_CHECK_EQUAL(r.major, 1); - BOOST_CHECK_EQUAL(r.minor, 0); - BOOST_CHECK_EQUAL(r.patch, 0); - BOOST_CHECK_EQUAL(r.release, ""); - BOOST_CHECK_EQUAL(r.build, ""); +// normal versions must be non negative integers +BOOST_AUTO_TEST_CASE(parse_normal_positive_ints) { + CHECK_NORMALS("1.2.3", 1, 2, 3); + CHECK_NORMALS("65535.65534.65533", 65535, 65534, 65533); + CHECK_PARSE_ERROR("-1.0.0"); + CHECK_PARSE_ERROR("1.-1.0"); + CHECK_PARSE_ERROR("1.1.-1"); + CHECK_PARSE_ERROR("a.0.0"); + CHECK_PARSE_ERROR("1.a.0"); + CHECK_PARSE_ERROR("1.0.a"); } -BOOST_AUTO_TEST_CASE(parse_normal_bad_format_1) { - BOOST_CHECK_THROW(parse("bad.version"), Parse_error); +// normal versions must not have leading 0 +BOOST_AUTO_TEST_CASE(parse_normal_leading_0) { + CHECK_PARSE_ERROR("01.0.0"); + CHECK_PARSE_ERROR("1.01.0"); + CHECK_PARSE_ERROR("1.0.01"); } -BOOST_AUTO_TEST_CASE(parse_normal_bad_format_2) { - BOOST_CHECK_THROW(parse("1.2.3.4"), Parse_error); +// prerel contains one or more dot-separated ids with distinct numeric and mixed ids +BOOST_AUTO_TEST_CASE(parse_prerel_ids_types) { + CHECK_PREREL("1.2.3-test", 1, 2, 3, Prerelease_identifiers({ {"test", A} })); + CHECK_PREREL("1.2.3-321", 1, 2, 3, Prerelease_identifiers({ {"321", N} })); + CHECK_PREREL("1.2.3-test.1", 1, 2, 3, Prerelease_identifiers({ {"test", A},{"1",N} })); + CHECK_PREREL("1.2.3-1.test", 1, 2, 3, Prerelease_identifiers({ {"1", N},{"test", A} })); + CHECK_PREREL("1.2.3-test.123456", 1, 2, 3, Prerelease_identifiers({ {"test", A},{"123456", N} })); + CHECK_PREREL("1.2.3-123456.test", 1, 2, 3, Prerelease_identifiers({ {"123456", N},{"test", A} })); + CHECK_PREREL("1.2.3-1.a.22.bb.333.ccc.4444.dddd.55555.fffff", 1, 2, 3, Prerelease_identifiers({ {"1", N},{"a", A}, + {"22", N},{"bb", A},{"333", N },{"ccc", A} ,{"4444", N},{"dddd", A} ,{"55555", N},{"fffff", A} })); } -BOOST_AUTO_TEST_CASE(parse_prerel_single) { - auto r = parse("1.0.0-prerelease"); - BOOST_CHECK_EQUAL(r.major, 1); - BOOST_CHECK_EQUAL(r.minor, 0); - BOOST_CHECK_EQUAL(r.patch, 0); - BOOST_CHECK_EQUAL(r.release, "prerelease"); - BOOST_CHECK_EQUAL(r.build, ""); +// prerel ids contain only alphanumerics and hyphen +BOOST_AUTO_TEST_CASE(parse_prerel_legal_chars) { + CHECK_PREREL("1.2.3-test-1-2-3-CAP", 1, 2, 3, Prerelease_identifiers({ { "test-1-2-3-CAP", A } })); + CHECK_PARSE_ERROR("1.2.3-test#1"); + CHECK_PARSE_ERROR("1.2.3-test.©2015"); + CHECK_PARSE_ERROR("1.2.3-????-????-1"); } -BOOST_AUTO_TEST_CASE(parse_prerel_ok_multiple_1) { - auto r = parse("1.0.0-prerelease.rel-1"); - BOOST_CHECK_EQUAL(r.major, 1); - BOOST_CHECK_EQUAL(r.minor, 0); - BOOST_CHECK_EQUAL(r.patch, 0); - BOOST_CHECK_EQUAL(r.release, "prerelease.rel-1"); - BOOST_CHECK_EQUAL(r.build, ""); +// prerel ids may not be empty +BOOST_AUTO_TEST_CASE(parse_prerel_empty_ids) { + CHECK_PARSE_ERROR("1.2.3-"); + CHECK_PARSE_ERROR("1.2.3-test."); + CHECK_PARSE_ERROR("1.2.3-test.."); + CHECK_PARSE_ERROR("1.2.3-test..1"); } -BOOST_AUTO_TEST_CASE(parse_prerel_ok_multiple_2) { - auto r = parse("1.0.0-prerelease.rel-1.b.c.f"); - BOOST_CHECK_EQUAL(r.major, 1); - BOOST_CHECK_EQUAL(r.minor, 0); - BOOST_CHECK_EQUAL(r.patch, 0); - BOOST_CHECK_EQUAL(r.release, "prerelease.rel-1.b.c.f"); - BOOST_CHECK_EQUAL(r.build, ""); +// prerel numeric ids must not have leading 0 +BOOST_AUTO_TEST_CASE(parse_prerel_num_ids_no_leading_0) { + CHECK_PARSE_ERROR("1.2.3-01"); + CHECK_PARSE_ERROR("1.2.3-test.0023"); + CHECK_PREREL("1.2.3-test.01a", 1, 2, 3, Prerelease_identifiers({ { "test", A },{ "01a", A } })); + CHECK_PREREL("1.2.3-test.01-s", 1, 2, 3, Prerelease_identifiers({ { "test", A },{ "01-s", A } })); } -BOOST_AUTO_TEST_CASE(parse_normal_build_single) { - auto r = parse("1.0.0+test"); - BOOST_CHECK_EQUAL(r.major, 1); - BOOST_CHECK_EQUAL(r.minor, 0); - BOOST_CHECK_EQUAL(r.patch, 0); - BOOST_CHECK_EQUAL(r.release, ""); - BOOST_CHECK_EQUAL(r.build, "test"); +// build contains one or more dot separated ids +BOOST_AUTO_TEST_CASE(parse_build_ids) { + CHECK_BUILD("1.2.3+test", 1, 2, 3, Build_identifiers({ "test" })); + CHECK_BUILD("1.2.3+321", 1, 2, 3, Build_identifiers({ "321" })); + CHECK_BUILD("1.2.3+test.1", 1, 2, 3, Build_identifiers({ "test","1" })); + CHECK_BUILD("1.2.3+1.test", 1, 2, 3, Build_identifiers({ "1","test" })); + CHECK_BUILD("1.2.3+test.123456", 1, 2, 3, Build_identifiers({ "test","123456" })); + CHECK_BUILD("1.2.3+123456.test", 1, 2, 3, Build_identifiers({ "123456","test" })); + CHECK_BUILD("1.2.3+1.a.22.bb.333.ccc.4444.dddd.55555.fffff", 1, 2, 3, Build_identifiers({ "1","a","22","bb", + "333","ccc","4444","dddd","55555","fffff" })); } -BOOST_AUTO_TEST_CASE(parse_normal_build_multiple) { - auto r = parse("1.0.0+test.123.4.5"); - BOOST_CHECK_EQUAL(r.major, 1); - BOOST_CHECK_EQUAL(r.minor, 0); - BOOST_CHECK_EQUAL(r.patch, 0); - BOOST_CHECK_EQUAL(r.release, ""); - BOOST_CHECK_EQUAL(r.build, "test.123.4.5"); +// build ids contain only alphanumerics and hyphen +BOOST_AUTO_TEST_CASE(parse_build_legal_chars) { + CHECK_BUILD("1.2.3+test-1-2-3-CAP", 1, 2, 3, Build_identifiers({ "test-1-2-3-CAP" })); + CHECK_PARSE_ERROR("1.2.3+test#1"); + CHECK_PARSE_ERROR("1.2.3+test.©2015"); + CHECK_PARSE_ERROR("1.2.3+????-????-1"); } -BOOST_AUTO_TEST_CASE(parse_normal_prerel_single_build_single) { - auto r = parse("1.0.0-prerel+test"); - BOOST_CHECK_EQUAL(r.major, 1); - BOOST_CHECK_EQUAL(r.minor, 0); - BOOST_CHECK_EQUAL(r.patch, 0); - BOOST_CHECK_EQUAL(r.release, "prerel"); - BOOST_CHECK_EQUAL(r.build, "test"); +// build ids may not be empty +BOOST_AUTO_TEST_CASE(parse_build_empty_ids) { + CHECK_PARSE_ERROR("1.2.3+"); + CHECK_PARSE_ERROR("1.2.3+test."); + CHECK_PARSE_ERROR("1.2.3+test.."); + CHECK_PARSE_ERROR("1.2.3+test..1"); } -BOOST_AUTO_TEST_CASE(parse_normal_prerel_single_build_multiple) { - auto r = parse("1.0.0-prerel+test.1.2.3"); - BOOST_CHECK_EQUAL(r.major, 1); - BOOST_CHECK_EQUAL(r.minor, 0); - BOOST_CHECK_EQUAL(r.patch, 0); - BOOST_CHECK_EQUAL(r.release, "prerel"); - BOOST_CHECK_EQUAL(r.build, "test.1.2.3"); +// optional prerel must come after patch and build after prerel +BOOST_AUTO_TEST_CASE(parse_prerel_build_order) { + CHECK_PREREL_BUILD("1.2.3-r4+b5", 1, 2, 3, Prerelease_identifiers({ {"r4",A} }), Build_identifiers({ "b5" })); + CHECK_PREREL_BUILD("1.2.3+b4-r5",1,2,3, no_rel_ids, Build_identifiers({ "b4-r5" })); } -BOOST_AUTO_TEST_CASE(parse_normal_prerel_multiple_build_single) { - auto r = parse("1.0.0-prerel.1.2.3+test"); - BOOST_CHECK_EQUAL(r.major, 1); - BOOST_CHECK_EQUAL(r.minor, 0); - BOOST_CHECK_EQUAL(r.patch, 0); - BOOST_CHECK_EQUAL(r.release, "prerel.1.2.3"); - BOOST_CHECK_EQUAL(r.build, "test"); -} - -BOOST_AUTO_TEST_CASE(parse_normal_prerel_multiple_build_multiple) { - auto r = parse("1.0.0-prerel.1.2.3+test.1.2.3"); - BOOST_CHECK_EQUAL(r.major, 1); - BOOST_CHECK_EQUAL(r.minor, 0); - BOOST_CHECK_EQUAL(r.patch, 0); - BOOST_CHECK_EQUAL(r.release, "prerel.1.2.3"); - BOOST_CHECK_EQUAL(r.build, "test.1.2.3"); -} - -BOOST_AUTO_TEST_CASE(parse_normal_all_empty) { - BOOST_CHECK_THROW(parse(".."), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_normal_major_empty) { - BOOST_CHECK_THROW(parse(".0.0"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_normal_minor_empty) { - BOOST_CHECK_THROW(parse("1..0"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_normal_patch_empty) { - BOOST_CHECK_THROW(parse("1.1."), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_normal_major_with_letters) { - BOOST_CHECK_THROW(parse("1a.0.0"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_normal_minor_with_letters) { - BOOST_CHECK_THROW(parse("1.0a.0"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_normal_patch_with_letters) { - BOOST_CHECK_THROW(parse("1.0.0a"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_normal_major_leading_0) { - BOOST_CHECK_THROW(parse("01.0.0"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_normal_minor_leading_0) { - BOOST_CHECK_THROW(parse("1.00.0"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_normal_patch_leading_0) { - BOOST_CHECK_THROW(parse("1.0.00"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_normal_major_negative) { - BOOST_CHECK_THROW(parse("-1.0.0"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_normal_minor_negative) { - BOOST_CHECK_THROW(parse("1.-1.0"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_normal_patch_negative) { - BOOST_CHECK_THROW(parse("1.0.-1"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_prerel_multiple) { - BOOST_CHECK_NO_THROW(parse("1.0.0-pre-rel.123.test.deadbeef.31415")); -} - -BOOST_AUTO_TEST_CASE(parse_prerel_invalid_chars) { - BOOST_CHECK_THROW(parse("1.0.0-prerelease#1"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_prerel_empty_id) { - BOOST_CHECK_THROW(parse("1.0.0-prerel..test"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_prerel_num_with_leading_0) { - BOOST_CHECK_THROW(parse("1.0.0-prerel.01"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_build_single_id) { - BOOST_CHECK_NO_THROW(parse("1.0.0+build")); -} - -BOOST_AUTO_TEST_CASE(parse_build_multiple_id) { - BOOST_CHECK_NO_THROW(parse("1.0.0+build.123.test.deadbeef.31415")); -} - -BOOST_AUTO_TEST_CASE(parse_build_invalid_chars) { - BOOST_CHECK_THROW(parse("1.0.0+build#1"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_build_empty_id) { - BOOST_CHECK_THROW(parse("1.0.0+build..test"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_prerel_single_build_single) { - BOOST_CHECK_NO_THROW(parse("1.0.0-prerelease+build")); -} - -BOOST_AUTO_TEST_CASE(parse_prerel_multiple_build_single) { - BOOST_CHECK_NO_THROW(parse("1.0.0-pre-rel.123.test.deadbeef.31415+build")); -} - -BOOST_AUTO_TEST_CASE(parse_prerel_invalid_chars_build_single) { - BOOST_CHECK_THROW(parse("1.0.0-prerelease#1+build"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_prerel_empty_id_build_single) { - BOOST_CHECK_THROW(parse("1.0.0-prerel..test+build"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_prerel_num_with_leading_0_build_single) { - BOOST_CHECK_THROW(parse("1.0.0-prerel.01+build"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_prerel_single_build_multiple) { - BOOST_CHECK_NO_THROW(parse("1.0.0-prerelease+build.1.2.3")); -} - -BOOST_AUTO_TEST_CASE(parse_prerel_multiple_build_multiple) { - BOOST_CHECK_NO_THROW(parse("1.0.0-pre-rel.123.test.deadbeef.31415+build.1.2.3")); -} - -BOOST_AUTO_TEST_CASE(parse_prerel_invalid_chars_build_multiple) { - BOOST_CHECK_THROW(parse("1.0.0-prerelease#1+build.1.2.3"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_prerel_empty_id_build_multiple) { - BOOST_CHECK_THROW(parse("1.0.0-prerel..test+build.1.2.3"), Parse_error); -} - -BOOST_AUTO_TEST_CASE(parse_prerel_num_with_leading_0_build_multiple) { - BOOST_CHECK_THROW(parse("1.0.0-prerel.01+build.1.2.3"), Parse_error); +// check some corner cases +BOOST_AUTO_TEST_CASE(parse_corner_cases) { + CHECK_PARSE_ERROR("1.2.3-r4.+b5"); + CHECK_PARSE_ERROR("1.2.3-r4+b5."); } diff --git a/test/semver200_parser_util.h b/test/semver200_parser_util.h new file mode 100644 index 0000000..25f4e32 --- /dev/null +++ b/test/semver200_parser_util.h @@ -0,0 +1,98 @@ +/* +The MIT License (MIT) + +Copyright (c) 2015 Marko Zivanovic + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once + +#include +#include +#include "version.h" + +version::Semver200_parser p; +const version::Prerelease_identifiers no_rel_ids; +const version::Build_identifiers no_build_ids; + +#define N Identifier_type::num +#define A Identifier_type::alnum + +namespace boost { + namespace test_tools { + namespace tt_detail { + template<> + void print_log_value::operator()(std::ostream& os, + const version::Build_identifiers& ids) { + for (const auto& id : ids) { + os << id << ","; + } + } + + template<> + void print_log_value::operator()(std::ostream& os, + const version::Prerelease_identifiers& ids) { + for (const auto& id : ids) { + os << id.first << "|" << static_cast(id.second) << "|" << ","; + } + } + } + } +} + +#define CHECK_NORMALS(VER, MAJOR, MINOR, PATCH) { \ + auto v = p.parse(VER); \ + BOOST_CHECK_EQUAL(v.major, MAJOR); \ + BOOST_CHECK_EQUAL(v.minor, MINOR); \ + BOOST_CHECK_EQUAL(v.patch, PATCH); \ + BOOST_CHECK_EQUAL(v.prerelease_ids, no_rel_ids); \ + BOOST_CHECK_EQUAL(v.build_ids, no_build_ids); \ +} + +#define CHECK_PARSE_ERROR(VER) { \ + BOOST_CHECK_THROW(p.parse(VER), version::Parse_error); \ +} + +#define CHECK_PREREL(VER, MAJOR, MINOR, PATCH, IDS) { \ + auto v = p.parse(VER); \ + BOOST_CHECK_EQUAL(v.major, MAJOR); \ + BOOST_CHECK_EQUAL(v.minor, MINOR); \ + BOOST_CHECK_EQUAL(v.patch, PATCH); \ + BOOST_CHECK_EQUAL(v.prerelease_ids, IDS); \ + BOOST_CHECK_EQUAL(v.build_ids, no_build_ids); \ +} + +#define CHECK_BUILD(VER, MAJOR, MINOR, PATCH, IDS) { \ + auto v = p.parse(VER); \ + BOOST_CHECK_EQUAL(v.major, MAJOR); \ + BOOST_CHECK_EQUAL(v.minor, MINOR); \ + BOOST_CHECK_EQUAL(v.patch, PATCH); \ + BOOST_CHECK_EQUAL(v.prerelease_ids, no_rel_ids); \ + BOOST_CHECK_EQUAL(v.build_ids, IDS); \ +} + +#define CHECK_PREREL_BUILD(VER, MAJOR, MINOR, PATCH, RIDS, BIDS) { \ + auto v = p.parse(VER); \ + BOOST_CHECK_EQUAL(v.major, MAJOR); \ + BOOST_CHECK_EQUAL(v.minor, MINOR); \ + BOOST_CHECK_EQUAL(v.patch, PATCH); \ + BOOST_CHECK_EQUAL(v.prerelease_ids, RIDS); \ + BOOST_CHECK_EQUAL(v.build_ids, BIDS); \ +}