diff --git a/Testing/CMakeLists.txt b/Testing/CMakeLists.txt index 4a30b10..35169ad 100644 --- a/Testing/CMakeLists.txt +++ b/Testing/CMakeLists.txt @@ -1,3 +1,3 @@ add_subdirectory( TableTest.test ) -add_library( unit-test SHARED testlib.cc ) +add_library( unit-test SHARED testlib.cc test.cc ) diff --git a/Testing/TableTest.h b/Testing/TableTest.h index ebf9b0f..45d2021 100644 --- a/Testing/TableTest.h +++ b/Testing/TableTest.h @@ -249,14 +249,20 @@ namespace Alepha::Hydrogen::Testing ::detail:: table_test using args_type= Meta::product_type_decay_t< typename function_traits_type::args_type >; using return_type= typename function_traits_type::return_type; - struct Cases + // The classic table-test engine would only support `Cases` which were run-and-test-value + // without the ability to test exceptions. The `ExceptionCases` construct was used to + // test throwing cases. + // + // A unified `Cases` type is forthcoming, and thus `ExecutionCases` exists for backwards + // compatibility. + struct ExecutionCases { using TestDescription= std::tuple< std::string, args_type, return_type >; std::vector< TestDescription > tests; explicit - Cases( std::initializer_list< TestDescription > initList ) + ExecutionCases( std::initializer_list< TestDescription > initList ) : tests( initList ) {} int @@ -394,6 +400,66 @@ namespace Alepha::Hydrogen::Testing ::detail:: table_test return failureCount; } }; + + class UniversalCases + { + using RunDescription= std::tuple< std::string, args_type, return_type >; + using Invoker= std::function< return_type () >; + + enum class TestResult { Passed, Failed }; + + struct UniversalHandler + { + std::function< TestResult ( Invoker, const std::string & ) > impl; + + bool operator() ( Invoker invoker ) const { return impl( invoker ); } + + UniversalHandler( const return_type expected ) : impl + { + [expected]( Invoker invoker, const std::string &comment ) + { + const auto witness= invoker(); + const auto result= witness == expected ? TestResult::Passed : TestResult::Failed; + + if( result == TestResult::Failed ) + { + std::cout << " " << C::testFail << "FAILED CASE" << resetStyle << ": " << comment << std::endl; + printDebugging< outputMode >( witness, expected ); + } + else std::cout << " " << C::testPass << "PASSED CASE" << resetStyle << ": " << comment << std::endl; + return result; + } + } + {} + + template< typename T > + UniversalHandler( std::type_identity< T > ) : impl + { + []( Invoker invoker ) + { + try { std::ignore= invoker(); } + catch( const T & ) { return TestResult::Passed; } + return TestResult::Failed; + } + } + {} + + UniversalHandler( const DerivedFrom< std::exception > auto exemplar ) : impl + { + [expected= std::string{ exemplar.what() }]( Invoker invoker ) + { + throw "Unimpl"; + } + } + {} + }; + + using TestDescription= std::tuple< std::string, args_type, UniversalHandler >; + }; + + // When the `UniversalCases` impl is ready to go, then this alias shim can be redirected to that form. Then I can + // retire the `ExceptionCases` and `ExecutionCases` forms and replace them with an alias to `UniversalCases`. + using Cases= ExecutionCases; }; #ifdef DISABLED diff --git a/Testing/test.cc b/Testing/test.cc new file mode 100644 index 0000000..059f195 --- /dev/null +++ b/Testing/test.cc @@ -0,0 +1,80 @@ +static_assert( __cplusplus > 2020'00 ); + +#include "test.h" + +namespace Alepha::Hydrogen::Testing::detail::testing +{ + StaticValue< std::vector< std::tuple< std::string, bool, std::function< void() > > > > registry; + + TestRegistration + impl::operator <= ( TestName name, std::function< void () > test ) + { + if( C::debugTestRegistration ) std::cerr << "Attempting to register: " << name.name << std::endl; + + registry().emplace_back( name.name, name.disabled, test ); + assert( not registry().empty() ); + assert( std::get< 1 >( registry().back() ) == name.disabled ); + + return {}; + }; + + [[nodiscard]] int + exports::runAllTests( const std::vector< std::string > selections ) + { + if( C::debugTestRun ) + { + std::cerr << "Going to run all tests. (I see " << registry().size() << " tests.)" << std::endl; + } + bool failed= false; + const auto selected= [ selections ]( const std::string test ) + { + for( const auto &selection: selections ) + { + if( test.find( selection ) != std::string::npos ) return true; + } + return empty( selections ); + }; + + const auto explicitlyNamed= [ selections ]( const std::string s ) + { + return std::find( begin( selections ), end( selections ), s ) != end( selections ); + }; + + for( const auto &[ name, disabled, test ]: registry() ) + { + if( C::debugTestRun ) std::cerr << "Trying test " << name << std::endl; + + if( explicitlyNamed( name ) or not disabled and selected( name ) ) + { + std::cout << C::testInfo << "BEGIN" << resetStyle << " : " << name << std::endl; + try + { + test(); + std::cout << " " << C::testPass << "SUCCESS" << resetStyle << ": " << name << std::endl; + } + catch( ... ) + { + try + { + failed= true; + std::cout << " " << C::testFail << "FAILURE" << resetStyle << ": " << name; + throw; + } + catch( const TestFailure &fail ) { std::cout << " -- " << fail.failureCount << " failures."; } + catch( ... ) { std::cout << " -- unknown failure count"; } + std::cout << std::endl; + } + + std::cout << C::testInfo << "FINISHED" << resetStyle << ": " << name << std::endl; + } + } + + return failed ? EXIT_FAILURE : EXIT_SUCCESS; + } + + [[nodiscard]] int + exports::runAllTests( const argcnt_t argcnt, const argvec_t argvec ) + { + return runAllTests( { argvec + 1, argvec + argcnt } ); + } +} diff --git a/Testing/test.h b/Testing/test.h index bcae02c..d5c1739 100644 --- a/Testing/test.h +++ b/Testing/test.h @@ -60,7 +60,7 @@ namespace Alepha::Hydrogen::Testing namespace exports { - struct TestFailureException; + struct TestFailure; inline namespace literals { @@ -72,31 +72,23 @@ namespace Alepha::Hydrogen::Testing } } - StaticValue< std::vector< std::tuple< std::string, bool, std::function< void() > > > > registry; - auto initRegistry= enroll <=registry; - // It is okay to discard this, if making tests in an enroll block. - inline auto - operator <= ( TestName name, std::function< void () > test ) + inline namespace impl { - struct TestRegistration {} rv; - if( C::debugTestRegistration ) std::cerr << "Attempting to register: " << name.name << std::endl; + struct TestRegistration {}; + TestRegistration operator <= ( TestName name, std::function< void () > test ); + } - registry().emplace_back( name.name, name.disabled, test ); - assert( not registry().empty() ); - assert( std::get< 1 >( registry().back() ) == name.disabled ); - - return rv; - }; - - struct exports::TestFailureException + struct exports::TestFailure { int failureCount= -1; + std::string message_; - explicit TestFailureException( const int failureCount ) : failureCount( failureCount ) {} + explicit TestFailure( const int failureCount ) + : failureCount( failureCount ) {} }; - template< typename Integer, typename= std::enable_if_t< std::is_integral_v< Integer > > > + template< Integral Integer > inline auto operator <= ( TestName name, std::function< Integer () > test ) { @@ -106,7 +98,7 @@ namespace Alepha::Hydrogen::Testing { if( not test() ) { - throw TestFailureException{ 1 }; + throw TestFailure{ 1 }; } }; @@ -117,7 +109,7 @@ namespace Alepha::Hydrogen::Testing auto wrapper= [test] { const int failures= test(); - if( failures > 0 ) throw TestFailureException{ failures }; + if( failures > 0 ) throw TestFailure{ failures }; }; return name <= wrapper; @@ -141,7 +133,7 @@ namespace Alepha::Hydrogen::Testing void demand( const bool state, const std::string test= "" ) { - if( not state ) throw TestFailureException( failures.size() + 1 ); + if( not state ) throw TestFailure( failures.size() + 1 ); } }; @@ -169,65 +161,9 @@ namespace Alepha::Hydrogen::Testing namespace exports { - [[nodiscard]] inline int - runAllTests( const std::vector< std::string > selections= {} ) - { - if( C::debugTestRun ) - { - std::cerr << "Going to run all tests. (I see " << registry().size() << " tests.)" << std::endl; - } - bool failed= false; - const auto selected= [ selections ]( const std::string test ) - { - for( const auto &selection: selections ) - { - if( test.find( selection ) != std::string::npos ) return true; - } - return empty( selections ); - }; + [[nodiscard]] int runAllTests( const std::vector< std::string > selections= {} ); - const auto explicitlyNamed= [ selections ]( const std::string s ) - { - return std::find( begin( selections ), end( selections ), s ) != end( selections ); - }; - - for( const auto &[ name, disabled, test ]: registry() ) - { - if( C::debugTestRun ) std::cerr << "Trying test " << name << std::endl; - - if( explicitlyNamed( name ) or not disabled and selected( name ) ) - { - std::cout << C::testInfo << "BEGIN" << resetStyle << " : " << name << std::endl; - try - { - test(); - std::cout << " " << C::testPass << "SUCCESS" << resetStyle << ": " << name << std::endl; - } - catch( ... ) - { - try - { - failed= true; - std::cout << " " << C::testFail << "FAILURE" << resetStyle << ": " << name; - throw; - } - catch( const TestFailureException &fail ) { std::cout << " -- " << fail.failureCount << " failures."; } - catch( ... ) { std::cout << " -- unknown failure count"; } - std::cout << std::endl; - } - - std::cout << C::testInfo << "FINISHED" << resetStyle << ": " << name << std::endl; - } - } - - return failed ? EXIT_FAILURE : EXIT_SUCCESS; - } - - [[nodiscard]] inline int - runAllTests( const argcnt_t argcnt, const argvec_t argvec ) - { - return runAllTests( { argvec + 1, argvec + argcnt } ); - } + [[nodiscard]] int runAllTests( const argcnt_t argcnt, const argvec_t argvec ); } } diff --git a/string_algorithms.test/0.cc b/string_algorithms.test/0.cc index b982d52..ba13f4e 100644 --- a/string_algorithms.test/0.cc +++ b/string_algorithms.test/0.cc @@ -55,5 +55,6 @@ static auto init= enroll <=[] { "Empty string", { "", ':' }, { "" } }, { "Single token", { "item", ':' }, { "item" } }, { "Two tokens", { "first:second", ':' }, { "first", "second" } }, + { "Empty string many tokens", { ":::", ':' }, { "", "", "", "" } }, }; };