Skip to content

Commit

Permalink
Add support toString for containers
Browse files Browse the repository at this point in the history
Standard container types were printed as `"{?}"`
(default `toString` implementation for unsupported class).
This was contradictory with the documentation:
> "Most [...] std types are supported out of the box"
when in fact only `string`, `vector` and `tupple` were supported.

 - Renamed the `toStringVector.cpp` test file to `toStringContainers.cpp`

 - Types are consider containers if they contain `value_type` and
   `const_iterator` members and have `begin` and `end` support
   (members or ADL findable) returning a `const_iterator`.
   `const_iterator::operator*` must also return a `const value_type &`

 - Beware that a trying to printing a type fulfilling those requirements
   but returning invalid iterators will results in undefined behaviour. In
   such case specialize the Catch::Detail::IsContainer trait to contain
   `static const bool value = false` to revert to the default behaviour
   (printing `"{?}"`).

Test pretty printing of `std::list`, `std::deque`, `std::forward_list`,
`std::array` in Catch assertion macro. More complex structure like
`std::queue` or `std::multi_map` should also be tested.

Signed-off-by: mat tso <mat-tso@topmail.ie>
  • Loading branch information
mat tso committed Oct 7, 2016
1 parent 50e5643 commit 9fc751e
Show file tree
Hide file tree
Showing 2 changed files with 209 additions and 23 deletions.
127 changes: 104 additions & 23 deletions include/internal/catch_tostring.h
Expand Up @@ -13,8 +13,8 @@
#include <sstream>
#include <iomanip>
#include <limits>
#include <vector>
#include <cstddef>
#include <iterator>

#ifdef __OBJC__
#include "catch_objc_arc.hpp"
Expand Down Expand Up @@ -76,6 +76,8 @@ namespace Detail {
template<typename T> BorgType( T const& );
};

// Without c++11's `decltype`, rely on function overloading returning
// different sized types that can be discriminated with `sizeof`.
struct TrueType { char sizer[1]; };
struct FalseType { char sizer[2]; };

Expand Down Expand Up @@ -174,18 +176,87 @@ namespace Detail {
std::string rangeToString( InputIterator first, InputIterator last );
}

//template<typename T, typename Allocator>
//struct StringMaker<std::vector<T, Allocator> > {
// static std::string convert( std::vector<T,Allocator> const& v ) {
// return Detail::rangeToString( v.begin(), v.end() );
// }
//};
// Implementation detail for constant containers detection

template<typename T, typename Allocator>
std::string toString( std::vector<T,Allocator> const& v ) {
return Detail::rangeToString( v.begin(), v.end() );
namespace Detail {

/// C++11 enable_if
template<bool B, class T = void>
struct enable_if {};
template<class T>
struct enable_if<true, T> { typedef T type; };

/// C++11 std::begin
template <class C>
typename C::const_iterator begin( C& c ) { return c.begin(); }

/// C++11 std::end
template <class C>
typename C::const_iterator end( C& c ) { return c.end(); }

template<typename T>
struct HasBasicContainerApi
{
static const T& c; // `c` used like `declval<const T>` would be in C++11

/// This overload will be chosen if all the operations in it's signature
/// can be resolved at compile time.
template<typename C>
static TrueType has( typename C::const_iterator*, typename C::value_type*,
size_t = sizeof(++begin(c) != end(c)) );
/// Lower priority overload will be picked if the previous one fails.
template<typename C>
static FalseType has( ... );
static const bool value = sizeof(has<T>(NULL, NULL)) == sizeof(TrueType);
};

// Compile type function that returns true if a type matches
// the container concept (http://en.cppreference.com/w/cpp/concept/Container),
// false otherwise.
template <class C, bool = HasBasicContainerApi<C>::value>
struct IsContainer {
static const bool value = false; // By default a type is not a container
};

// Pre `decltype` trick to test the type of an expression
template<class T, class U>
struct IsSame : FalseType {};
template<class T>
struct IsSame<T, T> : TrueType {};

template<class Left, class Right>
IsSame<Left, Right> isOfType( const Right& );

#define INTERNAL_CATCH_IS_OF_TYPE( TYPE, VALUE ) \
(sizeof(isOfType<TYPE>(VALUE)) == sizeof(TrueType))

// Specialisation for types that have a basic container api
// that can now be checked for type inconsistency.
template <class C>
struct IsContainer<C, true> {
typedef typename C::value_type value_type;
typedef typename C::const_iterator const_iterator;

static const C& c; // `c` used like `declval<const C>` would be in C++11
static const bool value =
INTERNAL_CATCH_IS_OF_TYPE( const_iterator, begin(c) ) &&
INTERNAL_CATCH_IS_OF_TYPE( const_iterator, end(c) ) &&
INTERNAL_CATCH_IS_OF_TYPE( value_type, *begin(c) ) &&
INTERNAL_CATCH_IS_OF_TYPE( value_type, *end(c) );
};
#undef INTERNAL_CATCH_IS_OF_TYPE

/// Print containers by printing their elements in succession
template<typename C>
typename Detail::enable_if<Detail::IsContainer<C>::value, std::string>::type
toStringInternal( C const& c ) {
return Detail::rangeToString( c.begin(), c.end() );
}

} // end namespace Detail



// toString for pairs
template<typename T1, typename T2>
struct StringMaker<std::pair<T1,T2> > {
Expand Down Expand Up @@ -247,6 +318,16 @@ namespace Detail {
std::string makeString( T const& value ) {
return StringMaker<T>::convert( value );
}

/// Instead of adding complex template overloading of public toString method,
/// use an internal dispatcher which template can get as complicated as needed
/// without impacting the public api.
template<typename T>
typename Detail::enable_if<!Detail::IsContainer<T>::value, std::string>::type
toStringInternal( T const& value ) {
return StringMaker<T>::convert( value );
}

} // end namespace Detail

/// \brief converts any type to a string
Expand All @@ -258,23 +339,23 @@ namespace Detail {
/// to provide an ostream overload for.
template<typename T>
std::string toString( T const& value ) {
return StringMaker<T>::convert( value );
return Detail::toStringInternal(value);
}


namespace Detail {
template<typename InputIterator>
std::string rangeToString( InputIterator first, InputIterator last ) {
std::ostringstream oss;
oss << "{ ";
if( first != last ) {
oss << Catch::toString( *first );
for( ++first ; first != last ; ++first )
oss << ", " << Catch::toString( *first );
}
oss << " }";
return oss.str();
namespace Detail {
template<typename InputIterator>
std::string rangeToString( InputIterator first, InputIterator last ) {
std::ostringstream oss;
oss << "{ ";
if( first != last ) {
oss << Catch::toString( *first );
while( ++first != last )
oss << ", " << Catch::toString( *first );
}
oss << " }";
return oss.str();
}
}

} // end namespace Catch
Expand Down
105 changes: 105 additions & 0 deletions projects/SelfTest/ToStringContainers.cpp
Expand Up @@ -3,6 +3,11 @@
#include <deque>
#include <list>

#if defined(CATCH_CPP11_OR_GREATER)
#include <array>
#include <forward_list>
#endif

/// \file Test Catch::to_string for standard containors.

/// \brief Test for sequence containors
Expand Down Expand Up @@ -38,6 +43,22 @@ TEST_CASE( "vector<string> -> toString", "[toString][containers][vector]" ) {
SequenceTest<std::vector>::strings();
}

// deque
TEST_CASE( "deque<int> -> toString", "[toString][containers][deque]" ) {
SequenceTest<std::deque>::integers();
}
TEST_CASE( "deque<string> -> toString", "[toString][containers][deque]" ) {
SequenceTest<std::deque>::strings();
}

// list
TEST_CASE( "list<int> -> toString", "[toString][containers][list]" ) {
SequenceTest<std::list>::integers();
}
TEST_CASE( "list<string> -> toString", "[toString][containers][list]" ) {
SequenceTest<std::list>::strings();
}

namespace {
/** \brief Custom allocator, should not impact toString. */
template<typename T>
Expand All @@ -59,3 +80,87 @@ TEST_CASE( "vector<int,allocator> -> toString", "[toString][containers][vector][
TEST_CASE( "vector<string,allocator> -> toString", "[toString][containers][vector][allocator]" ) {
SequenceTest<std::vector, MinimalAllocator>::strings();
}

// deque
TEST_CASE( "deque<int,allocator> -> toString", "[toString][containers][deque][allocator]" ) {
SequenceTest<std::deque, MinimalAllocator>::integers();
}
TEST_CASE( "deque<string,allocator> -> toString", "[toString][containers][deque][allocator]" ) {
SequenceTest<std::deque, MinimalAllocator>::strings();
}

// list
TEST_CASE( "list<int,allocator> -> toString", "[toString][containers][list][allocator]" ) {
SequenceTest<std::list, MinimalAllocator>::integers();
}
TEST_CASE( "list<string,allocator> -> toString", "[toString][containers][list][allocator]" ) {
SequenceTest<std::list, MinimalAllocator>::strings();
}

#if defined(CATCH_CPP11_OR_GREATER)

#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wc++98-compat"
#endif

// array
TEST_CASE( "array<int, N> -> toString", "[toString][containers][array]" ) {
std::array<int, 0> empty;
REQUIRE( Catch::toString(empty) == "{ }" );
std::array<int, 1> oneValue = { 42 };
REQUIRE( Catch::toString(oneValue) == "{ 42 }" );
std::array<int, 2> twoValues = { 42, 250 };
REQUIRE( Catch::toString(twoValues) == "{ 42, 250 }" );
}

TEST_CASE( "array<string, N> -> toString", "[toString][containers][array]" ) {
std::array<std::string, 0> empty;
REQUIRE( Catch::toString(empty) == "{ }" );
std::array<std::string, 1> oneValue = { "hello" };
REQUIRE( Catch::toString(oneValue) == "{ \"hello\" }" );
std::array<std::string, 2> twoValues = { "hello", "world" };
REQUIRE( Catch::toString(twoValues) == "{ \"hello\", \"world\" }" );
}

/// \brief Specialization for `forward_list` to use
// `push_front` instead of the unsupported `push_back`.
template <template <class T> class Allocator>
struct SequenceTest<std::forward_list, Allocator> {
static void integers() {
std::forward_list<int, Allocator<int> > integers;
REQUIRE( Catch::toString(integers) == "{ }" );
integers.push_front( 42 );
REQUIRE( Catch::toString(integers) == "{ 42 }" );
integers.push_front( 250 );
REQUIRE( Catch::toString(integers) == "{ 250, 42 }" );
};

static void strings() {
std::forward_list<std::string, Allocator<std::string> > strings;
REQUIRE( Catch::toString(strings) == "{ }" );
strings.push_front( "hello" );
REQUIRE( Catch::toString(strings) == "{ \"hello\" }" );
strings.push_front( "world" );
REQUIRE( Catch::toString(strings) == "{ \"world\", \"hello\" }" );
}
};

TEST_CASE( "forward_list<int> -> toString", "[toString][containers][forward_list]" ) {
SequenceTest<std::forward_list>::integers();
}
TEST_CASE( "forward_list<string> -> toString", "[toString][containers][forward_list]" ) {
SequenceTest<std::forward_list>::strings();
}

TEST_CASE( "forward_list<int,allocator> -> toString", "[toString][containers][forward_list][allocator]" ) {
SequenceTest<std::forward_list, MinimalAllocator>::integers();
}
TEST_CASE( "forward_list<string,allocator> -> toString", "[toString][containers][forward_list][allocator]" ) {
SequenceTest<std::forward_list, MinimalAllocator>::strings();
}

#ifdef __clang__
#pragma clang diagnostic pop
#endif
#endif // CATCH_CPP11_OR_GREATER

0 comments on commit 9fc751e

Please sign in to comment.