forked from Alepha/Alepha
523 lines
15 KiB
C++
523 lines
15 KiB
C++
static_assert( __cplusplus > 2020'99 );
|
|
|
|
#pragma once
|
|
|
|
#include <Alepha/Alepha.h>
|
|
|
|
#include <cstdint>
|
|
|
|
#include <tuple>
|
|
#include <string>
|
|
#include <variant>
|
|
#include <iostream>
|
|
#include <algorithm>
|
|
#include <typeinfo>
|
|
#include <numeric>
|
|
#include <iomanip>
|
|
|
|
#include <Alepha/function_traits.h>
|
|
#include <Alepha/template_for.h>
|
|
|
|
#include <Alepha/IOStreams/String.h>
|
|
|
|
#include <Alepha/Utility/evaluation_helpers.h>
|
|
#include <Alepha/Utility/TupleAdapter.h>
|
|
#include <Alepha/Utility/fancyTypeName.h>
|
|
|
|
#include <Alepha/Reflection/tuplizeAggregate.h>
|
|
|
|
#include <Alepha/Console.h>
|
|
|
|
#include <Alepha/Exception.h>
|
|
|
|
#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 (<Unknown>) 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 (<Unknown>) 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 (<Unknown>) 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;
|
|
}
|