diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1b769d964..b703bbe52 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,10 +11,8 @@ jobs: # Defines the compiler configurations for the other jobs. # ##### - define-config: + define-matrix: runs-on: ubuntu-latest - outputs: - config: ${{ steps.output-config.outputs.config }} env: config: | [ @@ -138,7 +136,7 @@ jobs: "os": "macos-latest", "compiler_name": "AppleClang", "compiler_version": 18, - "ldflags_workaround": "-L/opt/homebrew/opt/llvm/lib/c++ -L/opt/homebrew/opt/llvm/lib -lunwind", + "ldflags_workaround": "-L/opt/homebrew/opt/llvm/lib/c++ -L/opt/homebrew/opt/llvm/lib/unwind -lunwind", "asan": true }, { @@ -159,15 +157,45 @@ jobs: } ] + outputs: + config: ${{ steps.output-config.outputs.config }} + build_modes: ${{ steps.output-options.outputs.build_modes }} + cxx_versions: ${{ steps.output-options.outputs.cxx_versions }} + steps: - name: Output configs id: output-config shell: bash run: | - # seems to convert that to a single-line json and thus please the output step - # wrap in single '! - OUTPUT='${{ env.config }}' - echo "config=$(echo $OUTPUT)" >> "$GITHUB_OUTPUT" + # seems to convert that to a single-line json and thus please the output step + # wrap in single '! + OUTPUT='${{ env.config }}' + echo "config=$(echo $OUTPUT)" >> "$GITHUB_OUTPUT" + + # enables debug-mode and c++20 for all cases + - name: Enable base matrix + shell: bash + run: | + echo "BUILD_MODES=\"Debug\"" >> $GITHUB_ENV + echo "CXX_VERSIONS=20" >> $GITHUB_ENV + + # if its a PR from development or the main branch in general, add release-mode and c++23 + - name: Enable extended matrix + if: ${{ + (github.event_name == 'pull_request' && github.head_ref == 'development') + || github.ref_name == 'main' + }} + shell: bash + run: | + echo "BUILD_MODES=$(echo $BUILD_MODES, \"Release\")" >> $GITHUB_ENV + echo "CXX_VERSIONS=$(echo $CXX_VERSIONS, 23)" >> $GITHUB_ENV + + - name: Output build-modes and c++-versions + id: output-options + shell: bash + run: | + echo "build_modes=$(echo [ $BUILD_MODES ])" >> "$GITHUB_OUTPUT" + echo "cxx_versions=$(echo [ $CXX_VERSIONS ])" >> "$GITHUB_OUTPUT" ############ # @@ -182,7 +210,7 @@ jobs: # ##### run-unit-tests: - needs: define-config + needs: define-matrix name: | [UT] ${{ matrix.config.prefix }} @@ -195,11 +223,11 @@ jobs: strategy: fail-fast: false matrix: - build_mode: [Debug, Release] - cxx_standard: [20, 23] format_backend: [std, fmt] str_matcher: [char, unicode] - config: ${{ fromJSON(needs.define-config.outputs.config) }} + build_mode: ${{ fromJSON(needs.define-matrix.outputs.build_modes) }} + cxx_standard: ${{ fromJSON(needs.define-matrix.outputs.cxx_versions) }} + config: ${{ fromJSON(needs.define-matrix.outputs.config) }} exclude: # all listed compilers do not support std's format header @@ -216,6 +244,12 @@ jobs: compiler_name: "AppleClang" compiler_version: 16 + # The format_backend and str_matcher options are rather orthogonal. + # To see, whether support both str_matcher variants, let's use the fmt backend, + # as this is supported by all compilers. + - format_backend: "std" + str_matcher: "unicode" + steps: - uses: actions/checkout@v4 @@ -223,11 +257,14 @@ jobs: if: startsWith(matrix.config.os, 'macOS') shell: bash run: | + env brew install ninja llvm LLVM_NAME=llvm@${{ matrix.config.compiler_version }} - env brew install ninja $LLVM_NAME + env brew install $LLVM_NAME LLVM_PATH="$(brew --prefix $LLVM_NAME)" echo "CC=$(echo $LLVM_PATH/bin/clang)" >> $GITHUB_ENV echo "CXX=$(echo $LLVM_PATH/bin/clang++)" >> $GITHUB_ENV + + # solves this issue: https://github.com/Homebrew/homebrew-core/issues/178435 echo "LDFLAGS=$(echo $LDFLAGS ${{ matrix.config.ldflags_workaround }})" >> $GITHUB_ENV echo "CMAKE_CONFIG_EXTRA=$(echo $CMAKE_CONFIG_EXTRA -DCMAKE_BUILD_TYPE=${{ matrix.build_mode }})" >> $GITHUB_ENV @@ -320,7 +357,7 @@ jobs: # ##### run-adapter-tests: - needs: define-config + needs: define-matrix name: | [AT] ${{ matrix.config.prefix }} @@ -333,17 +370,9 @@ jobs: strategy: fail-fast: false matrix: - build_mode: [Debug, Release] - cxx_standard: [20, 23] - config: ${{ fromJSON(needs.define-config.outputs.config) }} - - exclude: - # This compiler does work, but fails compiling something from boost::filesystem, which is indirectly required - # by the BoostAdapter-Test. - # Maybe, we can find a workaround for this in the future. - - config: - compiler_name: "AppleClang" - compiler_version: 16 + build_mode: ${{ fromJSON(needs.define-matrix.outputs.build_modes) }} + cxx_standard: ${{ fromJSON(needs.define-matrix.outputs.cxx_versions) }} + config: ${{ fromJSON(needs.define-matrix.outputs.config) }} steps: - uses: actions/checkout@v4 @@ -352,8 +381,9 @@ jobs: if: startsWith(matrix.config.os, 'macOS') shell: bash run: | + env brew install ninja llvm LLVM_NAME=llvm@${{ matrix.config.compiler_version }} - env brew install ninja $LLVM_NAME + env brew install $LLVM_NAME LLVM_PATH="$(brew --prefix $LLVM_NAME)" echo "CC=$(echo $LLVM_PATH/bin/clang)" >> $GITHUB_ENV echo "CXX=$(echo $LLVM_PATH/bin/clang++)" >> $GITHUB_ENV diff --git a/CMakeLists.txt b/CMakeLists.txt index 843323392..a70978143 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,16 @@ +# Copyright Dominic (DNKpp) Koepke 2024 - 2024. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) + cmake_minimum_required(VERSION 3.15) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") -include(get_cpm) project( mimicpp LANGUAGES CXX - VERSION 2 + VERSION 3 DESCRIPTION "A modern and (mostly) macro free mocking-framework" HOMEPAGE_URL "https://github.com/DNKpp/mimicpp" ) @@ -40,7 +44,12 @@ target_compile_features( cxx_std_${CMAKE_CXX_STANDARD} ) -include(mimic++-configuration-options) +include(EnableConfigOptions) +target_link_libraries( + mimicpp + INTERFACE + mimicpp::internal::config-options +) if (CMAKE_SOURCE_DIR STREQUAL mimicpp_SOURCE_DIR) set(IS_TOP_LEVEL_PROJECT ON) diff --git a/README.md b/README.md index c67e2d983..9e214a748 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ As I'm mainly working on template or functional-style code, I wanted something s So, ``mimicpp::Mock`` objects can directly be used as functional objects, but they can also be used as member objects and thus serve as actual member functions. If you are curious, have a look at the [documentation](https://dnkpp.github.io/mimicpp/), investigate the examples folder or play around online at -[godbolt.org](https://godbolt.org/z/nfhT9xa4E). +[godbolt.org](https://godbolt.org/z/o3Thdcr7T). ### Core Design @@ -62,7 +62,8 @@ So, Mocks and Expectations are going together hand in hand. ### Examples -#### Mocks as function objects +
+Mocks as function objects As already said, ``mimicpp::Mock``s are already function objects. @@ -117,7 +118,10 @@ TEST_CASE("Mocks can be overloaded.") } ``` -#### Mocks as member functions +
+ +
+Mocks as member functions ``mimicpp::Mock``s can also serve as member functions. Sure, there are some limitations, but for the most cases it works well. @@ -148,7 +152,10 @@ TEST_CASE("Mocks can be used as member functions.") } ``` -#### Mocking interfaces +
+ +
+Mocking interfaces ``mimic++`` also provides helpers for interface mocking. @@ -235,6 +242,42 @@ TEST_CASE("Interface overload-sets are directly supported.") } ``` +
+ +
+Watching object-instances + +``mimicpp::Watched`` helper can report destruction and relocations of object-instances. + +```cpp +#include + +namespace expect = mimicpp::expect; +namespace then = mimicpp::then; + +TEST_CASE("LifetimeWatcher and RelocationWatcher can trace object instances.") +{ + mimicpp::Watched< + mimicpp::Mock, + mimicpp::LifetimeWatcher, + mimicpp::RelocationWatcher> watched{}; + + SCOPED_EXP watched.expect_destruct(); + int relocationCounter{}; + SCOPED_EXP watched.expect_relocate() + and then::invoke([&] { ++relocationCounter; }) + and expect::at_least(1); + + std::optional wrapped{std::move(watched)}; // satisfies one relocate-expectation + std::optional other{std::move(wrapped)}; // satisfies a second relocate-expectation + wrapped.reset(); // won't require a destruct-expectation, as moved-from objects are considered dead + other.reset(); // fulfills the destruct-expectation + REQUIRE(2 == relocationCounter); // let's see, how often the instance has been relocated +} +``` + +
+ ### Other Choices #### Always Stay Within The Language Definition @@ -365,6 +408,10 @@ Official adapters exist for the following frameworks: ``mimic++`` utilizes a strict testing policy, thus each official feature is well tested. The effect of those test-cases are always tracked by the extensive ci, which checks the compilation success, test cases outcomes and coverage on dozens of different os, compiler and build configurations. +For the test builds the flags ``-Wall -Wextra -Wpedantic -Werror`` (on MSVC ``/W4 /WX``) are set. +This is done to make sure, that ``mimic++`` won't flood your build output with endless warnings (or even worse: break your builds), +if you enable these flags in your own projects. + The coverage is generated via ``gcov`` and evaluated by [codacy](https://app.codacy.com/gh/DNKpp/mimicpp), [codecov](https://codecov.io/gh/DNKpp/mimicpp) and @@ -376,7 +423,8 @@ The goal is to be close to 100% as possible. On the other hand-side, there is a whole range of code which doesn't even get investigated by these tools: templates (and macros). ``mimic++`` has **a lot** of templating code at the very heart, which is at least of equal effort to get right (and tested). So, treat the coverage percentage with a grain of salt. -### CI Tests +
+CI Tests The listed configurations are explicitly tested, but other do probably work, too. As new compilers become available, they will be added to the workflow, but older compilers will probably never be supported. @@ -410,11 +458,11 @@ As new compilers become available, they will be added to the workflow, but older | Compiler | libstdc++ | libc++ | c++-20 | c++-23 | std-format | fmt | |-------------------|:---------:|:------:|:------:|:------:|:----------:|:---:| -| AppleClang-16.0.6^1^ | ? | x | x | x | x | x | +| AppleClang-16.0.6 | ? | x | x | x | x | x | | AppleClang-17.0.6 | ? | x | x | x | x | x | | AppleClang-18.1.6 | ? | x | x | x | x | x | -^1^ There is an issue with AppleClang-16 and ``boost::filesystem`` on the current ``macos-latest``, but this is just for the boost-test-adapter. Everything else works fine. +
--- diff --git a/cmake/EnableConfigOptions.cmake b/cmake/EnableConfigOptions.cmake new file mode 100644 index 000000000..6e881bdac --- /dev/null +++ b/cmake/EnableConfigOptions.cmake @@ -0,0 +1,94 @@ +# Copyright Dominic (DNKpp) Koepke 2024 - 2024. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) + +if (NOT TARGET enable-config-options) + + add_library(enable-config-options INTERFACE) + add_library(mimicpp::internal::config-options ALIAS enable-config-options) + + OPTION(MIMICPP_CONFIG_ONLY_PREFIXED_MACROS "When enabled, all macros will be prefixed with MIMICPP_." OFF) + OPTION( + MIMICPP_CONFIG_EXPERIMENTAL_CATCH2_MATCHER_INTEGRATION + "When enabled, catch2 matchers integration will be enabled, if catch2 adapter is used (experimental)." + OFF + ) + + target_compile_definitions( + enable-config-options + INTERFACE + $<$:MIMICPP_CONFIG_ONLY_PREFIXED_MACROS> + $<$:MIMICPP_CONFIG_EXPERIMENTAL_CATCH2_MATCHER_INTEGRATION> + ) + + # Config option, to utilize fmt instead of std formatting. + # Checks, whether fmt is already available. Fetches it instead. + # Eventually defines the macro MIMICPP_CONFIG_USE_FMT. + OPTION(MIMICPP_CONFIG_USE_FMT "When enabled, uses fmt instead of std formatting." OFF) + if (MIMICPP_CONFIG_USE_FMT) + + find_package(fmt QUIET) + if (NOT fmt_FOUND) + include(get_cpm) + + CPMAddPackage("gh:fmtlib/fmt#11.0.2") + endif() + + find_package(fmt REQUIRED) + target_link_libraries( + enable-config-options + INTERFACE + fmt::fmt + ) + + target_compile_definitions( + enable-config-options + INTERFACE + MIMICPP_CONFIG_USE_FMT + ) + + endif() + + # Config option, to enable unicode support for string matchers. + # This will download the cpp-unicodelib source and create an import target + # Eventually defines the macro MIMICPP_CONFIG_EXPERIMENTAL_UNICODE_STR_MATCHER. + OPTION(MIMICPP_CONFIG_EXPERIMENTAL_UNICODE_STR_MATCHER "When enabled, all case-insensitive string matchers are available." OFF) + if (MIMICPP_CONFIG_EXPERIMENTAL_UNICODE_STR_MATCHER) + + # on clang-builds this somehow emits an error, if not explicitly disabled + # Git the info, to turn this of from here: + # https://discourse.cmake.org/t/cmake-3-28-cmake-cxx-compiler-clang-scan-deps-notfound-not-found/9244/3 + set(CMAKE_CXX_SCAN_FOR_MODULES OFF) + + find_package(uni-algo QUIET) + if (NOT uni-algo_FOUND) + include(get_cpm) + + CPMAddPackage( + NAME uni-algo + GITHUB_REPOSITORY uni-algo/uni-algo + GIT_TAG v1.2.0 + EXCLUDE_FROM_ALL YES + SYSTEM YES + OPTIONS + UNI_ALGO_INSTALL YES + ) + endif() + + find_package(uni-algo REQUIRED) + target_link_libraries( + enable-config-options + INTERFACE + uni-algo::uni-algo + ) + + target_compile_definitions( + enable-config-options + INTERFACE + MIMICPP_CONFIG_EXPERIMENTAL_UNICODE_STR_MATCHER + ) + + endif() + +endif() diff --git a/cmake/SetupTestTarget.cmake b/cmake/EnableSanitizers.cmake similarity index 89% rename from cmake/SetupTestTarget.cmake rename to cmake/EnableSanitizers.cmake index cd4e7a102..8e3d5d2dc 100644 --- a/cmake/SetupTestTarget.cmake +++ b/cmake/EnableSanitizers.cmake @@ -3,11 +3,10 @@ # (See accompanying file LICENSE_1_0.txt or copy at # https://www.boost.org/LICENSE_1_0.txt) - -function(setup_test_target TARGET_NAME) +function(enable_sanitizers TARGET_NAME) + find_package(sanitizers-cmake) if (SANITIZE_ADDRESS) - # workaround linker errors on msvc # see: https://learn.microsoft.com/en-us/answers/questions/864574/enabling-address-sanitizer-results-in-error-lnk203 target_compile_definitions( @@ -16,9 +15,7 @@ function(setup_test_target TARGET_NAME) $<$:_DISABLE_VECTOR_ANNOTATION> $<$:_DISABLE_STRING_ANNOTATION> ) - endif() add_sanitizers(${TARGET_NAME}) - endfunction() \ No newline at end of file diff --git a/cmake/EnableWarnings.cmake b/cmake/EnableWarnings.cmake new file mode 100644 index 000000000..3b395c3c6 --- /dev/null +++ b/cmake/EnableWarnings.cmake @@ -0,0 +1,31 @@ +# Copyright Dominic (DNKpp) Koepke 2024 - 2024. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) + +if (NOT TARGET enable-warnings) + + add_library(enable-warnings INTERFACE) + add_library(mimicpp::internal::warnings ALIAS enable-warnings) + + # We need to circumvent the huge nonsense warnings from clang-cl + # see: https://discourse.cmake.org/t/wall-with-visual-studio-clang-toolchain-results-in-way-too-many-warnings/7927 + if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" + AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + + set(WARNING_FLAGS /W4 -Wextra -Wpedantic -Werror -Wno-unknown-attributes) + else() + set(WARNING_FLAGS + $, + /W4 /WX, + -Wall -Wextra -Wpedantic -Werror> + ) + endif() + + target_compile_options( + enable-warnings + INTERFACE + ${WARNING_FLAGS} + ) + +endif() diff --git a/cmake/FindCatch2.cmake b/cmake/FindCatch2.cmake index 7fe100514..310d07c29 100644 --- a/cmake/FindCatch2.cmake +++ b/cmake/FindCatch2.cmake @@ -1,3 +1,10 @@ +# Copyright Dominic (DNKpp) Koepke 2024 - 2024. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) + +include(get_cpm) + CPMAddPackage("gh:catchorg/Catch2@3.7.1") if (Catch2_ADDED) diff --git a/cmake/Findsanitizers-cmake.cmake b/cmake/Findsanitizers-cmake.cmake index b9e07337c..8bc5c0b3c 100644 --- a/cmake/Findsanitizers-cmake.cmake +++ b/cmake/Findsanitizers-cmake.cmake @@ -1,3 +1,10 @@ +# Copyright Dominic (DNKpp) Koepke 2024 - 2024. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) + +include(get_cpm) + CPMAddPackage( NAME sanitizers-cmake GITHUB_REPOSITORY "arsenm/sanitizers-cmake" @@ -7,10 +14,6 @@ CPMAddPackage( DOWNLOAD_ONLY YES ) -if (sanitizers-cmake_ADDED) - - list(APPEND CMAKE_MODULE_PATH "${sanitizers-cmake_SOURCE_DIR}/cmake") - -endif() +list(APPEND CMAKE_MODULE_PATH "${sanitizers-cmake_SOURCE_DIR}/cmake") find_package(Sanitizers REQUIRED) diff --git a/cmake/InstallRules.cmake b/cmake/InstallRules.cmake index a0808be96..c11e10b73 100644 --- a/cmake/InstallRules.cmake +++ b/cmake/InstallRules.cmake @@ -1,5 +1,7 @@ -# many thanks to jeremy rifkin, from which I took most of the following code -# https://github.com/jeremy-rifkin/libassert/blob/main/cmake/InstallRules.cmake +# Copyright Dominic (DNKpp) Koepke 2024 - 2024. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) include(CMakePackageConfigHelpers) @@ -21,7 +23,7 @@ configure_package_config_file( ) install( - TARGETS mimicpp + TARGETS mimicpp enable-config-options EXPORT mimicpp-targets PUBLIC_HEADER DESTINATION "${MIMICPP_INCLUDE_INSTALL_DIR}" ) diff --git a/cmake/mimic++-configuration-options.cmake b/cmake/mimic++-configuration-options.cmake deleted file mode 100644 index 02f68960e..000000000 --- a/cmake/mimic++-configuration-options.cmake +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright Dominic (DNKpp) Koepke 2024 - 2024. -# Distributed under the Boost Software License, Version 1.0. -# (See accompanying file LICENSE_1_0.txt or copy at -# https://www.boost.org/LICENSE_1_0.txt) - -include(get_cpm) - -OPTION(MIMICPP_CONFIG_ONLY_PREFIXED_MACROS "When enabled, all macros will be prefixed with MIMICPP_." OFF) -OPTION( - MIMICPP_CONFIG_EXPERIMENTAL_CATCH2_MATCHER_INTEGRATION - "When enabled, catch2 matchers integration will be enabled, if catch2 adapter is used (experimental)." - OFF -) - -target_compile_definitions( - mimicpp - INTERFACE - $<$:MIMICPP_CONFIG_ONLY_PREFIXED_MACROS> - $<$:MIMICPP_CONFIG_EXPERIMENTAL_CATCH2_MATCHER_INTEGRATION> -) - -# Config option, to utilize fmt instead of std formatting. -# Checks, whether fmt is already available. Fetches it instead. -# Eventually defines the macro MIMICPP_CONFIG_USE_FMT. -OPTION(MIMICPP_CONFIG_USE_FMT "When enabled, uses fmt instead of std formatting." OFF) -if (MIMICPP_CONFIG_USE_FMT) - - find_package(fmt QUIET) - if (NOT fmt_FOUND) - CPMAddPackage("gh:fmtlib/fmt#11.0.2") - endif() - - find_package(fmt REQUIRED) - target_link_libraries( - mimicpp - INTERFACE - fmt::fmt - ) - - target_compile_definitions( - mimicpp - INTERFACE - MIMICPP_CONFIG_USE_FMT - ) - -endif() - -# Config option, to enable unicode support for string matchers. -# This will download the cpp-unicodelib source and create an import target -# Eventually defines the macro MIMICPP_CONFIG_EXPERIMENTAL_UNICODE_STR_MATCHER. -OPTION(MIMICPP_CONFIG_EXPERIMENTAL_UNICODE_STR_MATCHER "When enabled, all case-insensitive string matchers are available." OFF) -if (MIMICPP_CONFIG_EXPERIMENTAL_UNICODE_STR_MATCHER) - - CPMAddPackage( - NAME cpp-unicodelib - GITHUB_REPOSITORY yhirose/cpp-unicodelib - GIT_TAG 797b1f0f1592ce13afabf3576f51ef26db5e884d - DOWNLOAD_ONLY YES - SYSTEM YES - ) - - if (cpp-unicodelib_ADDED) - - add_library(cpp-unicodelib INTERFACE IMPORTED) - add_library(cpp::unicodelib ALIAS cpp-unicodelib) - target_include_directories( - cpp-unicodelib - INTERFACE - "${cpp-unicodelib_SOURCE_DIR}" - ) - - target_link_libraries( - mimicpp - INTERFACE - cpp::unicodelib - ) - - target_compile_options( - mimicpp - INTERFACE - # cpp-unicodelib checks for this macro, but msvc doesn't define it properly - # see: https://devblogs.microsoft.com/cppblog/msvc-now-correctly-reports-__cplusplus/ - "$<$:/Zc:__cplusplus>" - ) - - target_compile_definitions( - mimicpp - INTERFACE - MIMICPP_CONFIG_EXPERIMENTAL_UNICODE_STR_MATCHER - ) - - endif() - -endif() diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt index a03b01ba7..de9f8bc2b 100644 --- a/docs/CMakeLists.txt +++ b/docs/CMakeLists.txt @@ -1,9 +1,16 @@ +# Copyright Dominic (DNKpp) Koepke 2024 - 2024. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) + #[[ Adds a custom target, which generates the doxygen documentation. The working directory is set to the project origin, as this is an effective way to use the relative paths for the documentation. #]] message(TRACE "Begin generating docs") +include(get_cpm) + CPMAddPackage("gh:jothepro/doxygen-awesome-css@2.3.2") set(DOXY_PROJECT_VERSION ${PROJECT_VERSION}) diff --git a/docs/FrameworkConfig.hpp b/docs/FrameworkConfig.hpp index 9bd9f6d32..61e6c1d05 100644 --- a/docs/FrameworkConfig.hpp +++ b/docs/FrameworkConfig.hpp @@ -38,7 +38,7 @@ * Name: ``MIMICPP_CONFIG_EXPERIMENTAL_CATCH2_MATCHER_INTEGRATION`` * * If enabled, all matchers from the ``Catch::Matcher`` namespaces can be directly used everywhere, where ``mimic++``-matchers are suitable. - * This is an experimental feature, and may be removed during any release. + * \attention This is an experimental feature, and may be removed during any release. * * ### Why is it an experimental feature? * Unfortunatly ``catch2`` matchers are desgined to be used in-place. Every combination you do, like negation via ``operator !`` or ``operator &&``, @@ -56,13 +56,16 @@ * * If enabled, all string-matchers get full unicode support. This is relevant, when comparing case-insensitively. It's not required for the base-version * of string-matchers. - * This is an experimental feature, and may be removed during any release. + * + * This will require the rather light-weight library ``uni-algo``, which can be found on github. + * \see https://github.com/uni-algo/uni-algo + * + * ``mimic++`` first tries to find the dependency via ``find_package``, but will fetch it from github if not available. + * + * \attention This is an experimental feature, and may be removed during any release. * * ### Why is it an experimental feature? * - * This will pull the rather light-weight library ``cpp-unicodelib`` from github. ``mimic++`` won't check whether it can already find the library, - * because it has very poor cmake support. This library is currently chosen, because it's very easy to integrate and offers the necessary - * algorithm (``to_case_fold``), but it's in no way an optimal solution. It comes also with a quite heavy runtime-cost, as we have to do many - * conversions back and forth, to get that working. If there is a better -- but similar light-weight -- option available, please tell me. - * \see https://github.com/yhirose/cpp-unicodelib + * I recently switched from ``cpp-unicodelib``, which I didn't like very much for several reasons. ``uni-algo`` seems more mature, but I would like + * to get some feedback, before I'll declare this as a stable feature. */ diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 56a4872b7..34599c0f6 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,6 +1,9 @@ -find_package(Catch2 REQUIRED) +# Copyright Dominic (DNKpp) Koepke 2024 - 2024. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) -find_package(sanitizers-cmake REQUIRED) +find_package(Catch2 REQUIRED) add_executable( mimicpp-examples @@ -12,15 +15,18 @@ add_executable( "Sequences.cpp" "SideEffects.cpp" "Times.cpp" + "Watcher.cpp" ) -include(SetupTestTarget) -setup_test_target(mimicpp-examples) +include(EnableWarnings) +include(EnableSanitizers) +enable_sanitizers(mimicpp-examples) target_link_libraries( mimicpp-examples PRIVATE mimicpp::mimicpp + mimicpp::internal::warnings Catch2::Catch2WithMain ) diff --git a/examples/Watcher.cpp b/examples/Watcher.cpp new file mode 100644 index 000000000..488fd92a0 --- /dev/null +++ b/examples/Watcher.cpp @@ -0,0 +1,230 @@ +// // Copyright Dominic (DNKpp) Koepke 2024 - 2024. +// // Distributed under the Boost Software License, Version 1.0. +// // (See accompanying file LICENSE_1_0.txt or copy at +// // https://www.boost.org/LICENSE_1_0.txt) + +#include "../test/unit-tests/SuppressionMacros.hpp" // needs to disable some warnings on gcc +#include "mimic++/ObjectWatcher.hpp" + +#include + +TEST_CASE( + "Watched reports violations during destruction.", + "[example][example::watched][example::watched::life-time]" +) +{ + //! [watched lifetime-watcher violation] + constexpr auto action = [] + { + struct not_nothrow_destructible + { + // explicitly make the destructor throwable, this isn't necessarily required in real tests + ~not_nothrow_destructible() noexcept(false) + { + } + }; + + const mimicpp::Watched< + not_nothrow_destructible, + mimicpp::LifetimeWatcher> watched{}; // let's create a watched instance + + // We purposely forget to define a destruction expectation. + // Due to this, a no-match will be reported during destruction. + }; + + // Depending on the active reporter, this may either raise an exception or terminate the program. + // The default reporter simply throws an exception. + REQUIRE_THROWS(action()); + //! [watched lifetime-watcher violation] +} + +TEST_CASE( + "Watched reports violations during copy-construction.", + "[example][example::watched][example::watched::life-time]" +) +{ + //! [watched lifetime-watcher copy-construction violation] + constexpr auto action = [] + { + struct my_copyable // Mock isn't a copyable type, thus use a custom one for this example + { + // explicitly make the destructor throwable, this isn't necessarily required in real tests + ~my_copyable() noexcept(false) + { + } + }; + + mimicpp::Watched< + my_copyable, + mimicpp::LifetimeWatcher> source{}; + + // setting up a destruction-expectation is mandatory to prevent violation reports during scope leave + SCOPED_EXP source.expect_destruct(); + // This is just a little trick for this small example. In real code, + // one would usually pass the ownership of the watched object somewhere else, + // but in this example we fake that by simply moving it below our expectation. + auto moved = std::move(source); // note: source is now a "moved-from"-object, which doesn't report any violations + + mimicpp::Watched other{source}; // now create a "copy" of source + + // other is a new instance without an existing destruction-expectation, + // because the copy-constructor doesn't semantically copy anything. + // As other goes out of scope without a destruction-expectation, a no-match is reported. + }; + + // Depending on the active reporter, this may either raise an exception or terminate the program. + // The default reporter simply throws an exception. + REQUIRE_THROWS(action()); + //! [watched lifetime-watcher copy-construction violation] +} + +TEST_CASE( + "Watched reports violations during copy-assignment.", + "[example][example::watched][example::watched::life-time]" +) +{ + SECTION("Copy-assignment to an watched object, which is not yet ready.") + { + //! [watched lifetime-watcher copy-assignment violation] + constexpr auto action = [] + { + struct my_copyable // Mock isn't a copyable type, thus use a custom one for this example + { + // explicitly make the destructor throwable, this isn't necessarily required in real tests + ~my_copyable() noexcept(false) + { + } + }; + + mimicpp::Watched< + my_copyable, + mimicpp::LifetimeWatcher> source{}; + + // setting up a destruction-expectation is mandatory to prevent violation reports during scope leave + SCOPED_EXP source.expect_destruct(); + // This is just a little trick for this small example. In real code, + // one would usually pass the ownership of the watched object somewhere else, + // but in this example we fake that by simply moving it below our expectation. + auto moved = std::move(source); // note: source is now a "moved-from"-object, which doesn't report any violations + + mimicpp::Watched< + my_copyable, + mimicpp::LifetimeWatcher> other{}; // now create another watched object + + // other will be destructed, but there is no existing destruction-expectation + other = source; // a no-match will be reported immediately + }; + + // Depending on the active reporter, this may either raise an exception or terminate the program. + // The default reporter simply throws an exception. + REQUIRE_THROWS(action()); + //! [watched lifetime-watcher copy-assignment violation] + } + + SECTION("Copy-assignment without setting up an destruction-expectation.") + { + //! [watched lifetime-watcher copy-assignment violation2] + constexpr auto action = [] + { + struct my_copyable // Mock isn't a copyable type, thus use a custom one for this example + { + // explicitly make the destructor throwable, this isn't necessarily required in real tests + ~my_copyable() noexcept(false) + { + } + }; + + mimicpp::Watched< + my_copyable, + mimicpp::LifetimeWatcher> source{}; + + // setting up a destruction-expectation is mandatory to prevent violation reports during scope leave + SCOPED_EXP source.expect_destruct(); + // This is just a little trick for this small example. In real code, + // one would usually pass the ownership of the watched object somewhere else, + // but in this example we fake that by simply moving it below our expectation. + auto moved = std::move(source); // note: source is now a "moved-from"-object, which doesn't report any violations + + mimicpp::Watched< + my_copyable, + mimicpp::LifetimeWatcher> other{}; // now create another watched object + SCOPED_EXP other.expect_destruct(); // setting it up accordingly + other = source; // other will be destructed and the expectation from the line above fulfilled + + // other is now a new instance without an existing destruction-expectation, + // because the copy-operator doesn't semantically copy anything. + // As other goes out of scope without a destruction-expectation, a no-match is reported. + }; + + // Depending on the active reporter, this may either raise an exception or terminate the program. + // The default reporter simply throws an exception. + REQUIRE_THROWS(action()); + //! [watched lifetime-watcher copy-assignment violation2] + } +} + +TEST_CASE( + "Watched is satisfied, if destruction actually happens.", + "[example][example::watched][example::watched::life-time]" +) +{ + //! [watched lifetime-watcher] + namespace expect = mimicpp::expect; + + // imagine this to be a function, we wanted to test + constexpr auto some_function = [](auto fun) + { + fun(); // let's just invoke the given fun. + }; + + mimicpp::Watched< + mimicpp::Mock, + mimicpp::LifetimeWatcher> watched{}; // let's create a watched mock + + // Let's say, we are very suspicious and want to get sure, that ``some_function`` + // invokes the provided functional, before its getting destroyed. + mimicpp::SequenceT sequence{}; + SCOPED_EXP watched.expect_call() + and expect::in_sequence(sequence); + SCOPED_EXP watched.expect_destruct() + and expect::in_sequence(sequence); + + // pass the mock to ``some_function`` and track from the outside, whether the expectations hold + some_function(std::move(watched)); + + // nothing to do here. Violations will be reported automatically (as usual). + //! [watched lifetime-watcher] +} + +// gcc constantly complaints about the optionals as "maybe-uninitialized" +START_WARNING_SUPPRESSION +SUPPRESS_MAYBE_UNINITIALIZED + +TEST_CASE( + "LifetimeWatcher and RelocationWatcher can trace object instances.", + "[example][example::watched]" +) +{ + //! [watched lifetime relocation] + namespace expect = mimicpp::expect; + namespace then = mimicpp::then; + + mimicpp::Watched< + mimicpp::Mock, + mimicpp::LifetimeWatcher, + mimicpp::RelocationWatcher> watched{}; + + SCOPED_EXP watched.expect_destruct(); + int relocationCounter{}; + SCOPED_EXP watched.expect_relocate() + and then::invoke([&] { ++relocationCounter; }) + and expect::at_least(1); + + std::optional wrapped{std::move(watched)}; // satisfies one relocate-expectation + std::optional other{std::move(wrapped)}; // satisfies a second relocate-expectation + wrapped.reset(); // won't require a destruct-expectation, as moved-from objects are considered dead + other.reset(); // fulfills the destruct-expectation + REQUIRE(2 == relocationCounter); // let's see, how often the instance has been relocated + //! [watched lifetime relocation] +} +STOP_WARNING_SUPPRESSION diff --git a/include/mimic++/ControlPolicy.hpp b/include/mimic++/ControlPolicy.hpp index 901c9f49d..6a5f28a68 100644 --- a/include/mimic++/ControlPolicy.hpp +++ b/include/mimic++/ControlPolicy.hpp @@ -120,22 +120,21 @@ namespace mimicpp std::apply( [&](const auto&... entries) { - const auto distribute = [&](auto& seq, const sequence::Id id) - { - if (const std::optional priority = seq->priority_of(id)) - { - ratings.emplace_back( - *priority, - seq->tag()); - } - else - { - inapplicable.emplace_back(seq->tag()); - } - }; - (..., - distribute( + std::invoke( + [&](auto& seq, const sequence::Id id) + { + if (const std::optional priority = seq->priority_of(id)) + { + ratings.emplace_back( + *priority, + seq->tag()); + } + else + { + inapplicable.emplace_back(seq->tag()); + } + }, std::get<0>(entries), std::get<1>(entries))); }, @@ -312,7 +311,7 @@ namespace mimicpp::expect /** * \brief Specifies a times policy with just a lower limit. - * \tparam min The lower limit. + * \param min The lower limit. * \return The newly created policy. * \details This requires the expectation to be matched at least ``min`` times or more. * \throws std::invalid_argument if ``min < 0``. diff --git a/include/mimic++/ExpectationBuilder.hpp b/include/mimic++/ExpectationBuilder.hpp index cb1938d9e..aa7bcaaca 100644 --- a/include/mimic++/ExpectationBuilder.hpp +++ b/include/mimic++/ExpectationBuilder.hpp @@ -195,34 +195,55 @@ namespace mimicpp namespace mimicpp::detail { - template + template requires matcher_for< std::remove_cvref_t, - signature_param_type_t> + Param> [[nodiscard]] - constexpr auto make_arg_policy(Arg&& arg, [[maybe_unused]] const priority_tag<2>) + constexpr auto make_arg_matcher(Arg&& arg, [[maybe_unused]] const priority_tag<2>) { - return expect::arg(std::forward(arg)); + return std::forward(arg); } - template > Arg> + template [[nodiscard]] - constexpr auto make_arg_policy(Arg&& arg, [[maybe_unused]] const priority_tag<1>) + constexpr auto make_arg_matcher(Arg&& arg, [[maybe_unused]] const priority_tag<1>) { - return expect::arg( - matches::eq(std::forward(arg))); + return matches::str::eq(std::forward(arg)); } - template - constexpr void - make_arg_policy( - [[maybe_unused]] Arg&& arg, - [[maybe_unused]] const priority_tag<0> - ) noexcept // NOLINT(cppcoreguidelines-missing-std-forward) + template Arg> + [[nodiscard]] + constexpr auto make_arg_matcher(Arg&& arg, [[maybe_unused]] const priority_tag<0>) { - static_assert( - always_false{}, - "The provided argument is neither a matcher, nor is it equality comparable with the selected param."); + return matches::eq(std::forward(arg)); + } + + constexpr priority_tag<2> max_make_arg_matcher_tag{}; + + template + concept requirement_for = requires + { + { + detail::make_arg_matcher( + std::declval(), + max_make_arg_matcher_tag) + } -> matcher_for; + }; + + template < + typename Signature, + std::size_t index, + typename Arg, + typename... Canary, + typename Param = signature_param_type_t> + requires requirement_for + constexpr auto make_arg_policy(Arg&& arg) + { + return expect::arg( + detail::make_arg_matcher( + std::forward(arg), + max_make_arg_matcher_tag)); } template @@ -237,8 +258,7 @@ namespace mimicpp::detail std::forward(builder) && ... && detail::make_arg_policy( - std::forward(args), - priority_tag<2>{})); + std::forward(args))); } template diff --git a/include/mimic++/ExpectationPolicies.hpp b/include/mimic++/ExpectationPolicies.hpp index e27880683..de5e02078 100644 --- a/include/mimic++/ExpectationPolicies.hpp +++ b/include/mimic++/ExpectationPolicies.hpp @@ -43,7 +43,7 @@ namespace mimicpp::expectation_policies } template - static constexpr void consume(const call::Info& info) noexcept + static constexpr void consume([[maybe_unused]] const call::Info& info) noexcept { assert(mimicpp::is_matching(info.fromCategory, expected) && "Call does not match."); } @@ -73,7 +73,7 @@ namespace mimicpp::expectation_policies } template - static constexpr void consume(const call::Info& info) noexcept + static constexpr void consume([[maybe_unused]] const call::Info& info) noexcept { assert(mimicpp::is_matching(info.fromConstness, constness) && "Call does not match."); } @@ -467,15 +467,15 @@ namespace mimicpp::finally && (!std::is_void_v&>>) [[nodiscard]] constexpr auto returns_result_of( - Fun&& fun // NOLINT(cppcoreguidelines-missing-std-forward) + Fun&& fun ) noexcept(std::is_nothrow_constructible_v, Fun>) { return expectation_policies::ReturnsResultOf{ [ - fun = std::forward(fun) + fn = std::forward(fun) ]([[maybe_unused]] const auto& call) mutable noexcept(std::is_nothrow_invocable_v) -> decltype(auto) { - return std::invoke(fun); + return std::invoke(fn); } }; } @@ -717,15 +717,15 @@ namespace mimicpp::then template [[nodiscard]] constexpr auto invoke( - Action&& action // NOLINT(cppcoreguidelines-missing-std-forward) + Action&& action ) noexcept(std::is_nothrow_constructible_v, Action>) { return expectation_policies::SideEffectAction{ [ - action = std::forward(action) + fn = std::forward(action) ]([[maybe_unused]] const auto& call) mutable noexcept(std::is_nothrow_invocable_v) { - std::invoke(action); + std::invoke(fn); } }; } diff --git a/include/mimic++/Fwd.hpp b/include/mimic++/Fwd.hpp index b1e8e1c12..ed7b38746 100644 --- a/include/mimic++/Fwd.hpp +++ b/include/mimic++/Fwd.hpp @@ -18,51 +18,129 @@ namespace mimicpp::call namespace mimicpp { + /** + * \brief Primary template, purposely undefined. + * \ingroup TYPE_TRAITS_SIGNATURE_ADD_NOEXCEPT + * \tparam Signature A function signature. + */ template struct signature_add_noexcept; + /** + * \brief Convenience alias, exposing the ``type`` member alias of the actual type-trait. + * \ingroup TYPE_TRAITS_SIGNATURE_ADD_NOEXCEPT + * \tparam Signature A function signature. + */ template using signature_add_noexcept_t = typename signature_add_noexcept::type; + /** + * \brief Primary template, purposely undefined. + * \ingroup TYPE_TRAITS_SIGNATURE_REMOVE_NOEXCEPT + * \tparam Signature A function signature. + */ template struct signature_remove_noexcept; + /** + * \brief Convenience alias, exposing the ``type`` member alias of the actual type-trait. + * \ingroup TYPE_TRAITS_SIGNATURE_REMOVE_NOEXCEPT + * \tparam Signature A function signature. + */ template using signature_remove_noexcept_t = typename signature_remove_noexcept::type; + /** + * \brief Primary template, purposely undefined. + * \ingroup TYPE_TRAITS_SIGNATURE_DECAY + * \tparam Signature A function signature. + */ template struct signature_decay; + /** + * \brief Convenience alias, exposing the ``type`` member alias of the actual type-trait. + * \ingroup TYPE_TRAITS_SIGNATURE_DECAY + * \tparam Signature A function signature. + */ template using signature_decay_t = typename signature_decay::type; + /** + * \brief Primary template, purposely undefined. + * \ingroup TYPE_TRAITS_SIGNATURE_RETURN_TYPE + * \tparam Signature A function signature. + */ template struct signature_return_type; + /** + * \brief Convenience alias, exposing the ``type`` member alias of the actual type-trait. + * \ingroup TYPE_TRAITS_SIGNATURE_RETURN_TYPE + * \tparam Signature A function signature. + */ template using signature_return_type_t = typename signature_return_type::type; + /** + * \brief Primary template, purposely undefined. + * \ingroup TYPE_TRAITS_SIGNATURE_PARAM_TYPE + * \tparam Signature A function signature. + */ template struct signature_param_type; + /** + * \brief Convenience alias, exposing the ``type`` member alias of the actual type-trait. + * \ingroup TYPE_TRAITS_SIGNATURE_PARAM_TYPE + * \tparam Signature A function signature. + */ template using signature_param_type_t = typename signature_param_type::type; + /** + * \brief Primary template, purposely undefined. + * \ingroup TYPE_TRAITS_SIGNATURE_PARAM_LIST + * \tparam Signature A function signature. + */ template struct signature_param_list; + /** + * \brief Convenience alias, exposing the ``type`` member alias of the actual type-trait. + * \ingroup TYPE_TRAITS_SIGNATURE_PARAM_LIST + * \tparam Signature A function signature. + */ template using signature_param_list_t = typename signature_param_list::type; template struct is_overloadable_with; + /** + * \brief Convenience constant, exposing the ``value`` member of the actual type-trait. + * \ingroup TYPE_TRAITS_IS_OVERLOADABLE_WITH + * \tparam First The first function signature. + * \tparam Second The first function signature. + */ template inline constexpr bool is_overloadable_with_v = is_overloadable_with::value; + /** + * \brief Primary template, purposely undefined. + * \ingroup TYPE_TRAITS_IS_OVERLOAD_SET + * \tparam First The first function signature. + * \tparam Others The other function signatures. + */ template struct is_overload_set; + /** + * \brief Convenience constant, exposing the ``value`` member of the actual type-trait. + * \ingroup TYPE_TRAITS_IS_OVERLOAD_SET + * \tparam First The first function signature. + * \tparam Others The other function signatures. + */ template inline constexpr bool is_overload_set_v = is_overload_set::value; diff --git a/include/mimic++/Matcher.hpp b/include/mimic++/Matcher.hpp index 2bc783cc2..7b77e6591 100644 --- a/include/mimic++/Matcher.hpp +++ b/include/mimic++/Matcher.hpp @@ -174,12 +174,14 @@ namespace mimicpp return std::apply( [&, this](auto&... additionalArgs) { - // std::make_format_args requires lvalue-refs, so let's transform rvalue-refs to const lvalue-refs - constexpr auto makeLvalue = [](auto&& val) noexcept -> const auto& { return val; }; return format::vformat( m_FormatString, format::make_format_args( - makeLvalue(mimicpp::print(additionalArgs))...)); + std::invoke( + // std::make_format_args requires lvalue-refs, so let's transform rvalue-refs to const lvalue-refs + [](auto&& val) noexcept -> const auto& { return val; }, + mimicpp::print(additionalArgs)) + ...)); }, m_AdditionalArgs); } @@ -579,7 +581,7 @@ namespace mimicpp::matches::str detail::check_string_compatibility(); auto patternView = detail::make_view(std::forward(stored)); - const auto [_, patternIter] = std::ranges::mismatch( + const auto [ignore, patternIter] = std::ranges::mismatch( detail::make_view(std::forward(target)), patternView); @@ -606,7 +608,7 @@ namespace mimicpp::matches::str detail::check_string_compatibility(); auto caseFoldedPattern = detail::make_case_folded_string(std::forward(stored)); - const auto [_, patternIter] = std::ranges::mismatch( + const auto [ignore, patternIter] = std::ranges::mismatch( detail::make_case_folded_string(std::forward(target)), caseFoldedPattern); @@ -634,7 +636,7 @@ namespace mimicpp::matches::str auto patternView = detail::make_view(std::forward(stored)) | std::views::reverse; - const auto [_, patternIter] = std::ranges::mismatch( + const auto [ignore, patternIter] = std::ranges::mismatch( detail::make_view(std::forward(target)) | std::views::reverse, patternView); @@ -662,7 +664,7 @@ namespace mimicpp::matches::str auto caseFoldedPattern = detail::make_case_folded_string(std::forward(stored)) | std::views::reverse; - const auto [_, patternIter] = std::ranges::mismatch( + const auto [ignore, patternIter] = std::ranges::mismatch( detail::make_case_folded_string(std::forward(target)) | std::views::reverse, caseFoldedPattern); diff --git a/include/mimic++/Mock.hpp b/include/mimic++/Mock.hpp index f6ce59858..7f8a0498b 100644 --- a/include/mimic++/Mock.hpp +++ b/include/mimic++/Mock.hpp @@ -15,10 +15,6 @@ namespace mimicpp::detail { - template - concept requirement_for = std::equality_comparable_with - || matcher_for, Target>; - template class BasicMockFrontend { diff --git a/include/mimic++/ObjectWatcher.hpp b/include/mimic++/ObjectWatcher.hpp new file mode 100644 index 000000000..3bf7ab5da --- /dev/null +++ b/include/mimic++/ObjectWatcher.hpp @@ -0,0 +1,419 @@ +// // Copyright Dominic (DNKpp) Koepke 2024 - 2024. +// // Distributed under the Boost Software License, Version 1.0. +// // (See accompanying file LICENSE_1_0.txt or copy at +// // https://www.boost.org/LICENSE_1_0.txt) + +#ifndef MIMICPP_OBJECT_WATCHER_HPP +#define MIMICPP_OBJECT_WATCHER_HPP + +#pragma once + +#include "mimic++/Mock.hpp" + +#include +#include + +namespace mimicpp +{ + /** + * \defgroup OBJECT_WATCHING object-watching + * \brief Contains utility for explicit tracking of special object functionalities. + * \details + * \snippet Watcher.cpp watched lifetime relocation + * + *\{ + */ + + /** + * \brief A watcher type, which reports it's destructor calls. + * \details This watcher is designed to track, whether the destructor has been called. + * During its destructor call, it reports the destruction to the framework, which can be tracked + * by a previously created destruction-expectation. + * + * \snippet Watcher.cpp watched lifetime-watcher + * + * ## Moving + * + * This watcher can be freely moved around. + * + * ## Copying + * + * This watcher is copyable, but with very special behaviour. + * + * As this watcher is generally designed to be part of a bigger object, it would be very limiting not supporting + * copy-operations at all. The question is, how should a copy look like? + * + * In general a copy should be a logical duplicate of its source and the general expectation is: + * if ``B`` is a copy of ``A``, then ``A == B`` should yield true. + * \note This doesn't say, that if ``B`` is *not* a copy of ``A``, then ``A == B`` has to yield false! + * + * \details This won't be the case for ``LifetimeWatcher``s, as active destruction-expectations won't be copied over + * to the target. In general, if a LifetimeWatcher is used, we want to be very precise with our object-lifetime, + * thus an implicit expectation copy would be against the purpose of this helper. + * Due to this, each ``LifetimeWatcher`` will be created as a fresh instance, when copy-construction is used. + * The same logic also applies to copy-assignment. + */ + class LifetimeWatcher + { + public: + /** + * \brief Destructor, which reports the call. + * \note A no-match error may occur, if no destruction-expectation has been defined. + * \snippet Watcher.cpp watched lifetime-watcher violation + */ + ~LifetimeWatcher() noexcept(false) + { + if (const auto destruction = std::exchange( + m_DestructionMock, + nullptr)) + { + (*destruction)(); + } + } + + /** + * \brief Defaulted default constructor. + */ + [[nodiscard]] + LifetimeWatcher() = default; + + /** + * \brief Copy-constructor. + * \param other The other object. + * \details This copy-constructor's purpose is to provide syntactically correct copy operations, + * but semantically this does not copy anything. + * In fact, it simply default-constructs the new instance, without even touching the ``other``. + * \note It is mandatory setting up a new destruction-expectation. Otherwise, a no-match will be reported + * during destruction. + * \snippet Watcher.cpp watched lifetime-watcher copy-construction violation + */ + [[nodiscard]] + LifetimeWatcher([[maybe_unused]] const LifetimeWatcher& other) + : LifetimeWatcher{} + { + } + + /** + * \brief Copy-assignment-operator. + * \param other The other object. + * \details This copy-assignment-operator's purpose is to provide syntactically correct copy operations, + * but semantically this does not copy anything. + * In fact, it simply deletes the previous content of this instance, default-constructs a new instance + * and move-assigns it to this instance, without even touching the ``other``. + * \note It is mandatory setting up a new destruction-expectation. Otherwise, a no-match will be reported + * during destruction. + * \snippet Watcher.cpp watched lifetime-watcher copy-assignment violation + * + * \note As this actually destructs a ``LifetimeWatcher``, violations will be reported, if the previous + * instance didn't have a valid destruction-expectation. + * \snippet Watcher.cpp watched lifetime-watcher copy-assignment violation2 + */ + LifetimeWatcher& operator =([[maybe_unused]] const LifetimeWatcher& other) + { + // let's make this a two-step. + // First destroy the previous instance, which may already report a violation. + // If we would already have the new instance created, this would lead also to + // a violation report, which actually might break everything. + { + LifetimeWatcher temp{std::move(*this)}; + } + + *this = LifetimeWatcher{}; + + return *this; + } + + /** + * \brief Defaulted move-constructor. + * \details This move-constructor simply transfers everything from the source to the destination object. + * As source is then a "moved-from"-object, it doesn't require any destruction-expectations. + */ + [[nodiscard]] + LifetimeWatcher(LifetimeWatcher&&) = default; + + /** + * \brief Defaulted move-assignment-operator. + * \details This move-assignment-operator simply transfers everything from the source to the destination object. + * As source is then a "moved-from"-object, it doesn't require any destruction-expectations. + */ + LifetimeWatcher& operator =(LifetimeWatcher&&) = default; + + /** + * \brief Begins a destruction-expectation construction. + * \return A newly created expectation-builder-instance. + * \note This function creates a new expectation-builder-instance, which isn't an expectation yet. + * User must convert this to an actual expectation, by handing it over to a new ``ScopedExpectation`` instance. + * This can either be done manually or via \ref MIMICPP_SCOPED_EXPECTATION (or the shorthand version \ref SCOPED_EXP). + * \throws std::logic_error if a destruction-expectation has already been created for this instance. + */ + [[nodiscard]] + auto expect_destruct() + { + if (std::exchange(m_HasDestructExpectation, true)) + { + throw std::logic_error{ + "LifetimeWatcher: A destruct expectation can not be created more than once for a single instance." + }; + } + + return m_DestructionMock->expect_call() + and expect::once(); // prevent further times specifications + } + + private: + bool m_HasDestructExpectation{}; + std::unique_ptr> m_DestructionMock{ + std::make_unique>() + }; + }; + + /** + * \brief A watcher type, which reports it's move-constructor and -assignment calls. + * \details This watcher is designed to track, whether a move has been performed. + * During a move, it reports the relocation to the framework, which can be tracked + * by a previously created relocation-expectation. + * + * \snippet Watcher.cpp watched lifetime relocation + * + * ## Moving + * + * This watcher can be freely moved around, but any relocation events must match with a previously created + * relocation-expectation. + * + * ## Copying + * + * This watcher is copyable, but with very special behaviour. + * + * As this watcher is generally designed to be part of a bigger object, it would be very limiting not supporting + * copy-operations at all. The question is, how should a copy look like? + * + * In general a copy should be a logical duplicate of its source and the general expectation is: + * if ``B`` is a copy of ``A``, then ``A == B`` should yield true. + * \note This doesn't say, that if ``B`` is *not* a copy of ``A``, then ``A == B`` has to yield false! + * + * \details This won't be the case for ``RelocationWatcher``s, as active relocation-expectations won't be copied over + * to the target. In general, if a ``RelocationWatcher`` is used, we want to be very precise with our move, + * thus an implicit expectation copy would be against the purpose of this helper. + * Due to this, each ``RelocationWatcher`` will be created as a fresh instance, when copy-construction is used. + * The same logic also applies to copy-assignment. + */ + class RelocationWatcher + { + public: + /** + * \brief Defaulted destructor. + */ + ~RelocationWatcher() = default; + + /** + * \brief Defaulted default constructor. + */ + [[nodiscard]] + RelocationWatcher() = default; + + /** + * \brief Copy-constructor. + * \param other The other object. + * \details This copy-constructor's purpose is to provide syntactically correct copy operations, + * but semantically this does not copy anything. + * In fact, it simply default-constructs the new instance, without even touching the ``other``. + */ + [[nodiscard]] + RelocationWatcher([[maybe_unused]] const RelocationWatcher& other) + : RelocationWatcher{} + { + } + + /** + * \brief Copy-assignment-operator. + * \param other The other object. + * \details This copy-assignment-operator's purpose is to provide syntactically correct copy operations, + * but semantically this does not copy anything. + * In fact, it simply overrides its internals with a fresh instance, without even touching the ``other``. + */ + RelocationWatcher& operator =([[maybe_unused]] const RelocationWatcher& other) + { + // explicitly circumvent default construct and assign, because that would + // involve the move-assignment. + m_RelocationMock = Mock{}; + + return *this; + } + + /** + * \brief Move-constructor, which reports a relocation. + * \note A no-match error may occur, if no relocation-expectation has been defined. + * \param other The other object. + */ + [[nodiscard]] + RelocationWatcher(RelocationWatcher&& other) noexcept(false) + { + *this = std::move(other); + } + + /** + * \brief Move-assignment-operator, which reports a relocation. + * \note A no-match error may occur, if no relocation-expectation has been defined. + * \param other The other object. + */ + RelocationWatcher& operator =(RelocationWatcher&& other) noexcept(false) + { + other.m_RelocationMock(); + m_RelocationMock = std::move(other).m_RelocationMock; + + return *this; + } + + /** + * \brief Begins a relocation-expectation construction. + * \return A newly created expectation-builder-instance. + * \note This function creates a new expectation-builder-instance, which isn't an expectation yet. + * User must convert this to an actual expectation, by handing it over to a new ``ScopedExpectation`` instance. + * This can either be done manually or via \ref MIMICPP_SCOPED_EXPECTATION (or the shorthand version \ref SCOPED_EXP). + */ + [[nodiscard]] + auto expect_relocate() + { + return m_RelocationMock.expect_call(); + } + + private: + Mock m_RelocationMock{}; + }; + + template + concept object_watcher = std::is_default_constructible_v + && std::is_copy_constructible_v + && std::is_copy_assignable_v + && std::is_move_constructible_v + && std::is_move_assignable_v + && std::is_destructible_v; + + namespace detail + { + template + class CombinedWatchers + : public Watchers... + { + public: + ~CombinedWatchers() noexcept(std::is_nothrow_destructible_v) = default; + + CombinedWatchers() = default; + + CombinedWatchers(const CombinedWatchers&) = default; + CombinedWatchers& operator =(const CombinedWatchers&) = default; + + CombinedWatchers(CombinedWatchers&& other) noexcept(std::is_nothrow_move_constructible_v) = default; + CombinedWatchers& operator =(CombinedWatchers&& other) noexcept(std::is_nothrow_move_assignable_v) = default; + }; + + template + class BasicWatched + : public CombinedWatchers, + public Base + { + public: + ~BasicWatched() = default; + + using Base::Base; + + BasicWatched(const BasicWatched&) = default; + BasicWatched& operator =(const BasicWatched&) = default; + BasicWatched(BasicWatched&&) = default; + BasicWatched& operator =(BasicWatched&&) = default; + }; + + template Base, typename... Watchers> + class BasicWatched + : public CombinedWatchers, + public Base + { + public: + ~BasicWatched() override = default; + + using Base::Base; + + BasicWatched(const BasicWatched&) = default; + BasicWatched& operator =(const BasicWatched&) = default; + BasicWatched(BasicWatched&&) = default; + BasicWatched& operator =(BasicWatched&&) = default; + }; + } + + /** + * \brief CRTP-type, inheriting first from all ``Watchers`` and then ``Base``, thus effectively couple them all together. + * \tparam Base The main type. + * \tparam Watchers All utilized watcher types. + * \details + * ## Move-constructor and -assignment-operator + * + * ``Watched`` automatically detects the specifications of the ``Base`` move-constructor and -assignment-operator, + * regardless of the ``Watchers`` specifications. This is done, so that the ``Watched`` instance does mimic the interface + * of ``Base`` as closely as possible. + * + * This is important to note, as this has implications when a ``RelocationWatcher`` is utilized. + * ``RelocationWatcher`` may, during either move-construction or -assignment, report violations to the currently active reporter. + * This reporter has to act accordingly, by either throwing an exception or terminating the program. + * + * So, if reporter throws due to a detected violation and the move-operation is declared ``noexcept``, this will inevitable lead + * to a ``std::terminate``. + * \see https://en.cppreference.com/w/cpp/error/terminate + * Nevertheless, this is usually fine, as watchers are merely used under controlled circumstances and to guarantee the expected + * behavior. If a violation is reported, an appropriate output will be generated, which should be enough of a hint to track + * down the bug. + * + * ## Destructor + * + * ``Watched`` automatically detects, whether ``Base`` has a virtual destructor and applies ``override`` if that's the case. + * It also forces the same ``noexcept``-ness for the destruct: If ``Base`` is nothrow destructible, ``Watched`` is it, too. + * + * This is important to note, as this has implications when a ``LifetimeWatcher`` is utilized. + * ``LifetimeWatcher`` may, during destruction, report violations to the currently active reporter. This reporter has to + * act accordingly, by either throwing an exception or terminating the program. + * + * As the destructor of the ``LifetimeWatcher`` will effectively be called from ``~Watched``, this will lead to a call to + * ``std::terminate``, if ``Base`` has a ``noexcept`` destructor (which is very likely, as it's a very strong default) and + * the reporter propagates the violation via an exception. + * \see https://en.cppreference.com/w/cpp/language/noexcept_spec + * \see https://en.cppreference.com/w/cpp/error/terminate + * + * There is no real way around that, beside explicitly ``~Watched`` as ``noexcept(false)``. Unfortunately, this would + * lead to inconsistencies with ``noexcept`` declared ``virtual`` destructors, because this requires all subclasses to match + * that specification. + * Besides that, there is an even stronger argument to strictly follow what ``Base`` offers: + * A ``Watched`` object should be as close to the original ``Base`` type as possible. + * If one wants to store a ``Watched`` inside e.g. ``std::vector`` and ``~Watched`` would have a different ``noexcept`` + * specification than ``Base``, that would lead to behavior changes. This should never be the case, + * as mocks are expected to behave like an actual implementation-object. + * + * So, what does all of this mean? + * + * Actually, there are no implications to working tests. If they satisfy the expectations, no one will notice anything different. + * When it comes to a violation, which is detected by the destructor of a ``LifetimeWatcher``, the reporter will be notified and + * should print the no-match report to the console. After that, the program will than probably terminate (or halt, if a debugger + * is attached), but you should at least have an idea, which test is affected. + */ + template + requires std::same_as> + class Watched + : public detail::BasicWatched + { + using SuperT = detail::BasicWatched; + + public: + ~Watched() = default; + + using SuperT::SuperT; + + Watched(const Watched&) = default; + Watched& operator =(const Watched&) = default; + Watched(Watched&&) = default; + Watched& operator =(Watched&&) = default; + }; + + /** + * \} + */ +} + +#endif diff --git a/include/mimic++/Printer.hpp b/include/mimic++/Printer.hpp index e44241425..cd883f551 100644 --- a/include/mimic++/Printer.hpp +++ b/include/mimic++/Printer.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -392,6 +393,79 @@ namespace mimicpp::detail } }; + template <> + class Printer + { + public: + template + static OutIter print(OutIter out, [[maybe_unused]] const std::nullopt_t) + { + return format::format_to( + std::move(out), + "nullopt"); + } + }; + + template + class Printer> + { + public: + template + static OutIter print(OutIter out, const std::optional& opt) + { + constexpr PrintFn print{}; + + if (opt) + { + out = format::format_to(std::move(out), "{{ value: "); + out = print(std::move(out), *opt); + return format::format_to(std::move(out), " }}"); + } + return print(std::move(out), std::nullopt); + } + }; + + template + OutIter tuple_element_print(OutIter out, Tuple&& tuple) + { + if constexpr (0u != index) + { + out = format::format_to(std::move(out), ", "); + } + + constexpr PrintFn printer{}; + return printer( + std::move(out), + std::get(std::forward(tuple))); + } + + template + requires requires + { + typename std::tuple_size::type; + requires std::convertible_to::type, std::size_t>; + requires 0u <= std::tuple_size_v; + } + class Printer + { + public: + template + static OutIter print(OutIter out, const T& tuple) + { + out = format::format_to(std::move(out), "{{ "); + + std::invoke( + [&]([[maybe_unused]] const std::index_sequence) + { + (..., + (out = tuple_element_print(std::move(out), tuple))); + }, + std::make_index_sequence>{}); + + return format::format_to(std::move(out), " }}"); + } + }; + template requires is_character_v struct character_literal_printer; diff --git a/include/mimic++/Sequence.hpp b/include/mimic++/Sequence.hpp index 8581e2354..160e892bf 100644 --- a/include/mimic++/Sequence.hpp +++ b/include/mimic++/Sequence.hpp @@ -135,8 +135,7 @@ namespace mimicpp::sequence constexpr void set_satisfied(const IdT id) noexcept { assert(is_valid(id)); - const auto index = to_underlying(id); - assert(m_Cursor <= index); + assert(m_Cursor <= to_underlying(id)); auto& element = m_Entries[to_underlying(id)]; assert(element == State::unsatisfied); diff --git a/include/mimic++/String.hpp b/include/mimic++/String.hpp index 54007cf96..204bbd4c2 100644 --- a/include/mimic++/String.hpp +++ b/include/mimic++/String.hpp @@ -11,7 +11,6 @@ #include "mimic++/Fwd.hpp" #include "mimic++/Utility.hpp" -#include #include #include #include @@ -175,7 +174,6 @@ namespace mimicpp /** * \brief Specialization for ``std::basic_string`` types. - * \tparam T Type to check. */ template struct string_traits> @@ -193,7 +191,6 @@ namespace mimicpp /** * \brief Specialization for ``std::basic_string_view`` types. - * \tparam T Type to check. */ template struct string_traits> @@ -308,14 +305,16 @@ struct mimicpp::string_case_fold_converter #else -// is missing from the unicodelib headers -#include - -#include -#include +#if __has_include() \ + && __has_include() +#include +#include +#else + #error "Unable to find uni_algo includes." +#endif /** - * \brief Specialized template for the ``char`` type (with unicodelib backend). + * \brief Specialized template for the ``char`` type (with uni_algo backend). * \ingroup TYPE_TRAITS_STRING_CASE_FOLD_CONVERTER */ template <> @@ -325,18 +324,16 @@ struct mimicpp::string_case_fold_converter [[nodiscard]] constexpr auto operator ()(String&& str) const { - return unicode::utf8::encode( - unicode::to_case_fold( - unicode::utf8::decode( - std::string_view{ - std::ranges::data(str), - std::ranges::size(str) - }))); + return una::cases::to_casefold_utf8( + std::string_view{ + std::ranges::data(str), + std::ranges::size(str) + }); } }; /** - * \brief Specialized template for the ``wchar_t`` type (with unicodelib backend). + * \brief Specialized template for the ``wchar_t`` type (with uni_algo backend). * \ingroup TYPE_TRAITS_STRING_CASE_FOLD_CONVERTER */ template <> @@ -346,18 +343,16 @@ struct mimicpp::string_case_fold_converter [[nodiscard]] constexpr auto operator ()(String&& str) const { - return unicode::to_wstring( - unicode::to_case_fold( - unicode::to_utf32( - std::wstring_view{ - std::ranges::data(str), - std::ranges::size(str) - }))); + return una::cases::to_casefold_utf16( + std::wstring_view{ + std::ranges::data(str), + std::ranges::size(str) + }); } }; /** - * \brief Specialized template for the ``char8_t`` type (with unicodelib backend). + * \brief Specialized template for the ``char8_t`` type (with uni_algo backend). * \ingroup TYPE_TRAITS_STRING_CASE_FOLD_CONVERTER */ template <> @@ -367,22 +362,16 @@ struct mimicpp::string_case_fold_converter [[nodiscard]] constexpr auto operator ()(String&& str) const { - const std::string caseFolded = std::invoke( - mimicpp::string_case_fold_converter{}, - std::string_view{ - std::bit_cast(std::ranges::data(str)), + return una::cases::to_casefold_utf8( + std::u8string_view{ + std::ranges::data(str), std::ranges::size(str) }); - - return std::u8string{ - caseFolded.cbegin(), - caseFolded.cend() - }; } }; /** - * \brief Specialized template for the ``char16_t`` type (with unicodelib backend). + * \brief Specialized template for the ``char16_t`` type (with uni_algo backend). * \ingroup TYPE_TRAITS_STRING_CASE_FOLD_CONVERTER */ template <> @@ -392,18 +381,16 @@ struct mimicpp::string_case_fold_converter [[nodiscard]] constexpr auto operator ()(String&& str) const { - return unicode::utf16::encode( - unicode::to_case_fold( - unicode::utf16::decode( - std::u16string_view{ - std::ranges::data(str), - std::ranges::size(str) - }))); + return una::cases::to_casefold_utf16( + std::u16string_view{ + std::ranges::data(str), + std::ranges::size(str) + }); } }; /** - * \brief Specialized template for the ``char32_t`` type (with unicodelib backend). + * \brief Specialized template for the ``char32_t`` type (with uni_algo backend). * \ingroup TYPE_TRAITS_STRING_CASE_FOLD_CONVERTER */ template <> @@ -413,11 +400,13 @@ struct mimicpp::string_case_fold_converter [[nodiscard]] constexpr auto operator ()(String&& str) const { - return unicode::to_case_fold( - std::u32string_view{ - std::ranges::data(str), - std::ranges::size(str) - }); + return una::utf8to32( + una::cases::to_casefold_utf8( + una::utf32to8u( + std::u32string_view{ + std::ranges::data(str), + std::ranges::size(str) + }))); } }; diff --git a/include/mimic++/TypeTraits.hpp b/include/mimic++/TypeTraits.hpp index cc57a8ce9..7ad6e4b98 100644 --- a/include/mimic++/TypeTraits.hpp +++ b/include/mimic++/TypeTraits.hpp @@ -17,6 +17,19 @@ namespace mimicpp { + /** + * \defgroup TYPE_TRAITS type-traits + * \brief Contains various type-traits + */ + + /** + * \defgroup TYPE_TRAITS_SIGNATURE_ADD_NOEXCEPT signature_add_noexcept + * \ingroup TYPE_TRAITS + * \brief Adds the ``noexcept`` specification to a signature. + * + *\{ + */ + template struct signature_add_noexcept { @@ -161,6 +174,18 @@ namespace mimicpp using type = Return(Params..., ...) const && noexcept; }; + /** + * \} + */ + + /** + * \defgroup TYPE_TRAITS_SIGNATURE_REMOVE_NOEXCEPT signature_remove_noexcept + * \ingroup TYPE_TRAITS + * \brief Removes the ``noexcept`` specification to a signature (if present). + * + *\{ + */ + template struct signature_remove_noexcept { @@ -305,6 +330,18 @@ namespace mimicpp using type = Return(Params..., ...) const &&; }; + /** + * \} + */ + + /** + * \defgroup TYPE_TRAITS_SIGNATURE_DECAY signature_decay + * \ingroup TYPE_TRAITS + * \brief Removes all specifications from the given signature. + * + *\{ + */ + template struct signature_decay { @@ -449,6 +486,18 @@ namespace mimicpp using type = Return(Params..., ...); }; + /** + * \} + */ + + /** + * \defgroup TYPE_TRAITS_SIGNATURE_RETURN_TYPE signature_return_type + * \ingroup TYPE_TRAITS + * \brief Extracts the return type from a given signature. + * + *\{ + */ + template requires std::is_function_v struct signature_return_type @@ -468,6 +517,18 @@ namespace mimicpp using type = Return; }; + /** + * \} + */ + + /** + * \defgroup TYPE_TRAITS_SIGNATURE_PARAM_TYPE signature_param_type + * \ingroup TYPE_TRAITS + * \brief Extracts the ``i``th param type from a given signature. + * + *\{ + */ + template requires std::is_function_v struct signature_param_type @@ -483,6 +544,18 @@ namespace mimicpp { }; + /** + * \} + */ + + /** + * \defgroup TYPE_TRAITS_SIGNATURE_PARAM_LIST signature_param_list + * \ingroup TYPE_TRAITS + * \brief Extracts all param types from a given signature (packed into a ``std::tuple``). + * + *\{ + */ + template requires std::is_function_v struct signature_param_list @@ -497,6 +570,10 @@ namespace mimicpp using type = std::tuple; }; + /** + * \} + */ + namespace detail { template @@ -561,6 +638,14 @@ namespace mimicpp }; } + /** + * \defgroup TYPE_TRAITS_IS_OVERLOADABLE_WITH is_overloadable_with + * \ingroup TYPE_TRAITS + * \brief Determines, whether two signatures are valid overloads. + * + *\{ + */ + template struct is_overloadable_with : public detail::is_overloadable_with< @@ -569,6 +654,18 @@ namespace mimicpp { }; + /** + * \} + */ + + /** + * \defgroup TYPE_TRAITS_IS_OVERLOAD_SET is_overload_set + * \ingroup TYPE_TRAITS + * \brief Determines, whether a list of signatures form a valid overloads-set. + * + *\{ + */ + template struct is_overload_set : public std::true_type @@ -583,6 +680,10 @@ namespace mimicpp is_overload_set> { }; + + /** + * \} + */ } #endif diff --git a/include/mimic++/Utility.hpp b/include/mimic++/Utility.hpp index 4d1a80e42..84696470f 100644 --- a/include/mimic++/Utility.hpp +++ b/include/mimic++/Utility.hpp @@ -92,6 +92,8 @@ namespace mimicpp [[noreturn]] inline void unreachable() { + assert(false); + // Uses compiler specific extensions if possible. // Even if no extension is used, undefined behavior is still raised by // an empty function body and the noreturn attribute. @@ -100,9 +102,6 @@ namespace mimicpp #else // GCC, Clang __builtin_unreachable(); #endif - - // ReSharper disable once CppUnreachableCode - assert(false); } #endif diff --git a/include/mimic++/mimic++.hpp b/include/mimic++/mimic++.hpp index 0e7ebba41..8458b9163 100644 --- a/include/mimic++/mimic++.hpp +++ b/include/mimic++/mimic++.hpp @@ -28,5 +28,6 @@ #include "mimic++/Expectation.hpp" #include "mimic++/Mock.hpp" #include "mimic++/InterfaceMock.hpp" +#include "mimic++/ObjectWatcher.hpp" #endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a5fb70992..2909ee832 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,4 +1,7 @@ -find_package(sanitizers-cmake REQUIRED) +# Copyright Dominic (DNKpp) Koepke 2024 - 2024. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) add_subdirectory("unit-tests") diff --git a/test/adapter-tests/CMakeLists.txt b/test/adapter-tests/CMakeLists.txt index 122705d04..d1ec3bf6a 100644 --- a/test/adapter-tests/CMakeLists.txt +++ b/test/adapter-tests/CMakeLists.txt @@ -1,3 +1,8 @@ +# Copyright Dominic (DNKpp) Koepke 2024 - 2024. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) + add_subdirectory("catch2") add_subdirectory("gtest") -add_subdirectory("boost-test") \ No newline at end of file +add_subdirectory("boost-test") diff --git a/test/adapter-tests/boost-test/CMakeLists.txt b/test/adapter-tests/boost-test/CMakeLists.txt index 38ce2a613..53f33d1d1 100644 --- a/test/adapter-tests/boost-test/CMakeLists.txt +++ b/test/adapter-tests/boost-test/CMakeLists.txt @@ -1,10 +1,16 @@ +# Copyright Dominic (DNKpp) Koepke 2024 - 2024. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) + add_executable( mimicpp-adapter-tests-boost-test "main.cpp" ) -include(SetupTestTarget) -setup_test_target(mimicpp-adapter-tests-boost-test) +include(EnableWarnings) +include(EnableSanitizers) +enable_sanitizers(mimicpp-adapter-tests-boost-test) if (WIN32) set(BOOST_ARCHIVE_URL "https://github.com/boostorg/boost/releases/download/boost-1.85.0/boost-1.85.0-cmake.zip") @@ -23,6 +29,7 @@ CPMAddPackage( target_link_libraries( mimicpp-adapter-tests-boost-test PRIVATE + mimicpp::internal::warnings mimicpp::mimicpp Boost::unit_test_framework ) diff --git a/test/adapter-tests/catch2/CMakeLists.txt b/test/adapter-tests/catch2/CMakeLists.txt index af750f447..082984f59 100644 --- a/test/adapter-tests/catch2/CMakeLists.txt +++ b/test/adapter-tests/catch2/CMakeLists.txt @@ -1,3 +1,8 @@ +# Copyright Dominic (DNKpp) Koepke 2024 - 2024. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) + find_package(Catch2 REQUIRED) add_executable( @@ -5,13 +10,15 @@ add_executable( "main.cpp" ) -include(SetupTestTarget) -setup_test_target(mimicpp-adapter-tests-catch2) +include(EnableWarnings) +include(EnableSanitizers) +enable_sanitizers(mimicpp-adapter-tests-catch2) target_link_libraries( mimicpp-adapter-tests-catch2 PRIVATE mimicpp::mimicpp + mimicpp::internal::warnings Catch2::Catch2WithMain ) diff --git a/test/adapter-tests/gtest/CMakeLists.txt b/test/adapter-tests/gtest/CMakeLists.txt index abda9b695..664806819 100644 --- a/test/adapter-tests/gtest/CMakeLists.txt +++ b/test/adapter-tests/gtest/CMakeLists.txt @@ -1,10 +1,16 @@ +# Copyright Dominic (DNKpp) Koepke 2024 - 2024. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) + add_executable( mimicpp-adapter-tests-gtest "main.cpp" ) -include(SetupTestTarget) -setup_test_target(mimicpp-adapter-tests-gtest) +include(EnableWarnings) +include(EnableSanitizers) +enable_sanitizers(mimicpp-adapter-tests-gtest) CPMAddPackage( NAME GTest @@ -20,6 +26,7 @@ target_link_libraries( mimicpp-adapter-tests-gtest PRIVATE mimicpp::mimicpp + mimicpp::internal::warnings GTest::gtest_main ) diff --git a/test/unit-tests/CMakeLists.txt b/test/unit-tests/CMakeLists.txt index 4c28d8292..b8e13d057 100644 --- a/test/unit-tests/CMakeLists.txt +++ b/test/unit-tests/CMakeLists.txt @@ -1,3 +1,8 @@ +# Copyright Dominic (DNKpp) Koepke 2024 - 2024. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) + find_package(Catch2 REQUIRED) CPMAddPackage("gh:rollbear/trompeloeil@48") @@ -12,6 +17,7 @@ add_executable( "InterfaceMock.cpp" "Matcher.cpp" "Mock.cpp" + "ObjectWatcher.cpp" "Printer.cpp" "Reports.cpp" "Reporter.cpp" @@ -23,21 +29,23 @@ add_executable( add_subdirectory(matchers) -include(SetupTestTarget) -setup_test_target(mimicpp-tests) +include(EnableWarnings) +include(EnableSanitizers) +enable_sanitizers(mimicpp-tests) target_compile_options( - mimicpp - INTERFACE + mimicpp-tests + PRIVATE # some test files exceed some limits # see: https://learn.microsoft.com/en-us/cpp/error-messages/compiler-errors-1/fatal-error-c1128?view=msvc-170 - "$<$:/bigobj>" + $<$:/bigobj> ) target_link_libraries( mimicpp-tests PRIVATE mimicpp::mimicpp + mimicpp::internal::warnings Catch2::Catch2WithMain trompeloeil::trompeloeil ) diff --git a/test/unit-tests/ControlPolicy.cpp b/test/unit-tests/ControlPolicy.cpp index adda5c414..c489b286b 100644 --- a/test/unit-tests/ControlPolicy.cpp +++ b/test/unit-tests/ControlPolicy.cpp @@ -144,6 +144,7 @@ TEST_CASE( SECTION("When single sequence is provided.") { std::optional sequence{std::in_place}; + CHECK(sequence); std::optional policy{ ControlPolicy{ detail::TimesConfig{}, diff --git a/test/unit-tests/Expectation.cpp b/test/unit-tests/Expectation.cpp index 863e3d074..a543ef15c 100644 --- a/test/unit-tests/Expectation.cpp +++ b/test/unit-tests/Expectation.cpp @@ -8,6 +8,7 @@ #include "mimic++/Printer.hpp" #include "mimic++/Utility.hpp" +#include "SuppressionMacros.hpp" #include "TestReporter.hpp" #include "TestTypes.hpp" @@ -930,7 +931,10 @@ TEST_CASE( SECTION("And then self move assigned.") { + START_WARNING_SUPPRESSION + SUPPRESS_SELF_MOVE otherExpectation = std::move(otherExpectation); + STOP_WARNING_SUPPRESSION SECTION("When calling is_satisfied()") { diff --git a/test/unit-tests/Mock.cpp b/test/unit-tests/Mock.cpp index 6bfc9c8d6..ee9e1ed6a 100644 --- a/test/unit-tests/Mock.cpp +++ b/test/unit-tests/Mock.cpp @@ -887,3 +887,27 @@ TEST_CASE( REQUIRE(firstExpectation.is_satisfied()); REQUIRE(secondExpectation.is_satisfied()); } + +TEST_CASE("Mocks support direct argument matchers.") +{ + SECTION("For arguments, which support operator ==") + { + Mock mock{}; + SCOPED_EXP mock.expect_call(1337); + mock(1337); + } + + SECTION("For strings.") + { + Mock mock{}; + SCOPED_EXP mock.expect_call("Hello, World!"); + mock("Hello, World!"); + } + + SECTION("With explicit matchers.") + { + Mock mock{}; + SCOPED_EXP mock.expect_call(matches::ne(42)); + mock(1337); + } +} diff --git a/test/unit-tests/ObjectWatcher.cpp b/test/unit-tests/ObjectWatcher.cpp new file mode 100644 index 000000000..64dcddeac --- /dev/null +++ b/test/unit-tests/ObjectWatcher.cpp @@ -0,0 +1,822 @@ +// // Copyright Dominic (DNKpp) Koepke 2024 - 2024. +// // Distributed under the Boost Software License, Version 1.0. +// // (See accompanying file LICENSE_1_0.txt or copy at +// // https://www.boost.org/LICENSE_1_0.txt) + +#include "mimic++/ObjectWatcher.hpp" +#include "mimic++/InterfaceMock.hpp" + +#include +#include + +#include "SuppressionMacros.hpp" +#include "TestReporter.hpp" + +using namespace mimicpp; + +TEST_CASE( + "LifetimeWatcher tracks destruction", + "[object-watcher][object-watcher::lifetime]" +) +{ + namespace Matches = Catch::Matchers; + + ScopedReporter reporter{}; + + SECTION("Reports a no-match-error, when destruction occurs without an expectation.") + { + const auto action = []() { LifetimeWatcher watcher{}; }; + + REQUIRE_THROWS_AS( + action(), + NoMatchError); + + REQUIRE_THAT( + reporter.full_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.inapplicable_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.no_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.unfulfilled_expectations(), + Matches::IsEmpty()); + } + + SECTION("Reports an unfulfilled expectation, if the expectation expires before destruction occurs.") + { + auto watcher = std::make_unique(); + std::optional expectation = watcher->expect_destruct(); + expectation.reset(); + + REQUIRE_THAT( + reporter.full_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.inapplicable_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.no_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.unfulfilled_expectations(), + Matches::SizeIs(1)); + + // we need to safely tear-down the watcher + CHECK_THROWS_AS( + std::invoke([&]{ delete watcher.release(); }), + NoMatchError); + } + + SECTION("Reports a full-match, if destruction occurs with an active expectation.") + { + SECTION("From an lvalue.") + { + auto expectation = std::invoke( + []() -> ScopedExpectation + { + LifetimeWatcher watcher{}; + return watcher.expect_destruct(); + }); + } + + SECTION("From an rvalue.") + { + ScopedExpectation expectation = LifetimeWatcher{}.expect_destruct(); + } + + REQUIRE_THAT( + reporter.full_match_reports(), + Matches::SizeIs(1)); + REQUIRE_THAT( + reporter.inapplicable_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.no_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.unfulfilled_expectations(), + Matches::IsEmpty()); + } + + SECTION("An exception is thrown, if a destruction expectation is set more than once.") + { + auto expectation = std::invoke( + []() -> ScopedExpectation + { + LifetimeWatcher watcher{}; + ScopedExpectation exp1 = watcher.expect_destruct(); + + REQUIRE_THROWS_AS( + watcher.expect_destruct(), + std::logic_error); + + REQUIRE_THROWS_AS( + watcher.expect_destruct(), + std::logic_error); + + return exp1; + }); + + REQUIRE_THAT( + reporter.full_match_reports(), + Matches::SizeIs(1)); + REQUIRE_THAT( + reporter.inapplicable_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.no_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.unfulfilled_expectations(), + Matches::IsEmpty()); + } + + SECTION("LifetimeWatcher can be moved.") + { + std::optional source{std::in_place}; + + SECTION("With an already active destruct-expectation") + { + ScopedExpectation firstExpectation = source->expect_destruct(); + + SECTION("When move constructed.") + { + LifetimeWatcher target{*std::move(source)}; + } + + SECTION("When move assigned.") + { + auto innerExp = std::invoke( + [&] + { + LifetimeWatcher target{}; + ScopedExpectation secondExpectation = target.expect_destruct(); + + target = *std::move(source); + + // let's also swap the expectations, so the tracking becomes easier. + using std::swap; + swap(firstExpectation, secondExpectation); + return secondExpectation; + }); + } + + SECTION("When self-move assigned.") + { + START_WARNING_SUPPRESSION + SUPPRESS_SELF_MOVE + *source = *std::move(source); + STOP_WARNING_SUPPRESSION + + // need to manually destroy the object, to prevent the expectation outliving the lifetime-watcher + source.reset(); + } + } + + SECTION("Without an active destruct-expectation.") + { + SECTION("When move constructed.") + { + auto expectation = std::invoke( + [&]() -> ScopedExpectation + { + LifetimeWatcher target{*std::move(source)}; + return target.expect_destruct(); + }); + } + + SECTION("When move assigned.") + { + auto innerExp = std::invoke( + [&]() -> ScopedExpectation + { + LifetimeWatcher target{}; + // note: The target must have an active expectation, as it's considered dead after the move happened. + ScopedExpectation targetExp = target.expect_destruct(); + + target = *std::move(source); + + return target.expect_destruct(); + }); + } + } + } + + SECTION("LifetimeWatcher can be copied.") + { + std::optional source{std::in_place}; + + SECTION("With an already active destruct-expectation") + { + MIMICPP_SCOPED_EXPECTATION source->expect_destruct(); + + SECTION("When copy-constructing.") + { + auto expectation = std::invoke( + [&]() -> ScopedExpectation + { + LifetimeWatcher target{*source}; + return target.expect_destruct(); + }); + + // need to manually destroy the object, to prevent the expectation outliving the lifetime-watcher + source.reset(); + } + + SECTION("When copy-assigning.") + { + auto expectation = std::invoke( + [&]() -> ScopedExpectation + { + LifetimeWatcher target{}; + MIMICPP_SCOPED_EXPECTATION target.expect_destruct(); + + target = *source; + return target.expect_destruct(); + }); + + // need to manually destroy the object, to prevent the expectation outliving the lifetime-watcher + source.reset(); + } + + SECTION("When self-copy-assigning.") + { + source = *source; + MIMICPP_SCOPED_EXPECTATION source->expect_destruct(); + + // need to manually destroy the object, to prevent the expectation outliving the lifetime-watcher + source.reset(); + } + } + + SECTION("Without an active destruct-expectation") + { + SECTION("When copy-constructing.") + { + auto expectation = std::invoke( + [&]() -> ScopedExpectation + { + LifetimeWatcher target{*source}; + return target.expect_destruct(); + }); + + MIMICPP_SCOPED_EXPECTATION source->expect_destruct(); + // need to manually destroy the object, to prevent the expectation outliving the lifetime-watcher + source.reset(); + } + + SECTION("When copy-assigning.") + { + auto expectation = std::invoke( + [&]() -> ScopedExpectation + { + LifetimeWatcher target{}; + MIMICPP_SCOPED_EXPECTATION target.expect_destruct(); + + target = *source; + return target.expect_destruct(); + }); + + MIMICPP_SCOPED_EXPECTATION source->expect_destruct(); + // need to manually destroy the object, to prevent the expectation outliving the lifetime-watcher + source.reset(); + } + } + } +} + +TEST_CASE( + "LifetimeWatcher supports finally::throws policy.", + "[object-watcher][object-watcher::lifetime]" +) +{ + struct my_exception + { + }; + + const auto action = [] + { + LifetimeWatcher watcher{}; + MIMICPP_SCOPED_EXPECTATION watcher.expect_destruct() + and finally::throws(my_exception{}); + + // it's very important, making sure, that the expectation outlives the LifetimeWatcher + LifetimeWatcher other{std::move(watcher)}; + }; + + REQUIRE_THROWS_AS( + action(), + my_exception); +} + +TEST_CASE( + "LifetimeWatcher watched can wrap the actual type to be watched with the utilized watcher types.", + "[object-watcher][object-watcher::lifetime]" +) +{ + STATIC_REQUIRE(std::is_nothrow_destructible_v, LifetimeWatcher>>); + + SECTION("Detects violations.") + { + ScopedReporter reporter{}; + + struct not_nothrow_destructible + { + ~not_nothrow_destructible() noexcept(false) + { + } + }; + + using WatcherT = Watched; + STATIC_REQUIRE(!std::is_nothrow_destructible_v); + + REQUIRE_THROWS_AS( + WatcherT{}, + NoMatchError); + } + + SECTION("Just plain usage.") + { + Watched< + Mock, + LifetimeWatcher> watched{}; + + MIMICPP_SCOPED_EXPECTATION watched.expect_destruct(); + MIMICPP_SCOPED_EXPECTATION watched.expect_call(42); + + watched(42); + + // extend lifetime, to outlive all expectations + auto temp{std::move(watched)}; + } + + SECTION("With explicit sequence.") + { + Watched< + Mock, + LifetimeWatcher> watched{}; + + SequenceT sequence{}; + { + Watched< + Mock, + LifetimeWatcher> other{}; + + MIMICPP_SCOPED_EXPECTATION other.expect_destruct() + and expect::in_sequence(sequence); + + // extend lifetime, to outlive its expectations + auto temp{std::move(other)}; + } + + MIMICPP_SCOPED_EXPECTATION watched.expect_call(42) + and expect::in_sequence(sequence); + MIMICPP_SCOPED_EXPECTATION watched.expect_destruct() + and expect::in_sequence(sequence); + + watched(42); + + // extend lifetime, to outlive its expectations + auto temp{std::move(watched)}; + } +} + +TEST_CASE( + "LifetimeWatcher watched can be used on interface-mocks.", + "[object-watcher][object-watcher::lifetime]" +) +{ + class Interface + { + public: + virtual ~Interface() = default; + virtual void foo() = 0; + }; + + class Derived + : public Interface + { + public: + MIMICPP_MOCK_METHOD(foo, void, ()); + }; + + STATIC_REQUIRE(std::is_nothrow_destructible_v>); + + auto watched = std::make_unique>(); + + MIMICPP_SCOPED_EXPECTATION watched->expect_destruct(); + MIMICPP_SCOPED_EXPECTATION watched->foo_.expect_call(); + + watched->foo(); + + // extend lifetime, to outlive its expectations + const std::unique_ptr temp{std::move(watched)}; +} + +TEST_CASE( + "Violations of LifetimeWatcher watched interface-implementations will be detected.", + "[object-watcher][object-watcher::lifetime]" +) +{ + class Interface + { + public: + // must not be noexcept, due to the installed reporter + virtual ~Interface() noexcept(false) + { + } + }; + + class Derived + : public Interface + { + }; + + STATIC_REQUIRE(!std::is_nothrow_destructible_v>); + + ScopedReporter reporter{}; + + REQUIRE_THROWS_AS( + (Watched{}), + NoMatchError); +} + +TEST_CASE( + "RelocationWatcher tracks object move-constructions and -assignments.", + "[object-watcher][object-watcher::relocation]" +) +{ + namespace Matches = Catch::Matchers; + + ScopedReporter reporter{}; + + SECTION("Reports a no-match, if move occurs without an expectation.") + { + RelocationWatcher watcher{}; + + SECTION("When move constructing.") + { + REQUIRE_THROWS_AS( + RelocationWatcher{std::move(watcher)}, + NoMatchError); + } + + SECTION("When move assigning.") + { + const auto action = [&] + { + RelocationWatcher target{}; + target = std::move(watcher); + }; + + REQUIRE_THROWS_AS( + action(), + NoMatchError); + } + + SECTION("When self assigning.") + { + START_WARNING_SUPPRESSION + SUPPRESS_SELF_MOVE + REQUIRE_THROWS_AS( + watcher = std::move(watcher), + NoMatchError); + STOP_WARNING_SUPPRESSION + } + + REQUIRE_THAT( + reporter.full_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.inapplicable_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.no_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.unfulfilled_expectations(), + Matches::IsEmpty()); + } + + SECTION("Reports an unfulfilled expectation, if the expectation expires before relocation occurs.") + { + RelocationWatcher watcher{}; + std::optional expectation = watcher.expect_relocate(); + expectation.reset(); + + REQUIRE_THAT( + reporter.full_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.inapplicable_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.no_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.unfulfilled_expectations(), + Matches::SizeIs(1)); + } + + SECTION("Is satisfied, if a relocation occurs with an existing expectation.") + { + RelocationWatcher watcher{}; + SCOPED_EXP watcher.expect_relocate(); + + SECTION("When move-constructing.") + { + const RelocationWatcher target{std::move(watcher)}; + } + + SECTION("When move-assigning.") + { + RelocationWatcher target{}; + target = std::move(watcher); + } + + SECTION("When self move-assigning.") + { + START_WARNING_SUPPRESSION + SUPPRESS_SELF_MOVE + watcher = std::move(watcher); + STOP_WARNING_SUPPRESSION + } + + REQUIRE_THAT( + reporter.full_match_reports(), + Matches::SizeIs(1)); + REQUIRE_THAT( + reporter.inapplicable_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.no_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.unfulfilled_expectations(), + Matches::IsEmpty()); + } +} + +TEST_CASE( + "Copying RelocationWatcher doesn't satisfy it.", + "[object-watcher][object-watcher::relocation]" +) +{ + namespace Matches = Catch::Matchers; + + ScopedReporter reporter{}; + + RelocationWatcher watcher{}; + std::optional expectation = watcher.expect_relocate(); + + SECTION("When copy-constructing.") + { + RelocationWatcher other{watcher}; + } + + SECTION("When copy-assigning.") + { + RelocationWatcher other{}; + + other = watcher; + } + + SECTION("When self copy-assigning.") + { + START_WARNING_SUPPRESSION + SUPPRESS_SELF_ASSIGN + watcher = watcher; + STOP_WARNING_SUPPRESSION + + SECTION("And it does not accept the expectation from the previous instance.") + { + START_WARNING_SUPPRESSION + SUPPRESS_SELF_MOVE + REQUIRE_THROWS_AS( + watcher = std::move(watcher), + NoMatchError); + STOP_WARNING_SUPPRESSION + } + } + + expectation.reset(); + + REQUIRE_THAT( + reporter.full_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.inapplicable_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.no_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.unfulfilled_expectations(), + Matches::SizeIs(1)); +} + +TEST_CASE( + "RelocationWatcher supports finally::throws policy.", + "[object-watcher][object-watcher::relocation]" +) +{ + namespace Matches = Catch::Matchers; + + ScopedReporter reporter{}; + + struct my_exception + { + }; + + RelocationWatcher watcher{}; + MIMICPP_SCOPED_EXPECTATION watcher.expect_relocate() + and finally::throws(my_exception{}); + + SECTION("When move-constructing.") + { + REQUIRE_THROWS_AS( + RelocationWatcher{std::move(watcher)}, + my_exception); + } + + SECTION("When move-assigning.") + { + RelocationWatcher target{}; + + REQUIRE_THROWS_AS( + target = std::move(watcher), + my_exception); + } + + REQUIRE_THAT( + reporter.full_match_reports(), + Matches::SizeIs(1)); + REQUIRE_THAT( + reporter.inapplicable_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.no_match_reports(), + Matches::IsEmpty()); + REQUIRE_THAT( + reporter.unfulfilled_expectations(), + Matches::IsEmpty()); +} + +TEST_CASE( + "RelocationWatcher watched can wrap the actual type to be watched with the utilized watcher types.", + "[object-watcher][object-watcher::relocation]" +) +{ + STATIC_REQUIRE(std::is_nothrow_destructible_v, LifetimeWatcher>>); + + SECTION("Detects violations.") + { + ScopedReporter reporter{}; + + struct not_nothrow_movable + { + ~not_nothrow_movable() = default; + not_nothrow_movable() = default; + + not_nothrow_movable(const not_nothrow_movable&) = delete; + not_nothrow_movable& operator =(const not_nothrow_movable&) = delete; + + not_nothrow_movable(not_nothrow_movable&&) noexcept(false) + { + } + + not_nothrow_movable& operator =(not_nothrow_movable&&) noexcept(false) + { + return *this; + } + }; + + Watched< + not_nothrow_movable, + RelocationWatcher> watched{}; + STATIC_REQUIRE(!std::is_nothrow_move_constructible_v); + STATIC_REQUIRE(!std::is_nothrow_move_assignable_v); + + REQUIRE_THROWS_AS( + Watched{std::move(watched)}, + NoMatchError); + } + + SECTION("Just plain usage.") + { + Watched< + Mock, + RelocationWatcher> watched{}; + STATIC_REQUIRE(std::is_nothrow_move_constructible_v); + STATIC_REQUIRE(std::is_nothrow_move_assignable_v); + + MIMICPP_SCOPED_EXPECTATION watched.expect_call(1337); + MIMICPP_SCOPED_EXPECTATION watched.expect_relocate() + and expect::twice(); + MIMICPP_SCOPED_EXPECTATION watched.expect_call(42); + + watched(42); + Watched other{std::move(watched)}; + other(1337); + watched = std::move(other); + } + + SECTION("With explicit sequence.") + { + Watched< + Mock, + RelocationWatcher> watched{}; + STATIC_REQUIRE(std::is_nothrow_move_constructible_v); + STATIC_REQUIRE(std::is_nothrow_move_assignable_v); + + SequenceT sequence{}; + + MIMICPP_SCOPED_EXPECTATION watched.expect_call(42) + and expect::in_sequence(sequence); + MIMICPP_SCOPED_EXPECTATION watched.expect_relocate() + and expect::in_sequence(sequence); + MIMICPP_SCOPED_EXPECTATION watched.expect_call(1337) + and expect::in_sequence(sequence); + MIMICPP_SCOPED_EXPECTATION watched.expect_relocate() + and expect::in_sequence(sequence); + + watched(42); + Watched other{std::move(watched)}; + other(1337); + watched = std::move(other); + } +} + +TEST_CASE( + "RelocationWatcher watched can be used on interface-mocks.", + "[object-watcher][object-watcher::relocation]" +) +{ + class Interface + { + public: + virtual ~Interface() = default; + virtual void foo() = 0; + }; + + class Derived + : public Interface + { + public: + MIMICPP_MOCK_METHOD(foo, void, ()); + }; + + STATIC_REQUIRE(std::is_nothrow_move_constructible_v>); + STATIC_REQUIRE(std::is_nothrow_move_assignable_v>); + + Watched watched{}; + + MIMICPP_SCOPED_EXPECTATION watched.expect_destruct(); + MIMICPP_SCOPED_EXPECTATION watched.foo_.expect_call(); + + watched.foo(); + Watched other{std::move(watched)}; +} + +TEST_CASE( + "Violations of RelocationWatcher watched interface-implementations will be detected.", + "[object-watcher][object-watcher::lifetime]" +) +{ + class Interface + { + public: + ~Interface() = default; + Interface() = default; + + Interface(const Interface&) = delete; + Interface& operator =(const Interface&) = delete; + + Interface(Interface&&) noexcept(false) + { + } + + Interface& operator =(Interface&&) noexcept(false) + { + return *this; + } + }; + + class Derived + : public Interface + { + }; + + Watched watched{}; + STATIC_REQUIRE(!std::is_nothrow_move_constructible_v); + STATIC_REQUIRE(!std::is_nothrow_move_assignable_v); + + ScopedReporter reporter{}; + + REQUIRE_THROWS_AS( + Watched{std::move(watched)}, + NoMatchError); +} diff --git a/test/unit-tests/Printer.cpp b/test/unit-tests/Printer.cpp index 880a1aeea..95bb6e1de 100644 --- a/test/unit-tests/Printer.cpp +++ b/test/unit-tests/Printer.cpp @@ -409,6 +409,38 @@ TEST_CASE( Catch::Matchers::Matches(".+\\[\\d+:\\d+\\], .+")); } + SECTION("std::optional and std::nullopt_t have special treatment") + { + REQUIRE_THAT( + mimicpp::print(std::nullopt), + Catch::Matchers::Equals("nullopt")); + REQUIRE_THAT( + mimicpp::print(std::optional{}), + Catch::Matchers::Equals("nullopt")); + REQUIRE_THAT( + mimicpp::print(std::optional{}), + Catch::Matchers::Equals("nullopt")); + REQUIRE_THAT( + mimicpp::print(std::optional{1337}), + Catch::Matchers::Equals("{ value: 1337 }")); + REQUIRE_THAT( + mimicpp::print(std::optional{NonPrintable{}}), + Catch::Matchers::Equals("{ value: {?} }")); + } + + SECTION("Tuple-likes have special treatment") + { + REQUIRE_THAT( + mimicpp::print(std::tuple{}), + Catch::Matchers::Equals("{ }")); + REQUIRE_THAT( + mimicpp::print(std::tuple{1337}), + Catch::Matchers::Equals("{ 1337 }")); + REQUIRE_THAT( + mimicpp::print(std::tuple{NonPrintable{}, 1337}), + Catch::Matchers::Equals("{ {?}, 1337 }")); + } + SECTION("When nothing matches, a default token is inserted.") { constexpr NonPrintable value{}; diff --git a/test/unit-tests/Reporter.cpp b/test/unit-tests/Reporter.cpp index 9c18aed90..21746f396 100644 --- a/test/unit-tests/Reporter.cpp +++ b/test/unit-tests/Reporter.cpp @@ -11,6 +11,9 @@ #include #include +#include "SuppressionMacros.hpp" +#include "TestTypes.hpp" + using namespace mimicpp; namespace @@ -49,6 +52,10 @@ namespace }; } +START_WARNING_SUPPRESSION +// required for the REQUIRE_THROWS_AS tests +SUPPRESS_UNREACHABLE_CODE // on msvc, that must be set before the actual test-case + TEST_CASE( "free report functions forward to the currently installed reporter.", "[reporting][detail]" @@ -130,6 +137,12 @@ TEST_CASE( } } +STOP_WARNING_SUPPRESSION + +START_WARNING_SUPPRESSION +// required for the REQUIRE_THROWS_AS tests +SUPPRESS_UNREACHABLE_CODE // on msvc, that must be set before the actual test-case + TEST_CASE( "DefaultReporter throws exceptions on expectation violations.", "[reporting]" @@ -249,6 +262,8 @@ TEST_CASE( } } +STOP_WARNING_SUPPRESSION + namespace { struct saturated diff --git a/test/unit-tests/Sequence.cpp b/test/unit-tests/Sequence.cpp index babd4ed13..cc22fcf84 100644 --- a/test/unit-tests/Sequence.cpp +++ b/test/unit-tests/Sequence.cpp @@ -28,14 +28,14 @@ TEST_CASE( "[sequence]" ) { - using SequenceT = sequence::detail::BasicSequence; + using TestSequenceT = sequence::detail::BasicSequence; - STATIC_REQUIRE(std::is_default_constructible_v); + STATIC_REQUIRE(std::is_default_constructible_v); - STATIC_REQUIRE(!std::is_copy_constructible_v); - STATIC_REQUIRE(!std::is_copy_assignable_v); - STATIC_REQUIRE(!std::is_move_constructible_v); - STATIC_REQUIRE(!std::is_move_assignable_v); + STATIC_REQUIRE(!std::is_copy_constructible_v); + STATIC_REQUIRE(!std::is_copy_assignable_v); + STATIC_REQUIRE(!std::is_move_constructible_v); + STATIC_REQUIRE(!std::is_move_assignable_v); } TEMPLATE_TEST_CASE( @@ -87,10 +87,10 @@ TEST_CASE( { namespace Matches = Catch::Matchers; - using SequenceT = sequence::detail::BasicSequence; + using TestSequenceT = sequence::detail::BasicSequence; ScopedReporter reporter{}; - std::optional sequence{std::in_place}; + std::optional sequence{std::in_place}; SECTION("When Sequence contains zero elements.") { @@ -99,8 +99,8 @@ TEST_CASE( static constexpr std::array consumeStateActions = std::to_array( { - +[](SequenceT&, const Id) { assert(true); }, - +[](SequenceT& seq, const Id v) { seq.consume(v); } + +[](TestSequenceT&, const Id) { assert(true); }, + +[](TestSequenceT& seq, const Id v) { seq.consume(v); } }); SECTION("When sequence contains one id, that id must be satisfied.") @@ -150,9 +150,9 @@ TEST_CASE( { static constexpr std::array alterStateActions = std::to_array( { - +[](SequenceT&, const Id) { assert(true); }, - +[](SequenceT& seq, const Id v) { seq.set_satisfied(v); }, - +[](SequenceT& seq, const Id v) { seq.set_saturated(v); } + +[](TestSequenceT&, const Id) { assert(true); }, + +[](TestSequenceT& seq, const Id v) { seq.set_satisfied(v); }, + +[](TestSequenceT& seq, const Id v) { seq.set_saturated(v); } }); const std::vector ids{ diff --git a/test/unit-tests/SuppressionMacros.hpp b/test/unit-tests/SuppressionMacros.hpp new file mode 100644 index 000000000..bda1170fc --- /dev/null +++ b/test/unit-tests/SuppressionMacros.hpp @@ -0,0 +1,53 @@ +// // Copyright Dominic (DNKpp) Koepke 2024 - 2024. +// // Distributed under the Boost Software License, Version 1.0. +// // (See accompanying file LICENSE_1_0.txt or copy at +// // https://www.boost.org/LICENSE_1_0.txt) + +#pragma once + +#include + +#if defined(_MSC_VER) \ + && !defined(__clang__) + +#define START_WARNING_SUPPRESSION __pragma(warning(push)) +#define STOP_WARNING_SUPPRESSION __pragma(warning(pop)) + +#define SUPPRESS_UNREACHABLE_CODE __pragma(warning(disable: 4702)) +#define SUPPRESS_SELF_MOVE // seems not required on msvc +#define SUPPRESS_SELF_ASSIGN // seems not required on msvc +#define SUPPRESS_MAYBE_UNINITIALIZED// seems not required on msvc + +#else + +// clang accepts GCC diagnostic +#define START_WARNING_SUPPRESSION _Pragma("GCC diagnostic push") +#define STOP_WARNING_SUPPRESSION _Pragma("GCC diagnostic pop") + +#define SUPPRESS_UNREACHABLE_CODE _Pragma("GCC diagnostic ignored \"-Wunreachable-code\"") + +// clang doesn't know -Wmaybe-uninitialized, +// but gcc doesn't know -Wunknown-warning-option +// but this combination works +#define SUPPRESS_MAYBE_UNINITIALIZED \ + _Pragma("GCC diagnostic ignored \"-Wpragmas\"") \ + _Pragma("GCC diagnostic ignored \"-Wunknown-warning-option\"") \ + _Pragma("GCC diagnostic ignored \"-Wmaybe-uninitialized\"") + +// gcc 12 doesn't know -Wself-move option +#if !defined(__clang__) \ + && defined(__GNUC__) \ + && 12 >= __GNUC__ +#define SUPPRESS_SELF_MOVE +#else +#define SUPPRESS_SELF_MOVE _Pragma("GCC diagnostic ignored \"-Wself-move\"") +#endif + +// gcc doesn't know -Wself-assign-overloaded option +#if defined(__clang__) +#define SUPPRESS_SELF_ASSIGN _Pragma("GCC diagnostic ignored \"-Wself-assign-overloaded\"") +#else +#define SUPPRESS_SELF_ASSIGN +#endif + +#endif diff --git a/tools/amalgamate-headers/CMakeLists.txt b/tools/amalgamate-headers/CMakeLists.txt index 2d0407c5f..5369f148a 100644 --- a/tools/amalgamate-headers/CMakeLists.txt +++ b/tools/amalgamate-headers/CMakeLists.txt @@ -1,3 +1,8 @@ +# Copyright Dominic (DNKpp) Koepke 2024 - 2024. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# https://www.boost.org/LICENSE_1_0.txt) + message(TRACE "Begin amalgamate headers") set(AMALGAMATE_VERSION "v0.99.0") @@ -10,6 +15,7 @@ else() message(FATAL_ERROR "Unsupported platform.") endif() +include(get_cpm) CPMAddPackage( NAME amalgamate URL "https://github.com/rindeal/Amalgamate/releases/download/${AMALGAMATE_VERSION}/amalgamate-${AMALGAMATE_VERSION}-${AMALGAMATE_URL_SUFFIX}"