From 13575d24048cb3f3c3128e89575fdac70fb0d469 Mon Sep 17 00:00:00 2001 From: Dave Abrahams Date: Tue, 20 Aug 2024 16:21:40 -0700 Subject: [PATCH] Configuration file, minimal handler support (#17) * Documentation updates * implement configuration file inclusion and inline handler, * Fix emscripten; use minimal trap on all GCCs --- README.md | 15 +++++--- include/adobe/contract_checks.hpp | 52 +++++++++++++++++----------- test/CMakeLists.txt | 13 +++++-- test/default_handler_tests.cpp | 32 +---------------- test/inline_handler_assurance.cpp | 4 +++ test/minimal_configuration.hpp | 1 + test/minimal_configuration_tests.cpp | 18 ++++++++++ test/portable_death_tests.hpp | 47 +++++++++++++++++++++++++ 8 files changed, 122 insertions(+), 60 deletions(-) create mode 100644 test/inline_handler_assurance.cpp create mode 100644 test/minimal_configuration.hpp create mode 100644 test/minimal_configuration_tests.cpp create mode 100644 test/portable_death_tests.hpp diff --git a/README.md b/README.md index 52d938b..e3a33c7 100644 --- a/README.md +++ b/README.md @@ -390,11 +390,16 @@ ctest --output-on-failure --test-dir ../build # test ## Reference -- `ADOBE_MINIMAL_TRAP()`: Invoke this macro to inject minimal code - that stops your program; it's usually just one machine instruction - and more efficient than calling `terminate()`. You'd typically use - this macro from a minimal contract violation handler (see the - example project). +- `ADOBE_MINIMAL_TRAP()`: Invoke this macro, followed by a semicolon, + to inject minimal code that stops your program; it's usually just + one machine instruction and more efficient than calling + `terminate()`. You'd typically use this macro from a minimal + contract violation handler (see the example project). + +- `ADOBE_MINIMAL_INLINE_CONTRACT_VIOLATION_HANDLER()`: Invoke this + macro from your + [`ADOBE_CONTRACT_CHECKS_CONFIGURATION`](#configuration) file to + inject a handler that only invokes `ADOBE_MINIMAL_TRAP`. ### Configuration diff --git a/include/adobe/contract_checks.hpp b/include/adobe/contract_checks.hpp index d821129..b4e07c7 100644 --- a/include/adobe/contract_checks.hpp +++ b/include/adobe/contract_checks.hpp @@ -5,6 +5,37 @@ #include #include +// ADOBE_MINIMAL_TRAP(): followed with a semicolon, expands to a +// minimal statement that stops the program. +#if !defined(__EMSCRIPTEN__) && (defined(__clang__) || defined(__GNUC__)) +#define ADOBE_MINIMAL_TRAP() __builtin_trap() +#elif defined(_MSC_VER) +#define ADOBE_MINIMAL_TRAP() __debugbreak() +#else +#define ADOBE_MINIMAL_TRAP() std::abort() +#endif + +// Injects an inline definition of ::adobe::contract_violated that +// stops the program in the most efficient known way, without any +// diagnostic output. This macro is only useful in your +// ADOBE_CONTRACT_CHECKS_CONFIGURATION file. +#define ADOBE_MINIMAL_INLINE_CONTRACT_VIOLATION_HANDLER() \ + namespace adobe { \ + [[noreturn]] inline void contract_violated(const char *const, \ + int, \ + const char *const, \ + std::uint32_t const, \ + const char *const) noexcept \ + { \ + ADOBE_MINIMAL_TRAP(); \ + } \ + } + +// Do not move this #include up; the included file needs to be able to use the above macros. +#ifdef ADOBE_CONTRACT_CHECKS_CONFIGURATION +#include ADOBE_CONTRACT_CHECKS_CONFIGURATION +#endif + #ifdef ADOBE_CONTRACT_VIOLATED_THROWS #define INTERNAL_ADOBE_CONTRACT_VIOLATED_NOEXCEPT #else @@ -134,31 +165,10 @@ class contract_violation final : public ::std::logic_error #include -#if defined(__clang__) || defined(__GNUC__) && __GNUC__ < 10 -#define INTERNAL_ADOBE_BUILTIN_TRAP() __builtin_trap() -#elif defined(_MSC_VER) -#define INTERNAL_ADOBE_BUILTIN_TRAP() __debugbreak() -#else -#define INTERNAL_ADOBE_BUILTIN_TRAP() std::abort() -#endif - // Part of a workaround for an MSVC preprocessor bug. See // https://stackoverflow.com/a/5134656. #define INTERNAL_ADOBE_MSVC_EXPAND(x) x -// Injects a definition of ::adobe::contract_violated that stops the -// program in the most efficient known way, without any diagnostic -// output. -#define ADOBE_MINIMAL_CONTRACT_VIOLATION_HANDLER() \ - [[noreturn]] void ::adobe::contract_violated(const char *const, \ - ::adobe::contract_violation::kind_t, \ - const char *const, \ - std::uint32_t const, \ - const char *const) noexcept \ - { \ - INTERNAL_ADOBE_BUILTIN_TRAP(); \ - } - // Contract checking macros take a condition and an optional second argument. // // Information on how to simulate optional arguments is here: diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d70732f..b59b605 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -27,16 +27,18 @@ include(GoogleTest) # Adds a test called verify_, built from .cpp # with test executable target named , that passes iff it -# calls abort() and outputs a string matching the regular expression -# . +# exits as expected and outputs a string matching the regular +# expression . Additional arguments are +# interpreted as source files. function(adobe_contract_checking_add_test base_name) - add_executable("${base_name}" "${base_name}.cpp" win32_abort_detection.cpp) + add_executable("${base_name}" "${base_name}.cpp" win32_abort_detection.cpp ${ARGN}) adobe_contract_checking_apply_standard_options("${base_name}") target_link_libraries( "${base_name}" PRIVATE adobe-contract-checking GTest::gtest_main) + target_include_directories("${base_name}" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") gtest_discover_tests("${base_name}") if(EMSCRIPTEN) handle_emscripten_death_tests("${base_name}") @@ -73,4 +75,9 @@ endfunction() adobe_contract_checking_add_test(default_handler_tests) adobe_contract_checking_add_test(throwing_tests) +adobe_contract_checking_add_test(minimal_configuration_tests + inline_handler_assurance.cpp) +target_compile_definitions(minimal_configuration_tests PRIVATE + "ADOBE_CONTRACT_CHECKS_CONFIGURATION=\"minimal_configuration.hpp\"") + # adobe_contract_checking_add_test(wrapper_header_tests) diff --git a/test/default_handler_tests.cpp b/test/default_handler_tests.cpp index b018d03..90bd977 100644 --- a/test/default_handler_tests.cpp +++ b/test/default_handler_tests.cpp @@ -1,39 +1,9 @@ #include "adobe/contract_checks.hpp" +#include "portable_death_tests.hpp" #include -#if !defined(GTEST_OS_WINDOWS) && !defined(__EMSCRIPTEN__) -#include -#endif ADOBE_DEFAULT_CONTRACT_VIOLATION_HANDLER() -#if defined(__EMSCRIPTEN__) - -// GoogleTest doesn't support death tests under emscripten, so instead -// we handle death by setting the test's WILL_FAIL property in CMake. -// Therefore the test simply executes the code that aborts with no -// wrapper. There's currently no facility for checking test output. - -// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) -#define EXPECT_ABORT(code, expected_output_regex) code - -#elif defined(GTEST_OS_WINDOWS) -// GoogleTest doesn't support checking for the abort signal on -// Windows, so we use an auxilliary file, win32_abort_detection.cpp, -// to ensure that an unusual string is printed, which we can check for -// with the EXPECT_DEATH macro. - -// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) -#define EXPECT_ABORT(code, expected_output_regex) \ - EXPECT_DEATH(code, expected_output_regex ".*\n*##ABORTED##"); - -#else - -// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) -#define EXPECT_ABORT(code, expected_output_regex) \ - EXPECT_EXIT(code, testing::KilledBySignal(SIGABRT), expected_output_regex); - -#endif - TEST(DefaultHandler, ContractNonViolationsDoNotCauseAbort) { ADOBE_PRECONDITION(true); diff --git a/test/inline_handler_assurance.cpp b/test/inline_handler_assurance.cpp new file mode 100644 index 0000000..83a736d --- /dev/null +++ b/test/inline_handler_assurance.cpp @@ -0,0 +1,4 @@ +// This file merely ensures that the handler was defined inline by including the file that defines +// it in a second translation unit. The program should fail to link if the handler was defined +// out-of-line. +#include "adobe/contract_checks.hpp"// NOLINT diff --git a/test/minimal_configuration.hpp b/test/minimal_configuration.hpp new file mode 100644 index 0000000..7163549 --- /dev/null +++ b/test/minimal_configuration.hpp @@ -0,0 +1 @@ +ADOBE_MINIMAL_INLINE_CONTRACT_VIOLATION_HANDLER() diff --git a/test/minimal_configuration_tests.cpp b/test/minimal_configuration_tests.cpp new file mode 100644 index 0000000..ac10496 --- /dev/null +++ b/test/minimal_configuration_tests.cpp @@ -0,0 +1,18 @@ +#include "adobe/contract_checks.hpp" +#include "portable_death_tests.hpp" +#include + +TEST(MinimalConfigurationDeathTests, FailedChecksDie) +{ + EXPECT_PORTABLE_DEATH(ADOBE_PRECONDITION(false), ""); + EXPECT_PORTABLE_DEATH(ADOBE_POSTCONDITION(false), ""); + EXPECT_PORTABLE_DEATH(ADOBE_INVARIANT(false), ""); +} + +// ****** This should be the last test in the file. ********* +// +// For unknown reasons if the last test is a death test, under +// emscripten, the test fails even if the executable aborts. +#if defined(__EMSCRIPTEN__) +TEST(MinimalConfiguration, EmscriptenDummy) {} +#endif diff --git a/test/portable_death_tests.hpp b/test/portable_death_tests.hpp new file mode 100644 index 0000000..4bc56d4 --- /dev/null +++ b/test/portable_death_tests.hpp @@ -0,0 +1,47 @@ +#include +#if !defined(GTEST_OS_WINDOWS) && !defined(__EMSCRIPTEN__) +#include +#endif + +// gtest-style test macros that, given the use of handle_emscripten_death_tests in CMakeLists.txt, +// portably detect various kinds of abnormal exits. +// +// EXPECT_ABORT: succeeds when the test case aborts. +// EXPECT_PORTABLE_DEATH: succeeds when the test case exits abnormally. + +#if defined(__EMSCRIPTEN__) + +// GoogleTest doesn't support death tests under emscripten, so instead +// we handle death by setting the test's WILL_FAIL property in CMake. +// Therefore the test simply executes the code that aborts with no +// wrapper. There's currently no facility for checking test output. + +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define EXPECT_ABORT(code, expected_output_regex) code + +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define EXPECT_PORTABLE_DEATH(code, expected_output_regex) code + +#elif defined(GTEST_OS_WINDOWS) +// GoogleTest doesn't support checking for the abort signal on +// Windows, so we use an auxilliary file, win32_abort_detection.cpp, +// to ensure that an unusual string is printed, which we can check for +// with the EXPECT_DEATH macro. + +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define EXPECT_ABORT(code, expected_output_regex) \ + EXPECT_DEATH(code, expected_output_regex ".*\n*##ABORTED##"); + +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define EXPECT_PORTABLE_DEATH(code, expected_output_regex) EXPECT_DEATH(code, expected_output_regex) + +#else + +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define EXPECT_ABORT(code, expected_output_regex) \ + EXPECT_EXIT(code, testing::KilledBySignal(SIGABRT), expected_output_regex); + +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define EXPECT_PORTABLE_DEATH(code, expected_output_regex) EXPECT_DEATH(code, expected_output_regex) + +#endif