From 284aa77e135a6f43cbf4622001bcf86297a36d97 Mon Sep 17 00:00:00 2001 From: Marko Zivanovic Date: Thu, 8 Oct 2015 23:27:01 +0200 Subject: [PATCH] Implement relational operators on Basic_version class; Improve documentation; Fix bug in parser regarding prerelease token treatment on transition between prerelease and build parsing; Add unit tests for missing cases --- CMakeLists.txt | 1 + include/version.h | 88 +++++++++++++++++++++++------ src/CMakeLists.txt | 2 +- src/Semver200_comparator.cpp | 2 +- src/Semver200_parser.cpp | 4 +- test/CMakeLists.txt | 7 ++- test/semver200_comparator_tests.cpp | 4 +- test/semver200_parser_tests.cpp | 6 +- test/semver200_validator_tests.cpp | 31 ---------- test/semver200_version_tests.cpp | 67 ++++++++++++++++++++++ 10 files changed, 155 insertions(+), 57 deletions(-) delete mode 100644 test/semver200_validator_tests.cpp create mode 100644 test/semver200_version_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f9c6ece..015e568 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,3 +37,4 @@ add_subdirectory(test) enable_testing() add_test(NAME semver200_parser_tests COMMAND semver200_parser_tests) add_test(NAME semver200_comparator_tests COMMAND semver200_comparator_tests) +add_test(NAME semver200_version_tests COMMAND semver200_version_tests) diff --git a/include/version.h b/include/version.h index fed2982..b664250 100644 --- a/include/version.h +++ b/include/version.h @@ -24,11 +24,17 @@ SOFTWARE. #pragma once +#include #include #include namespace version { + /// Parse_error is thrown on all parsing and validation errors. + class Parse_error : public std::runtime_error { + using std::runtime_error::runtime_error; + }; + /// Type of prerelease identifier: alphanumeric or numeric. /** Type of identifier affects comparison: alphanumeric identifiers are compared as ASCII strings, while @@ -69,23 +75,46 @@ namespace version { Build_identifiers build_ids; }; - /// Generic version class. Specializations are used to perform usefull work. + /// Generic version description and comparison class. /** - This class describes general version object without prescribing parsing, validation and comparison rules. - These rules are contained in Parser and Comparator objects provided. + Basic_version class describes general version object without prescribing parsing, + validation and comparison rules. These rules are implemented by supplied Parser and + Comparator parameters. */ template class Basic_version { public: /// Construct Basic_version object using Parser to parse version string and Comparator for comparison. - Basic_version(const std::string&, Parser, Comparator); + Basic_version(const std::string& v, Parser p, Comparator c) + : parser_{ p }, comparator_{ c }, ver_{ parser_.parse(v) } {}; - bool operator>(const Basic_version&); - bool operator>=(const Basic_version&); - bool operator<(const Basic_version&); - bool operator<=(const Basic_version&); - bool operator==(const Basic_version&); - bool operator!=(const Basic_version&); + /// Compare if left version object is less than the right. + friend bool operator<(const Basic_version& l, const Basic_version& r) { + return l.comparator_.compare(l.ver_, r.ver_) == -1; + } + /// Compare if two version objects are equal. + friend bool operator==(const Basic_version& l, const Basic_version& r) { + return l.comparator_.compare(l.ver_, r.ver_) == 0; + } + /// Output version object to stream using standard semver format (X.Y.Z-PR+B). + friend std::ostream& operator<<(std::ostream& os, const Basic_version& v) { + os << v.ver_.major << "." << v.ver_.minor << "." << v.ver_.patch; + if (!v.ver_.prerelease_ids.empty()) { + os << "-"; + for (auto& it = v.ver_.prerelease_ids.cbegin(); it < v.ver_.prerelease_ids.cend() - 1; ++it) { + os << it->first << "."; + } + os << v.ver_.prerelease_ids.crbegin()->first; + } + if (!v.ver_.build_ids.empty()) { + os << "+"; + for (auto& it = v.ver_.build_ids.cbegin(); it < v.ver_.build_ids.cend() - 1; ++it) { + os << *it << "."; + } + os << *v.ver_.build_ids.crbegin(); + } + return os; + } private: Parser parser_; @@ -95,20 +124,43 @@ namespace version { /// Parse string into Version_data structure according to semantic versioning 2.0.0 rules. struct Semver200_parser { - Version_data parse(const std::string&); + Version_data parse(const std::string&) const; }; /// Compare Version_data to another using semantic versioning 2.0.0 rules. struct Semver200_comparator { - int compare(const Version_data&, const Version_data&); + int compare(const Version_data&, const Version_data&) const; }; - /// Concrete Basic_version class that binds all semver 2.0.0 functionality together. - class Semver200 : public Basic_version {}; - - /// Parse_error is thrown on all parsing and validation errors. - class Parse_error : public std::runtime_error { - using std::runtime_error::runtime_error; + /// Concrete version class that binds all semver 2.0.0 functionality together. + class Semver200_version : public Basic_version { + public: + Semver200_version(const std::string& v) + : Basic_version{ v, Semver200_parser(), Semver200_comparator() } {} }; + /// Compare if two version objects are different. + template + inline bool operator!=(const Basic_version& l, const Basic_version& r) { + return !(l == r); + } + + /// Compare if left version object is greater than the right. + template + inline bool operator>(const Basic_version& l, const Basic_version& r) { + return r < l; + } + + /// Compare if left version object is greater than or equal the right. + template + inline bool operator>=(const Basic_version& l, const Basic_version& r) { + return !(l < r); + } + + /// Compare if left version object is less than or equal the right. + template + inline bool operator<=(const Basic_version& l, const Basic_version& r) { + return !(l > r); + } + } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 379d626..f02845b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,5 +3,5 @@ include_directories( ) add_library(semver - Semver200_parser.cpp Semver200_comparator.cpp + Semver200_comparator.cpp Semver200_parser.cpp ) diff --git a/src/Semver200_comparator.cpp b/src/Semver200_comparator.cpp index ad68bab..d84a0b3 100644 --- a/src/Semver200_comparator.cpp +++ b/src/Semver200_comparator.cpp @@ -61,7 +61,7 @@ namespace { namespace version { - int Semver200_comparator::compare(const Version_data& l, const Version_data& r) { + int Semver200_comparator::compare(const Version_data& l, const Version_data& r) const { int cmp = compare_normal(l, r); if (cmp != 0) return cmp; diff --git a/src/Semver200_parser.cpp b/src/Semver200_parser.cpp index 045fa8f..5a4d5fc 100644 --- a/src/Semver200_parser.cpp +++ b/src/Semver200_parser.cpp @@ -44,9 +44,9 @@ namespace { const vector& transitions, string& target, Validator validate) { for (const auto& t : transitions) { if (c == get<0>(t)) { + if (get<2>(t)) get<2>(t)(target); prev_phase = phase; phase = get<1>(t); - if (get<2>(t)) get<2>(t)(target); return; } } @@ -76,7 +76,7 @@ namespace { namespace version { - Version_data Semver200_parser::parse(const string& s) { + Version_data Semver200_parser::parse(const string& s) const { string major; string minor; string patch; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index afe37cc..97088b0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -2,7 +2,6 @@ set(Boost_USE_STATIC_LIBS ON) set(Boost_USE_MULTITHREADED ON) find_package(Boost COMPONENTS unit_test_framework) -#find_package(Threads) include_directories(../include) @@ -21,3 +20,9 @@ target_link_libraries(semver200_comparator_tests ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY} semver ) + +add_executable(semver200_version_tests semver200_version_tests.cpp) +target_link_libraries(semver200_version_tests + ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY} + semver +) diff --git a/test/semver200_comparator_tests.cpp b/test/semver200_comparator_tests.cpp index 7c2540a..399d731 100644 --- a/test/semver200_comparator_tests.cpp +++ b/test/semver200_comparator_tests.cpp @@ -38,9 +38,9 @@ inline int compare(const std::string& l, const std::string& r) { return c.compare(lv, rv); } -#define GT(L, R) BOOST_CHECK(compare(L, R) > 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 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) diff --git a/test/semver200_parser_tests.cpp b/test/semver200_parser_tests.cpp index 98763a8..0396666 100644 --- a/test/semver200_parser_tests.cpp +++ b/test/semver200_parser_tests.cpp @@ -122,11 +122,15 @@ BOOST_AUTO_TEST_CASE(parse_build_empty_ids) { // 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" })); + CHECK_PREREL_BUILD("1.2.3+b4-r5", 1, 2, 3, no_rel_ids, Build_identifiers({ "b4-r5" })); } // 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."); + + CHECK_PREREL_BUILD("1.2.3-alpha+build.314", 1, 2, 3, Prerelease_identifiers({ {"alpha", A} }), + Build_identifiers({ "build","314" })); + } diff --git a/test/semver200_validator_tests.cpp b/test/semver200_validator_tests.cpp deleted file mode 100644 index 54fb6b6..0000000 --- a/test/semver200_validator_tests.cpp +++ /dev/null @@ -1,31 +0,0 @@ -/* -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. -*/ - -#define BOOST_TEST_MODULE semver200_validator_tests - -#include -#include "version.h" - -using namespace version; - diff --git a/test/semver200_version_tests.cpp b/test/semver200_version_tests.cpp new file mode 100644 index 0000000..0bea638 --- /dev/null +++ b/test/semver200_version_tests.cpp @@ -0,0 +1,67 @@ +/* +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. +*/ + +#define BOOST_TEST_MODULE semver200_version_tests + +#include +#include "version.h" + +using namespace version; + +using v = Semver200_version; + +/// Check parsing logic by performing roundtrip - parse string to version object, then +/// generate string from that object and check if it's identical to source. +#define CHECK_RT(SRC) { \ +std::stringstream ss; \ +ss << v(SRC); \ +BOOST_CHECK_EQUAL(ss.str(), SRC); \ +} + +BOOST_AUTO_TEST_CASE(test_relational_operators) { + BOOST_CHECK(v("1.0.0-alpha") < v("1.0.0-alpha.1")); + BOOST_CHECK(v("1.0.0-alpha.1") < v("1.0.0-alpha.beta")); + BOOST_CHECK(v("1.0.0-alpha.beta") < v("1.0.0-beta")); + BOOST_CHECK(v("1.0.0-beta") < v("1.0.0-beta.2")); + BOOST_CHECK(v("1.0.0-beta.2") < v("1.0.0-beta.11")); + BOOST_CHECK(v("1.0.0-beta.11") < v("1.0.0-rc.1")); + BOOST_CHECK(v("1.0.0-rc.1") < v("1.0.0")); + + BOOST_CHECK(v("1.0.0+rc.1") == v("1.0.0+rc22")); + + BOOST_CHECK(v("1.0.0+rc.1") != v("1.0.0-rc22")); + + BOOST_CHECK(v("1.0.0") >= v("1.0.0")); + BOOST_CHECK(v("1.0.0") >= v("0.0.9")); +} + +BOOST_AUTO_TEST_CASE(test_ostream_output) { + CHECK_RT("1.2.3"); + CHECK_RT("1.2.3-alpha"); + CHECK_RT("1.2.3-alpha.1.2.3"); + CHECK_RT("1.2.3+build.1.2.3"); + CHECK_RT("1.2.3-alpha+build.314"); + CHECK_RT("1.2.3-alpha.1+build.314"); + CHECK_RT("1.2.3-alpha.1.2.3+build.314"); +} \ No newline at end of file