From 987aa454dbdeb9fa518fd8b14e512395efa6356b Mon Sep 17 00:00:00 2001 From: ADAM David Alan Martin Date: Wed, 25 Oct 2023 23:58:16 -0400 Subject: [PATCH 1/6] Make test framework start using concepts... --- Testing/test.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Testing/test.h b/Testing/test.h index bcae02c..1e0076e 100644 --- a/Testing/test.h +++ b/Testing/test.h @@ -96,7 +96,7 @@ namespace Alepha::Hydrogen::Testing explicit TestFailureException( 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 ) { From 685d33527e81048fc51b6d50d7a01a4cf3507536 Mon Sep 17 00:00:00 2001 From: ADAM David Alan Martin Date: Wed, 25 Oct 2023 23:59:45 -0400 Subject: [PATCH 2/6] Table test output alignment. --- Testing/TableTest.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Testing/TableTest.h b/Testing/TableTest.h index 942deaa..08663ff 100644 --- a/Testing/TableTest.h +++ b/Testing/TableTest.h @@ -269,11 +269,11 @@ namespace Alepha::Hydrogen::Testing ::detail:: table_test const auto result= witness == expected; if( not result ) { - std::cout << " " << C::testFail << "FAILED CASE" << resetStyle << ": " << comment << std::endl; + std::cout << " " << C::testFail << "FAILED CASE" << resetStyle << ": " << comment << std::endl; ++failureCount; printDebugging< outputMode >( witness, expected ); } - else std::cout << " " << C::testPass << "PASSED CASE" << resetStyle << ": " << comment << std::endl; + else std::cout << " " << C::testPass << "PASSED CASE" << resetStyle << ": " << comment << std::endl; } return failureCount; From 172ad645966c92d9b863f3d76a0fcf5c0340bfd0 Mon Sep 17 00:00:00 2001 From: ADAM David Alan Martin Date: Wed, 25 Oct 2023 23:59:56 -0400 Subject: [PATCH 3/6] Add an extra test to `split`. --- string_algorithms.test/0.cc | 1 + 1 file changed, 1 insertion(+) 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", { ":::", ':' }, { "", "", "", "" } }, }; }; From 6b3492636a18f87192fc46f4bc7bc02a0e399fc9 Mon Sep 17 00:00:00 2001 From: ADAM David Alan Martin Date: Thu, 26 Oct 2023 00:38:05 -0400 Subject: [PATCH 4/6] Make test exception not part of the hierarchy. (It shouldn't be caught, but for the framework. Although `Condition` may be the right type?) --- Testing/test.h | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Testing/test.h b/Testing/test.h index 1e0076e..ff29d31 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 { @@ -89,11 +89,13 @@ namespace Alepha::Hydrogen::Testing 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< Integral Integer > @@ -106,7 +108,7 @@ namespace Alepha::Hydrogen::Testing { if( not test() ) { - throw TestFailureException{ 1 }; + throw TestFailure{ 1 }; } }; @@ -117,7 +119,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 +143,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 ); } }; @@ -211,7 +213,7 @@ namespace Alepha::Hydrogen::Testing std::cout << " " << C::testFail << "FAILURE" << resetStyle << ": " << name; throw; } - catch( const TestFailureException &fail ) { std::cout << " -- " << fail.failureCount << " failures."; } + catch( const TestFailure &fail ) { std::cout << " -- " << fail.failureCount << " failures."; } catch( ... ) { std::cout << " -- unknown failure count"; } std::cout << std::endl; } From bba2544780b904c0670346fbd977f3a9f17a1eb5 Mon Sep 17 00:00:00 2001 From: ADAM David Alan Martin Date: Thu, 26 Oct 2023 00:54:33 -0400 Subject: [PATCH 5/6] Split out some of the test core into its own TU. This might cut down on test build times? It will let me polish up some of this stuff without needing to recompile some things. --- Testing/CMakeLists.txt | 2 +- Testing/test.cc | 80 ++++++++++++++++++++++++++++++++++++++++++ Testing/test.h | 78 ++++------------------------------------ 3 files changed, 87 insertions(+), 73 deletions(-) create mode 100644 Testing/test.cc 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/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 ff29d31..d5c1739 100644 --- a/Testing/test.h +++ b/Testing/test.h @@ -72,22 +72,12 @@ 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; - - registry().emplace_back( name.name, name.disabled, test ); - assert( not registry().empty() ); - assert( std::get< 1 >( registry().back() ) == name.disabled ); - - return rv; - }; + struct TestRegistration {}; + TestRegistration operator <= ( TestName name, std::function< void () > test ); + } struct exports::TestFailure { @@ -171,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 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]] 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 ); } } From bd3309e7ce6c7902bb548aaa6c8e2b3fe52ba1e3 Mon Sep 17 00:00:00 2001 From: ADAM David Alan Martin Date: Thu, 26 Oct 2023 01:32:46 -0400 Subject: [PATCH 6/6] The beginnings of `UniversalCases`. The rest has to be fleshed out. Then I can pivot the existing cases to this, I think. --- Testing/TableTest.h | 70 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/Testing/TableTest.h b/Testing/TableTest.h index 890418d..46f6930 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