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 <utility>, <tuple> and <chrono> in common
path, unless requested.
This commit is contained in:
Martin Hořeňovský 2017-10-09 12:31:22 +02:00
parent f972732737
commit 79b405fd3f
12 changed files with 408 additions and 48 deletions

View File

@ -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

View File

@ -1,14 +1,15 @@
<a id="top"></a>
# 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)

View File

@ -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 `<tuple>` and `<utility>` 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

View File

@ -12,7 +12,6 @@
#include <sstream>
#include <vector>
#include <cstddef>
#include <tuple>
#include <type_traits>
#include <string>
@ -57,7 +56,7 @@ namespace Catch {
public:
static const bool value = decltype(test<std::ostream, const T&>(0))::value;
};
} // namespace Detail
// If we decide for C++14, change these to enable_if_ts
@ -241,7 +240,53 @@ namespace Catch {
}
};
// === Pair ===
template<typename T>
struct EnumStringMaker {
static std::string convert(const T& t) {
return ::Catch::Detail::stringify(static_cast<typename std::underlying_type<T>::type>(t));
}
};
#ifdef __OBJC__
template<>
struct StringMaker<NSString*> {
static std::string convert(NSString * nsstring) {
if (!nsstring)
return "nil";
return std::string("@") + [nsstring UTF8String];
}
};
template<>
struct StringMaker<NSObject*> {
static std::string convert(NSObject* nsObject) {
return ::Catch::Detail::stringify([nsObject description]);
}
};
namespace Detail {
inline std::string stringify( NSString* nsstring ) {
return StringMaker<NSString*>::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 <utility>
namespace Catch {
template<typename T1, typename T2>
struct StringMaker<std::pair<T1, T2> > {
static std::string convert(const std::pair<T1, T2>& 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 <tuple>
namespace Catch {
namespace Detail {
template<
typename Tuple,
@ -292,40 +341,126 @@ namespace Catch {
return os.str();
}
};
}
#endif // CATCH_CONFIG_ENABLE_TUPLE_STRINGMAKER
template<typename T>
struct EnumStringMaker {
static std::string convert(const T& t) {
return ::Catch::Detail::stringify(static_cast<typename std::underlying_type<T>::type>(t));
// Separate std::chrono::duration specialization
#if defined(CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER)
#include <chrono>
#include <ratio>
template <class Ratio>
struct ratio_string {
static std::string symbol();
};
template <class Ratio>
std::string ratio_string<Ratio>::symbol() {
std::ostringstream oss;
oss << '[' << Ratio::num << '/'
<< Ratio::den << ']';
return oss.str();
}
template <>
struct ratio_string<std::atto> {
static std::string symbol() { return "a"; }
};
template <>
struct ratio_string<std::femto> {
static std::string symbol() { return "f"; }
};
template <>
struct ratio_string<std::pico> {
static std::string symbol() { return "p"; }
};
template <>
struct ratio_string<std::nano> {
static std::string symbol() { return "n"; }
};
template <>
struct ratio_string<std::micro> {
static std::string symbol() { return "u"; }
};
template <>
struct ratio_string<std::milli> {
static std::string symbol() { return "m"; }
};
namespace Catch {
////////////
// std::chrono::duration specializations
template<typename Value, typename Ratio>
struct StringMaker<std::chrono::duration<Value, Ratio>> {
static std::string convert(std::chrono::duration<Value, Ratio> const& duration) {
std::ostringstream oss;
oss << duration.count() << ' ' << ratio_string<Ratio>::symbol() << 's';
return oss.str();
}
};
template<typename Value>
struct StringMaker<std::chrono::duration<Value, std::ratio<1>>> {
static std::string convert(std::chrono::duration<Value, std::ratio<1>> const& duration) {
std::ostringstream oss;
oss << duration.count() << " s";
return oss.str();
}
};
template<typename Value>
struct StringMaker<std::chrono::duration<Value, std::ratio<60>>> {
static std::string convert(std::chrono::duration<Value, std::ratio<60>> const& duration) {
std::ostringstream oss;
oss << duration.count() << " m";
return oss.str();
}
};
template<typename Value>
struct StringMaker<std::chrono::duration<Value, std::ratio<3600>>> {
static std::string convert(std::chrono::duration<Value, std::ratio<3600>> const& duration) {
std::ostringstream oss;
oss << duration.count() << " h";
return oss.str();
}
};
#ifdef __OBJC__
template<>
struct StringMaker<NSString*> {
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<system_clock>
template<typename Clock, typename Duration>
struct StringMaker<std::chrono::time_point<Clock, Duration>> {
static std::string convert(std::chrono::time_point<Clock, Duration> const& time_point) {
return ::Catch::Detail::stringify(time_point.time_since_epoch()) + " since epoch";
}
};
template<>
struct StringMaker<NSObject*> {
static std::string convert(NSObject* nsObject) {
return ::Catch::Detail::stringify([nsObject description]);
}
// std::chrono::time_point<system_clock> specialization
template<typename Duration>
struct StringMaker<std::chrono::time_point<std::chrono::system_clock, Duration>> {
static std::string convert(std::chrono::time_point<std::chrono::system_clock, Duration> 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<NSString*>::convert( nsstring );
}
}
#endif // CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER
} // namespace Detail
#endif // __OBJC__
} // namespace Catch
#ifdef _MSC_VER
#pragma warning(pop)

View File

@ -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

View File

@ -4613,6 +4613,84 @@ PASSED:
with expansion:
11 == 11
-------------------------------------------------------------------------------
Stringifying std::chrono::duration helpers
-------------------------------------------------------------------------------
ToStringChrono.cpp:<line number>
...............................................................................
ToStringChrono.cpp:<line number>:
PASSED:
REQUIRE( minute == seconds )
with expansion:
1 m == 60 s
ToStringChrono.cpp:<line number>:
PASSED:
REQUIRE( hour != seconds )
with expansion:
1 h != 60 s
ToStringChrono.cpp:<line number>:
PASSED:
REQUIRE( micro != milli )
with expansion:
1 us != 1 ms
ToStringChrono.cpp:<line number>:
PASSED:
REQUIRE( nano != micro )
with expansion:
1 ns != 1 us
-------------------------------------------------------------------------------
Stringifying std::chrono::duration with weird ratios
-------------------------------------------------------------------------------
ToStringChrono.cpp:<line number>
...............................................................................
ToStringChrono.cpp:<line number>:
PASSED:
REQUIRE( half_minute != femto_second )
with expansion:
1 [30/1]s != 1 fs
-------------------------------------------------------------------------------
Stringifying std::chrono::time_point<Clock>
-------------------------------------------------------------------------------
ToStringChrono.cpp:<line number>
...............................................................................
ToStringChrono.cpp:<line number>:
PASSED:
REQUIRE( now != later )
with expansion:
{iso8601-timestamp}
!=
{iso8601-timestamp}
ToStringChrono.cpp:<line number>:
PASSED:
REQUIRE( now2 != later2 )
with expansion:
{since-epoch-report}
!=
{since-epoch-report}
-------------------------------------------------------------------------------
Stringifying std::chrono::time_point<system_clock>
-------------------------------------------------------------------------------
ToStringChrono.cpp:<line number>
...............................................................................
ToStringChrono.cpp:<line number>:
PASSED:
REQUIRE( now != later )
with expansion:
{iso8601-timestamp}
!=
{iso8601-timestamp}
-------------------------------------------------------------------------------
Tabs and newlines show in output
-------------------------------------------------------------------------------
@ -7434,6 +7512,6 @@ MiscTests.cpp:<line number>:
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

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuitesloose text artifact
>
<testsuite name="<exe-name>" errors="15" failures="85" tests="878" hostname="tbd" time="{duration}" timestamp="{iso8601-timestamp}">
<testsuite name="<exe-name>" errors="15" failures="85" tests="886" hostname="tbd" time="{duration}" timestamp="{iso8601-timestamp}">
<testcase classname="<exe-name>.global" name="# A test name that starts with a #" time="{duration}"/>
<testcase classname="<exe-name>.global" name="#1005: Comparing pointer to int and long (NULL can be either on various systems)" time="{duration}"/>
<testcase classname="<exe-name>.global" name="#748 - captures with unexpected exceptions/outside assertions" time="{duration}">
@ -519,6 +519,10 @@ StringRef.tests.cpp:<line number>
<testcase classname="<exe-name>.global" name="StringRef/to std::string/implicitly constructed" time="{duration}"/>
<testcase classname="<exe-name>.global" name="StringRef/to std::string/explicitly constructed" time="{duration}"/>
<testcase classname="<exe-name>.global" name="StringRef/to std::string/assigned" time="{duration}"/>
<testcase classname="<exe-name>.global" name="Stringifying std::chrono::duration helpers" time="{duration}"/>
<testcase classname="<exe-name>.global" name="Stringifying std::chrono::duration with weird ratios" time="{duration}"/>
<testcase classname="<exe-name>.global" name="Stringifying std::chrono::time_point&lt;Clock>" time="{duration}"/>
<testcase classname="<exe-name>.global" name="Stringifying std::chrono::time_point&lt;system_clock>" time="{duration}"/>
<testcase classname="<exe-name>.global" name="Tabs and newlines show in output" time="{duration}">
<failure message="&quot;if ($b == 10) {
$a = 20;

View File

@ -5250,6 +5250,88 @@ Message from section two
</Section>
<OverallResult success="false"/>
</TestCase>
<TestCase name="Stringifying std::chrono::duration helpers" tags="[chrono][toString]" filename="projects/<exe-name>/ToStringChrono.cpp" >
<Expression success="true" type="REQUIRE" filename="projects/<exe-name>/ToStringChrono.cpp" >
<Original>
minute == seconds
</Original>
<Expanded>
1 m == 60 s
</Expanded>
</Expression>
<Expression success="true" type="REQUIRE" filename="projects/<exe-name>/ToStringChrono.cpp" >
<Original>
hour != seconds
</Original>
<Expanded>
1 h != 60 s
</Expanded>
</Expression>
<Expression success="true" type="REQUIRE" filename="projects/<exe-name>/ToStringChrono.cpp" >
<Original>
micro != milli
</Original>
<Expanded>
1 us != 1 ms
</Expanded>
</Expression>
<Expression success="true" type="REQUIRE" filename="projects/<exe-name>/ToStringChrono.cpp" >
<Original>
nano != micro
</Original>
<Expanded>
1 ns != 1 us
</Expanded>
</Expression>
<OverallResult success="true"/>
</TestCase>
<TestCase name="Stringifying std::chrono::duration with weird ratios" tags="[chrono][toString]" filename="projects/<exe-name>/ToStringChrono.cpp" >
<Expression success="true" type="REQUIRE" filename="projects/<exe-name>/ToStringChrono.cpp" >
<Original>
half_minute != femto_second
</Original>
<Expanded>
1 [30/1]s != 1 fs
</Expanded>
</Expression>
<OverallResult success="true"/>
</TestCase>
<TestCase name="Stringifying std::chrono::time_point&lt;Clock>" tags="[chrono][toString]" filename="projects/<exe-name>/ToStringChrono.cpp" >
<Expression success="true" type="REQUIRE" filename="projects/<exe-name>/ToStringChrono.cpp" >
<Original>
now != later
</Original>
<Expanded>
{iso8601-timestamp}
!=
{iso8601-timestamp}
</Expanded>
</Expression>
<Expression success="true" type="REQUIRE" filename="projects/<exe-name>/ToStringChrono.cpp" >
<Original>
now2 != later2
</Original>
<Expanded>
{since-epoch-report}
!=
{since-epoch-report}
</Expanded>
</Expression>
<OverallResult success="true"/>
</TestCase>
<TestCase name="Stringifying std::chrono::time_point&lt;system_clock>" tags="[chrono][toString]" filename="projects/<exe-name>/ToStringChrono.cpp" >
<Expression success="true" type="REQUIRE" filename="projects/<exe-name>/ToStringChrono.cpp" >
<Original>
now != later
</Original>
<Expanded>
{iso8601-timestamp}
!=
{iso8601-timestamp}
</Expanded>
</Expression>
<OverallResult success="true"/>
</TestCase>
<TestCase name="Tabs and newlines show in output" tags="[.][failing][whitespace]" filename="projects/<exe-name>/MiscTests.cpp" >
<Expression success="false" type="CHECK" filename="projects/<exe-name>/MiscTests.cpp" >
<Original>
@ -8209,7 +8291,7 @@ loose text artifact
</Section>
<OverallResult success="true"/>
</TestCase>
<OverallResults successes="757" failures="100" expectedFailures="21"/>
<OverallResults successes="765" failures="100" expectedFailures="21"/>
</Group>
<OverallResults successes="757" failures="99" expectedFailures="21"/>
<OverallResults successes="765" failures="99" expectedFailures="21"/>
</Catch>

View File

@ -0,0 +1,41 @@
#define CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER
#include "catch.hpp"
#include <chrono>
#include <cstdint>
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<int64_t, std::ratio<30>> half_minute(1);
std::chrono::duration<int64_t, std::ratio<1, 1000000000000000>> femto_second(1);
REQUIRE(half_minute != femto_second);
}
TEST_CASE("Stringifying std::chrono::time_point<system_clock>", "[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<Clock>", "[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);
}

View File

@ -1,3 +1,4 @@
#define CATCH_CONFIG_ENABLE_PAIR_STRINGMAKER
#include "catch.hpp"
TEST_CASE( "std::pair<int,std::string> -> toString", "[toString][pair]" ) {

View File

@ -1,3 +1,4 @@
#define CATCH_CONFIG_ENABLE_TUPLE_STRINGMAKER
#include "catch.hpp"
#include <tuple>

View File

@ -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