From 79b405fd3f752bd61a077d03798001c4e9d711b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ho=C5=99e=C5=88ovsk=C3=BD?= Date: Mon, 9 Oct 2017 12:31:22 +0200 Subject: [PATCH] Add stringification for std::chrono::{duration,time_point} Also hides std::chrono, std::pair and std::chrono::* behind new configuration macros, CATCH_CONFIG_ENABLE_*_STRINGMAKER to avoid dragging in , and in common path, unless requested. --- CMakeLists.txt | 1 + docs/configuration.md | 28 ++- docs/release-notes.md | 7 +- include/internal/catch_tostring.h | 193 +++++++++++++++--- .../Baselines/console.std.approved.txt | 4 +- .../Baselines/console.sw.approved.txt | 82 +++++++- .../SelfTest/Baselines/junit.sw.approved.txt | 6 +- .../SelfTest/Baselines/xml.sw.approved.txt | 86 +++++++- projects/SelfTest/ToStringChrono.cpp | 41 ++++ projects/SelfTest/ToStringPair.cpp | 1 + projects/SelfTest/ToStringTuple.cpp | 1 + scripts/approvalTests.py | 6 +- 12 files changed, 408 insertions(+), 48 deletions(-) create mode 100644 projects/SelfTest/ToStringChrono.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c8a17f18..17248cfa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,7 @@ set(TEST_SOURCES ${SELF_TEST_DIR}/PartTrackerTests.cpp ${SELF_TEST_DIR}/TagAliasTests.cpp ${SELF_TEST_DIR}/TestMain.cpp + ${SELF_TEST_DIR}/ToStringChrono.cpp ${SELF_TEST_DIR}/ToStringGeneralTests.cpp ${SELF_TEST_DIR}/ToStringPair.cpp ${SELF_TEST_DIR}/ToStringTuple.cpp diff --git a/docs/configuration.md b/docs/configuration.md index 937e6122..bbb27ac0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,14 +1,15 @@ # Compile-time configuration -**Contents** -[main()/ implementation](#main-implementation) -[Prefixing Catch macros](#prefixing-catch-macros) -[Terminal colour](#terminal-colour) -[Console width](#console-width) -[stdout](#stdout) -[Other toggles](#other-toggles) -[Windows header clutter](#windows-header-clutter) +**Contents** +[main()/ implementation](#main-implementation) +[Prefixing Catch macros](#prefixing-catch-macros) +[Terminal colour](#terminal-colour) +[Console width](#console-width) +[stdout](#stdout) +[Other toggles](#other-toggles) +[Windows header clutter](#windows-header-clutter) +[Enabling stringification](#enabling-stringification) Catch is designed to "just work" as much as possible. For most people the only configuration needed is telling Catch which source file should host all the implementation code (```CATCH_CONFIG_MAIN```). @@ -121,6 +122,17 @@ On Windows Catch includes `windows.h`. To minimize global namespace clutter in t CATCH_CONFIG_NO_NOMINMAX // Stops Catch from using NOMINMAX macro CATCH_CONFIG_NO_WIN32_LEAN_AND_MEAN // Stops Catch from using WIN32_LEAN_AND_MEAN macro + +## Enabling stringification + +By default, Catch does not stringify some types from the standard library. This is done to avoid dragging in various standard library headers by default. However, Catch does contain these and can be configured to provide them, using these macros: + + CATCH_CONFIG_ENABLE_PAIR_STRINGMAKER // Provide StringMaker specialization for std::pair + CATCH_CONFIG_ENABLE_TUPLE_STRINGMAKER // Provide StringMaker specialization for std::tuple + CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER // Provide StringMaker specialization for std::chrono::duration, std::chrono::timepoint + CATCH_CONFIG_ENABLE_ALL_STRINGMAKERS // Defines all of the above + + --- [Home](Readme.md#top) diff --git a/docs/release-notes.md b/docs/release-notes.md index 62faabdc..3c7a97ea 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,7 +22,9 @@ * Removed support for non-const comparison operators * Non-const comparison operators are an abomination that should not exist * They were breaking support for comparing function to function pointer - +* `std::pair` and `std::tuple` are no longer stringified by default + * This is done to avoid dragging in `` and `` headers in common path + * Their stringification can be enabled per-file via new configuration macros ## Improvements * Reporters and Listeners can be defined in files different from the main file @@ -53,7 +55,8 @@ * Reporters/Listeners are now notified of fatal errors * This means specific signals or structured exceptions * The Reporter/Listener interface provides default, empty, implementation to preserve backward compatibility - +* Stringification of `std::chrono::duration` and `std::chrono::time_point` is now supported + * Needs to be enabled by a per-file compile time configuration option ## Fixes * Don't use console colour if running in XCode diff --git a/include/internal/catch_tostring.h b/include/internal/catch_tostring.h index b5d31397..568232ba 100644 --- a/include/internal/catch_tostring.h +++ b/include/internal/catch_tostring.h @@ -12,7 +12,6 @@ #include #include #include -#include #include #include @@ -57,7 +56,7 @@ namespace Catch { public: static const bool value = decltype(test(0))::value; }; - + } // namespace Detail // If we decide for C++14, change these to enable_if_ts @@ -241,7 +240,53 @@ namespace Catch { } }; - // === Pair === + template + struct EnumStringMaker { + static std::string convert(const T& t) { + return ::Catch::Detail::stringify(static_cast::type>(t)); + } + }; + +#ifdef __OBJC__ + template<> + struct StringMaker { + static std::string convert(NSString * nsstring) { + if (!nsstring) + return "nil"; + return std::string("@") + [nsstring UTF8String]; + } + }; + template<> + struct StringMaker { + static std::string convert(NSObject* nsObject) { + return ::Catch::Detail::stringify([nsObject description]); + } + + }; + namespace Detail { + inline std::string stringify( NSString* nsstring ) { + return StringMaker::convert( nsstring ); + } + + } // namespace Detail +#endif // __OBJC__ + +} // namespace Catch + +////////////////////////////////////////////////////// +// Separate std-lib types stringification, so it can be selectively enabled +// This means that we do not bring in + +#if defined(CATCH_CONFIG_ENABLE_ALL_STRINGMAKERS) +# define CATCH_CONFIG_ENABLE_PAIR_STRINGMAKER +# define CATCH_CONFIG_ENABLE_TUPLE_STRINGMAKER +# define CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER +#endif + +// Separate std::pair specialization +#if defined(CATCH_CONFIG_ENABLE_PAIR_STRINGMAKER) +#include +namespace Catch { template struct StringMaker > { static std::string convert(const std::pair& pair) { @@ -254,9 +299,13 @@ namespace Catch { return oss.str(); } }; +} +#endif // CATCH_CONFIG_ENABLE_PAIR_STRINGMAKER - - +// Separate std::tuple specialization +#if defined(CATCH_CONFIG_ENABLE_TUPLE_STRINGMAKER) +#include +namespace Catch { namespace Detail { template< typename Tuple, @@ -292,40 +341,126 @@ namespace Catch { return os.str(); } }; +} +#endif // CATCH_CONFIG_ENABLE_TUPLE_STRINGMAKER - template - struct EnumStringMaker { - static std::string convert(const T& t) { - return ::Catch::Detail::stringify(static_cast::type>(t)); +// Separate std::chrono::duration specialization +#if defined(CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER) +#include + +#include + +template +struct ratio_string { + static std::string symbol(); +}; + +template +std::string ratio_string::symbol() { + std::ostringstream oss; + oss << '[' << Ratio::num << '/' + << Ratio::den << ']'; + return oss.str(); +} +template <> +struct ratio_string { + static std::string symbol() { return "a"; } +}; +template <> +struct ratio_string { + static std::string symbol() { return "f"; } +}; +template <> +struct ratio_string { + static std::string symbol() { return "p"; } +}; +template <> +struct ratio_string { + static std::string symbol() { return "n"; } +}; +template <> +struct ratio_string { + static std::string symbol() { return "u"; } +}; +template <> +struct ratio_string { + static std::string symbol() { return "m"; } +}; + +namespace Catch { + //////////// + // std::chrono::duration specializations + template + struct StringMaker> { + static std::string convert(std::chrono::duration const& duration) { + std::ostringstream oss; + oss << duration.count() << ' ' << ratio_string::symbol() << 's'; + return oss.str(); + } + }; + template + struct StringMaker>> { + static std::string convert(std::chrono::duration> const& duration) { + std::ostringstream oss; + oss << duration.count() << " s"; + return oss.str(); + } + }; + template + struct StringMaker>> { + static std::string convert(std::chrono::duration> const& duration) { + std::ostringstream oss; + oss << duration.count() << " m"; + return oss.str(); + } + }; + template + struct StringMaker>> { + static std::string convert(std::chrono::duration> const& duration) { + std::ostringstream oss; + oss << duration.count() << " h"; + return oss.str(); } }; -#ifdef __OBJC__ - template<> - struct StringMaker { - static std::string convert(NSString * nsstring) { - if (!nsstring) - return "nil"; - return std::string("@") + [nsstring UTF8String]; + //////////// + // std::chrono::time_point specialization + // Generic time_point cannot be specialized, only std::chrono::time_point + template + struct StringMaker> { + static std::string convert(std::chrono::time_point const& time_point) { + return ::Catch::Detail::stringify(time_point.time_since_epoch()) + " since epoch"; } }; - template<> - struct StringMaker { - static std::string convert(NSObject* nsObject) { - return ::Catch::Detail::stringify([nsObject description]); - } + // std::chrono::time_point specialization + template + struct StringMaker> { + static std::string convert(std::chrono::time_point const& time_point) { + auto converted = std::chrono::system_clock::to_time_t(time_point); +#ifdef _MSC_VER + std::tm timeInfo = {}; + gmtime_s(&timeInfo, &converted); +#else + std::tm* timeInfo = std::gmtime(&converted); +#endif + + auto const timeStampSize = sizeof("2017-01-16T17:06:45Z"); + char timeStamp[timeStampSize]; + const char * const fmt = "%Y-%m-%dT%H:%M:%SZ"; + +#ifdef _MSC_VER + std::strftime(timeStamp, timeStampSize, fmt, &timeInfo); +#else + std::strftime(timeStamp, timeStampSize, fmt, timeInfo); +#endif + return std::string(timeStamp); + } }; - namespace Detail { - inline std::string stringify( NSString* nsstring ) { - return StringMaker::convert( nsstring ); - } +} +#endif // CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER - } // namespace Detail -#endif // __OBJC__ - -} // namespace Catch #ifdef _MSC_VER #pragma warning(pop) diff --git a/projects/SelfTest/Baselines/console.std.approved.txt b/projects/SelfTest/Baselines/console.std.approved.txt index 9b2defd5..939d0d89 100644 --- a/projects/SelfTest/Baselines/console.std.approved.txt +++ b/projects/SelfTest/Baselines/console.std.approved.txt @@ -1003,6 +1003,6 @@ with expansion: "{?}" == "1" =============================================================================== -test cases: 176 | 125 passed | 47 failed | 4 failed as expected -assertions: 878 | 761 passed | 96 failed | 21 failed as expected +test cases: 180 | 129 passed | 47 failed | 4 failed as expected +assertions: 886 | 769 passed | 96 failed | 21 failed as expected diff --git a/projects/SelfTest/Baselines/console.sw.approved.txt b/projects/SelfTest/Baselines/console.sw.approved.txt index be582975..5441de43 100644 --- a/projects/SelfTest/Baselines/console.sw.approved.txt +++ b/projects/SelfTest/Baselines/console.sw.approved.txt @@ -4613,6 +4613,84 @@ PASSED: with expansion: 11 == 11 +------------------------------------------------------------------------------- +Stringifying std::chrono::duration helpers +------------------------------------------------------------------------------- +ToStringChrono.cpp: +............................................................................... + +ToStringChrono.cpp:: +PASSED: + REQUIRE( minute == seconds ) +with expansion: + 1 m == 60 s + +ToStringChrono.cpp:: +PASSED: + REQUIRE( hour != seconds ) +with expansion: + 1 h != 60 s + +ToStringChrono.cpp:: +PASSED: + REQUIRE( micro != milli ) +with expansion: + 1 us != 1 ms + +ToStringChrono.cpp:: +PASSED: + REQUIRE( nano != micro ) +with expansion: + 1 ns != 1 us + +------------------------------------------------------------------------------- +Stringifying std::chrono::duration with weird ratios +------------------------------------------------------------------------------- +ToStringChrono.cpp: +............................................................................... + +ToStringChrono.cpp:: +PASSED: + REQUIRE( half_minute != femto_second ) +with expansion: + 1 [30/1]s != 1 fs + +------------------------------------------------------------------------------- +Stringifying std::chrono::time_point +------------------------------------------------------------------------------- +ToStringChrono.cpp: +............................................................................... + +ToStringChrono.cpp:: +PASSED: + REQUIRE( now != later ) +with expansion: + {iso8601-timestamp} + != + {iso8601-timestamp} + +ToStringChrono.cpp:: +PASSED: + REQUIRE( now2 != later2 ) +with expansion: + {since-epoch-report} + != + {since-epoch-report} + +------------------------------------------------------------------------------- +Stringifying std::chrono::time_point +------------------------------------------------------------------------------- +ToStringChrono.cpp: +............................................................................... + +ToStringChrono.cpp:: +PASSED: + REQUIRE( now != later ) +with expansion: + {iso8601-timestamp} + != + {iso8601-timestamp} + ------------------------------------------------------------------------------- Tabs and newlines show in output ------------------------------------------------------------------------------- @@ -7434,6 +7512,6 @@ MiscTests.cpp:: PASSED: =============================================================================== -test cases: 176 | 123 passed | 49 failed | 4 failed as expected -assertions: 877 | 757 passed | 99 failed | 21 failed as expected +test cases: 180 | 127 passed | 49 failed | 4 failed as expected +assertions: 885 | 765 passed | 99 failed | 21 failed as expected diff --git a/projects/SelfTest/Baselines/junit.sw.approved.txt b/projects/SelfTest/Baselines/junit.sw.approved.txt index 9c0c8204..0dd6ab60 100644 --- a/projects/SelfTest/Baselines/junit.sw.approved.txt +++ b/projects/SelfTest/Baselines/junit.sw.approved.txt @@ -1,7 +1,7 @@ - + @@ -519,6 +519,10 @@ StringRef.tests.cpp: + + + + + + + + minute == seconds + + + 1 m == 60 s + + + + + hour != seconds + + + 1 h != 60 s + + + + + micro != milli + + + 1 us != 1 ms + + + + + nano != micro + + + 1 ns != 1 us + + + + + + + + half_minute != femto_second + + + 1 [30/1]s != 1 fs + + + + + + + + now != later + + + {iso8601-timestamp} +!= +{iso8601-timestamp} + + + + + now2 != later2 + + + {since-epoch-report} +!= +{since-epoch-report} + + + + + + + + now != later + + + {iso8601-timestamp} +!= +{iso8601-timestamp} + + + + @@ -8209,7 +8291,7 @@ loose text artifact - + - + diff --git a/projects/SelfTest/ToStringChrono.cpp b/projects/SelfTest/ToStringChrono.cpp new file mode 100644 index 00000000..1716ce64 --- /dev/null +++ b/projects/SelfTest/ToStringChrono.cpp @@ -0,0 +1,41 @@ +#define CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER +#include "catch.hpp" + +#include +#include + +TEST_CASE("Stringifying std::chrono::duration helpers", "[toString][chrono]") { + // No literals because we still support c++11 + auto hour = std::chrono::hours(1); + auto minute = std::chrono::minutes(1); + auto seconds = std::chrono::seconds(60); + auto micro = std::chrono::microseconds(1); + auto milli = std::chrono::milliseconds(1); + auto nano = std::chrono::nanoseconds(1); + REQUIRE(minute == seconds); + REQUIRE(hour != seconds); + REQUIRE(micro != milli); + REQUIRE(nano != micro); +} + +TEST_CASE("Stringifying std::chrono::duration with weird ratios", "[toString][chrono]") { + std::chrono::duration> half_minute(1); + std::chrono::duration> femto_second(1); + REQUIRE(half_minute != femto_second); +} + +TEST_CASE("Stringifying std::chrono::time_point", "[toString][chrono]") { + auto now = std::chrono::system_clock::now(); + auto later = now + std::chrono::minutes(2); + REQUIRE(now != later); +} + +TEST_CASE("Stringifying std::chrono::time_point", "[toString][chrono]") { + auto now = std::chrono::high_resolution_clock::now(); + auto later = now + std::chrono::minutes(2); + REQUIRE(now != later); + + auto now2 = std::chrono::steady_clock::now(); + auto later2 = now2 + std::chrono::minutes(2); + REQUIRE(now2 != later2); +} diff --git a/projects/SelfTest/ToStringPair.cpp b/projects/SelfTest/ToStringPair.cpp index 38f30854..1445c54f 100644 --- a/projects/SelfTest/ToStringPair.cpp +++ b/projects/SelfTest/ToStringPair.cpp @@ -1,3 +1,4 @@ +#define CATCH_CONFIG_ENABLE_PAIR_STRINGMAKER #include "catch.hpp" TEST_CASE( "std::pair -> toString", "[toString][pair]" ) { diff --git a/projects/SelfTest/ToStringTuple.cpp b/projects/SelfTest/ToStringTuple.cpp index 330e427e..fe19ce01 100644 --- a/projects/SelfTest/ToStringTuple.cpp +++ b/projects/SelfTest/ToStringTuple.cpp @@ -1,3 +1,4 @@ +#define CATCH_CONFIG_ENABLE_TUPLE_STRINGMAKER #include "catch.hpp" #include diff --git a/scripts/approvalTests.py b/scripts/approvalTests.py index 5c0c63ec..a963531b 100755 --- a/scripts/approvalTests.py +++ b/scripts/approvalTests.py @@ -24,7 +24,7 @@ filelocParser = re.compile(r''' lineNumberParser = re.compile(r' line="[0-9]*"') hexParser = re.compile(r'\b(0[xX][0-9a-fA-F]+)\b') durationsParser = re.compile(r' time="[0-9]*\.[0-9]*"') -timestampsParser = re.compile(r' timestamp="\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}Z"') +timestampsParser = re.compile(r'\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}Z') versionParser = re.compile(r'Catch v[0-9]+\.[0-9]+\.[0-9]+(-develop\.[0-9]+)?') nullParser = re.compile(r'\b(__null|nullptr)\b') exeNameParser = re.compile(r''' @@ -44,6 +44,7 @@ errnoParser = re.compile(r''' | \(\*_errno\(\)\) ''', re.VERBOSE) +sinceEpochParser = re.compile(r'\d+ .+ since epoch') if len(sys.argv) == 2: cmdPath = sys.argv[1] @@ -97,9 +98,10 @@ def filterLine(line): # strip durations and timestamps line = durationsParser.sub(' time="{duration}"', line) - line = timestampsParser.sub(' timestamp="{iso8601-timestamp}"', line) + line = timestampsParser.sub('{iso8601-timestamp}', line) line = specialCaseParser.sub('file:\g<1>', line) line = errnoParser.sub('errno', line) + line = sinceEpochParser.sub('{since-epoch-report}', line) return line