1
0
forked from Alepha/Alepha
Files
Alepha/Testing/TableTest.h

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 &param )
{
// 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;
}