From 85c0e3d42b060f521af14698ce53ada3c6753f8a Mon Sep 17 00:00:00 2001 From: Phil Nash Date: Fri, 21 Sep 2012 07:48:03 +0100 Subject: [PATCH] Tag command line parsing implementation --- include/internal/catch_commandline.hpp | 46 +++- include/internal/catch_impl.hpp | 4 + include/internal/catch_tags.hpp | 204 +++++++++++++++++- include/internal/catch_test_case_info.h | 1 + include/internal/catch_test_case_info.hpp | 42 +--- .../SelfTest/SurrogateCpps/catch_tags.cpp | 10 +- projects/SelfTest/TestMain.cpp | 21 ++ .../CatchSelfTest.xcodeproj/project.pbxproj | 14 ++ 8 files changed, 288 insertions(+), 54 deletions(-) diff --git a/include/internal/catch_commandline.hpp b/include/internal/catch_commandline.hpp index 38c60855..d6467b93 100644 --- a/include/internal/catch_commandline.hpp +++ b/include/internal/catch_commandline.hpp @@ -235,7 +235,7 @@ namespace Catch { "\n" "If spec is prefixed with exclude: or the ~ character then the pattern matches an exclusion. " "This means that tests matching the pattern are excluded from the set - even if a prior " - "inclusion spec included them. Subsequent inclusion specs will take precendence, however. " + "inclusion spec included them. Subsequent inclusion specs will take precedence, however. " "Inclusions and exclusions are evaluated in left-to-right order.\n" "\n" "Examples:\n" @@ -249,7 +249,7 @@ namespace Catch { " -t a/* ~a/b/* a/b/c \tMatches all tests that start with 'a/', except those " "that start with 'a/b/', except 'a/b/c', which is included"; } - + virtual void parseIntoConfig( const Command& cmd, ConfigData& config ) { std::string groupName; for( std::size_t i = 0; i < cmd.argsCount(); ++i ) { @@ -264,6 +264,42 @@ namespace Catch { } }; + class TagOptionParser : public OptionParser { + public: + TagOptionParser() : OptionParser( 1, -1 ) { + m_optionNames.push_back( "-g" ); + m_optionNames.push_back( "--tag" ); + } + virtual std::string argsSynopsis() const { + return " [,...]"; + } + virtual std::string optionSummary() const { + return "Matches test cases against tags or tag patterns"; + } + + // Lines are split at the nearest prior space char to the 80 char column. + // Tab chars are removed from the output but their positions are used to align + // subsequently wrapped lines + virtual std::string optionDescription() const { + return + "!TBD"; + } + + virtual void parseIntoConfig( const Command& cmd, ConfigData& config ) { +// std::string groupName; +// for( std::size_t i = 0; i < cmd.argsCount(); ++i ) { +// if( i != 0 ) +// groupName += " "; +// groupName += cmd[i]; +// } +// TestCaseFilters filters( groupName ); +// for( std::size_t i = 0; i < cmd.argsCount(); ++i ) +// filters.addFilter( TestCaseFilter( cmd[i] ) ); +// config.filters.push_back( filters ); + } + }; + + class ListOptionParser : public OptionParser { public: ListOptionParser() : OptionParser( 0, 2 ) { @@ -369,9 +405,9 @@ namespace Catch { virtual std::string optionDescription() const { return "Use this option to send all output to a file or a stream. By default output is " - "sent to stdout (note that uses ofstdout and stderr from within test cases are " + "sent to stdout (note that uses of stdout and stderr from within test cases are " "redirected and included in the report - so even stderr will effectively end up " - "on stdout). If the name begins with % it is interpretted as a stream. " + "on stdout). If the name begins with % it is interpreted as a stream. " "Otherwise it is treated as a filename.\n" "\n" "Examples are:\n" @@ -407,7 +443,7 @@ namespace Catch { return "Usually you only want to see reporting for failed tests. Sometimes it's useful " "to see all the output (especially when you don't trust that that test you just " - "added worked first time!). To see successul, as well as failing, test results " + "added worked first time!). To see successful, as well as failing, test results " "just pass this option."; } virtual void parseIntoConfig( const Command&, ConfigData& config ) { diff --git a/include/internal/catch_impl.hpp b/include/internal/catch_impl.hpp index 9815f271..68b9ece4 100644 --- a/include/internal/catch_impl.hpp +++ b/include/internal/catch_impl.hpp @@ -23,6 +23,7 @@ #include "catch_resultinfo.hpp" #include "catch_resultinfo_builder.hpp" #include "catch_test_case_info.hpp" +#include "catch_tags.hpp" #include "../reporters/catch_reporter_basic.hpp" #include "../reporters/catch_reporter_xml.hpp" @@ -53,6 +54,9 @@ namespace Catch { FreeFunctionTestCase::~FreeFunctionTestCase() {} IGeneratorInfo::~IGeneratorInfo() {} IGeneratorsForTest::~IGeneratorsForTest() {} + TagParser::~TagParser() {} + TagExtracter::~TagExtracter() {} + TagExpressionParser::~TagExpressionParser() {} void Config::dummy() {} diff --git a/include/internal/catch_tags.hpp b/include/internal/catch_tags.hpp index 2a012d8b..d94eaad2 100644 --- a/include/internal/catch_tags.hpp +++ b/include/internal/catch_tags.hpp @@ -1,9 +1,197 @@ -// -// catch_tags.hpp -// CatchSelfTest -// -// Created by Phil Nash on 19/09/2012. -// -// +/* + * Created by Phil on 14/08/2012. + * Copyright 2012 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_TAGS_HPP_INCLUDED +#define TWOBLUECUBES_CATCH_TAGS_HPP_INCLUDED -#include "catch_tags.h" +#include +#include +#include +#include + +#ifdef __clang__ +#pragma clang diagnostic ignored "-Wpadded" +#endif + +namespace Catch { + class TagParser { + public: + virtual ~TagParser(); + + void parse( const std::string& str ) { + std::size_t pos = 0; + while( pos < str.size() ) { + char c = str[pos]; + if( c == '[' ) { + std::size_t end = str.find_first_of( ']', pos ); + if( end != std::string::npos ) { + acceptTag( str.substr( pos+1, end-pos-1 ) ); + pos = end+1; + } + else { + acceptChar( c ); + pos++; + } + } + else { + acceptChar( c ); + pos++; + } + } + endParse(); + } + + protected: + virtual void acceptTag( const std::string& tag ) = 0; + virtual void acceptChar( char c ) = 0; + virtual void endParse() {} + + private: + }; + + class TagExtracter : public TagParser { + public: + + TagExtracter( std::set& tags ) + : m_tags( tags ) + {} + virtual ~TagExtracter(); + + void parse( std::string& description ) { + TagParser::parse( description ); + description = m_remainder; + } + + private: + virtual void acceptTag( const std::string& tag ) { + m_tags.insert( tag ); + } + virtual void acceptChar( char c ) { + m_remainder += c; + } + + std::set& m_tags; + std::string m_remainder; + }; + + class Tag + { + public: + Tag() + : m_isNegated( false ) + {} + + Tag( const std::string& name, bool isNegated ) + : m_name( name ), + m_isNegated( isNegated ) + {} + + std::string getName() const { + return m_name; + } + bool isNegated() const { + return m_isNegated; + } + + bool operator ! () const { + return m_name.empty(); + } + + private: + std::string m_name; + bool m_isNegated; + }; + + class TagSet + { + typedef std::map TagMap; + public: + void add( const Tag& tag ) { + m_tags.insert( std::make_pair( tag.getName(), tag ) ); + } + + // needed? + Tag find( const std::string& name ) const { + TagMap::const_iterator it = m_tags.find( name ); + if( it == m_tags.end() ) + return Tag(); + else + return it->second; + } + bool empty() const { + return m_tags.empty(); + } + + bool matches( const std::set& tags ) const { + TagMap::const_iterator it = m_tags.begin(); + TagMap::const_iterator itEnd = m_tags.end(); + for(; it != itEnd; ++it ) { + bool found = tags.find( it->first ) != tags.end(); + if( found == it->second.isNegated() ) + return false; + } + return true; + } + + private: + TagMap m_tags; + }; + + class TagExpression { + public: + bool matches( const std::set& tags ) const { + std::vector::const_iterator it = m_tagSets.begin(); + std::vector::const_iterator itEnd = m_tagSets.end(); + for(; it != itEnd; ++it ) + if( it->matches( tags ) ) + return true; + return false; + } + + private: + friend class TagExpressionParser; + + std::vector m_tagSets; + }; + + class TagExpressionParser : public TagParser { + public: + TagExpressionParser( TagExpression& exp ) + : m_isNegated( false ), + m_exp( exp ) + {} + + ~TagExpressionParser(); + + private: + virtual void acceptTag( const std::string& tag ) { + m_currentTagSet.add( Tag( tag, m_isNegated ) ); + m_isNegated = false; + } + virtual void acceptChar( char c ) { + switch( c ) { + case '~': + m_isNegated = true; + break; + case ',': + m_exp.m_tagSets.push_back( m_currentTagSet ); + break; + } + } + virtual void endParse() { + if( !m_currentTagSet.empty() ) + m_exp.m_tagSets.push_back( m_currentTagSet ); + } + + bool m_isNegated; + TagSet m_currentTagSet; + TagExpression& m_exp; + }; + +} // end namespace Catch + +#endif // TWOBLUECUBES_CATCH_TAGS_HPP_INCLUDED diff --git a/include/internal/catch_test_case_info.h b/include/internal/catch_test_case_info.h index 1aebf0cf..d09673b6 100644 --- a/include/internal/catch_test_case_info.h +++ b/include/internal/catch_test_case_info.h @@ -36,6 +36,7 @@ namespace Catch { const SourceLineInfo& getLineInfo() const; bool isHidden() const; bool hasTag( const std::string& tag ) const; + bool matchesTags( const std::string& tagPattern ) const; const std::set& tags() const; void swap( TestCaseInfo& other ); diff --git a/include/internal/catch_test_case_info.hpp b/include/internal/catch_test_case_info.hpp index be4db842..90693a5d 100644 --- a/include/internal/catch_test_case_info.hpp +++ b/include/internal/catch_test_case_info.hpp @@ -8,41 +8,13 @@ #ifndef TWOBLUECUBES_CATCH_TESTCASEINFO_HPP_INCLUDED #define TWOBLUECUBES_CATCH_TESTCASEINFO_HPP_INCLUDED +#include "catch_tags.hpp" #include "catch_test_case_info.h" #include "catch_interfaces_testcase.h" namespace Catch { - namespace { - void extractTags( std::string& str, std::set& tags ) { - std::string remainder; - std::size_t last = 0; - std::size_t begin = str.find_first_of( '[' ); - while( begin != std::string::npos ) { - std::size_t end = str.find_first_of( ']', begin ); - if( end != std::string::npos ) { - std::string tag = str.substr( begin+1, end-begin-1 ); - tags.insert( tag ); - if( begin - last > 0 ) - remainder += str.substr( last, begin-last ); - last = end+1; - } - else if( begin != str.size()-1 ) { - end = begin+1; - } - else { - break; - } - begin = str.find_first_of( '[', end ); - } - if( !tags.empty() ) { - if( last < str.size() ) - str = remainder + str.substr( last ); - else - str = remainder; - } - } - } + TestCaseInfo::TestCaseInfo( ITestCase* testCase, const char* name, const char* description, @@ -53,7 +25,7 @@ namespace Catch { m_lineInfo( lineInfo ), m_isHidden( startsWith( name, "./" ) ) { - extractTags( m_description, m_tags ); + TagExtracter( m_tags ).parse( m_description ); if( hasTag( "hide" ) ) m_isHidden = true; } @@ -106,6 +78,11 @@ namespace Catch { bool TestCaseInfo::hasTag( const std::string& tag ) const { return m_tags.find( tag ) != m_tags.end(); } + bool TestCaseInfo::matchesTags( const std::string& tagPattern ) const { + TagExpression exp; + TagExpressionParser( exp ).parse( tagPattern ); + return exp.matches( m_tags ); + } const std::set& TestCaseInfo::tags() const { return m_tags; } @@ -129,6 +106,7 @@ namespace Catch { swap( temp ); return *this; } -} + +} // end namespace Catch #endif // TWOBLUECUBES_CATCH_TESTCASEINFO_HPP_INCLUDED diff --git a/projects/SelfTest/SurrogateCpps/catch_tags.cpp b/projects/SelfTest/SurrogateCpps/catch_tags.cpp index 12967052..47664eff 100644 --- a/projects/SelfTest/SurrogateCpps/catch_tags.cpp +++ b/projects/SelfTest/SurrogateCpps/catch_tags.cpp @@ -1,9 +1 @@ -// -// catch_tags.cpp -// CatchSelfTest -// -// Created by Phil Nash on 19/09/2012. -// -// - -#include "catch_tags.h" +#include "catch_tags.hpp" diff --git a/projects/SelfTest/TestMain.cpp b/projects/SelfTest/TestMain.cpp index f745f69b..2c0c2514 100644 --- a/projects/SelfTest/TestMain.cpp +++ b/projects/SelfTest/TestMain.cpp @@ -333,6 +333,12 @@ TEST_CASE( "selftest/option parsers", "" ) } TEST_CASE( "selftest/tags", "" ) { + + std::string p1 = "[one]"; + std::string p2 = "[one],[two]"; + std::string p3 = "[one][two]"; + std::string p4 = "[one][two],[three]"; + std::string p5 = "[one][two]~[hide],[three]"; SECTION( "one tag", "" ) { Catch::TestCaseInfo oneTag( NULL, "test", "[one]", CATCH_INTERNAL_LINEINFO ); @@ -340,6 +346,12 @@ TEST_CASE( "selftest/tags", "" ) { CHECK( oneTag.getDescription() == "" ); CHECK( oneTag.hasTag( "one" ) ); CHECK( oneTag.tags().size() == 1 ); + + CHECK( oneTag.matchesTags( p1 ) == true ); + CHECK( oneTag.matchesTags( p2 ) == true ); + CHECK( oneTag.matchesTags( p3 ) == false ); + CHECK( oneTag.matchesTags( p4 ) == false ); + CHECK( oneTag.matchesTags( p5 ) == false ); } SECTION( "two tags", "" ) { @@ -350,6 +362,12 @@ TEST_CASE( "selftest/tags", "" ) { CHECK( twoTags.hasTag( "two" ) ); CHECK( twoTags.hasTag( "three" ) == false ); CHECK( twoTags.tags().size() == 2 ); + + CHECK( twoTags.matchesTags( p1 ) == true ); + CHECK( twoTags.matchesTags( p2 ) == true ); + CHECK( twoTags.matchesTags( p3 ) == true ); + CHECK( twoTags.matchesTags( p4 ) == true ); + CHECK( twoTags.matchesTags( p5 ) == true ); } SECTION( "one tag with characters either side", "" ) { @@ -376,6 +394,9 @@ TEST_CASE( "selftest/tags", "" ) { CHECK( oneTag.getDescription() == "" ); CHECK( oneTag.hasTag( "hide" ) ); CHECK( oneTag.isHidden() ); + + CHECK( oneTag.matchesTags( "~[hide]" ) == false ); + } } diff --git a/projects/XCode4/CatchSelfTest/CatchSelfTest.xcodeproj/project.pbxproj b/projects/XCode4/CatchSelfTest/CatchSelfTest.xcodeproj/project.pbxproj index 0e2795e9..c851d77f 100644 --- a/projects/XCode4/CatchSelfTest/CatchSelfTest.xcodeproj/project.pbxproj +++ b/projects/XCode4/CatchSelfTest/CatchSelfTest.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 4A6D0C3D149B3D9E00DB3EAA /* MiscTests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A6D0C34149B3D9E00DB3EAA /* MiscTests.cpp */; }; 4A6D0C3E149B3D9E00DB3EAA /* TestMain.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A6D0C35149B3D9E00DB3EAA /* TestMain.cpp */; }; 4A6D0C3F149B3D9E00DB3EAA /* TrickyTests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A6D0C36149B3D9E00DB3EAA /* TrickyTests.cpp */; }; + 4A8E4DD2160A352200194CBD /* catch_tags.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4A8E4DD0160A352200194CBD /* catch_tags.cpp */; }; 4AA7FF4315F3E89E009AD7F9 /* BDDTests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4AA7FF4115F3E89D009AD7F9 /* BDDTests.cpp */; }; 4AE1840B14EE4F230066340D /* catch_self_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4AE1840A14EE4F230066340D /* catch_self_test.cpp */; }; /* End PBXBuildFile section */ @@ -93,6 +94,8 @@ 4A6D0C67149B3E3D00DB3EAA /* catch_reporter_junit.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = catch_reporter_junit.hpp; sourceTree = ""; }; 4A6D0C68149B3E3D00DB3EAA /* catch_reporter_xml.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = catch_reporter_xml.hpp; sourceTree = ""; }; 4A7ADB4314F631E10094FE10 /* catch_totals.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = catch_totals.hpp; sourceTree = ""; }; + 4A8E4DCC160A344100194CBD /* catch_tags.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = catch_tags.hpp; sourceTree = ""; }; + 4A8E4DD0160A352200194CBD /* catch_tags.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = catch_tags.cpp; path = ../../../SelfTest/SurrogateCpps/catch_tags.cpp; sourceTree = ""; }; 4A90B59B15D0F61A00EF71BC /* catch_interfaces_generators.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = catch_interfaces_generators.h; sourceTree = ""; }; 4A90B59D15D24FE900EF71BC /* catch_resultinfo.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = catch_resultinfo.hpp; sourceTree = ""; }; 4A90B59E15D2521E00EF71BC /* catch_resultinfo_builder.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = catch_resultinfo_builder.hpp; sourceTree = ""; }; @@ -170,6 +173,7 @@ 4A6D0C41149B3DE900DB3EAA /* Catch */ = { isa = PBXGroup; children = ( + 4A8E4DCF160A34E200194CBD /* SurrogateCpps */, 4A6D0C44149B3E1500DB3EAA /* catch.hpp */, 4A6D0C42149B3E1500DB3EAA /* catch_runner.hpp */, 4A6D0C43149B3E1500DB3EAA /* catch_with_main.hpp */, @@ -206,6 +210,14 @@ path = ../../../../include/reporters; sourceTree = ""; }; + 4A8E4DCF160A34E200194CBD /* SurrogateCpps */ = { + isa = PBXGroup; + children = ( + 4A8E4DD0160A352200194CBD /* catch_tags.cpp */, + ); + name = SurrogateCpps; + sourceTree = ""; + }; 4AC91CB4155B9EBF00DC5117 /* impl */ = { isa = PBXGroup; children = ( @@ -265,6 +277,7 @@ 4AB77CB71553B72B00857BF0 /* catch_section_info.hpp */, 4AB77CB81553BB3800857BF0 /* catch_running_test.hpp */, 4A084F1D15DAD15F0027E631 /* catch_test_spec.h */, + 4A8E4DCC160A344100194CBD /* catch_tags.hpp */, ); name = "Test execution"; sourceTree = ""; @@ -377,6 +390,7 @@ 4A6D0C3F149B3D9E00DB3EAA /* TrickyTests.cpp in Sources */, 4AE1840B14EE4F230066340D /* catch_self_test.cpp in Sources */, 4AA7FF4315F3E89E009AD7F9 /* BDDTests.cpp in Sources */, + 4A8E4DD2160A352200194CBD /* catch_tags.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };