diff --git a/doc/lightweight_test.qbk b/doc/lightweight_test.qbk index 8c7fa4dd..afbd95e0 100644 --- a/doc/lightweight_test.qbk +++ b/doc/lightweight_test.qbk @@ -59,6 +59,19 @@ namespace core void lwt_init(); +class lwt_context +{ +public: + template explicit lwt_context(const T* value); + template lwt_context(const T0* value0, const T1* value1); + template lwt_context(const T0* value0, const T1* value1, const T2* value2); + + lwt_context(const lwt_context&) = delete; + lwt_context& operator=(const lwt_context&) = delete; + + ~lwt_context(); +}; + } // namespace core } // namespace boost `` @@ -273,6 +286,59 @@ to e.g. an assertion failure before the first test macro is invoked. [endsect] +[section lwt_context] + +This class can be used to attach context to tests. When an assertion fails, +the values passed to `lwt_context` will be printed. This is useful when +the assertion's source location is not enough by itself to diagnose +the cause of failures. Potential use cases include: + +* Parametric tests: when running the same test function over a range of inputs, + `lwt_context` can be used to print the parameter value that caused the assertion to fail. +* When writing utility functions called from several tests, `lwt_context` can be used to + diagnose which test caused the function to fail. + +The context is arranged as a stack composed of frames. When constructing a `lwt_context` +object, a new context frame is pushed to the stack. When it is destroyed, +the frame is popped. + + +`` +template +explicit lwt_context(const T* value); + +template +lwt_context(const T0* value0, const T1* value1); + +template +lwt_context(const T0* value0, const T1* value1, const T2* value2); +`` + +Pushes a new frame to the top of the context stack. Depending on the chosen constructor, +the frame will contain one, two, or three values. +These values will be printed using `operator<<` to `std::cerr` when an assertion fails. + +The caller retains ownership of the passed values. They should be kept alive until +the context stack frame is popped by `lwt_context::~lwt_context()`. + +`` +lwt_context(const lwt_context&) = delete; +lwt_context& operator=(const lwt_context&) = delete; +`` + +This class is neither copyable not movable. + +`` +~lwt_context(); +`` + +Removes the top frame from the context stack. + + +[endsect] + + + [section Example] `` @@ -294,6 +360,42 @@ int main() [endsect] +[section Parametric test example] + +`` +#include + +int sqr( int x ) +{ + return x * x; +} + +int main() +{ + struct + { + int input; + int expected; + } test_cases [] = { + { 2, 4 }, + { -3, 9 }, + { 0, 0 } + }; + + for (const auto& tc : test_cases) + { + // Print the offending parameter on error + boost::core::lwt_context frame (&input); + + BOOST_TEST_EQ( sqr(tc.input), tc.expected ); + } + + return boost::report_errors(); +} +`` + +[endsect] + [endsect] [section Header ] diff --git a/include/boost/core/lightweight_test.hpp b/include/boost/core/lightweight_test.hpp index 43baea05..2a1d3af4 100644 --- a/include/boost/core/lightweight_test.hpp +++ b/include/boost/core/lightweight_test.hpp @@ -45,11 +45,93 @@ namespace boost namespace detail { +// specialize test output for char pointers to avoid printing as cstring +template inline const T& test_output_impl(const T& v) { return v; } +inline const void* test_output_impl(const char* v) { return v; } +inline const void* test_output_impl(const unsigned char* v) { return v; } +inline const void* test_output_impl(const signed char* v) { return v; } +inline const void* test_output_impl(char* v) { return v; } +inline const void* test_output_impl(unsigned char* v) { return v; } +inline const void* test_output_impl(signed char* v) { return v; } +template inline const void* test_output_impl(T volatile* v) { return const_cast(v); } + +#if !defined( BOOST_NO_CXX11_NULLPTR ) +inline const void* test_output_impl(std::nullptr_t) { return nullptr; } +#endif + +// print chars as numeric + +inline int test_output_impl( signed char const& v ) { return v; } +inline unsigned test_output_impl( unsigned char const& v ) { return v; } + +// Whether wchar_t is signed is implementation-defined + +template struct lwt_long_type {}; +template<> struct lwt_long_type { typedef long type; }; +template<> struct lwt_long_type { typedef unsigned long type; }; + +inline lwt_long_type<(static_cast(-1) < static_cast(0))>::type test_output_impl( wchar_t const& v ) { return v; } + +#if !defined( BOOST_NO_CXX11_CHAR16_T ) +inline unsigned long test_output_impl( char16_t const& v ) { return v; } +#endif + +#if !defined( BOOST_NO_CXX11_CHAR32_T ) +inline unsigned long test_output_impl( char32_t const& v ) { return v; } +#endif + +inline std::string test_output_impl( char const& v ) +{ + if( std::isprint( static_cast( v ) ) ) + { + return std::string( 1, v ); + } + else + { + static const char char_table[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + char buffer[ 4 ]; + buffer[ 0 ] = '\\'; + buffer[ 1 ] = 'x'; + buffer[ 2 ] = char_table[ (static_cast( v ) >> 4u) & 0x0f ]; + buffer[ 3 ] = char_table[ static_cast( v ) & 0x0f ]; + + return std::string( buffer, 4u ); + } +} + +// Allow associating arbitrary context values to tests that get printed on error +struct lwt_context_frame +{ + static const int max_values = 3; + + template + static void do_print(const void* value) + { + BOOST_LIGHTWEIGHT_TEST_OSTREAM << '\'' + << boost::detail::test_output_impl(*static_cast(value)) << '\''; + } + + // A type-erased reference to a streamable value + struct context_value { + const void* obj; + void (*print_fn) (const void*); + + void print() const { print_fn(obj); } + void set_null() { obj = NULL; print_fn = NULL; } + template void set_value(const T* value) { obj = value; print_fn = &do_print; } + }; + + // Multiple context frames are allowed, creating a stack (forward linked list). + // It's rooted at test_result, with the last pushed frame first. + lwt_context_frame* next; + context_value values [max_values]; +}; + class test_result { public: - test_result(): report_( false ), errors_( 0 ) + test_result(): report_( false ), errors_( 0 ), context_ ( NULL ) { core::detail::lwt_unattended(); } @@ -63,7 +145,7 @@ class test_result } } - int& errors() + int errors() const { return errors_; } @@ -73,10 +155,56 @@ class test_result report_ = true; } + void push_context(lwt_context_frame& frame) + { + frame.next = context_; + context_ = &frame; + } + + void pop_context() + { + context_ = context_->next; + } + + void on_error() + { + ++errors_; + print_context(); + } + private: bool report_; int errors_; + lwt_context_frame* context_; + + void print_context() + { + // If there is no context, do nothing + if (!context_) + return; + + BOOST_LIGHTWEIGHT_TEST_OSTREAM << "Failure happened in the following context:\n"; + + // Go through the linked list + lwt_context_frame* frame = context_; + for (int i = 0; frame; ++i, frame = frame->next) + { + // Print the header and the first value, which should always be present + BOOST_LIGHTWEIGHT_TEST_OSTREAM << " #" << i << ": "; + frame->values[0].print(); + + // Print any other values, if present + for (int j = 1; j < lwt_context_frame::max_values && frame->values[j].obj; ++j) + { + BOOST_LIGHTWEIGHT_TEST_OSTREAM << ", "; + frame->values[j].print(); + } + + // Finish the frame + BOOST_LIGHTWEIGHT_TEST_OSTREAM << '\n'; + } + } }; inline test_result& test_results() @@ -85,11 +213,6 @@ inline test_result& test_results() return instance; } -inline int& test_errors() -{ - return test_results().errors(); -} - inline bool test_impl(char const * expr, char const * file, int line, char const * function, bool v) { if( v ) @@ -102,7 +225,7 @@ inline bool test_impl(char const * expr, char const * file, int line, char const BOOST_LIGHTWEIGHT_TEST_OSTREAM << file << "(" << line << "): test '" << expr << "' failed in function '" << function << "'" << std::endl; - ++test_results().errors(); + test_results().on_error(); return false; } } @@ -112,7 +235,7 @@ inline void error_impl(char const * msg, char const * file, int line, char const BOOST_LIGHTWEIGHT_TEST_OSTREAM << file << "(" << line << "): " << msg << " in function '" << function << "'" << std::endl; - ++test_results().errors(); + test_results().on_error(); } inline void throw_failed_impl(const char* expr, char const * excep, char const * file, int line, char const * function) @@ -120,7 +243,7 @@ inline void throw_failed_impl(const char* expr, char const * excep, char const * BOOST_LIGHTWEIGHT_TEST_OSTREAM << file << "(" << line << "): expression '" << expr << "' did not throw exception '" << excep << "' in function '" << function << "'" << std::endl; - ++test_results().errors(); + test_results().on_error(); } inline void no_throw_failed_impl(const char* expr, const char* file, int line, const char* function) @@ -128,7 +251,7 @@ inline void no_throw_failed_impl(const char* expr, const char* file, int line, c BOOST_LIGHTWEIGHT_TEST_OSTREAM << file << "(" << line << "): expression '" << expr << "' threw an exception in function '" << function << "'" << std::endl; - ++test_results().errors(); + test_results().on_error(); } inline void no_throw_failed_impl(const char* expr, const char* what, const char* file, int line, const char* function) @@ -136,7 +259,7 @@ inline void no_throw_failed_impl(const char* expr, const char* what, const char* BOOST_LIGHTWEIGHT_TEST_OSTREAM << file << "(" << line << "): expression '" << expr << "' threw an exception in function '" << function << "': " << what << std::endl; - ++test_results().errors(); + test_results().on_error(); } // In the comparisons below, it is possible that T and U are signed and unsigned integer types, which generates warnings in some compilers. @@ -156,59 +279,6 @@ inline void no_throw_failed_impl(const char* expr, const char* what, const char* # pragma GCC diagnostic ignored "-Wsign-conversion" #endif -// specialize test output for char pointers to avoid printing as cstring -template inline const T& test_output_impl(const T& v) { return v; } -inline const void* test_output_impl(const char* v) { return v; } -inline const void* test_output_impl(const unsigned char* v) { return v; } -inline const void* test_output_impl(const signed char* v) { return v; } -inline const void* test_output_impl(char* v) { return v; } -inline const void* test_output_impl(unsigned char* v) { return v; } -inline const void* test_output_impl(signed char* v) { return v; } -template inline const void* test_output_impl(T volatile* v) { return const_cast(v); } - -#if !defined( BOOST_NO_CXX11_NULLPTR ) -inline const void* test_output_impl(std::nullptr_t) { return nullptr; } -#endif - -// print chars as numeric - -inline int test_output_impl( signed char const& v ) { return v; } -inline unsigned test_output_impl( unsigned char const& v ) { return v; } - -// Whether wchar_t is signed is implementation-defined - -template struct lwt_long_type {}; -template<> struct lwt_long_type { typedef long type; }; -template<> struct lwt_long_type { typedef unsigned long type; }; - -inline lwt_long_type<(static_cast(-1) < static_cast(0))>::type test_output_impl( wchar_t const& v ) { return v; } - -#if !defined( BOOST_NO_CXX11_CHAR16_T ) -inline unsigned long test_output_impl( char16_t const& v ) { return v; } -#endif - -#if !defined( BOOST_NO_CXX11_CHAR32_T ) -inline unsigned long test_output_impl( char32_t const& v ) { return v; } -#endif - -inline std::string test_output_impl( char const& v ) -{ - if( std::isprint( static_cast( v ) ) ) - { - return std::string( 1, v ); - } - else - { - static const char char_table[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; - char buffer[ 4 ]; - buffer[ 0 ] = '\\'; - buffer[ 1 ] = 'x'; - buffer[ 2 ] = char_table[ (static_cast( v ) >> 4u) & 0x0f ]; - buffer[ 3 ] = char_table[ static_cast( v ) & 0x0f ]; - - return std::string( buffer, 4u ); - } -} // predicates @@ -303,7 +373,7 @@ inline bool test_with_impl(BinaryPredicate pred, char const * expr1, char const << file << "(" << line << "): test '" << expr1 << " " << lwt_predicate_name(pred) << " " << expr2 << "' ('" << test_output_impl(t) << "' " << lwt_predicate_name(pred) << " '" << test_output_impl(u) << "') failed in function '" << function << "'" << std::endl; - ++test_results().errors(); + test_results().on_error(); return false; } } @@ -321,7 +391,7 @@ inline bool test_cstr_eq_impl( char const * expr1, char const * expr2, BOOST_LIGHTWEIGHT_TEST_OSTREAM << file << "(" << line << "): test '" << expr1 << " == " << expr2 << "' ('" << t << "' == '" << u << "') failed in function '" << function << "'" << std::endl; - ++test_results().errors(); + test_results().on_error(); return false; } } @@ -339,7 +409,7 @@ inline bool test_cstr_ne_impl( char const * expr1, char const * expr2, BOOST_LIGHTWEIGHT_TEST_OSTREAM << file << "(" << line << "): test '" << expr1 << " != " << expr2 << "' ('" << t << "' != '" << u << "') failed in function '" << function << "'" << std::endl; - ++test_results().errors(); + test_results().on_error(); return false; } } @@ -409,7 +479,7 @@ bool test_all_eq_impl(FormattedOutputFunction& output, else { output << std::endl; - ++test_results().errors(); + test_results().on_error(); return false; } } @@ -480,7 +550,7 @@ bool test_all_with_impl(FormattedOutputFunction& output, else { output << std::endl; - ++test_results().errors(); + test_results().on_error(); return false; } } @@ -527,6 +597,48 @@ inline void lwt_init() boost::detail::test_results(); } +class lwt_context +{ + boost::detail::lwt_context_frame frame_; + + // Disallow copy and movement + BOOST_DELETED_FUNCTION(lwt_context(const lwt_context&)) + BOOST_DELETED_FUNCTION(lwt_context& operator=(const lwt_context&)) + +public: + template + explicit lwt_context(const T* value) + { + frame_.values[0].set_value(value); + frame_.values[1].set_null(); + frame_.values[2].set_null(); + boost::detail::test_results().push_context(frame_); + } + + template + lwt_context(const T0* value0, const T1* value1) + { + frame_.values[0].set_value(value0); + frame_.values[1].set_value(value1); + frame_.values[2].set_null(); + boost::detail::test_results().push_context(frame_); + } + + template + lwt_context(const T0* value0, const T1* value1, const T2* value2) + { + frame_.values[0].set_value(value0); + frame_.values[1].set_value(value1); + frame_.values[2].set_value(value2); + boost::detail::test_results().push_context(frame_); + } + + ~lwt_context() + { + boost::detail::test_results().pop_context(); + } +}; + } // namespace core } // namespace boost diff --git a/include/boost/core/lightweight_test_trait.hpp b/include/boost/core/lightweight_test_trait.hpp index eea23717..62761f6c 100644 --- a/include/boost/core/lightweight_test_trait.hpp +++ b/include/boost/core/lightweight_test_trait.hpp @@ -46,7 +46,7 @@ template< class T > inline void test_trait_impl( char const * trait, void (*)( T << "' (should have been " << ( expected? "true": "false" ) << ")" << std::endl; - ++test_results().errors(); + test_results().on_error(); } } @@ -71,7 +71,7 @@ template inline void test_trait_same_impl( char const * type << "' != '" << boost::core::type_name() << "')" << std::endl; - ++test_results().errors(); + test_results().on_error(); } } diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index c8d302f5..a22a89e8 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -176,6 +176,8 @@ run lightweight_test_with_test.cpp : : : $(pedantic-errors) ; run-fail lightweight_test_with_fail.cpp ; +run lightweight_test_context1.cpp ; + run is_same_test.cpp ; run typeinfo_test.cpp ; diff --git a/test/lightweight_test_context1.cpp b/test/lightweight_test_context1.cpp new file mode 100644 index 00000000..ee0d83cf --- /dev/null +++ b/test/lightweight_test_context1.cpp @@ -0,0 +1,57 @@ +// +// Negative test for BOOST_TEST +// +// Copyright (c) 2014 Peter Dimov +// +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// + +#include +#include +#include +#include + +static std::stringstream test_log; // Intercept logging + +#define BOOST_LIGHTWEIGHT_TEST_OSTREAM ::test_log +#include + +static bool check_log(const char* expected) +{ + std::string str = test_log.str(); + std::string::size_type pos = str.find('\n'); + std::string substr; + if (pos != std::string::npos) { + substr = str.substr(pos + 1); + } + if (substr != expected) { + std::cerr << "Error: expected the following log:\n" << expected << "\n, got:\n" << substr << std::endl; + return false; + } + return true; +} + +int main() +{ + // Run a test that fails + int x = 42; + boost::core::lwt_context frame (&x); + BOOST_TEST( x == 1 ); + + // Check the log + const char* const expected_log = "Failure happened in the following context:\n #0: '42'\n"; + bool log_ok = check_log(expected_log); + + // Check that we get the number of expected errors + int actual_errors = boost::report_errors(); + const int expected_errors = 1; + if (actual_errors != expected_errors) + { + std::cerr << "Expected " << expected_errors << ", got " << actual_errors << " errors\n"; + return 1; + } + + return log_ok ? 0 : 1; +}