static_assert( __cplusplus > 2020'99 ); #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "colors.h" #include "printDebugging.h" namespace Alepha::Hydrogen::Testing ::detail:: TableTest_m { inline namespace exports {} inline void breakpoint() {} namespace C:: inline Colors { using namespace Testing::detail::colors_m::C:: Colors; } enum class TestResult { Passed, Failed }; struct BlankBase {}; template< typename T > static consteval auto compute_base_f() noexcept { if constexpr ( Aggregate< T > ) return std::type_identity< Utility::TupleAdapter< T > >{}; else if constexpr( std::is_class_v< T > ) return std::type_identity< T >{}; else return std::type_identity< BlankBase >{}; } template< typename T > using compute_base_t= typename decltype( compute_base_f< std::decay_t< T > >() )::type; template< typename T > requires( DerivedFrom< T, std::exception > or DerivedFrom< T, Alepha::Exception > ) std::string getMessageFromException( const T &ex ) { if constexpr( DerivedFrom< T, std::exception > ) return ex.what(); else return ex.message(); } template< typename T > concept ExceptionLike= DerivedFrom< T, std::exception > or DerivedFrom< T, Alepha::Exception >; template< typename T, typename Param > concept FunctionOver= Concepts::UnaryFunction< T > and Concepts::SameAs< get_args_t< T >, std::tuple< Param > >; template< typename T > concept FunctionTakingThrowable= UnaryFunction< T > and ExceptionLike< get_arg_t< T, 0 > >; template< typename return_type, OutputMode outputMode > struct BasicUniversalHandler : compute_base_t< return_type > { using ComputedBase= compute_base_t< return_type >; using ComputedBase::ComputedBase; using Invoker= std::function< return_type () >; std::function< std::tuple< TestResult, std::optional< std::string > > ( Invoker, const std::string & ) > impl; template< FunctionTakingThrowable Verifier > BasicUniversalHandler( Verifier verifier ) : impl { [verifier] ( Invoker invoker, const std::string &comment ) -> std::tuple< TestResult, std::optional< std::string > > { try { invoker(); return { TestResult::Failed, "Expected an exception but none was thrown." }; } catch( const get_arg_t< Verifier, 0 > &ex ) { if( verifier( ex ) ) return { TestResult::Passed, std::nullopt }; return { TestResult::Failed, "Verification of exception contents failed." }; } catch( ... ) { try { throw; } catch( const std::exception &ex ) { return { TestResult::Failed, "Unexpected Exception Type: " + Utility::fancyTypeName( typeid( ex ) ) }; } catch( const Alepha::Exception &ex ) { return { TestResult::Failed, "Unexpected Exception Type: " + Utility::fancyTypeName( typeid( ex ) ) }; } catch( ... ) { return { TestResult::Failed, "Unknown Exception Type." }; } } } } {} BasicUniversalHandler( const return_type expected ) : impl { [expected]( Invoker invoker, const std::string &comment ) -> std::tuple< TestResult, std::optional< std::string > > { struct ErrorMessage { std::string mesg; }; const auto witness= Utility::evaluate <=[&]() -> std::variant< return_type, ErrorMessage > { try { return invoker(); } catch( const std::exception &ex ) { return ErrorMessage{ Utility::fancyTypeName( typeid( ex ) ) }; } catch( ... ) { return ErrorMessage{ "Unknown Exception Type." }; } }; const auto result= ( std::holds_alternative< return_type >( witness ) and std::get< return_type >( witness ) == expected ) ? TestResult::Passed : TestResult::Failed; std::ostringstream oss; if( result == TestResult::Failed ) { if( std::holds_alternative< return_type >( witness ) ) { streamDebugging< outputMode >( oss, std::get< return_type >( witness ), expected ); } else oss << "Unexpected exception of type: " << std::get< ErrorMessage >( witness ).mesg; } return { result, oss.str().empty() ? std::optional< std::string >{} : std::move( oss ).str() }; } } {} template< typename ... Params > std::tuple< TestResult, std::optional< std::string > > operator() ( Invoker invoker, const std::string &comment, const std::tuple< Params... > params ) const { if( impl != nullptr ) return impl( invoker, comment ); if constexpr( std::is_base_of_v< std::decay_t< return_type >, ComputedBase > ) { const return_type *const expected_p= this; const auto expected= *expected_p; breakpoint(); struct ErrorMessage { std::string mesg; }; const auto witness= Utility::evaluate <=[&]() -> std::variant< return_type, ErrorMessage > { try { return invoker(); } catch( const std::exception &ex ) { return ErrorMessage{ Utility::fancyTypeName( typeid( ex ) ) }; } catch( ... ) { return ErrorMessage{ "Unknown Exception Type." }; } }; const auto result= ( std::holds_alternative< return_type >( witness ) and std::get< return_type >( witness ) == expected ) ? TestResult::Passed : TestResult::Failed; std::ostringstream oss; if( result == TestResult::Failed ) { if( std::holds_alternative< return_type >( witness ) ) { streamDebugging< outputMode >( oss, std::get< return_type >( witness ), expected ); } else oss << "Unexpected exception of type: " << std::get< ErrorMessage >( witness ).mesg; oss << std::endl << "Test inputs were: " << std::endl; int index= 0; template_for( params ) <=[&]( const auto ¶m ) { // Debugging output for test inputs is relaxed, as it's not required they be streamable? oss << "Argument " << index++ << ": " << streamAdaptValue< OutputMode::Relaxed >( param ) << std::endl; }; } return { result, oss.str().empty() ? std::optional< std::string >{} : std::move( oss ).str() }; } else throw std::logic_error( "Somehow we didn't setup impl, and it's not an adapted case!" ); } template< typename T > requires( not SameAs< T, void > ) BasicUniversalHandler( std::type_identity< T > ) : impl { []( Invoker invoker, const std::string &comment ) -> std::tuple< TestResult, std::optional< std::string > > { try { try { std::ignore= invoker(); breakpoint(); return { TestResult::Failed, IOStreams::String() << "No exception was thrown, but " << Utility::fancyTypeName< T >() << " expected." }; } catch( const T & ) { return { TestResult::Passed, std::nullopt }; } } catch( const std::exception &ex ) { return { TestResult::Failed, IOStreams::String() << "Incorrect exception type " << Utility::fancyTypeName( typeid( ex ) ) << " was thrown, but " << Utility::fancyTypeName< T >() << " was expected." }; } catch( ... ) { return { TestResult::Failed, IOStreams::String() << "Incorrect exception type () was thrown, but " << Utility::fancyTypeName< T >() << " was expected." }; } } } {} template< typename T > requires( SameAs< T, std::type_identity< void > > or SameAs< T, std::nothrow_t > ) BasicUniversalHandler( T ) : impl { []( Invoker invoker, const std::string &comment ) -> std::tuple< TestResult, std::optional< std::string > > { try { std::ignore= invoker(); return { TestResult::Passed, std::nullopt }; } catch( const std::exception &ex ) { return { TestResult::Failed, IOStreams::String() << "Exception type " << Utility::fancyTypeName( typeid( ex ) ) << " was thrown, but " "no exception was expected." }; } catch( ... ) { return { TestResult::Failed, IOStreams::String() << "Exception type () was thrown, but " "no exception was expected." }; } } } {} template< typename T > requires ( ( false or DerivedFrom< T, std::exception > or DerivedFrom< T, Alepha::Exception > ) and not std::is_same_v< return_type, T > ) BasicUniversalHandler( const T exemplar ) : impl { [expected= getMessageFromException( exemplar )]( Invoker invoker, const std::string &comment ) -> std::tuple< TestResult, std::optional< std::string > > { try { try { invoker(); return { TestResult::Failed, IOStreams::String{} << "expected exception `" << Utility::fancyTypeName< T >() << "` wasn't thrown." }; } catch( const T &ex ) { const std::string witness= getMessageFromException( ex ); const TestResult rv= witness == expected ? TestResult::Passed : TestResult::Failed; std::ostringstream oss; if( rv == TestResult::Failed ) { oss << "expected message did not match." << std::endl; streamDebugging< outputMode >( oss, witness, expected ); } return { rv, oss.str().empty() ? std::optional< std::string >{} : std::move( oss ).str() }; } } catch( const std::exception &ex ) { return { TestResult::Failed, IOStreams::String() << "Incorrect exception type " << Utility::fancyTypeName( typeid( ex ) ) << " was thrown, but " << Utility::fancyTypeName< T >() << " was expected." }; } catch( const Exception &ex ) { return { TestResult::Failed, IOStreams::String() << "Incorrect exception type " << Utility::fancyTypeName( typeid( ex ) ) << " was thrown, but " << Utility::fancyTypeName< T >() << " was expected." }; } catch( ... ) { return { TestResult::Failed, IOStreams::String() << "Incorrect exception type () was thrown, but " << Utility::fancyTypeName< T >() << " was expected." }; } } } {} }; template< typename F > concept FunctionVariable= requires( const F &f ) { { std::function{ f } }; }; namespace exports { template< FunctionVariable auto, OutputMode outputMode= OutputMode::All > struct TableTest; } namespace C { const bool debug= false; const bool debugCaseTypes= false or C::debug; } using std::begin, std::end; using namespace Utility::exports::evaluation_helpers_m; template< template< typename, typename... > class Sequence, typename ... TupleArgs > auto withIndex( const Sequence< std::tuple< TupleArgs... > > &original ) { auto indices= evaluate <=[&] { std::vector< int > v{ std::distance( begin( original ), end( original ) ) }; std::iota( begin( v ), end( v ), 0 ); return v; }; auto bindIndex= []( const auto i, const auto e ) { return std::tuple_cat( i, e ); }; using indexed_table_entry= decltype( bindIndex( indices.front(), original.front() ) ); std::vector< indexed_table_entry > rv; std::transform( begin( indices ), end( indices ), begin( original ), std::back_inserter( rv ), bindIndex ); return rv; } struct UniversalCasesBase { using TestFunction_t= std::tuple< TestResult, std::optional< std::string > > ( const std::string & ); using TestFunction= std::function< TestFunction_t >; struct TestRunner { std::string comment; TestFunction handler; }; std::vector< TestRunner > tests; virtual ~UniversalCasesBase()= default; int operator() () const; }; struct CaseComment { enum State : bool { Enabled= true, Disabled= false } state= Enabled; std::string comment; CaseComment( const Concepts::ConvertibleTo< std::string > auto &comment ) : comment( comment ) {} explicit CaseComment( const State state, const std::string &comment ) : state( state ), comment( comment ) {} auto operator not () const { return CaseComment{ State( not state ), comment }; } auto operator -() const { return CaseComment{ State( not state ), comment }; } }; namespace exports { inline namespace literals { inline CaseComment operator ""_case ( const char *const ch, const std::size_t amt ) { return CaseComment{ std::string{ ch, ch + amt } }; } } enum { Enable= 1, Skip= -1, Disable= -1 }; inline auto operator - ( const decltype( Enable ) e ) { return decltype( Enable )( -int( e ) ); } inline auto operator not ( const decltype( Enable ) e ) { return e == Enable ? Enable : Disable; } inline auto operator <= ( const decltype( Enable ) token, const CaseComment &comment ) { if( token == Disable ) return CaseComment{ CaseComment::Disabled, comment.comment }; else return CaseComment{ CaseComment::Enabled, comment.comment }; } } template< FunctionVariable auto function, OutputMode outputMode > struct exports::TableTest { using function_traits_type= function_traits< decltype( function ) >; using args_type= Meta::product_type_decay_t< typename function_traits_type::args_type >; using return_type= typename function_traits_type::return_type; using ComputedBase= compute_base_t< return_type >; struct UniversalCases { using RunDescription= std::tuple< std::string, args_type, return_type >; using Invoker= std::function< return_type () >; using UniversalHandler= BasicUniversalHandler< return_type, outputMode >; struct TestDescription { CaseComment comment; args_type args; UniversalHandler handler; }; UniversalCasesBase tests; UniversalCases( std::initializer_list< TestDescription > initList ) { for( const auto [ comment, params, handler ]: initList ) { if( C::debugCaseTypes ) std::cerr << Utility::fancyTypeName( typeid( params ) ) << std::endl; auto invoker= [=] { breakpoint(); return std::apply( function, params ); }; const auto checker= [=]( const auto comment ) { return handler( invoker, comment, params ); }; if( comment.state == CaseComment::Enabled ) { tests.tests.push_back( { comment.comment, checker } ); } else { auto skippedHandler= [] ( const auto comment ) -> std::tuple< TestResult, std::optional< std::string > > { return { TestResult::Passed, "Skipped." }; }; tests.tests.push_back( { comment.comment, skippedHandler } ); } } } int operator() () const { return tests(); } }; // 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; using Cases= UniversalCases; //using ExceptionCases= ExceptionCases_real; using ExceptionCases= UniversalCases; }; } namespace Alepha::Hydrogen::Testing::inline exports::inline TableTest_m { using namespace detail::TableTest_m::exports; } namespace Alepha::Hydrogen::Testing::inline exports::inline literals::inline test_literals { using namespace detail::TableTest_m::exports::literals; }