diff --git a/docs/tostring.md b/docs/tostring.md index 933f2e61..60dc0663 100644 --- a/docs/tostring.md +++ b/docs/tostring.md @@ -6,6 +6,7 @@ [Catch::StringMaker specialisation](#catchstringmaker-specialisation)
[Catch::is_range specialisation](#catchis_range-specialisation)
[Exceptions](#exceptions)
+[Enums](#enums)
Catch needs to be able to convert types you use in assertions and logging expressions into strings (for logging and reporting purposes). Most built-in or std types are supported out of the box but there are two ways that you can tell Catch how to convert your own types (or other, third-party types) into strings. @@ -66,6 +67,44 @@ CATCH_TRANSLATE_EXCEPTION( MyType& ex ) { } ``` +## Enums + +Enums that already have a `<<` overload for `std::ostream` will convert to strings as expected. +If you only need to convert enums to strings for test reporting purposes you can provide a `StringMaker` specialisations as any other type. +However, as a convenience, Catch provides the `REGISTER_ENUM` helper macro that will generate the `StringMaker` specialiation for you with minimal code. +Simply provide it the (qualified) enum name, followed by all the enum values, and you're done! + +E.g. + +``` +enum class Fruits { Banana, Apple, Mango }; + +CATCH_REGISTER_ENUM( Fruits, Fruits::Banana, Fruits::Apple, Fruits::Mango ); + +TEST_CASE() { + REQUIRE( Fruits::Mango == Fruits::Apple ); +} +``` + +... or if the enum is in a namespace: +``` +namespace Bikeshed { + enum class Colours { Red, Green, Blue }; +} + +// Important!: This macro must appear at top level scope - not inside a namespace +// You can fully qualify the names, or use a using if you prefer +CATCH_REGISTER_ENUM( Bikeshed::Colours, + Bikeshed::Colours::Red, + Bikeshed::Colours::Green, + Bikeshed::Colours::Blue ); + +TEST_CASE() { + REQUIRE( Bikeshed::Colours::Red == Bikeshed::Colours::Blue ); +} +``` + + --- [Home](Readme.md#top) diff --git a/include/internal/catch_enum_values_registry.cpp b/include/internal/catch_enum_values_registry.cpp new file mode 100644 index 00000000..9e6f2ecb --- /dev/null +++ b/include/internal/catch_enum_values_registry.cpp @@ -0,0 +1,65 @@ +/* + * Created by Phil on 4/4/2019. + * Copyright 2019 Two Blue Cubes Ltd. All rights reserved. + * + * Distributed under the Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + */ +#include "catch_enum_values_registry.h" +#include "catch_string_manip.h" +#include "catch_stream.h" + +#include +#include + +namespace Catch { + + IMutableEnumValuesRegistry::~IMutableEnumValuesRegistry() {} + + namespace Detail { + + std::vector parseEnums( StringRef enums ) { + auto enumValues = splitStringRef( enums, ',' ); + std::vector parsed; + parsed.reserve( enumValues.size() ); + for( auto const& enumValue : enumValues ) { + auto identifiers = splitStringRef( enumValue, ':' ); + parsed.push_back( Catch::trim( identifiers.back() ) ); + } + return parsed; + } + + EnumInfo::~EnumInfo() {} + + StringRef EnumInfo::lookup( int value ) const { + for( auto const& valueToName : m_values ) { + if( valueToName.first == value ) + return valueToName.second; + } + return "{** unexpected enum value **}"; + } + + std::unique_ptr makeEnumInfo( StringRef enumName, StringRef allValueNames, std::vector const& values ) { + std::unique_ptr enumInfo( new EnumInfo ); + enumInfo->m_name = enumName; + enumInfo->m_values.reserve( values.size() ); + + const auto valueNames = Catch::Detail::parseEnums( allValueNames ); + assert( valueNames.size() == values.size() ); + std::size_t i = 0; + for( auto value : values ) + enumInfo->m_values.push_back({ value, valueNames[i++] }); + + return enumInfo; + } + + EnumInfo const& EnumValuesRegistry::registerEnum( StringRef enumName, StringRef allValueNames, std::vector const& values ) { + auto enumInfo = makeEnumInfo( enumName, allValueNames, values ); + EnumInfo* raw = enumInfo.get(); + m_enumInfos.push_back( std::move( enumInfo ) ); + return *raw; + } + + } // Detail +} // Catch + diff --git a/include/internal/catch_enum_values_registry.h b/include/internal/catch_enum_values_registry.h new file mode 100644 index 00000000..985687bd --- /dev/null +++ b/include/internal/catch_enum_values_registry.h @@ -0,0 +1,34 @@ +/* + * Created by Phil on 4/4/2019. + * Copyright 2019 Two Blue Cubes Ltd. All rights reserved. + * + * Distributed under the Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + */ +#ifndef TWOBLUECUBES_CATCH_ENUMVALUESREGISTRY_H_INCLUDED +#define TWOBLUECUBES_CATCH_ENUMVALUESREGISTRY_H_INCLUDED + +#include "catch_interfaces_enum_values_registry.h" + +#include + +namespace Catch { + + namespace Detail { + + std::unique_ptr makeEnumInfo( StringRef enumName, StringRef allValueNames, std::vector const& values ); + + class EnumValuesRegistry : public IMutableEnumValuesRegistry { + + std::vector> m_enumInfos; + + EnumInfo const& registerEnum( StringRef enumName, StringRef allEnums, std::vector const& values) override; + }; + + std::vector parseEnums( StringRef enums ); + + } // Detail + +} // Catch + +#endif //TWOBLUECUBES_CATCH_ENUMVALUESREGISTRY_H_INCLUDED \ No newline at end of file diff --git a/include/internal/catch_interfaces_enum_values_registry.h b/include/internal/catch_interfaces_enum_values_registry.h new file mode 100644 index 00000000..fea6847d --- /dev/null +++ b/include/internal/catch_interfaces_enum_values_registry.h @@ -0,0 +1,45 @@ +/* + * Created by Phil on 4/4/2019. + * Copyright 2019 Two Blue Cubes Ltd. All rights reserved. + * + * Distributed under the Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + */ +#ifndef TWOBLUECUBES_CATCH_INTERFACESENUMVALUESREGISTRY_H_INCLUDED +#define TWOBLUECUBES_CATCH_INTERFACESENUMVALUESREGISTRY_H_INCLUDED + +#include "catch_stringref.h" + +#include + +namespace Catch { + + namespace Detail { + struct EnumInfo { + StringRef m_name; + std::vector> m_values; + + ~EnumInfo(); + + StringRef lookup( int value ) const; + }; + } // namespace Detail + + struct IMutableEnumValuesRegistry { + virtual ~IMutableEnumValuesRegistry(); + + virtual Detail::EnumInfo const& registerEnum( StringRef enumName, StringRef allEnums, std::vector const& values ) = 0; + + template + Detail::EnumInfo const& registerEnum( StringRef enumName, StringRef allEnums, std::initializer_list values ) { + std::vector intValues; + intValues.reserve( values.size() ); + for( auto enumValue : values ) + intValues.push_back( static_cast( enumValue ) ); + return registerEnum( enumName, allEnums, intValues ); + } + }; + +} // Catch + +#endif //TWOBLUECUBES_CATCH_INTERFACESENUMVALUESREGISTRY_H_INCLUDED diff --git a/include/internal/catch_interfaces_registry_hub.h b/include/internal/catch_interfaces_registry_hub.h index 8e1da61e..19ffbf26 100644 --- a/include/internal/catch_interfaces_registry_hub.h +++ b/include/internal/catch_interfaces_registry_hub.h @@ -22,6 +22,8 @@ namespace Catch { struct IReporterRegistry; struct IReporterFactory; struct ITagAliasRegistry; + struct IMutableEnumValuesRegistry; + class StartupExceptionRegistry; using IReporterFactoryPtr = std::shared_ptr; @@ -32,7 +34,6 @@ namespace Catch { virtual IReporterRegistry const& getReporterRegistry() const = 0; virtual ITestCaseRegistry const& getTestCaseRegistry() const = 0; virtual ITagAliasRegistry const& getTagAliasRegistry() const = 0; - virtual IExceptionTranslatorRegistry const& getExceptionTranslatorRegistry() const = 0; @@ -47,6 +48,7 @@ namespace Catch { virtual void registerTranslator( const IExceptionTranslator* translator ) = 0; virtual void registerTagAlias( std::string const& alias, std::string const& tag, SourceLineInfo const& lineInfo ) = 0; virtual void registerStartupException() noexcept = 0; + virtual IMutableEnumValuesRegistry& getMutableEnumValuesRegistry() = 0; }; IRegistryHub const& getRegistryHub(); diff --git a/include/internal/catch_registry_hub.cpp b/include/internal/catch_registry_hub.cpp index a5062398..8a3c7a97 100644 --- a/include/internal/catch_registry_hub.cpp +++ b/include/internal/catch_registry_hub.cpp @@ -15,6 +15,7 @@ #include "catch_tag_alias_registry.h" #include "catch_startup_exception_registry.h" #include "catch_singletons.hpp" +#include "catch_enum_values_registry.h" namespace Catch { @@ -60,6 +61,9 @@ namespace Catch { void registerStartupException() noexcept override { m_exceptionRegistry.add(std::current_exception()); } + IMutableEnumValuesRegistry& getMutableEnumValuesRegistry() override { + return m_enumValuesRegistry; + } private: TestRegistry m_testCaseRegistry; @@ -67,6 +71,7 @@ namespace Catch { ExceptionTranslatorRegistry m_exceptionTranslatorRegistry; TagAliasRegistry m_tagAliasRegistry; StartupExceptionRegistry m_exceptionRegistry; + Detail::EnumValuesRegistry m_enumValuesRegistry; }; } diff --git a/include/internal/catch_string_manip.cpp b/include/internal/catch_string_manip.cpp index 904d1013..9f459de0 100644 --- a/include/internal/catch_string_manip.cpp +++ b/include/internal/catch_string_manip.cpp @@ -6,11 +6,13 @@ */ #include "catch_string_manip.h" +#include "catch_stringref.h" #include #include #include #include +#include namespace Catch { @@ -65,6 +67,21 @@ namespace Catch { return replaced; } + std::vector splitStringRef( StringRef str, char delimiter ) { + std::vector subStrings; + std::size_t start = 0; + for(std::size_t pos = 0; pos < str.size(); ++pos ) { + if( str[pos] == delimiter ) { + if( pos - start > 1 ) + subStrings.push_back( str.substr( start, pos-start ) ); + start = pos+1; + } + } + if( start < str.size() ) + subStrings.push_back( str.substr( start, str.size()-start ) ); + return subStrings; + } + pluralise::pluralise( std::size_t count, std::string const& label ) : m_count( count ), m_label( label ) diff --git a/include/internal/catch_string_manip.h b/include/internal/catch_string_manip.h index 6292cd57..b551ded6 100644 --- a/include/internal/catch_string_manip.h +++ b/include/internal/catch_string_manip.h @@ -7,8 +7,11 @@ #ifndef TWOBLUECUBES_CATCH_STRING_MANIP_H_INCLUDED #define TWOBLUECUBES_CATCH_STRING_MANIP_H_INCLUDED +#include "catch_stringref.h" + #include #include +#include namespace Catch { @@ -20,6 +23,9 @@ namespace Catch { void toLowerInPlace( std::string& s ); std::string toLower( std::string const& s ); std::string trim( std::string const& str ); + + // !!! Be aware, returns refs into original string - make sure original string outlives them + std::vector splitStringRef( StringRef str, char delimiter ); bool replaceInPlace( std::string& str, std::string const& replaceThis, std::string const& withThis ); struct pluralise { diff --git a/include/internal/catch_tostring.h b/include/internal/catch_tostring.h index 13a43b0c..402b6dbc 100644 --- a/include/internal/catch_tostring.h +++ b/include/internal/catch_tostring.h @@ -15,6 +15,7 @@ #include #include "catch_compiler_capabilities.h" #include "catch_stream.h" +#include "catch_interfaces_enum_values_registry.h" #ifdef CATCH_CONFIG_CPP17_STRING_VIEW #include @@ -639,6 +640,15 @@ struct ratio_string { } #endif // CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER +#define INTERNAL_CATCH_REGISTER_ENUM( enumName, ... ) \ + template<> struct Catch::StringMaker { \ + static std::string convert( enumName value ) { \ + static const auto& enumInfo = ::Catch::getMutableRegistryHub().getMutableEnumValuesRegistry().registerEnum( #enumName, #__VA_ARGS__, { __VA_ARGS__ } ); \ + return enumInfo.lookup( static_cast( value ) ); \ + } \ + }; + +#define CATCH_REGISTER_ENUM( enumName, ... ) INTERNAL_CATCH_REGISTER_ENUM( enumName, __VA_ARGS__ ) #ifdef _MSC_VER #pragma warning(pop) diff --git a/projects/CMakeLists.txt b/projects/CMakeLists.txt index 3fbd9b42..3029e823 100644 --- a/projects/CMakeLists.txt +++ b/projects/CMakeLists.txt @@ -22,6 +22,7 @@ set(TEST_SOURCES ${SELF_TEST_DIR}/IntrospectiveTests/Tag.tests.cpp ${SELF_TEST_DIR}/IntrospectiveTests/String.tests.cpp ${SELF_TEST_DIR}/IntrospectiveTests/Xml.tests.cpp + ${SELF_TEST_DIR}/IntrospectiveTests/ToString.tests.cpp ${SELF_TEST_DIR}/UsageTests/Approx.tests.cpp ${SELF_TEST_DIR}/UsageTests/BDD.tests.cpp ${SELF_TEST_DIR}/UsageTests/Benchmark.tests.cpp @@ -97,6 +98,7 @@ set(INTERNAL_HEADERS ${HEADER_DIR}/internal/catch_decomposer.h ${HEADER_DIR}/internal/catch_default_main.hpp ${HEADER_DIR}/internal/catch_enforce.h + ${HEADER_DIR}/internal/catch_enum_values_registry.h ${HEADER_DIR}/internal/catch_errno_guard.h ${HEADER_DIR}/internal/catch_exception_translator_registry.h ${HEADER_DIR}/internal/catch_external_interfaces.h @@ -107,6 +109,7 @@ set(INTERNAL_HEADERS ${HEADER_DIR}/internal/catch_impl.hpp ${HEADER_DIR}/internal/catch_interfaces_capture.h ${HEADER_DIR}/internal/catch_interfaces_config.h + ${HEADER_DIR}/internal/catch_interfaces_enum_values_registry.h ${HEADER_DIR}/internal/catch_interfaces_exception.h ${HEADER_DIR}/internal/catch_interfaces_registry_hub.h ${HEADER_DIR}/internal/catch_interfaces_reporter.h @@ -182,6 +185,7 @@ set(IMPL_SOURCES ${HEADER_DIR}/internal/catch_debugger.cpp ${HEADER_DIR}/internal/catch_decomposer.cpp ${HEADER_DIR}/internal/catch_enforce.cpp + ${HEADER_DIR}/internal/catch_enum_values_registry.cpp ${HEADER_DIR}/internal/catch_errno_guard.cpp ${HEADER_DIR}/internal/catch_exception_translator_registry.cpp ${HEADER_DIR}/internal/catch_fatal_condition.cpp diff --git a/projects/SelfTest/IntrospectiveTests/String.tests.cpp b/projects/SelfTest/IntrospectiveTests/String.tests.cpp index ae21bb3c..cb2c350c 100644 --- a/projects/SelfTest/IntrospectiveTests/String.tests.cpp +++ b/projects/SelfTest/IntrospectiveTests/String.tests.cpp @@ -202,3 +202,15 @@ TEST_CASE( "replaceInPlace", "[Strings][StringManip]" ) { CHECK( s == "didn|'t" ); } } + +TEST_CASE( "splitString", "[Strings]" ) { + using namespace Catch::Matchers; + using Catch::splitStringRef; + using Catch::StringRef; + + CHECK_THAT( splitStringRef("", ',' ), Equals(std::vector() ) ); + CHECK_THAT( splitStringRef("abc", ',' ), Equals(std::vector{"abc"} ) ); + CHECK_THAT( splitStringRef("abc,def", ',' ), Equals(std::vector{"abc", "def"} ) ); +} + + diff --git a/projects/SelfTest/IntrospectiveTests/ToString.tests.cpp b/projects/SelfTest/IntrospectiveTests/ToString.tests.cpp new file mode 100644 index 00000000..fc0e2a4f --- /dev/null +++ b/projects/SelfTest/IntrospectiveTests/ToString.tests.cpp @@ -0,0 +1,42 @@ +#include "catch.hpp" + +#include "internal/catch_enum_values_registry.h" + +enum class EnumClass3 { Value1, Value2, Value3, Value4 }; + + +TEST_CASE( "parseEnums", "[Strings][enums]" ) { + using namespace Catch::Matchers; + using Catch::Detail::parseEnums; + + SECTION( "No enums" ) + CHECK_THAT( parseEnums( "" ), Equals( std::vector{} ) ); + + SECTION( "One enum value" ) { + CHECK_THAT( parseEnums( "ClassName::EnumName::Value1" ), + Equals(std::vector{"Value1"} ) ); + CHECK_THAT( parseEnums( "Value1" ), + Equals( std::vector{"Value1"} ) ); + CHECK_THAT( parseEnums( "EnumName::Value1" ), + Equals(std::vector{"Value1"} ) ); + } + + SECTION( "Multiple enum values" ) { + CHECK_THAT( parseEnums( "ClassName::EnumName::Value1, ClassName::EnumName::Value2" ), + Equals( std::vector{"Value1", "Value2"} ) ); + CHECK_THAT( parseEnums( "ClassName::EnumName::Value1, ClassName::EnumName::Value2, ClassName::EnumName::Value3" ), + Equals( std::vector{"Value1", "Value2", "Value3"} ) ); + CHECK_THAT( parseEnums( "ClassName::EnumName::Value1,ClassName::EnumName::Value2 , ClassName::EnumName::Value3" ), + Equals( std::vector{"Value1", "Value2", "Value3"} ) ); + } +} + +TEST_CASE( "Directly creating an EnumInfo" ) { + + using namespace Catch::Detail; + std::unique_ptr enumInfo = makeEnumInfo( "EnumName", "EnumName::Value1, EnumName::Value2", {0, 1} ); + + CHECK( enumInfo->lookup(0) == "Value1" ); + CHECK( enumInfo->lookup(1) == "Value2" ); + CHECK( enumInfo->lookup(3) == "{** unexpected enum value **}" ); +} diff --git a/projects/SelfTest/UsageTests/EnumToString.tests.cpp b/projects/SelfTest/UsageTests/EnumToString.tests.cpp index 0b188a82..81ec5937 100644 --- a/projects/SelfTest/UsageTests/EnumToString.tests.cpp +++ b/projects/SelfTest/UsageTests/EnumToString.tests.cpp @@ -61,6 +61,39 @@ TEST_CASE( "toString(enum class w/operator<<)", "[toString][enum][enumClass]" ) EnumClass2 e1 = EnumClass2::EnumClass2Value1; CHECK( ::Catch::Detail::stringify(e1) == "E2/V1" ); - EnumClass2 e3 = static_cast(10); + auto e3 = static_cast(10); CHECK( ::Catch::Detail::stringify(e3) == "Unknown enum value 10" ); } + +enum class EnumClass3 { Value1, Value2, Value3, Value4 }; + +CATCH_REGISTER_ENUM( EnumClass3, EnumClass3::Value1, EnumClass3::Value2, EnumClass3::Value3 ) + + +TEST_CASE( "Enums can quickly have stringification enabled using REGISTER_ENUM" ) { + using Catch::Detail::stringify; + REQUIRE( stringify( EnumClass3::Value1 ) == "Value1" ); + REQUIRE( stringify( EnumClass3::Value2 ) == "Value2" ); + REQUIRE( stringify( EnumClass3::Value3 ) == "Value3" ); + REQUIRE( stringify( EnumClass3::Value4 ) == "{** unexpected enum value **}" ); + + EnumClass3 ec3 = EnumClass3 ::Value2; + REQUIRE( stringify( ec3 ) == "Value2" ); +} + +namespace Bikeshed { + enum class Colours { Red, Green, Blue }; +} + +// Important!: This macro must appear at top level scope - not inside a namespace +// You can fully qualify the names, or use a using if you prefer +CATCH_REGISTER_ENUM( Bikeshed::Colours, + Bikeshed::Colours::Red, + Bikeshed::Colours::Green, + Bikeshed::Colours::Blue ); + +TEST_CASE( "Enums in namespaces can quickly have stringification enabled using REGISTER_ENUM" ) { + using Catch::Detail::stringify; + REQUIRE( stringify( Bikeshed::Colours::Red ) == "Red" ); + REQUIRE( stringify( Bikeshed::Colours::Blue ) == "Blue" ); +}