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 a56f77b
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 23 deletions.
137 changes: 114 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 @@ -174,18 +174,99 @@ 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();
}

// Without c++11's `decltype`, rely on function overloading returning different
// sized types that can be discriminated with `sizeof`.
struct No { char v[1]; }; // Will never be instantiated
struct Yes { char v[10]; }; // Will never be instantiated

template<typename T>
struct HasBasicContainerApi
{
static const T* c; // `*c` used like `decltype<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 Yes has(typename C::const_iterator *, typename C::value_type *,
size_t a = sizeof(++begin(*c) != end(*c)));
/// Lower priority overload will be picked if the previous one fails.
template<typename C>
static No has(...);
static const bool value = sizeof(has<T>(NULL, NULL)) == sizeof(Yes);
};

// 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 : No {};
template<class T>
struct IsSame<T, T> : Yes {};

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

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

// 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 `decltype<const *T>` 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 Container, class = typename
Detail::enable_if<Detail::IsContainer<Container>::value>::type,
// Additional template parameter to discriminate
// the two template toStringInternal overloads.
class = void>
std::string toStringInternal( Container 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 +328,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, class =
typename Detail::enable_if<!Detail::IsContainer<T>::value>::type>
std::string toStringInternal( T const& value ) {
return StringMaker<T>::convert( value );
}

} // end namespace Detail

/// \brief converts any type to a string
Expand All @@ -258,23 +349,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 );
for( ++first ; first != last ; ++first )
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 a56f77b

Please sign in to comment.