From a51fd07bd006101c90662317c98de0ebe9e97312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ho=C5=99e=C5=88ovsk=C3=BD?= Date: Mon, 4 Apr 2022 17:06:47 +0200 Subject: [PATCH] Add helper for parsing the new reporter specs The new reporter spec generalizes key-value options that can be passed to the reporter, looking like this `reporterName[::key=value]*`. A key can be either Catch2-recognized, which currently means either `out` or `colour`, or reporter-specific which is anything prefixed with `X`, e.g. `Xfoo`. --- .../internal/catch_reporter_spec_parser.cpp | 93 +++++++++++++++++++ .../internal/catch_reporter_spec_parser.hpp | 57 +++++++++++- .../CmdLineHelpers.tests.cpp | 47 ++++++++++ 3 files changed, 196 insertions(+), 1 deletion(-) diff --git a/src/catch2/internal/catch_reporter_spec_parser.cpp b/src/catch2/internal/catch_reporter_spec_parser.cpp index 9f896ae2..a4f7105e 100644 --- a/src/catch2/internal/catch_reporter_spec_parser.cpp +++ b/src/catch2/internal/catch_reporter_spec_parser.cpp @@ -9,9 +9,28 @@ #include #include +#include + +#include +#include namespace Catch { + namespace { + struct kvPair { + StringRef key, value; + }; + + kvPair splitKVPair(StringRef kvString) { + auto splitPos = static_cast( std::distance( + kvString.begin(), + std::find( kvString.begin(), kvString.end(), '=' ) ) ); + + return { kvString.substr( 0, splitPos ), + kvString.substr( splitPos + 1, kvString.size() ) }; + } + } + namespace Detail { std::vector splitReporterSpec( StringRef reporterSpec ) { static constexpr auto separator = "::"; @@ -78,4 +97,78 @@ namespace Catch { } // namespace Detail + bool operator==( ReporterSpec const& lhs, ReporterSpec const& rhs ) { + return lhs.m_name == rhs.m_name && + lhs.m_outputFileName == rhs.m_outputFileName && + lhs.m_colourMode == rhs.m_colourMode && + lhs.m_customOptions == rhs.m_customOptions; + } + + Optional parseReporterSpec( StringRef reporterSpec ) { + auto parts = Detail::splitReporterSpec( reporterSpec ); + + assert( parts.size() > 0 && "Split should never return empty vector" ); + + std::map kvPairs; + Optional outputFileName; + Optional colourMode; + + // First part is always reporter name, so we skip it + for ( size_t i = 1; i < parts.size(); ++i ) { + auto kv = splitKVPair( parts[i] ); + auto key = kv.key, value = kv.value; + + if ( key.empty() || value.empty() ) { + return {}; + } else if ( key[0] == 'X' ) { + // This is a reporter-specific option, we don't check these + // apart from basic sanity checks + if ( key.size() == 1 ) { + return {}; + } + + auto ret = kvPairs.emplace( kv.key, kv.value ); + if ( !ret.second ) { + // Duplicated key. We might want to handle this differently, + // e.g. by overwriting the existing value? + return {}; + } + } else if ( key == "out" ) { + // Duplicated key + if ( outputFileName ) { + return {}; + } + outputFileName = static_cast( value ); + } else if ( key == "colour" ) { + // Duplicated key + if ( colourMode ) { + return {}; + } + colourMode = Detail::stringToColourMode( value ); + // Parsing failed + if ( !colourMode ) { + return {}; + } + } else { + // Unrecognized option + return {}; + } + } + + return ReporterSpec{ CATCH_MOVE( parts[0] ), + CATCH_MOVE( outputFileName ), + CATCH_MOVE( colourMode ), + CATCH_MOVE( kvPairs ) }; + } + +ReporterSpec::ReporterSpec( + std::string name, + Optional outputFileName, + Optional colourMode, + std::map customOptions ): + m_name( CATCH_MOVE( name ) ), + m_outputFileName( CATCH_MOVE( outputFileName ) ), + m_colourMode( CATCH_MOVE( colourMode ) ), + m_customOptions( CATCH_MOVE( customOptions ) ) {} + } // namespace Catch diff --git a/src/catch2/internal/catch_reporter_spec_parser.hpp b/src/catch2/internal/catch_reporter_spec_parser.hpp index 3dd9f17e..242e1022 100644 --- a/src/catch2/internal/catch_reporter_spec_parser.hpp +++ b/src/catch2/internal/catch_reporter_spec_parser.hpp @@ -8,11 +8,13 @@ #ifndef CATCH_REPORTER_SPEC_PARSER_HPP_INCLUDED #define CATCH_REPORTER_SPEC_PARSER_HPP_INCLUDED +#include #include #include -#include +#include #include +#include namespace Catch { @@ -25,6 +27,59 @@ namespace Catch { Optional stringToColourMode( StringRef colourMode ); } + /** + * Structured reporter spec that a reporter can be created from + * + * Parsing has been validated, but semantics have not. This means e.g. + * that the colour mode is known to Catch2, but it might not be + * compiled into the binary, and the output filename might not be + * openable. + */ + class ReporterSpec { + std::string m_name; + Optional m_outputFileName; + Optional m_colourMode; + std::map m_customOptions; + + friend bool operator==( ReporterSpec const& lhs, + ReporterSpec const& rhs ); + friend bool operator!=( ReporterSpec const& lhs, + ReporterSpec const& rhs ) { + return !( lhs == rhs ); + } + + public: + ReporterSpec( + std::string name, + Optional outputFileName, + Optional colourMode, + std::map customOptions ); + + std::string const& name() const { return m_name; } + + Optional const& outputFile() const { + return m_outputFileName; + } + + Optional const& colourMode() const { return m_colourMode; } + + std::map const& customOptions() const { + return m_customOptions; + } + }; + + /** + * Parses provided reporter spec string into + * + * Returns empty optional on errors, e.g. + * * field that is not first and not a key+value pair + * * duplicated keys in kv pair + * * unknown catch reporter option + * * empty key/value in an custom kv pair + * * ... + */ + Optional parseReporterSpec( StringRef reporterSpec ); + } #endif // CATCH_REPORTER_SPEC_PARSER_HPP_INCLUDED diff --git a/tests/SelfTest/IntrospectiveTests/CmdLineHelpers.tests.cpp b/tests/SelfTest/IntrospectiveTests/CmdLineHelpers.tests.cpp index 0eacfdf3..9045ad2a 100644 --- a/tests/SelfTest/IntrospectiveTests/CmdLineHelpers.tests.cpp +++ b/tests/SelfTest/IntrospectiveTests/CmdLineHelpers.tests.cpp @@ -62,3 +62,50 @@ TEST_CASE( "Parsing colour mode", "[cli][colour][approvals]" ) { REQUIRE_FALSE( stringToColourMode( "asdbjsdb kasbd" ) ); } } + + +TEST_CASE("Parsing reporter specs", "[cli][reporter-spec][approvals]") { + using Catch::parseReporterSpec; + using Catch::ReporterSpec; + using namespace std::string_literals; + + SECTION( "Correct specs" ) { + REQUIRE( parseReporterSpec( "someReporter" ) == + ReporterSpec( "someReporter"s, {}, {}, {} ) ); + REQUIRE( parseReporterSpec( "otherReporter::Xk=v::out=c:\\blah" ) == + ReporterSpec( + "otherReporter"s, "c:\\blah"s, {}, { { "Xk"s, "v"s } } ) ); + REQUIRE( parseReporterSpec( "diffReporter::Xk1=v1::Xk2==v2" ) == + ReporterSpec( "diffReporter", + {}, + {}, + { { "Xk1"s, "v1"s }, { "Xk2"s, "=v2"s } } ) ); + REQUIRE( parseReporterSpec( + "Foo:bar:reporter::colour=ansi::Xk 1=v 1::Xk2=v:3" ) == + ReporterSpec( "Foo:bar:reporter", + {}, + Catch::ColourMode::ANSI, + { { "Xk 1"s, "v 1"s }, { "Xk2"s, "v:3"s } } ) ); + } + + SECTION( "Bad specs" ) { + REQUIRE_FALSE( parseReporterSpec( "::" ) ); + // Unknown Catch2 arg (should be "out") + REQUIRE_FALSE( parseReporterSpec( "reporter::output=filename" ) ); + // Wrong colour spec + REQUIRE_FALSE( parseReporterSpec( "reporter::colour=custom" ) ); + // Duplicated colour spec + REQUIRE_FALSE( parseReporterSpec( "reporter::colour=ansi::colour=ansi" ) ); + // Duplicated out arg + REQUIRE_FALSE( parseReporterSpec( "reporter::out=f.txt::out=z.txt" ) ); + // Duplicated custom arg + REQUIRE_FALSE( parseReporterSpec( "reporter::Xa=foo::Xa=bar" ) ); + // Empty key + REQUIRE_FALSE( parseReporterSpec( "reporter::X=foo" ) ); + REQUIRE_FALSE( parseReporterSpec( "reporter::=foo" ) ); + // Empty value + REQUIRE_FALSE( parseReporterSpec( "reporter::Xa=" ) ); + // non-key value later field + REQUIRE_FALSE( parseReporterSpec( "reporter::Xab" ) ); + } +}