diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2b05aaf07..22a7fd541 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -133,8 +133,8 @@ jobs: if: ${{ matrix.config.compiler_name == 'clang' && matrix.config.libcxx == true }} shell: bash run: | - echo "CXX_FLAGS=$(echo $CXX_FLAGS -stdlib=libc++)" >> $GITHUB_ENV - echo "LINK_FLAGS=$(echo $LINK_FLAGS -lc++abi)" >> $GITHUB_ENV + echo "CXXFLAGS=$(echo $CXXFLAGS -stdlib=libc++)" >> $GITHUB_ENV + echo "LDFLAGS=$(echo $LDFLAGS -lc++abi)" >> $GITHUB_ENV - name: Setup linux if: ${{ matrix.config.prefix == 'Linux' }} @@ -170,9 +170,8 @@ jobs: -S . \ -B build \ -D CMAKE_VERBOSE_MAKEFILE=yes \ - -D CMAKE_CXX_FLAGS:STRING="${{ env.CXX_FLAGS }}" \ - -D CMAKE_EXE_LINKER_FLAGS:STRING="${{ env.LINK_FLAGS }}" \ -D MIMICPP_FORCED_CXX_STANDARD="${{ matrix.cxx_standard }}" \ + -D MIMICPP_ENABLE_ADAPTER_TESTS=YES \ ${{ env.CMAKE_CONFIG_EXTRA }} - name: Build @@ -186,7 +185,13 @@ jobs: shell: bash env: CTEST_OUTPUT_ON_FAILURE: 1 - run: ctest --test-dir build/test -C ${{ matrix.build_mode }} -j4 + run: ctest --test-dir build/test/unit-tests -C ${{ matrix.build_mode }} -j4 + + - name: Run adapter tests + shell: bash + env: + CTEST_OUTPUT_ON_FAILURE: 1 + run: ctest --test-dir build/test/adapter-tests -C ${{ matrix.build_mode }} -j4 - name: Run examples shell: bash diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 2043bd68d..1fdac0f48 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -29,11 +29,12 @@ jobs: - name: Configure env: LDFLAGS: "-fprofile-arcs" + CXXFLAGS: "-g -O0 --coverage -fno-inline -fprofile-abs-path -fkeep-inline-functions -fkeep-static-functions" run: | cmake -S . \ -B build \ - -D CMAKE_CXX_FLAGS="-g -O0 --coverage -fno-inline -fprofile-abs-path -fkeep-inline-functions -fkeep-static-functions" \ - -D CMAKE_BUILD_TYPE="Debug" + -D CMAKE_BUILD_TYPE="Debug" \ + -D MIMICPP_ENABLE_ADAPTER_TESTS=YES - name: Build run: | @@ -42,7 +43,12 @@ jobs: - name: Run tests env: CTEST_OUTPUT_ON_FAILURE: 1 - run: ctest --test-dir build/test -C Debug -j4 + run: ctest --test-dir build/test/unit-tests -C Debug -j4 + + - name: Run adapter tests + env: + CTEST_OUTPUT_ON_FAILURE: 1 + run: ctest --test-dir build/test/adapter-tests -C Debug -j4 - name: Run examples env: @@ -53,9 +59,11 @@ jobs: run: | # circumvnet "fatal: detected dubious ownership in repository" error git config --global --add safe.directory /__w/mimicpp/mimicpp - gcovr --root build/test --filter "include/mimic++" --keep -j4 -v \ + gcovr --root build/test/unit-tests --filter "include/mimic++" --keep -j4 -v \ --exclude-lines-by-pattern "\s*assert\(" \ + --exclude-lines-by-pattern "\s*unreachable\(\);" \ --exclude-unreachable-branches \ + --exclude-function-lines \ --exclude-noncode-lines \ --exclude-throw-branches \ --decisions \ @@ -68,7 +76,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: gcov-files - path: "build/test/*.gcov" + path: "build/test/unit-tests/*.gcov" - name: Upload generated report artifacts uses: actions/upload-artifact@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index 358547844..9d15aaf2b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,10 +29,14 @@ target_include_directories( ) set(CMAKE_CXX_STANDARD 20) +if (MIMICPP_FORCED_CXX_STANDARD) + set(CMAKE_CXX_STANDARD ${MIMICPP_FORCED_CXX_STANDARD}) +endif() + target_compile_features( mimicpp INTERFACE - cxx_std_20 + cxx_std_${CMAKE_CXX_STANDARD} ) if (CMAKE_SOURCE_DIR STREQUAL mimicpp_SOURCE_DIR) @@ -41,10 +45,6 @@ else() set(IS_TOP_LEVEL_PROJECT OFF) endif() -if (MIMICPP_FORCED_CXX_STANDARD) - set(CMAKE_CXX_STANDARD ${MIMICPP_FORCED_CXX_STANDARD}) -endif() - OPTION(MIMICPP_BUILD_TESTS "Determines, whether the tests shall be built." ${IS_TOP_LEVEL_PROJECT}) if (MIMICPP_BUILD_TESTS) include(CTest) diff --git a/Folder.DotSettings b/Folder.DotSettings index 0767b94a8..5164e5d25 100644 --- a/Folder.DotSettings +++ b/Folder.DotSettings @@ -1,6 +1,7 @@  True True + DO_NOT_SHOW SUGGESTION SUGGESTION WARNING diff --git a/cmake/SetupTestTarget.cmake b/cmake/SetupTestTarget.cmake new file mode 100644 index 000000000..cd4e7a102 --- /dev/null +++ b/cmake/SetupTestTarget.cmake @@ -0,0 +1,24 @@ +# 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) + + +function(setup_test_target TARGET_NAME) + + 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( + ${TARGET_NAME} + PRIVATE + $<$:_DISABLE_VECTOR_ANNOTATION> + $<$:_DISABLE_STRING_ANNOTATION> + ) + + endif() + + add_sanitizers(${TARGET_NAME}) + +endfunction() \ No newline at end of file diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in index f25f9f983..6471afa2a 100644 --- a/docs/Doxyfile.in +++ b/docs/Doxyfile.in @@ -502,7 +502,7 @@ LOOKUP_CACHE_SIZE = 0 # DOT_NUM_THREADS setting. # Minimum value: 0, maximum value: 32, default value: 1. -NUM_PROC_THREADS = 1 +NUM_PROC_THREADS = 0 # If the TIMESTAMP tag is set different from NO then each generated page will # contain the date or date and time when the page was generated. Setting this to @@ -510,7 +510,7 @@ NUM_PROC_THREADS = 1 # Possible values are: YES, NO, DATETIME and DATE. # The default value is: NO. -TIMESTAMP = NO +TIMESTAMP = DATE #--------------------------------------------------------------------------- # Build related configuration options diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 93880ad22..f9110605f 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,4 +1,4 @@ -CPMAddPackage("gh:catchorg/Catch2@3.5.4") +find_package(Catch2 REQUIRED) include("${Catch2_SOURCE_DIR}/extras/Catch.cmake") add_executable( @@ -9,31 +9,8 @@ add_executable( "SideEffects.cpp" ) -if (MSVC) - - # When using github ci, exceptions seems to be disabled by default. - target_compile_options( - mimicpp-examples - PRIVATE - /EHsc - ) - -endif() - -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( - mimicpp-examples - PRIVATE - $<$:_DISABLE_VECTOR_ANNOTATION> - $<$:_DISABLE_STRING_ANNOTATION> - ) - -endif() - -add_sanitizers(mimicpp-tests) +include(SetupTestTarget) +setup_test_target(mimicpp-examples) target_link_libraries( mimicpp-examples @@ -42,10 +19,4 @@ target_link_libraries( Catch2::Catch2WithMain ) -target_compile_features( - mimicpp - INTERFACE - cxx_std_${CMAKE_CXX_STANDARD} -) - catch_discover_tests(mimicpp-examples) diff --git a/examples/Finalizers.cpp b/examples/Finalizers.cpp index b2ce54f4e..3a2532279 100644 --- a/examples/Finalizers.cpp +++ b/examples/Finalizers.cpp @@ -5,8 +5,6 @@ #include "mimic++/Mock.hpp" -#include "../test/TestReporter.hpp" - #include #include diff --git a/examples/Requirements.cpp b/examples/Requirements.cpp index 806d88a94..ab593de86 100644 --- a/examples/Requirements.cpp +++ b/examples/Requirements.cpp @@ -5,8 +5,6 @@ #include "mimic++/Mock.hpp" -#include "../test/TestReporter.hpp" - #include TEST_CASE( @@ -161,7 +159,9 @@ TEST_CASE( return std::ranges::find(target, element) != std::ranges::end(target); }, // specify a descriptive format message, which will be applied to std::format. - "range {} contains the element {}", + "contains element {}", + // specify the inverted message, which will also be applied to std::format, when inversion is used + "contains not element {}", // capture additional data, which will be forwarded to both, the predicate and the description std::tuple{42} }; diff --git a/examples/Sequences.cpp b/examples/Sequences.cpp index fc71adb88..fccf889dd 100644 --- a/examples/Sequences.cpp +++ b/examples/Sequences.cpp @@ -6,8 +6,6 @@ #include "mimic++/Mock.hpp" #include "mimic++/Sequence.hpp" -#include "../test/TestReporter.hpp" - #include TEST_CASE( diff --git a/examples/SideEffects.cpp b/examples/SideEffects.cpp index dfccfb736..1c90d90bd 100644 --- a/examples/SideEffects.cpp +++ b/examples/SideEffects.cpp @@ -5,8 +5,6 @@ #include "mimic++/Mock.hpp" -#include "../test/TestReporter.hpp" - #include TEST_CASE( diff --git a/include/mimic++/Call.hpp b/include/mimic++/Call.hpp index bc80eb81f..3781c1b40 100644 --- a/include/mimic++/Call.hpp +++ b/include/mimic++/Call.hpp @@ -8,18 +8,13 @@ #pragma once +#include "mimic++/Fwd.hpp" #include "mimic++/TypeTraits.hpp" #include "mimic++/Utility.hpp" -#include -#include -#include -#include #include -#include #include -#include -#include +#include namespace mimicpp::call::detail { @@ -79,117 +74,4 @@ namespace mimicpp::call }; } -namespace mimicpp::call -{ - enum class MatchCategory - { - no, - non_applicable, - ok - }; -} - -template -struct std::formatter - : public std::formatter, Char> -{ - using MatchCategoryT = mimicpp::call::MatchCategory; - - auto format( - const MatchCategoryT category, - auto& ctx - ) const - { - constexpr auto toString = [](const MatchCategoryT cat) - { - switch (cat) - { - case MatchCategoryT::no: return "no match"; - case MatchCategoryT::non_applicable: return "non applicable match"; - case MatchCategoryT::ok: return "full match"; - } - - throw std::invalid_argument{"Unknown category value."}; - }; - - return std::formatter, Char>::format( - toString(category), - ctx); - } -}; - -namespace mimicpp::call -{ - class SubMatchResult - { - public: - bool matched{}; - std::optional msg{}; - - [[nodiscard]] - friend bool operator ==(const SubMatchResult&, const SubMatchResult&) = default; - }; - - template - class GenericMatchResult - : public std::integral_constant - { - public: - std::vector subMatchResults{}; - - [[nodiscard]] - friend bool operator ==(const GenericMatchResult&, const GenericMatchResult&) = default; - }; - - using MatchResult_NoT = GenericMatchResult; - using MatchResult_NotApplicableT = GenericMatchResult; - using MatchResult_OkT = GenericMatchResult; - - using MatchResultT = std::variant< - MatchResult_NoT, - MatchResult_NotApplicableT, - MatchResult_OkT - >; -} - -namespace mimicpp::call::detail -{ - [[nodiscard]] - inline MatchResultT evaluate_sub_match_results(const bool isApplicable, std::vector subResults) noexcept - { - static_assert(3 == std::variant_size_v, "Unexpected MatchResult alternative count."); - - if (!std::ranges::all_of(subResults, &SubMatchResult::matched)) - { - return MatchResult_NoT{ - .subMatchResults = std::move(subResults) - }; - } - - if (!isApplicable) - { - return MatchResult_NotApplicableT{ - .subMatchResults = std::move(subResults) - }; - } - - return MatchResult_OkT{ - .subMatchResults = std::move(subResults) - }; - } -} - -namespace mimicpp::detail -{ - template - [[nodiscard]] - constexpr const Derived& derived_cast(const Base& self) noexcept - { - static_assert( - std::derived_from, - "Derived must inherit from Base."); - return static_cast(self); - } -} - #endif diff --git a/include/mimic++/Expectation.hpp b/include/mimic++/Expectation.hpp index 1710db66a..89d10b655 100644 --- a/include/mimic++/Expectation.hpp +++ b/include/mimic++/Expectation.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -23,43 +24,21 @@ namespace mimicpp::detail { - template - [[noreturn]] - void handle_call_match_fail( - call::Info call, - std::vector noMatches, - std::vector partialMatches - ) - { - if (!std::ranges::empty(partialMatches)) - { - report_fail( - std::move(call), - std::move(partialMatches)); - } - else - { - report_fail( - std::move(call), - std::move(noMatches)); - } - } - template - std::optional make_match_result( + std::optional make_match_report( const call::Info& call, - const std::shared_ptr>& expectation + const Expectation& expectation ) noexcept { try { - return expectation->matches(call); + return expectation.matches(call); } catch (...) { report_unhandled_exception( - call, - expectation, + make_call_report(call), + expectation.report(), std::current_exception()); } @@ -86,11 +65,14 @@ namespace mimicpp Expectation(Expectation&&) = delete; Expectation& operator =(Expectation&&) = delete; + [[nodiscard]] + virtual ExpectationReport report() const = 0; + [[nodiscard]] virtual bool is_satisfied() const noexcept = 0; [[nodiscard]] - virtual call::MatchResultT matches(const CallInfoT& call) const = 0; + virtual MatchReport matches(const CallInfoT& call) const = 0; virtual void consume(const CallInfoT& call) = 0; [[nodiscard]] @@ -128,7 +110,7 @@ namespace mimicpp m_Expectations.emplace_back(std::move(expectation)); } - void remove(std::shared_ptr expectation) noexcept + void remove(std::shared_ptr expectation) { const std::scoped_lock lock{m_ExpectationsMx}; @@ -138,47 +120,56 @@ namespace mimicpp if (!expectation->is_satisfied()) { - report_unsatisfied_expectation(std::move(expectation)); + detail::report_unfulfilled_expectation( + expectation->report()); } } [[nodiscard]] ReturnT handle_call(const CallInfoT& call) { - static_assert(3 == std::variant_size_v, "Unexpected MatchResult alternative count."); - - std::vector noMatches{}; - std::vector exhaustedMatches{}; + std::vector noMatches{}; + std::vector inapplicableMatches{}; for (const std::scoped_lock lock{m_ExpectationsMx}; auto& exp : m_Expectations | std::views::reverse) { - if (std::optional matchResult = detail::make_match_result(call, exp)) + if (std::optional matchReport = detail::make_match_report(call, *exp)) { - if (auto* match = std::get_if(&*matchResult)) + switch (evaluate_match_report(*matchReport)) { - report_ok( - call, - std::move(*match)); + using enum MatchResult; + case none: + noMatches.emplace_back(*std::move(matchReport)); + break; + case inapplicable: + inapplicableMatches.emplace_back(*std::move(matchReport)); + break; + case full: + detail::report_full_match( + make_call_report(call), + *std::move(matchReport)); exp->consume(call); return exp->finalize_call(call); - } - if (auto* match = std::get_if(&*matchResult)) - { - noMatches.emplace_back(std::move(*match)); - } - else - { - exhaustedMatches.emplace_back(std::get(*std::move(matchResult))); + // GCOVR_EXCL_START + default: + unreachable(); + // GCOVR_EXCL_STOP } } } - detail::handle_call_match_fail( - call, - std::move(noMatches), - std::move(exhaustedMatches)); + if (!std::ranges::empty(inapplicableMatches)) + { + detail::report_inapplicable_matches( + make_call_report(call), + std::move(inapplicableMatches)); + } + + detail::report_no_matches( + make_call_report(call), + std::move(noMatches)); } private: @@ -193,7 +184,8 @@ namespace mimicpp && requires(T& policy, const call::info_for_signature_t& info) { { std::as_const(policy).is_satisfied() } noexcept -> std::convertible_to; - { std::as_const(policy).matches(info) } -> std::convertible_to; + { std::as_const(policy).matches(info) } -> std::convertible_to; + { std::as_const(policy).describe() } -> std::convertible_to>; { policy.consume(info) }; }; @@ -214,6 +206,7 @@ namespace mimicpp { { std::as_const(policy).is_satisfied() } noexcept -> std::convertible_to; { std::as_const(policy).is_applicable() } noexcept -> std::convertible_to; + { std::as_const(policy).describe_state() } -> std::convertible_to>; policy.consume(); }; @@ -238,6 +231,7 @@ namespace mimicpp && std::constructible_from && std::constructible_from constexpr explicit BasicExpectation( + const std::source_location& sourceLocation, TimesArg&& timesArg, FinalizerArg&& finalizerArg, PolicyArgs&&... args @@ -245,12 +239,31 @@ namespace mimicpp std::is_nothrow_constructible_v && std::is_nothrow_constructible_v && (std::is_nothrow_constructible_v && ...)) - : m_Times{std::forward(timesArg)}, - m_Finalizer{std::forward(finalizerArg)}, - m_Policies{std::forward(args)...} + : m_SourceLocation{sourceLocation}, + m_Policies{std::forward(args)...}, + m_Times{std::forward(timesArg)}, + m_Finalizer{std::forward(finalizerArg)} { } + [[nodiscard]] + ExpectationReport report() const override + { + return ExpectationReport{ + .sourceLocation = m_SourceLocation, + .finalizerDescription = std::nullopt, + .timesDescription = m_Times.describe_state(), + .expectationDescriptions = std::apply( + [&](const auto&... policies) + { + return std::vector>{ + policies.describe()... + }; + }, + m_Policies) + }; + } + [[nodiscard]] constexpr bool is_satisfied() const noexcept override { @@ -264,18 +277,27 @@ namespace mimicpp } [[nodiscard]] - call::MatchResultT matches(const CallInfoT& call) const override + MatchReport matches(const CallInfoT& call) const override { - return call::detail::evaluate_sub_match_results( - m_Times.is_applicable(), - std::apply( + return MatchReport{ + .sourceLocation = m_SourceLocation, + .finalizeReport = {std::nullopt}, + .timesReport = MatchReport::Times{ + .isApplicable = m_Times.is_applicable(), + .description = m_Times.describe_state() + }, + .expectationReports = std::apply( [&](const auto&... policies) { - return std::vector{ - policies.matches(call)... + return std::vector{ + MatchReport::Expectation{ + .isMatching = policies.matches(call), + .description = policies.describe() + }... }; }, - m_Policies)); + m_Policies) + }; } constexpr void consume(const CallInfoT& call) override @@ -295,10 +317,17 @@ namespace mimicpp return m_Finalizer.finalize_call(call); } + [[nodiscard]] + constexpr const std::source_location& from() const noexcept + { + return m_SourceLocation; + } + private: + std::source_location m_SourceLocation; + PolicyListT m_Policies; [[no_unique_address]] TimesT m_Times{}; [[no_unique_address]] FinalizerT m_Finalizer{}; - PolicyListT m_Policies; }; template @@ -308,7 +337,7 @@ namespace mimicpp using StorageT = ExpectationCollection; using ExpectationT = Expectation; - ~ScopedExpectation() + ~ScopedExpectation() noexcept(false) { if (m_Storage && m_Expectation) @@ -332,13 +361,13 @@ namespace mimicpp } template - requires requires + requires requires(const std::source_location& loc) { - { std::declval().finalize() } -> std::convertible_to; + { std::declval().finalize(loc) } -> std::convertible_to; } [[nodiscard]] - explicit(false) constexpr ScopedExpectation(T&& object) - : ScopedExpectation{std::forward(object).finalize()} + explicit(false) constexpr ScopedExpectation(T&& object, const std::source_location& loc = std::source_location::current()) + : ScopedExpectation{std::forward(object).finalize(loc)} { } @@ -359,6 +388,12 @@ namespace mimicpp throw std::runtime_error{"Expired expectation."}; } + [[nodiscard]] + const ExpectationT& expectation() const noexcept + { + return *m_Expectation; + } + private: std::shared_ptr m_Storage{}; std::shared_ptr m_Expectation{}; diff --git a/include/mimic++/ExpectationBuilder.hpp b/include/mimic++/ExpectationBuilder.hpp index e7f4c9ee2..13481fee3 100644 --- a/include/mimic++/ExpectationBuilder.hpp +++ b/include/mimic++/ExpectationBuilder.hpp @@ -124,7 +124,7 @@ namespace mimicpp } [[nodiscard]] - constexpr ScopedExpectationT finalize() && + constexpr ScopedExpectationT finalize(const std::source_location& sourceLocation) && { static_assert( finalize_policy_for, @@ -138,6 +138,7 @@ namespace mimicpp [&](auto&... policies) { return std::make_unique( + sourceLocation, std::move(m_TimesPolicy), std::move(m_FinalizePolicy), std::move(policies)...); diff --git a/include/mimic++/ExpectationPolicies.hpp b/include/mimic++/ExpectationPolicies.hpp index 7a17c7d32..ce466132d 100644 --- a/include/mimic++/ExpectationPolicies.hpp +++ b/include/mimic++/ExpectationPolicies.hpp @@ -16,6 +16,63 @@ #include #include +namespace mimicpp::expectation_policies::detail +{ + [[nodiscard]] + inline StringT describe_times_state(const std::size_t current, const std::size_t min, const std::size_t max) + { + const auto verbalizeValue = [](const std::size_t value)-> StringT + { + switch (value) + { + case 0: + return "never"; + case 1: + return "once"; + case 2: + return "twice"; + default: + return format::format("{} times", value); + } + }; + + if (current == max) + { + return format::format( + "inapplicable: already saturated (matched {})", + verbalizeValue(current)); + } + + if (min <= current) + { + return format::format( + "applicable: accepts further matches (matched {} out of {} times)", + current, + max); + } + + const auto verbalizeInterval = [verbalizeValue](const std::size_t start, const std::size_t end) + { + if (start < end) + { + return format::format( + "between {} and {} times", + start, + end); + } + + return format::format( + "exactly {}", + verbalizeValue(end)); + }; + + return format::format( + "unsatisfied: matched {} - {} is expected", + verbalizeValue(current), + verbalizeInterval(min, max)); + } +} + namespace mimicpp::expectation_policies { class InitFinalize @@ -45,6 +102,15 @@ namespace mimicpp::expectation_policies return m_Count < max; } + [[nodiscard]] + StringT describe_state() const + { + return detail::describe_times_state( + m_Count, + min, + max); + } + constexpr void consume() noexcept { ++m_Count; @@ -86,6 +152,15 @@ namespace mimicpp::expectation_policies return m_Count < m_Max; } + [[nodiscard]] + StringT describe_state() const + { + return detail::describe_times_state( + m_Count, + m_Min, + m_Max); + } + constexpr void consume() noexcept { ++m_Count; @@ -107,20 +182,9 @@ namespace mimicpp::expectation_policies } template - static call::SubMatchResult matches(const call::Info& info) + static constexpr bool matches(const call::Info& info) noexcept { - if (mimicpp::is_matching(info.fromCategory, expected)) - { - return { - .matched = true, - .msg = format::format(" matches Category {}", expected) - }; - } - - return { - .matched = false, - .msg = format::format(" does not match Category {}", expected) - }; + return mimicpp::is_matching(info.fromCategory, expected); } template @@ -128,6 +192,14 @@ namespace mimicpp::expectation_policies { assert(mimicpp::is_matching(info.fromCategory, expected) && "Call does not match."); } + + [[nodiscard]] + static StringT describe() + { + return format::format( + "expect: from {} category overload", + expected); + } }; template @@ -140,20 +212,9 @@ namespace mimicpp::expectation_policies } template - static constexpr call::SubMatchResult matches(const call::Info& info) noexcept + static constexpr bool matches(const call::Info& info) noexcept { - if (mimicpp::is_matching(info.fromConstness, constness)) - { - return { - .matched = true, - .msg = format::format(" matches Constness {}", constness) - }; - } - - return { - .matched = false, - .msg = format::format(" does not match Constness {}", constness) - }; + return mimicpp::is_matching(info.fromConstness, constness); } template @@ -161,6 +222,14 @@ namespace mimicpp::expectation_policies { assert(mimicpp::is_matching(info.fromConstness, constness) && "Call does not match."); } + + [[nodiscard]] + static StringT describe() + { + return format::format( + "expect: from {} qualified overload", + constness); + } }; template @@ -255,32 +324,14 @@ namespace mimicpp::expectation_policies template requires std::invocable&> - && requires(std::invoke_result_t&> target) - { - requires matcher_for< - Matcher, - decltype(target)>; - requires std::convertible_to< - std::invoke_result_t< - const Describer&, - decltype(target), - StringT, - bool>, - std::optional>; - } + && matcher_for< + Matcher, + std::invoke_result_t&>> [[nodiscard]] - constexpr call::SubMatchResult matches(const call::Info& info) const + constexpr bool matches(const call::Info& info) const { - auto& target = std::invoke(m_Projection, info); - const bool matchResult = m_Matcher.matches(target); - return { - .matched = matchResult, - .msg = std::invoke( - m_Describer, - target, - m_Matcher.describe(target), - matchResult) - }; + return m_Matcher.matches( + std::invoke(m_Projection, info)); } template @@ -288,6 +339,19 @@ namespace mimicpp::expectation_policies { } + [[nodiscard]] + std::optional describe() const + { + if (const std::optional description = m_Matcher.describe()) + { + return std::invoke( + m_Describer, + *description); + } + + return std::nullopt; + } + private: [[no_unique_address]] Matcher m_Matcher; [[no_unique_address]] Projection m_Projection; @@ -322,9 +386,15 @@ namespace mimicpp::expectation_policies template [[nodiscard]] - static constexpr call::SubMatchResult matches(const call::Info&) noexcept + static constexpr bool matches(const call::Info&) noexcept + { + return true; + } + + [[nodiscard]] + static std::nullopt_t describe() noexcept { - return {true}; + return std::nullopt; } template @@ -514,15 +584,12 @@ namespace mimicpp::expect { [[nodiscard]] constexpr StringT operator ()( - [[maybe_unused]] auto&& target, - const StringViewT matcherDescription, - const bool result + const StringViewT matcherDescription ) const { return format::format( - "arg[{}] {} requirement: {}", + "expect: arg[{}] {}", index, - result ? "passed" : "failed", matcherDescription); } }; @@ -537,7 +604,7 @@ namespace mimicpp::expect * \note An expectation without requirements matches any call. * * \details Requirements are checked during the ``matches`` step. If all requirements match, an additional ``is_applicable`` check is - * performed on the times requirements. If this returns ``false``, the expectation is treated as ``non-applicable`` and will be skipped + * performed on the times requirements. If this returns ``false``, the expectation is treated as ``inapplicable`` and will be skipped * (but reported if no other match can be found). Otherwise, the call is matched. * *\{ diff --git a/include/mimic++/Fwd.hpp b/include/mimic++/Fwd.hpp new file mode 100644 index 000000000..67564bb17 --- /dev/null +++ b/include/mimic++/Fwd.hpp @@ -0,0 +1,40 @@ +// // 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_FWD_HPP +#define MIMICPP_FWD_HPP + +#pragma once + +#include + +namespace mimicpp::call +{ + template + class Info; +} + +namespace mimicpp +{ + template + class Expectation; + + enum class MatchResult + { + none, + inapplicable, + full + }; + + class CallReport; + class MatchReport; + class ExpectationReport; + + using CharT = char; + using CharTraitsT = std::char_traits; + using StringT = std::basic_string; +} + +#endif diff --git a/include/mimic++/Matcher.hpp b/include/mimic++/Matcher.hpp index 23328f59f..dbb884924 100644 --- a/include/mimic++/Matcher.hpp +++ b/include/mimic++/Matcher.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -27,7 +28,7 @@ namespace mimicpp && requires(const T& matcher, Target& target) { { matcher.matches(target) } -> std::convertible_to; - { matcher.describe(target) } -> std::convertible_to; + { matcher.describe() } -> std::convertible_to>; }; /** @@ -47,11 +48,13 @@ namespace mimicpp explicit constexpr PredicateMatcher( Predicate predicate, StringT fmt, + StringT invertedFmt, std::tuple additionalArgs = std::tuple{} ) noexcept(std::is_nothrow_move_constructible_v && (... && std::is_nothrow_move_constructible_v)) : m_Predicate{std::move(predicate)}, m_FormatString{std::move(fmt)}, + m_InvertedFormatString{std::move(invertedFmt)}, m_AdditionalArgs{std::move(additionalArgs)} { } @@ -74,9 +77,8 @@ namespace mimicpp m_AdditionalArgs); } - template [[nodiscard]] - constexpr StringT describe(T& target) const + constexpr StringT describe() const { return std::apply( [&, this](auto&... additionalArgs) @@ -86,7 +88,6 @@ namespace mimicpp return format::vformat( m_FormatString, format::make_format_args( - makeLvalue(mimicpp::print(target)), makeLvalue(mimicpp::print(additionalArgs))...)); }, m_AdditionalArgs); @@ -99,6 +100,7 @@ namespace mimicpp { return make_inverted( m_Predicate, + m_InvertedFormatString, m_FormatString, std::move(m_AdditionalArgs)); } @@ -108,23 +110,31 @@ namespace mimicpp { return make_inverted( std::move(m_Predicate), - m_FormatString, + std::move(m_InvertedFormatString), + std::move(m_FormatString), std::move(m_AdditionalArgs)); } private: [[no_unique_address]] Predicate m_Predicate; StringT m_FormatString; + StringT m_InvertedFormatString; mutable std::tuple m_AdditionalArgs{}; template [[nodiscard]] - static constexpr auto make_inverted(Fn&& fn, const StringViewT fmt, std::tuple tuple) + static constexpr auto make_inverted( + Fn&& fn, + StringT fmt, + StringT invertedFmt, + std::tuple tuple + ) { using NotFnT = decltype(std::not_fn(std::forward(fn))); return PredicateMatcher{ std::not_fn(std::forward(fn)), - format::format("not ({})", fmt), + std::move(fmt), + std::move(invertedFmt), std::move(tuple) }; } @@ -144,11 +154,9 @@ namespace mimicpp return true; } - static constexpr StringT describe(auto&& target) + static constexpr std::nullopt_t describe() noexcept { - return format::format( - "{} without constraints", - mimicpp::print(target)); + return std::nullopt; } }; } @@ -205,7 +213,8 @@ namespace mimicpp::matches { return PredicateMatcher{ std::ranges::equal_to{}, - "{} == {}", + "== {}", + "!= {}", std::tuple{std::forward(value)} }; } @@ -221,7 +230,8 @@ namespace mimicpp::matches { return PredicateMatcher{ std::ranges::not_equal_to{}, - "{} != {}", + "!= {}", + "== {}", std::tuple{std::forward(value)} }; } @@ -237,7 +247,8 @@ namespace mimicpp::matches { return PredicateMatcher{ std::ranges::less{}, - "{} < {}", + "< {}", + ">= {}", std::tuple{std::forward(value)} }; } @@ -253,7 +264,8 @@ namespace mimicpp::matches { return PredicateMatcher{ std::ranges::less_equal{}, - "{} <= {}", + "<= {}", + "> {}", std::tuple{std::forward(value)} }; } @@ -269,7 +281,8 @@ namespace mimicpp::matches { return PredicateMatcher{ std::ranges::greater{}, - "{} > {}", + "> {}", + "<= {}", std::tuple{std::forward(value)} }; } @@ -285,7 +298,8 @@ namespace mimicpp::matches { return PredicateMatcher{ std::ranges::greater_equal{}, - "{} >= {}", + ">= {}", + "< {}", std::tuple{std::forward(value)} }; } @@ -294,16 +308,22 @@ namespace mimicpp::matches * \brief Tests, whether the target fulfills the given predicate. * \tparam UnaryPredicate Predicate type. * \param predicate The predicate to test. - * \param description The formatting string. May contain a ``{}``-token for the target. + * \param description The formatting string. + * \param invertedDescription The formatting string for the inversion. * \snippet Requirements.cpp matcher predicate */ template [[nodiscard]] - constexpr auto predicate(UnaryPredicate&& predicate, StringT description = "{} satisfies predicate") + constexpr auto predicate( + UnaryPredicate&& predicate, + StringT description = "passes predicate", + StringT invertedDescription = "fails predicate" + ) { return PredicateMatcher{ std::forward(predicate), - std::move(description) + std::move(description), + std::move(invertedDescription), }; } @@ -340,7 +360,8 @@ namespace mimicpp::matches::str { return target == exp; }, - "string {} is equal to {}", + "is equal to {}", + "is not equal to {}", std::tuple{std::move(expected)} }; } @@ -398,7 +419,8 @@ namespace mimicpp::matches::range range, std::ref(comp)); }, - "range {} is equal to {}", + "elements are {}", + "elements are not {}", std::tuple{std::views::all(std::forward(expected))} }; } @@ -427,7 +449,8 @@ namespace mimicpp::matches::range range, std::ref(comp)); }, - "range {} is permutation of {}", + "is a permutation of {}", + "is not a permutation of {}", std::tuple{std::views::all(std::forward(expected))} }; } @@ -452,7 +475,8 @@ namespace mimicpp::matches::range target, std::ref(rel)); }, - "range {} is sorted" + "is a sorted range", + "is an unsorted range" }; } @@ -467,7 +491,8 @@ namespace mimicpp::matches::range { return std::ranges::empty(target); }, - "range {} is empty" + "is an empty range", + "is not an empty range" }; } @@ -485,7 +510,8 @@ namespace mimicpp::matches::range size, std::ranges::size(target)); }, - "range {} has size {}", + "has size of {}", + "has different size than {}", std::tuple{expected} }; } diff --git a/include/mimic++/Printer.hpp b/include/mimic++/Printer.hpp index d62c515aa..3672b2da6 100644 --- a/include/mimic++/Printer.hpp +++ b/include/mimic++/Printer.hpp @@ -8,8 +8,11 @@ #pragma once +#include "Fwd.hpp" + #include #include +#include #include #include #include @@ -17,9 +20,6 @@ namespace mimicpp { - using CharT = char; - using CharTraitsT = std::char_traits; - using StringT = std::basic_string; using StringViewT = std::basic_string_view; using StringStreamT = std::basic_ostringstream; @@ -90,7 +90,7 @@ namespace mimicpp::detail OutIter print( OutIter out, T&& value, - const priority_tag<4> + const priority_tag<5> ) requires requires { @@ -104,7 +104,7 @@ namespace mimicpp::detail OutIter print( OutIter out, String&& str, - priority_tag<3> + priority_tag<4> ) { return format::format_to( @@ -117,7 +117,7 @@ namespace mimicpp::detail OutIter print( OutIter out, Range&& range, - priority_tag<2> + priority_tag<3> ); @@ -169,12 +169,28 @@ namespace mimicpp::detail OutIter print( OutIter out, T& value, - const priority_tag<1> + const priority_tag<2> ) { return format::format_to(out, "{}", value); } + template + OutIter print( + OutIter out, + const std::source_location& loc, + const priority_tag<1> + ) + { + return format::format_to( + out, + "{}[{}:{}], {}", + loc.file_name(), + loc.line(), + loc.column(), + loc.function_name()); + } + template OutIter print( OutIter out, @@ -195,7 +211,7 @@ namespace mimicpp::detail ) const { static_assert( - requires(const priority_tag<4> tag) + requires(const priority_tag<5> tag) { { print(out, std::forward(value), tag) } -> std::convertible_to; }, @@ -204,7 +220,7 @@ namespace mimicpp::detail return print( out, std::forward(value), - priority_tag<4>{}); + priority_tag<5>{}); } template @@ -222,7 +238,7 @@ namespace mimicpp::detail OutIter print( OutIter out, Range&& range, - const priority_tag<2> + const priority_tag<3> ) { out = format::format_to(out, "{{ "); diff --git a/include/mimic++/Reporter.hpp b/include/mimic++/Reporter.hpp index 853d88fcd..f06533e2f 100644 --- a/include/mimic++/Reporter.hpp +++ b/include/mimic++/Reporter.hpp @@ -9,50 +9,802 @@ #pragma once #include "mimic++/Call.hpp" +#include "mimic++/Fwd.hpp" #include "mimic++/Printer.hpp" +#include +#include #include +#include #include +#include +#include +#include #include namespace mimicpp { + /** + * \defgroup REPORTING_REPORTS reports + * \ingroup REPORTING + * \brief Contains reports of ``mimic++`` types. + * \details Reports are simplified object representations of ``mimic++`` types. In fact, reports are used to communicate with + * independent domains (e.g. unit-test frameworks) over the ``IReporter`` interface and are thus designed to provide as much + * transparent information as possible, without requiring them to be a generic type. + * + * \{ + */ + + /** + * \brief Contains the extracted info from a typed ``call::Info``. + * \details This type is meant to be used to communicate with independent domains via the reporter interface and thus contains + * the generic information as plain ``std`` types (e.g. the return type is provided as ``std::type_index`` instead of an actual + * type). + */ + class CallReport + { + public: + class Arg + { + public: + std::type_index typeIndex; + StringT stateString; + + [[nodiscard]] + friend bool operator ==(const Arg&, const Arg&) = default; + }; + + std::type_index returnTypeIndex; + std::vector argDetails{}; + std::source_location fromLoc{}; + ValueCategory fromCategory{}; + Constness fromConstness{}; + + [[nodiscard]] + friend bool operator ==(const CallReport& lhs, const CallReport& rhs) + { + return lhs.returnTypeIndex == rhs.returnTypeIndex + && lhs.argDetails == rhs.argDetails + && is_same_source_location(lhs.fromLoc, rhs.fromLoc) + && lhs.fromCategory == rhs.fromCategory + && lhs.fromConstness == rhs.fromConstness; + } + }; + + /** + * \brief Generates the call report for a given call info. + * \tparam Return The function return type. + * \tparam Params The function parameter types. + * \param callInfo The call info. + * \return The call report. + * \relatesalso call::Info + */ template + [[nodiscard]] + CallReport make_call_report(const call::Info& callInfo) + { + return CallReport{ + .returnTypeIndex = typeid(Return), + .argDetails = std::apply( + [](auto&... args) + { + return std::vector{ + CallReport::Arg{ + .typeIndex = typeid(Params), + .stateString = mimicpp::print(args.get()) + }... + }; + }, + callInfo.args), + .fromLoc = callInfo.fromSourceLocation, + .fromCategory = callInfo.fromCategory, + .fromConstness = callInfo.fromConstness + }; + } + + /** + * \brief Converts the given report to text. + * \param report The report. + * \return The report text. + * \relatesalso CallReport + */ + [[nodiscard]] + inline StringT stringify_call_report(const CallReport& report) + { + StringStreamT out{}; + format_to( + std::ostreambuf_iterator{out}, + "call from {}\n", + mimicpp::print(report.fromLoc)); + + format_to( + std::ostreambuf_iterator{out}, + "constness: {}\n" + "value category: {}\n" + "return type: {}\n", + report.fromConstness, + report.fromCategory, + report.returnTypeIndex.name()); + + if (!std::ranges::empty(report.argDetails)) + { + out << "args:\n"; + for (const std::size_t i : std::views::iota(0u, std::ranges::size(report.argDetails))) + { + format_to( + std::ostreambuf_iterator{out}, + "\targ[{}]: {{\n" + "\t\ttype: {},\n" + "\t\tvalue: {}\n" + "\t}},\n", + i, + report.argDetails[i].typeIndex.name(), + report.argDetails[i].stateString); + } + } + + return std::move(out).str(); + } + + /** + * \brief Contains the extracted info from a typed expectation. + * \details This type is meant to be used to communicate with independent domains via the reporter interface and thus contains + * the generic information as plain ``std`` types. + */ + class ExpectationReport + { + public: + std::optional sourceLocation{}; + std::optional finalizerDescription{}; + std::optional timesDescription{}; + std::vector> expectationDescriptions{}; + + [[nodiscard]] + friend bool operator ==(const ExpectationReport& lhs, const ExpectationReport& rhs) + { + return lhs.finalizerDescription == rhs.finalizerDescription + && lhs.timesDescription == rhs.timesDescription + && lhs.expectationDescriptions == rhs.expectationDescriptions + && lhs.sourceLocation.has_value() == rhs.sourceLocation.has_value() + && (!lhs.sourceLocation.has_value() + || is_same_source_location(*lhs.sourceLocation, *rhs.sourceLocation)); + } + }; + + /** + * \brief Converts the given report to text. + * \param report The report. + * \return The report text. + * \relatesalso ExpectationReport + */ + [[nodiscard]] + inline StringT stringify_expectation_report(const ExpectationReport& report) + { + StringStreamT out{}; + + out << "Expectation report:\n"; + + if (report.sourceLocation) + { + out << "from: "; + mimicpp::print( + std::ostreambuf_iterator{out}, + *report.sourceLocation); + out << "\n"; + } + + if (report.timesDescription) + { + format_to( + std::ostreambuf_iterator{out}, + "times: {}\n", + *report.timesDescription); + } + + if (std::ranges::any_of( + report.expectationDescriptions, + [](const auto& desc) { return desc.has_value(); })) + { + out << "expects:\n"; + for (const auto& desc + : report.expectationDescriptions + | std::views::filter([](const auto& desc) { return desc.has_value(); })) + { + format_to( + std::ostreambuf_iterator{out}, + "\t{},\n", + *desc); + } + } + + if (report.finalizerDescription) + { + format_to( + std::ostreambuf_iterator{out}, + "finally: {}\n", + *report.finalizerDescription); + } + + return std::move(out).str(); + } + + /** + * \brief Contains the detailed information for match outcomes. + * \details This type is meant to be used to communicate with independent domains via the reporter interface and thus contains + * the generic information as plain ``std`` types. + */ + class MatchReport + { + public: + /** + * \brief Information about the used finalizer. + */ + class Finalize + { + public: + std::optional description{}; + + [[nodiscard]] + friend bool operator ==(const Finalize&, const Finalize&) = default; + }; + + /** + * \brief Information about the current times state. + * \details This type contains a description about the current state of the ``times`` policy. This description is gather + * in parallel to the ``matches`` (before the ``consume`` step) and thus contains more detailed information about the + * outcome. + */ + class Times + { + public: + bool isApplicable{}; + std::optional description{}; + + [[nodiscard]] + friend bool operator ==(const Times&, const Times&) = default; + }; + + /** + * \brief Information a used expectation policy. + * \details This type contains a description about a given expectation policy. + */ + class Expectation + { + public: + bool isMatching{}; + std::optional description{}; + + [[nodiscard]] + friend bool operator ==(const Expectation&, const Expectation&) = default; + }; + + std::optional sourceLocation{}; + Finalize finalizeReport{}; + Times timesReport{}; + std::vector expectationReports{}; + + [[nodiscard]] + friend bool operator ==(const MatchReport& lhs, const MatchReport& rhs) + { + return lhs.finalizeReport == rhs.finalizeReport + && lhs.timesReport == rhs.timesReport + && lhs.expectationReports == rhs.expectationReports + && lhs.sourceLocation.has_value() == rhs.sourceLocation.has_value() + && (!lhs.sourceLocation.has_value() + || is_same_source_location(*lhs.sourceLocation, *rhs.sourceLocation)); + } + }; + + /** + * \brief Determines, whether a match report actually denotes a ``full``, ``inapplicable`` or ``no`` match. + * \param report The report to evaluate. + * \return The actual result. + */ + [[nodiscard]] + inline MatchResult evaluate_match_report(const MatchReport& report) noexcept + { + if (!std::ranges::all_of(report.expectationReports, &MatchReport::Expectation::isMatching)) + { + return MatchResult::none; + } + + if (!report.timesReport.isApplicable) + { + return MatchResult::inapplicable; + } + + return MatchResult::full; + } + + /** + * \brief Converts the given report to text. + * \param report The report. + * \return The report text. + * \relatesalso MatchReport + */ + [[nodiscard]] + inline StringT stringify_match_report(const MatchReport& report) + { + std::vector matchedExpectationDescriptions{}; + std::vector unmatchedExpectationDescriptions{}; + + for (const auto& [isMatching, description] : report.expectationReports) + { + if (description) + { + if (isMatching) + { + matchedExpectationDescriptions.emplace_back(*description); + } + else + { + unmatchedExpectationDescriptions.emplace_back(*description); + } + } + } + + StringStreamT out{}; + + switch (evaluate_match_report(report)) + { + case MatchResult::full: + out << "Matched expectation: {\n"; + break; + + case MatchResult::inapplicable: + format_to( + std::ostreambuf_iterator{out}, + "Inapplicable, but otherwise matched expectation: {{\n" + "reason: {}\n", + report.timesReport.description.value_or("No reason provided.")); + break; + + case MatchResult::none: + out << "Unmatched expectation: {\n"; + break; + + // GCOVR_EXCL_START + default: // NOLINT(clang-diagnostic-covered-switch-default) + unreachable(); + // GCOVR_EXCL_STOP + } + + if (report.sourceLocation) + { + out << "from: "; + mimicpp::print( + std::ostreambuf_iterator{out}, + *report.sourceLocation); + out << "\n"; + } + + if (!std::ranges::empty(unmatchedExpectationDescriptions)) + { + out << "failed:\n"; + for (const auto& desc : unmatchedExpectationDescriptions) + { + format_to( + std::ostreambuf_iterator{out}, + "\t{},\n", + desc); + } + } + + if (!std::ranges::empty(matchedExpectationDescriptions)) + { + out << "passed:\n"; + for (const auto& desc : matchedExpectationDescriptions) + { + format_to( + std::ostreambuf_iterator{out}, + "\t{},\n", + desc); + } + } + + out << "}\n"; + + return std::move(out).str(); + } + + /** + * \} + */ + + /** + * \defgroup REPORTING reporting + * \brief Contains reporting related symbols + * \details Reporting is executed, when something notably has been detected by ``mimic++``; often it is expected, that the reporter + * reacts to such a report in a specific manner (e.g. aborting the test case). For example the ``DefaultReporter`` simply throws + * exceptions on error reports, while other more specialized reporters handle such cases slightly different (but still abort the + * current test). + * These specialized Reporters are used to send reports to a specific destination (e.g. the utilized unit-test framework), + * which often provide more advanced mechanics for printing failed tests to the users. + * + * Users may provide their own reporter implementation; e.g. if there is no reporter for the desired unit-test framework. + * + * At any time there exists exactly one global reporter, which may be directly or indirectly exchanged by users. + * Reports are sent to the currently installed reporter via the ``report_xyz`` free-functions. Most of those functions require, that + * reports are handled in a specific manner (e.g. ``report_no_matches`` is expected to never return) and custom reporters **must** + * follow that specification, otherwise this will lead to undefined behavior. For further details, have a look at the specific + * function documentation. + * + * \note In general users shall not directly interact with the installed reporter, except when they want to replace it. + * + * \{ + */ + + /** + * \brief The reporter interface. + * \details This is the central interface to be used, when creating reporters for external domains. + */ + class IReporter + { + public: + /** + * \brief Defaulted virtual destructor. + */ + virtual ~IReporter() = default; + + /** + * \brief Expects reports about all ``none`` matching expectations. This is only called, if there are no better options available. + * \param call The call report. + * \param matchReports Reports of all ``none`` matching expectations. + * \details This function is called, when no match has been found and there are no other expectations, which are matching but + * inapplicable. In fact, this is the fallback reporting mechanism, for unmatched calls. + * \note ``matchReports`` may be empty. + * + * \attention Derived reporter implementations must never return and shall instead leave the function via a thrown exception or + * a terminating mechanism (e.g. ``std::terminate``). Otherwise, this will result in undefined behavior. + */ + [[noreturn]] + virtual void report_no_matches( + CallReport call, + std::vector matchReports + ) = 0; + + /** + * \brief Expects reports about all ``inapplicable`` matching expectations. This is only called, if there are no better options available. + * \param call The call report. + * \param matchReports Reports of all ``inapplicable`` matching expectations. + * \details This function is called, when no applicable match has been found, but actually the call expectations are fulfilled. This in fact + * happens, when the ``times`` policy is already saturated (e.g. it was once expected and already matched once) or otherwise not applicable + * (e.g. a sequence element is not the current element). + * + * \attention Derived reporter implementations must never return and shall instead leave the function via a thrown exception or + * a terminating mechanism (e.g. ``std::terminate``). Otherwise, this will result in undefined behavior. + */ + [[noreturn]] + virtual void report_inapplicable_matches( + CallReport call, + std::vector matchReports + ) = 0; + + /** + * \brief Expects the report about a ``full`` matching expectation. + * \param call The call report. + * \param matchReport Report of the ``full`` matching expectation. + * \details This function is called, when a match has been found. There are no other expectations on the behavior of this function; + * except the ``noexcept`` guarantee. Implementations shall simply return to the caller. + */ + virtual void report_full_match( + CallReport call, + MatchReport matchReport + ) noexcept = 0; + + /** + * \brief Expects the report of an unfulfilled expectation. + * \param expectationReport The expectation report. + * \details This function is called, when an unfulfilled expectation goes out of scope. In fact this happens, when the ``times`` policy is not + * satisfied. + * + * \note In general, it is expected that this function does not return, but throws an exception instead. But, as this function is always called + * when an unfulfilled expectation goes out of scope, implementations shall check whether an uncaught exception already exists (e.g. via + * ``std::uncaught_exceptions``) before throwing by themselves. + * \see ``DefaultReporter::report_unfulfilled_expectation`` for an example. + */ + virtual void report_unfulfilled_expectation( + ExpectationReport expectationReport + ) = 0; + + /** + * \brief Expects rather unspecific errors. + * \param message The error message. + * \details This function is called, when an unspecific error occurs. + * + * \note In general, it is expected that this function does not return, but throws an exception instead. But, as this function may be called + * due to any reason, implementations shall check whether an uncaught exception already exists (e.g. via ``std::uncaught_exceptions``) before + * throwing by themselves. + * \see ``DefaultReporter::report_error`` for an example. + */ + virtual void report_error(StringT message) = 0; + + /** + * \brief Expects reports about unhandled exceptions, during ``handle_call``. + * \param call The call report. + * \param expectationReport The expectation report. + * \param exception The exception. + * \details This function is called, when an expectation throws during a ``matches`` call. There are no expectations on the behavior of this + * function. As this function is called inside a ``catch`` block, throwing exceptions will result in a terminate call. + */ + virtual void report_unhandled_exception( + CallReport call, + ExpectationReport expectationReport, + std::exception_ptr exception + ) = 0; + + protected: + [[nodiscard]] + IReporter() = default; + + IReporter(const IReporter&) = default; + IReporter& operator =(const IReporter&) = default; + IReporter(IReporter&&) = default; + IReporter& operator =(IReporter&&) = default; + }; + + template + class Error final + : public std::runtime_error + { + public: + [[nodiscard]] + explicit Error( + const std::string& what, + Data&& data = Data{}, + const std::source_location& loc = std::source_location::current() + ) + : std::runtime_error{what}, + m_Data{std::move(data)}, + m_Loc{loc} + { + } + + [[nodiscard]] + const Data& data() const noexcept + { + return m_Data; + } + + [[nodiscard]] + const std::source_location& where() const noexcept + { + return m_Loc; + } + + private: + Data m_Data; + std::source_location m_Loc; + }; + + using UnmatchedCallT = Error>>; + using UnfulfilledExpectationT = Error; + + /** + * \brief The default reporter. + */ + class DefaultReporter final + : public IReporter + { + public: + [[noreturn]] + void report_no_matches( + CallReport call, + std::vector matchReports + ) override + { + assert( + std::ranges::all_of( + matchReports, + std::bind_front(std::equal_to{}, MatchResult::none), + &evaluate_match_report)); + + const std::source_location loc{call.fromLoc}; + throw UnmatchedCallT{ + "No match found.", + {std::move(call), std::move(matchReports)}, + loc + }; + } + + [[noreturn]] + void report_inapplicable_matches( + CallReport call, + std::vector matchReports + ) override + { + assert( + std::ranges::all_of( + matchReports, + std::bind_front(std::equal_to{}, MatchResult::inapplicable), + &evaluate_match_report)); + + const std::source_location loc{call.fromLoc}; + throw UnmatchedCallT{ + "No applicable match found.", + {std::move(call), std::move(matchReports)}, + loc + }; + } + + void report_full_match( + CallReport call, + MatchReport matchReport + ) noexcept override + { + assert(MatchResult::full == evaluate_match_report(matchReport)); + } + + void report_unfulfilled_expectation( + ExpectationReport expectationReport + ) override + { + if (0 == std::uncaught_exceptions()) + { + throw UnfulfilledExpectationT{ + "Expectation is unfulfilled.", + std::move(expectationReport) + }; + } + } + + void report_error(StringT message) override + { + if (0 == std::uncaught_exceptions()) + { + throw Error{message}; + } + } + + void report_unhandled_exception( + CallReport call, + ExpectationReport expectationReport, + std::exception_ptr exception + ) override + { + } + }; + + /** + * \} + */ +} + +namespace mimicpp::detail +{ + [[nodiscard]] + inline std::unique_ptr& get_reporter() noexcept + { + static std::unique_ptr reporter{ + std::make_unique() + }; + return reporter; + } + [[noreturn]] - void report_fail( - const call::Info& callInfo, - std::vector results - ); + inline void report_no_matches( + CallReport callReport, + std::vector matchReports + ) + { + get_reporter() + // GCOVR_EXCL_START + ->report_no_matches( + // GCOVR_EXCL_STOP + std::move(callReport), + std::move(matchReports)); + + // GCOVR_EXCL_START + // ReSharper disable once CppUnreachableCode + unreachable(); + // GCOVR_EXCL_STOP + } - template [[noreturn]] - void report_fail( - const call::Info& callInfo, - std::vector results - ); + inline void report_inapplicable_matches( + CallReport callReport, + std::vector matchReports + ) + { + get_reporter() + // GCOVR_EXCL_START + ->report_inapplicable_matches( + // GCOVR_EXCL_STOP + std::move(callReport), + std::move(matchReports)); - template - void report_ok( - const call::Info& callInfo, - call::MatchResult_OkT result - ); - - inline void report_error(StringT message); - - template - class Expectation; - - template - void report_unhandled_exception( - const call::Info& callInfo, - std::shared_ptr> expectation, - std::exception_ptr exception - ); - - template - void report_unsatisfied_expectation( - std::shared_ptr> expectation - ); + // GCOVR_EXCL_START + // ReSharper disable once CppUnreachableCode + unreachable(); + // GCOVR_EXCL_STOP + } + + inline void report_full_match( + CallReport callReport, + MatchReport matchReport + ) noexcept + { + get_reporter() + ->report_full_match( + std::move(callReport), + std::move(matchReport)); + } + + inline void report_unfulfilled_expectation( + ExpectationReport expectationReport + ) + { + get_reporter() + ->report_unfulfilled_expectation(std::move(expectationReport)); + } + + inline void report_error(StringT message) + { + get_reporter() + ->report_error(std::move(message)); + } + + inline void report_unhandled_exception( + CallReport callReport, + ExpectationReport expectationReport, + const std::exception_ptr& exception + ) + { + get_reporter() + ->report_unhandled_exception( + std::move(callReport), + std::move(expectationReport), + exception); + } +} + +namespace mimicpp +{ + /** + * \brief Replaces the previous reporter with a newly constructed one. + * \tparam T The desired reporter type. + * \tparam Args The constructor argument types for ``T``. + * \param args The constructor arguments. + * \ingroup REPORTING + * \details This function accesses the globally available reporter and replaces it with a new instance. + */ + template T, typename... Args> + requires std::constructible_from + void install_reporter(Args&&... args) // NOLINT(cppcoreguidelines-missing-std-forward) + { + detail::get_reporter() = std::make_unique( + std::forward(args)...); + } + + namespace detail + { + template + class ReporterInstaller + { + public: + template + explicit ReporterInstaller(Args&&... args) + { + install_reporter( + std::forward(args)...); + } + }; + } + + /** + * \defgroup REPORTING_ADAPTERS test framework adapters + * \ingroup REPORTING + * \brief Reporter integrations for various third-party frameworks. + * \details These reporters are specialized implementations, which provide seamless integrations of ``mimic++`` into the desired + * unit-test framework. Integrations are enabled by simply including the specific header into any source file. The include order + * doesn't matter. + * + * \note Including multiple headers of the ``adapters`` subdirectory into one executable is possible, but with caveats. It's unspecified + * which reporter will be active at the program start. So, if you need multiple reporters in one executable, you should explicitly + * install the desired reporter on a per test case basis. + * + *\{ + */ } #endif diff --git a/include/mimic++/Sequence.hpp b/include/mimic++/Sequence.hpp index 534646b62..d43b7056c 100644 --- a/include/mimic++/Sequence.hpp +++ b/include/mimic++/Sequence.hpp @@ -8,6 +8,7 @@ #pragma once +#include "mimic++/Printer.hpp" #include "mimic++/Reporter.hpp" #include "mimic++/Utility.hpp" @@ -233,6 +234,22 @@ namespace mimicpp::expectation_policies [](const entry& info){ return info.sequence->is_consumable(info.id); }); } + [[nodiscard]] + StringT describe_state() const + { + if (is_applicable()) + { + return "applicable: Sequence element expects further matches."; + } + + if (is_satisfied()) + { + return "inapplicable: Sequence element is already saturated."; + } + + return "inapplicable: Sequence element is not the current element."; + } + // ReSharper disable once CppMemberFunctionMayBeConst constexpr void consume() noexcept { diff --git a/include/mimic++/Utility.hpp b/include/mimic++/Utility.hpp index ccdd05cea..c9993a77f 100644 --- a/include/mimic++/Utility.hpp +++ b/include/mimic++/Utility.hpp @@ -8,16 +8,19 @@ #pragma once +#include "mimic++/Fwd.hpp" + #include #include +#include namespace mimicpp { enum class Constness { non_const = 0b01, - as_const = 0b10, - any = non_const | as_const + as_const = 0b10, + any = non_const | as_const }; [[nodiscard]] @@ -43,8 +46,8 @@ namespace mimicpp } template <> -struct std::formatter - : public std::formatter +struct std::formatter + : public std::formatter { using ValueCategoryT = mimicpp::ValueCategory; @@ -62,18 +65,18 @@ struct std::formatter case ValueCategoryT::any: return "any"; } - throw std::runtime_error{"Unknown category value."}; + throw std::invalid_argument{"Unknown category value."}; }; - return std::formatter::format( + return std::formatter::format( toString(category), ctx); } }; template <> -struct std::formatter - : public std::formatter +struct std::formatter + : public std::formatter { using ConstnessT = mimicpp::Constness; @@ -91,10 +94,10 @@ struct std::formatter case ConstnessT::any: return "any"; } - throw std::runtime_error{"Unknown constness value."}; + throw std::invalid_argument{"Unknown constness value."}; }; - return std::formatter::format( + return std::formatter::format( toString(category), ctx); } @@ -102,7 +105,7 @@ struct std::formatter namespace mimicpp { - template + template struct always_false : public std::bool_constant { @@ -142,6 +145,33 @@ namespace mimicpp { return static_cast>(value); } + + // GCOVR_EXCL_START + +#ifdef __cpp_lib_unreachable + using std::unreachable; +#else + + /** + * \brief Invokes undefined behavior + * \see https://en.cppreference.com/w/cpp/utility/unreachable + * \note Implementation directly taken from https://en.cppreference.com/w/cpp/utility/unreachable + */ + [[noreturn]] + inline void unreachable() + { + // 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. +#if defined(_MSC_VER) && !defined(__clang__) // MSVC + __assume(false); +#else // GCC, Clang + __builtin_unreachable(); +#endif + } +#endif + + // GCOVR_EXCL_STOP } #endif diff --git a/include/mimic++/adapters/Catch2.hpp b/include/mimic++/adapters/Catch2.hpp new file mode 100644 index 000000000..b6b287614 --- /dev/null +++ b/include/mimic++/adapters/Catch2.hpp @@ -0,0 +1,189 @@ +#ifndef MIMICPP_ADAPTERS_CATCH2_HPP +#define MIMICPP_ADAPTERS_CATCH2_HPP + +#pragma once + +#include "mimic++/Reporter.hpp" + +#include + +#if __has_include() + #include +#else + #error "Unable to find catch2 includes." +#endif + +namespace mimicpp::detail +{ + [[noreturn]] + inline void send_catch_fail(const StringViewT msg) + { +#ifdef CATCH_CONFIG_PREFIX_ALL + CATCH_FAIL(msg); +#else + FAIL(msg); +#endif + + unreachable(); + } + + inline void send_catch_succeed(const StringViewT msg) + { +#ifdef CATCH_CONFIG_PREFIX_ALL + CATCH_SUCCEED(msg); +#else + SUCCEED(msg); +#endif + } + + inline void send_catch_warn(const StringViewT msg) + { +#ifdef CATCH_CONFIG_PREFIX_MESSAGES + CATCH_WARN(msg); +#else + WARN(msg); +#endif + } +} + +namespace mimicpp +{ + /** + * \brief Reporter for the integration into Catch2. + * \ingroup REPORTING_ADAPTERS + * \details This reporter enables the integration of ``mimic++`` into ``Catch2`` and prefixes the headers + * of ``Catch2`` with ``catch2/``. + * + * This reporter installs itself by simply including this header file into any source file of the test executable. + */ + class Catch2Reporter final + : public IReporter + { + public: + [[noreturn]] + void report_no_matches(const CallReport call, const std::vector matchReports) override + { + StringStreamT ss{}; + format_to( + std::ostreambuf_iterator{ss}, + "No match for {}\n", + stringify_call_report(call)); + + if (std::ranges::empty(matchReports)) + { + ss << "No expectations available.\n"; + } + else + { + format_to( + std::ostreambuf_iterator{ss}, + "{} available expectations:\n", + std::ranges::size(matchReports)); + + for (const auto& report : matchReports) + { + ss << stringify_match_report(report) << "\n"; + } + } + + detail::send_catch_fail(ss.view()); + } + + [[noreturn]] + void report_inapplicable_matches(const CallReport call, const std::vector matchReports) override + { + StringStreamT ss{}; + format_to( + std::ostreambuf_iterator{ss}, + "No applicable match for {}\n", + stringify_call_report(call)); + + ss << "Tested expectations:\n"; + for (const auto& report : matchReports) + { + ss << stringify_match_report(report) << "\n"; + } + + detail::send_catch_fail(ss.view()); + } + + void report_full_match(const CallReport call, const MatchReport matchReport) noexcept override + { + StringStreamT ss{}; + format_to( + std::ostreambuf_iterator{ss}, + "Found match for {}\n", + stringify_call_report(call)); + + ss << stringify_match_report(matchReport) << "\n"; + + detail::send_catch_succeed(ss.view()); + } + + void report_unfulfilled_expectation(const ExpectationReport expectationReport) override + { + if (0 == std::uncaught_exceptions()) + { + StringStreamT ss{}; + ss << "Unfulfilled expectation:\n" + << stringify_expectation_report(expectationReport) << "\n"; + + detail::send_catch_fail(ss.view()); + } + } + + void report_error(const StringT message) override + { + if (0 == std::uncaught_exceptions()) + { + detail::send_catch_fail(message); + } + } + + void report_unhandled_exception( + const CallReport call, + const ExpectationReport expectationReport, + const std::exception_ptr exception + ) override + { + StringStreamT ss{}; + ss << "Unhandled exception: "; + + try + { + std::rethrow_exception(exception); + } + catch (const std::exception& e) + { + format_to( + std::ostreambuf_iterator{ss}, + "what: {}\n", + e.what()); + } + catch (...) + { + ss << "Unknown exception type.\n"; + } + + format_to( + std::ostreambuf_iterator{ss}, + "while checking expectation:\n" + "{}\n", + stringify_expectation_report(expectationReport)); + + format_to( + std::ostreambuf_iterator{ss}, + "For {}\n", + stringify_call_report(call)); + + detail::send_catch_warn(ss.view()); + } + }; +} + +namespace mimicpp::detail +{ + inline const ReporterInstaller installer{}; +} + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e2f350e25..02012d6c0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,60 +1,6 @@ -CPMAddPackage("gh:catchorg/Catch2@3.5.4") -CPMAddPackage("gh:rollbear/trompeloeil@47") -include("${Catch2_SOURCE_DIR}/extras/Catch.cmake") - -add_executable( - mimicpp-tests - "Call.cpp" - #"ComplexMock.cpp" - "Expectation.cpp" - "ExpectationBuilder.cpp" - "ExpectationPolicies.cpp" - "Matcher.cpp" - "Mock.cpp" - "Printer.cpp" - "Sequence.cpp" - "TypeTraits.cpp" - "Utility.cpp" -) - -if (MSVC) - - # When using github ci, exceptions seems to be disabled by default. - target_compile_options( - mimicpp-tests - PRIVATE - /EHsc - ) +add_subdirectory("unit-tests") +option(MIMICPP_ENABLE_ADAPTER_TESTS "Determines, whether the adapter tests shall be built." OFF) +if (MIMICPP_ENABLE_ADAPTER_TESTS) + add_subdirectory("adapter-tests") endif() - -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( - mimicpp-tests - PRIVATE - $<$:_DISABLE_VECTOR_ANNOTATION> - $<$:_DISABLE_STRING_ANNOTATION> - ) - -endif() - -add_sanitizers(mimicpp-tests) - -target_link_libraries( - mimicpp-tests - PRIVATE - mimicpp::mimicpp - Catch2::Catch2WithMain - trompeloeil::trompeloeil -) - -target_compile_features( - mimicpp - INTERFACE - cxx_std_${CMAKE_CXX_STANDARD} -) - -catch_discover_tests(mimicpp-tests) diff --git a/test/TestReporter.hpp b/test/TestReporter.hpp deleted file mode 100644 index 059693274..000000000 --- a/test/TestReporter.hpp +++ /dev/null @@ -1,166 +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) - -#pragma once - -#include "mimic++/Reporter.hpp" - -#include -#include - -#define MIMICPP_REPORTER_DEFINED - -inline std::vector g_NoMatchResults{}; -inline std::vector g_NonApplicableMatchResults{}; -inline std::vector g_OkMatchResults{}; - -struct unhandled_exception_info -{ - std::any call{}; - std::any expectation{}; - std::exception_ptr exception{}; -}; -inline std::vector g_UnhandledExceptions{}; -inline std::vector g_UnsatisfiedExpectations{}; -inline std::vector g_Errors{}; - -class NoMatchError -{ -}; - -class NonApplicableMatchError -{ -}; - -namespace mimicpp -{ - template - void report_fail( - const call::Info& callInfo, - std::vector results - ) - { - g_NoMatchResults.insert( - std::ranges::end(g_NoMatchResults), - std::ranges::begin(results), - std::ranges::end(results)); - - throw NoMatchError{}; - } - - template - void report_fail( - const call::Info& callInfo, - std::vector results - ) - { - g_NonApplicableMatchResults.insert( - std::ranges::end(g_NonApplicableMatchResults), - std::ranges::begin(results), - std::ranges::end(results)); - - throw NonApplicableMatchError{}; - } - - template - void report_ok( - const call::Info& callInfo, - call::MatchResult_OkT result - ) - { - g_OkMatchResults.emplace_back(std::move(result)); - } - - void report_error(StringT message) - { - g_Errors.emplace_back(std::move(message)); - } - - template - void report_unhandled_exception( - const call::Info& callInfo, - std::shared_ptr> expectation, - std::exception_ptr exception - ) - { - g_UnhandledExceptions.emplace_back( - unhandled_exception_info{ - .call = callInfo, - .expectation = std::move(expectation), - .exception = exception - }); - } - - template - void report_unsatisfied_expectation( - std::shared_ptr> expectation - ) - { - g_UnsatisfiedExpectations.emplace_back(std::move(expectation)); - } - - class ScopedReporter - { - public: - // ReSharper disable CppMemberFunctionMayBeStatic - - ~ScopedReporter() noexcept - { - clear(); - } - - ScopedReporter() noexcept - { - clear(); - } - - void clear() - { - g_OkMatchResults.clear(); - g_NonApplicableMatchResults.clear(); - g_NoMatchResults.clear(); - g_Errors.clear(); - g_UnhandledExceptions.clear(); - g_UnsatisfiedExpectations.clear(); - } - - ScopedReporter(const ScopedReporter&) = delete; - ScopedReporter& operator =(const ScopedReporter&) = delete; - ScopedReporter(ScopedReporter&&) = delete; - ScopedReporter& operator =(ScopedReporter&&) = delete; - - auto& no_match_reports() noexcept - { - return g_NoMatchResults; - } - - auto& non_applicable_match_reports() noexcept - { - return g_NonApplicableMatchResults; - } - - auto& ok_match_reports() noexcept - { - return g_OkMatchResults; - } - - auto& errors() noexcept - { - return g_Errors; - } - - auto& unhandled_exceptions() noexcept - { - return g_UnhandledExceptions; - } - - auto& unsatisfied_expectations() noexcept - { - return g_UnsatisfiedExpectations; - } - - // ReSharper restore CppMemberFunctionMayBeStatic - }; -} diff --git a/test/adapter-tests/CMakeLists.txt b/test/adapter-tests/CMakeLists.txt new file mode 100644 index 000000000..a368fa1b0 --- /dev/null +++ b/test/adapter-tests/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory("catch2") \ No newline at end of file diff --git a/test/adapter-tests/catch2/CMakeLists.txt b/test/adapter-tests/catch2/CMakeLists.txt new file mode 100644 index 000000000..911e5db97 --- /dev/null +++ b/test/adapter-tests/catch2/CMakeLists.txt @@ -0,0 +1,19 @@ +find_package(Catch2 REQUIRED) +include("${Catch2_SOURCE_DIR}/extras/Catch.cmake") + +add_executable( + mimicpp-adapter-tests-catch2 + "main.cpp" +) + +include(SetupTestTarget) +setup_test_target(mimicpp-adapter-tests-catch2) + +target_link_libraries( + mimicpp-adapter-tests-catch2 + PRIVATE + mimicpp::mimicpp + Catch2::Catch2WithMain +) + +catch_discover_tests(mimicpp-adapter-tests-catch2) diff --git a/test/adapter-tests/catch2/main.cpp b/test/adapter-tests/catch2/main.cpp new file mode 100644 index 000000000..7f0ed7160 --- /dev/null +++ b/test/adapter-tests/catch2/main.cpp @@ -0,0 +1,193 @@ +// // 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 "catch2/catch_test_macros.hpp" +#include "mimic++/Mock.hpp" +#include "mimic++/adapters/Catch2.hpp" + +#include +#include + +#include + +namespace +{ + inline std::atomic_int g_SuccessCounter{0}; + + class SuccessListener final + : public Catch::EventListenerBase + { + using SuperT = EventListenerBase; + + public: + [[nodiscard]] + explicit SuccessListener(const Catch::IConfig* config) + : SuperT{config} + { + m_preferences.shouldReportAllAssertions = true; + } + + void assertionEnded(const Catch::AssertionStats& assertionStats) override + { + if (assertionStats.assertionResult.succeeded()) + { + ++g_SuccessCounter; + } + } + }; +} + +CATCH_REGISTER_LISTENER(SuccessListener) + +TEST_CASE( + "Catch2Reporter reports matches as succeeded statements.", + "[adapter][adapter::catch2]" +) +{ + g_SuccessCounter = 0; + mimicpp::Mock mock{}; + + SCOPED_EXP mock.expect_call(42); + + CHECK(g_SuccessCounter == 0); + mock(42); + REQUIRE(g_SuccessCounter == 2); // the CHECK and SUCCEED +} + +TEST_CASE( + "Catch2Reporter reports failure, when no match can be found.", + "[!shouldfail][adapter][adapter::catch2]" +) +{ + mimicpp::Mock mock{}; + + SCOPED_EXP mock.expect_call(42); + SCOPED_EXP mock.expect_call(-42); + + mock(1337); +} + +TEST_CASE( + "Catch2Reporter reports failure, when no applicable match can be found.", + "[!shouldfail][adapter][adapter::catch2]" +) +{ + mimicpp::Mock mock{}; + + SCOPED_EXP mock.expect_call(42); + + mock(42); + mock(42); +} + +namespace +{ + class TestException + { + }; + + class ThrowOnMatches + { + public: + [[maybe_unused]] + static constexpr bool is_satisfied() noexcept + { + return true; + } + + [[maybe_unused]] + static bool matches([[maybe_unused]] const auto& info) + { + throw TestException{}; + } + + [[maybe_unused]] + static constexpr std::nullopt_t describe() noexcept + { + return std::nullopt; + } + + [[maybe_unused]] + static constexpr void consume([[maybe_unused]] const auto& info) noexcept + { + } + }; +} + +TEST_CASE( + "Catch2Reporter::report_unhandled_exception just creates a warning.", + "[adapter][adapter::catch2]" +) +{ + mimicpp::Mock mock{}; + + SCOPED_EXP mock.expect_call(); + + SCOPED_EXP mock.expect_call() + | mimicpp::expect::times<0, 1>() + | ThrowOnMatches{}; + + REQUIRE_NOTHROW(mock()); +} + +TEST_CASE( + "Catch2Reporter::report_unfulfilled_expectation cancels the test, when no other exception exists.", + "[!shouldfail][adapter][adapter::catch2]" +) +{ + mimicpp::Mock mock{}; + + SCOPED_EXP mock.expect_call(); +} + +TEST_CASE( + "Catch2Reporter::report_unfulfilled_expectation does nothing, when already an uncaught exception exists.", + "[adapter][adapter::catch2]" +) +{ + mimicpp::Mock mock{}; + + const auto runTest = [&] + { + SCOPED_EXP mock.expect_call(); + throw 42; + }; + + REQUIRE_THROWS_AS( + runTest(), + int); +} + +TEST_CASE( + "Catch2Reporter::report_error cancels the test, when no other exception exists.", + "[!shouldfail][adapter][adapter::catch2]" +) +{ + mimicpp::detail::report_error("Hello, World!"); +} + +TEST_CASE( + "Catch2Reporter::report_error does nothing, when already an uncaught exception exists.", + "[adapter][adapter::catch2]" +) +{ + struct helper + { + ~helper() + { + mimicpp::detail::report_error("Hello, World!"); + } + }; + + const auto runTest = [] + { + helper h{}; + throw 42; + }; + + REQUIRE_THROWS_AS( + runTest(), + int); +} diff --git a/test/unit-tests/CMakeLists.txt b/test/unit-tests/CMakeLists.txt new file mode 100644 index 000000000..9ea9bb810 --- /dev/null +++ b/test/unit-tests/CMakeLists.txt @@ -0,0 +1,32 @@ +CPMAddPackage("gh:catchorg/Catch2@3.6.0") +CPMAddPackage("gh:rollbear/trompeloeil@47") +include("${Catch2_SOURCE_DIR}/extras/Catch.cmake") + +add_executable( + mimicpp-tests + "Call.cpp" + #"ComplexMock.cpp" + "Expectation.cpp" + "ExpectationBuilder.cpp" + "ExpectationPolicies.cpp" + "Matcher.cpp" + "Mock.cpp" + "Printer.cpp" + "Reporter.cpp" + "Sequence.cpp" + "TypeTraits.cpp" + "Utility.cpp" +) + +include(SetupTestTarget) +setup_test_target(mimicpp-tests) + +target_link_libraries( + mimicpp-tests + PRIVATE + mimicpp::mimicpp + Catch2::Catch2WithMain + trompeloeil::trompeloeil +) + +catch_discover_tests(mimicpp-tests) diff --git a/test/Call.cpp b/test/unit-tests/Call.cpp similarity index 74% rename from test/Call.cpp rename to test/unit-tests/Call.cpp index 5b8389cf8..b07e73724 100644 --- a/test/Call.cpp +++ b/test/unit-tests/Call.cpp @@ -54,32 +54,3 @@ TEST_CASE( REQUIRE(expected == !(info != other)); REQUIRE(expected == !(other != info)); } - -TEST_CASE( - "call::MatchCategory is formattable.", - "[call]" -) -{ - namespace Matches = Catch::Matchers; - - SECTION("When valid category is given.") - { - const auto [expected, category] = GENERATE( - (table)({ - {"no match", call::MatchCategory::no}, - {"non applicable match", call::MatchCategory::non_applicable}, - {"full match", call::MatchCategory::ok}, - })); - - REQUIRE_THAT( - format::format("{}", category), - Matches::Equals(expected)); - } - - SECTION("When an invalid category is given, std::invalid_argument is thrown.") - { - REQUIRE_THROWS_AS( - format::format("{}", call::MatchCategory{42}), - std::invalid_argument); - } -} diff --git a/test/ComplexMock.cpp b/test/unit-tests/ComplexMock.cpp similarity index 100% rename from test/ComplexMock.cpp rename to test/unit-tests/ComplexMock.cpp diff --git a/test/Expectation.cpp b/test/unit-tests/Expectation.cpp similarity index 63% rename from test/Expectation.cpp rename to test/unit-tests/Expectation.cpp index c93ec81a7..8fe074387 100644 --- a/test/Expectation.cpp +++ b/test/unit-tests/Expectation.cpp @@ -3,10 +3,11 @@ // // (See accompanying file LICENSE_1_0.txt or copy at // // https://www.boost.org/LICENSE_1_0.txt) +#include "TestReporter.hpp" + #include "mimic++/Expectation.hpp" #include "mimic++/Printer.hpp" -#include "TestReporter.hpp" #include "TestTypes.hpp" #include @@ -19,7 +20,8 @@ #include #include #include -#include +#include +#include namespace { @@ -28,17 +30,17 @@ namespace { public: using CallInfoT = mimicpp::call::info_for_signature_t; - using MatchResultT = mimicpp::call::MatchResultT; + MAKE_CONST_MOCK0(report, mimicpp::ExpectationReport(), override); MAKE_CONST_MOCK0(is_satisfied, bool(), noexcept override); - MAKE_CONST_MOCK1(matches, MatchResultT(const CallInfoT&), override); + MAKE_CONST_MOCK1(matches, mimicpp::MatchReport(const CallInfoT&), override); MAKE_MOCK1(consume, void(const CallInfoT&), override); MAKE_MOCK1(finalize_call, void(const CallInfoT&), override); }; } TEST_CASE( - "mimicpp::ExpectationCollection collects expectations and reports when they are removed but unsatisfied.", + "mimicpp::ExpectationCollection collects expectations and reports when they are removed but unfulfilled.", "[expectation]" ) { @@ -49,33 +51,65 @@ TEST_CASE( REQUIRE_NOTHROW(storage.push(expectation)); - mimicpp::ScopedReporter reporter{}; + ScopedReporter reporter{}; SECTION("When expectation is satisfied, nothing is reported.") { REQUIRE_CALL(*expectation, is_satisfied()) .RETURN(true); REQUIRE_NOTHROW(storage.remove(expectation)); REQUIRE_THAT( - reporter.unsatisfied_expectations(), + reporter.unfulfilled_expectations(), Catch::Matchers::IsEmpty()); } - SECTION("When expectation is unsatisfied, get's reported.") + SECTION("When expectation is unfulfilled, get's reported.") { + const mimicpp::ExpectationReport expReport{ + .timesDescription = "times description" + }; + REQUIRE_CALL(*expectation, is_satisfied()) .RETURN(false); + REQUIRE_CALL(*expectation, report()) + .RETURN(expReport); REQUIRE_NOTHROW(storage.remove(expectation)); REQUIRE_THAT( - reporter.unsatisfied_expectations(), + reporter.unfulfilled_expectations(), Catch::Matchers::SizeIs(1)); - auto reportedExpectation = std::any_cast>>( - reporter.unsatisfied_expectations().at(0)); - REQUIRE(expectation == reportedExpectation); + REQUIRE(expReport == reporter.unfulfilled_expectations().at(0)); } } +namespace +{ + inline const mimicpp::MatchReport commonNoMatchReport{ + .timesReport = { + .isApplicable = true + }, + .expectationReports = { + { + .isMatching = false + } + } + }; + + inline const mimicpp::MatchReport commonFullMatchReport{ + .timesReport = { + .isApplicable = true + }, + .expectationReports = {} + }; + + inline const mimicpp::MatchReport commonInapplicableMatchReport{ + .timesReport = { + .isApplicable = false + }, + .expectationReports = {} + }; +} + TEST_CASE( - "mimicpp::ExpectationCollection queries expectation, whether they match the call, in reverse order of construction.", + "mimicpp::ExpectationCollection queries its expectations, whether they match the call, in reverse order of construction.", "[expectation]" ) { @@ -84,7 +118,7 @@ TEST_CASE( using CallInfoT = Info; using trompeloeil::_; - mimicpp::ScopedReporter reporter{}; + ScopedReporter reporter{}; StorageT storage{}; std::vector> expectations(4); for (auto& exp : expectations) @@ -105,15 +139,15 @@ TEST_CASE( REQUIRE_CALL(*expectations[3], matches(_)) .LR_WITH(&_1 == &call) .IN_SEQUENCE(sequence) - .RETURN(MatchResult_NoT{}); + .RETURN(commonNoMatchReport); REQUIRE_CALL(*expectations[2], matches(_)) .LR_WITH(&_1 == &call) .IN_SEQUENCE(sequence) - .RETURN(MatchResult_NoT{}); + .RETURN(commonNoMatchReport); REQUIRE_CALL(*expectations[1], matches(_)) .LR_WITH(&_1 == &call) .IN_SEQUENCE(sequence) - .RETURN(MatchResult_OkT{}); + .RETURN(commonFullMatchReport); // expectations[3] is never queried REQUIRE_CALL(*expectations[1], consume(_)) .LR_WITH(&_1 == &call) @@ -127,27 +161,29 @@ TEST_CASE( reporter.no_match_reports(), Catch::Matchers::IsEmpty()); REQUIRE_THAT( - reporter.non_applicable_match_reports(), + reporter.inapplicable_match_reports(), Catch::Matchers::IsEmpty()); REQUIRE_THAT( - reporter.ok_match_reports(), + reporter.full_match_reports(), Catch::Matchers::SizeIs(1)); } - SECTION("If at least one matches but is exhausted.") + SECTION("If at least one matches but is inapplicable.") { + using match_report_t = mimicpp::MatchReport; const auto [count, result0, result1, result2, result3] = GENERATE( - (table)( + (table)( { - {1u, MatchResult_NotApplicableT{}, MatchResult_NoT{}, MatchResult_NoT{}, MatchResult_NoT{}}, - {1u, MatchResult_NoT{}, MatchResult_NotApplicableT{}, MatchResult_NoT{}, MatchResult_NoT{}}, - {1u, MatchResult_NoT{}, MatchResult_NoT{}, MatchResult_NotApplicableT{}, MatchResult_NoT{}}, - {1u, MatchResult_NoT{}, MatchResult_NoT{}, MatchResult_NoT{}, MatchResult_NotApplicableT{}}, - - {2u, MatchResult_NotApplicableT{}, MatchResult_NoT{}, MatchResult_NotApplicableT{}, MatchResult_NoT{}}, - {2u, MatchResult_NoT{}, MatchResult_NotApplicableT{}, MatchResult_NoT{}, MatchResult_NotApplicableT{}}, - {3u, MatchResult_NotApplicableT{}, MatchResult_NoT{}, MatchResult_NotApplicableT{}, MatchResult_NotApplicableT{}}, - {4u, MatchResult_NotApplicableT{}, MatchResult_NotApplicableT{}, MatchResult_NotApplicableT{}, MatchResult_NotApplicableT{}} + {1u, commonInapplicableMatchReport, commonNoMatchReport, commonNoMatchReport, commonNoMatchReport}, + {1u, commonNoMatchReport, commonInapplicableMatchReport, commonNoMatchReport, commonNoMatchReport}, + {1u, commonNoMatchReport, commonNoMatchReport, commonInapplicableMatchReport, commonNoMatchReport}, + {1u, commonNoMatchReport, commonNoMatchReport, commonNoMatchReport, commonInapplicableMatchReport}, + + {2u, commonInapplicableMatchReport, commonNoMatchReport, commonInapplicableMatchReport, commonNoMatchReport}, + {2u, commonNoMatchReport, commonInapplicableMatchReport, commonNoMatchReport, commonInapplicableMatchReport}, + {3u, commonInapplicableMatchReport, commonNoMatchReport, commonInapplicableMatchReport, commonInapplicableMatchReport}, + {4u, commonInapplicableMatchReport, commonInapplicableMatchReport, commonInapplicableMatchReport, + commonInapplicableMatchReport} })); trompeloeil::sequence sequence{}; @@ -175,32 +211,32 @@ TEST_CASE( reporter.no_match_reports(), Catch::Matchers::IsEmpty()); REQUIRE_THAT( - reporter.non_applicable_match_reports(), + reporter.inapplicable_match_reports(), Catch::Matchers::SizeIs(count)); REQUIRE_THAT( - reporter.ok_match_reports(), + reporter.full_match_reports(), Catch::Matchers::IsEmpty()); } - SECTION("If all do not match.") + SECTION("If none matches.") { trompeloeil::sequence sequence{}; REQUIRE_CALL(*expectations[3], matches(_)) .LR_WITH(&_1 == &call) .IN_SEQUENCE(sequence) - .RETURN(MatchResult_NoT{}); + .RETURN(commonNoMatchReport); REQUIRE_CALL(*expectations[2], matches(_)) .LR_WITH(&_1 == &call) .IN_SEQUENCE(sequence) - .RETURN(MatchResult_NoT{}); + .RETURN(commonNoMatchReport); REQUIRE_CALL(*expectations[1], matches(_)) .LR_WITH(&_1 == &call) .IN_SEQUENCE(sequence) - .RETURN(MatchResult_NoT{}); + .RETURN(commonNoMatchReport); REQUIRE_CALL(*expectations[0], matches(_)) .LR_WITH(&_1 == &call) .IN_SEQUENCE(sequence) - .RETURN(MatchResult_NoT{}); + .RETURN(commonNoMatchReport); REQUIRE_THROWS_AS( storage.handle_call(call), @@ -209,10 +245,10 @@ TEST_CASE( reporter.no_match_reports(), Catch::Matchers::SizeIs(4)); REQUIRE_THAT( - reporter.non_applicable_match_reports(), + reporter.inapplicable_match_reports(), Catch::Matchers::IsEmpty()); REQUIRE_THAT( - reporter.ok_match_reports(), + reporter.full_match_reports(), Catch::Matchers::IsEmpty()); } } @@ -229,7 +265,7 @@ TEST_CASE( using CallInfoT = Info; using trompeloeil::_; - mimicpp::ScopedReporter reporter{}; + ScopedReporter reporter{}; StorageT storage{}; auto throwingExpectation = std::make_shared(); auto otherExpectation = std::make_shared(); @@ -246,7 +282,11 @@ TEST_CASE( { }; - const auto matches = [&](const unhandled_exception_info& info) + const mimicpp::ExpectationReport throwingReport{ + .timesDescription = "times description" + }; + + const auto matches = [&](const auto& info) { try { @@ -254,8 +294,8 @@ TEST_CASE( } catch (const Exception&) { - return call == std::any_cast(info.call) - && throwingExpectation == std::any_cast>>(info.expectation); + return info.call == mimicpp::make_call_report(call) + && info.expectation ==throwingReport; } catch (...) { @@ -263,24 +303,26 @@ TEST_CASE( } }; - SECTION("When thrown during matches.") + SECTION("When an exception is thrown during matches.") { REQUIRE_CALL(*throwingExpectation, matches(_)) .THROW(Exception{}); + REQUIRE_CALL(*throwingExpectation, report()) + .RETURN(throwingReport); REQUIRE_CALL(*otherExpectation, matches(_)) - .RETURN(MatchResult_OkT{}); + .RETURN(commonFullMatchReport); REQUIRE_CALL(*otherExpectation, consume(_)); REQUIRE_CALL(*otherExpectation, finalize_call(_)); REQUIRE_NOTHROW(storage.handle_call(call)); CHECK_THAT( - reporter.ok_match_reports(), + reporter.full_match_reports(), Matches::SizeIs(1)); CHECK_THAT( reporter.no_match_reports(), Matches::IsEmpty()); CHECK_THAT( - reporter.non_applicable_match_reports(), + reporter.inapplicable_match_reports(), Matches::IsEmpty()); REQUIRE_THAT( @@ -327,53 +369,27 @@ TEMPLATE_TEST_CASE_SIG( STATIC_REQUIRE(expected == mimicpp::times_policy); } -namespace +TEST_CASE( + "mimicpp::BasicExpectation stores std::source_location.", + "[expectation]" +) { - const std::array allSubMatchResultAlternatives = std::to_array( - { - {.matched = false}, - {.matched = true} - }); - - class CallMatchCategoryMatcher final - : public Catch::Matchers::MatcherGenericBase - { - public: - using CategoryT = mimicpp::call::MatchCategory; + using TimesT = TimesFake; + using FinalizerT = FinalizerFake; - [[nodiscard]] - explicit CallMatchCategoryMatcher(const CategoryT category) noexcept - : m_Category{category} - { - } + constexpr auto loc = std::source_location::current(); - [[maybe_unused]] bool match(const auto& result) const - { - return m_Category == std::visit( - [](const auto& inner) { return inner.value; }, - result); - } - - std::string describe() const override - { - return mimicpp::format::format( - "matches category: {}", - m_Category); - } - - private: - CategoryT m_Category; + mimicpp::BasicExpectation expectation{ + loc, + TimesT{}, + FinalizerT{} }; - [[nodiscard]] - CallMatchCategoryMatcher matches_category(const mimicpp::call::MatchCategory category) noexcept - { - return CallMatchCategoryMatcher{category}; - } + REQUIRE(mimicpp::is_same_source_location(loc, expectation.from())); } TEST_CASE( - "Times policy of mimicpp::BasicExpectation controls, how often an expectations an expectation must be matched.", + "Times policy of mimicpp::BasicExpectation controls, how often its expectations must be matched.", "[expectation]" ) { @@ -384,6 +400,7 @@ TEST_CASE( using PolicyRefT = PolicyFacade>, UnwrapReferenceWrapper>; using TimesPolicyT = TimesFacade, UnwrapReferenceWrapper>; using CallInfoT = mimicpp::call::info_for_signature_t; + using TimesReportT = mimicpp::MatchReport::Times; const CallInfoT call{ .args = {}, @@ -396,6 +413,7 @@ TEST_CASE( SECTION("With no other expectation policies.") { mimicpp::BasicExpectation expectation{ + std::source_location::current(), std::ref(times), FinalizerT{} }; @@ -408,18 +426,26 @@ TEST_CASE( REQUIRE(isSatisfied == std::as_const(expectation).is_satisfied()); } - SECTION("When times is not saturated, call is matched.") + SECTION("When times is applicable, call is matched.") { REQUIRE_CALL(times, is_applicable()) .RETURN(true); - REQUIRE(std::holds_alternative(std::as_const(expectation).matches(call))); + REQUIRE_CALL(times, describe_state()) + .RETURN("times state applicable"); + const mimicpp::MatchReport matchReport = std::as_const(expectation).matches(call); + REQUIRE(matchReport.timesReport == TimesReportT{true, "times state applicable"}); + REQUIRE(mimicpp::MatchResult::full == evaluate_match_report(matchReport)); } - SECTION("When times is saturated, match exhausted.") + SECTION("When times is not applicable => inapplicable.") { REQUIRE_CALL(times, is_applicable()) .RETURN(false); - REQUIRE(std::holds_alternative(std::as_const(expectation).matches(call))); + REQUIRE_CALL(times, describe_state()) + .RETURN("times state inapplicable"); + const mimicpp::MatchReport matchReport = std::as_const(expectation).matches(call); + REQUIRE(matchReport.timesReport == TimesReportT{false, "times state inapplicable"}); + REQUIRE(mimicpp::MatchResult::inapplicable == evaluate_match_report(matchReport)); } SECTION("Consume calls times.consume().") @@ -433,6 +459,7 @@ TEST_CASE( { PolicyMockT policy{}; mimicpp::BasicExpectation expectation{ + std::source_location::current(), std::ref(times), FinalizerT{}, std::ref(policy) @@ -457,13 +484,17 @@ TEST_CASE( SECTION("When policy is not matched, then the result is always no match.") { + REQUIRE_CALL(policy, matches(_)) + .LR_WITH(&_1 == &call) + .RETURN(false); + REQUIRE_CALL(policy, describe()) + .RETURN(mimicpp::StringT{}); const bool isApplicable = GENERATE(false, true); REQUIRE_CALL(times, is_applicable()) .RETURN(isApplicable); - REQUIRE_CALL(policy, matches(_)) - .LR_WITH(&_1 == &call) - .RETURN(mimicpp::call::SubMatchResult{false}); - REQUIRE(std::holds_alternative(std::as_const(expectation).matches(call))); + REQUIRE_CALL(times, describe_state()) + .RETURN(std::nullopt); + REQUIRE(mimicpp::MatchResult::none == evaluate_match_report(std::as_const(expectation).matches(call))); } SECTION("When policy is matched.") @@ -474,18 +505,26 @@ TEST_CASE( .RETURN(true); REQUIRE_CALL(policy, matches(_)) .LR_WITH(&_1 == &call) - .RETURN(mimicpp::call::SubMatchResult{true}); - REQUIRE(std::holds_alternative(std::as_const(expectation).matches(call))); + .RETURN(true); + REQUIRE_CALL(policy, describe()) + .RETURN(mimicpp::StringT{}); + REQUIRE_CALL(times, describe_state()) + .RETURN(std::nullopt); + REQUIRE(mimicpp::MatchResult::full == evaluate_match_report(std::as_const(expectation).matches(call))); } - SECTION("And when times is saturated => exhausted") + SECTION("And when times not applicable => inapplicable") { REQUIRE_CALL(times, is_applicable()) .RETURN(false); REQUIRE_CALL(policy, matches(_)) .LR_WITH(&_1 == &call) - .RETURN(mimicpp::call::SubMatchResult{true}); - REQUIRE(std::holds_alternative(std::as_const(expectation).matches(call))); + .RETURN(true); + REQUIRE_CALL(policy, describe()) + .RETURN(mimicpp::StringT{}); + REQUIRE_CALL(times, describe_state()) + .RETURN(std::nullopt); + REQUIRE(mimicpp::MatchResult::inapplicable == evaluate_match_report(std::as_const(expectation).matches(call))); } } @@ -511,6 +550,8 @@ TEMPLATE_TEST_CASE( using PolicyMockT = PolicyMock; using PolicyRefT = PolicyFacade>, UnwrapReferenceWrapper>; using CallInfoT = mimicpp::call::info_for_signature_t; + using ExpectationReportT = mimicpp::MatchReport::Expectation; + using TimesReportT = mimicpp::MatchReport::Times; const CallInfoT call{ .args = {}, @@ -521,12 +562,18 @@ TEMPLATE_TEST_CASE( SECTION("With no policies at all.") { mimicpp::BasicExpectation expectation{ + std::source_location::current(), TimesFake{.isSatisfied = true}, FinalizerT{} }; REQUIRE(std::as_const(expectation).is_satisfied()); - REQUIRE(std::holds_alternative(std::as_const(expectation).matches(call))); + const mimicpp::MatchReport matchReport = std::as_const(expectation).matches(call); + REQUIRE(matchReport.timesReport == TimesReportT{true}); + REQUIRE_THAT( + matchReport.expectationReports, + Catch::Matchers::IsEmpty()); + REQUIRE(mimicpp::MatchResult::full == evaluate_match_report(matchReport)); REQUIRE_NOTHROW(expectation.consume(call)); } @@ -534,6 +581,7 @@ TEMPLATE_TEST_CASE( { PolicyMockT policy{}; mimicpp::BasicExpectation expectation{ + std::source_location::current(), TimesFake{.isSatisfied = true}, FinalizerT{}, PolicyRefT{std::ref(policy)} @@ -548,16 +596,30 @@ TEMPLATE_TEST_CASE( { REQUIRE_CALL(policy, matches(_)) .LR_WITH(&_1 == &call) - .RETURN(mimicpp::call::SubMatchResult{true}); - REQUIRE(std::holds_alternative(std::as_const(expectation).matches(call))); + .RETURN(true); + REQUIRE_CALL(policy, describe()) + .RETURN("policy description"); + const mimicpp::MatchReport matchReport = std::as_const(expectation).matches(call); + REQUIRE(matchReport.timesReport == TimesReportT{true}); + REQUIRE_THAT( + matchReport.expectationReports, + Catch::Matchers::RangeEquals(std::vector{{true, "policy description"}})); + REQUIRE(mimicpp::MatchResult::full == evaluate_match_report(matchReport)); } SECTION("When not matched => no match") { REQUIRE_CALL(policy, matches(_)) .LR_WITH(&_1 == &call) - .RETURN(mimicpp::call::SubMatchResult{false}); - REQUIRE(std::holds_alternative(std::as_const(expectation).matches(call))); + .RETURN(false); + REQUIRE_CALL(policy, describe()) + .RETURN("policy description"); + const mimicpp::MatchReport matchReport = std::as_const(expectation).matches(call); + REQUIRE(matchReport.timesReport == TimesReportT{true}); + REQUIRE_THAT( + matchReport.expectationReports, + Catch::Matchers::RangeEquals(std::vector{{false, "policy description"}})); + REQUIRE(mimicpp::MatchResult::none == evaluate_match_report(matchReport)); } REQUIRE_CALL(policy, consume(_)) @@ -570,6 +632,7 @@ TEMPLATE_TEST_CASE( PolicyMockT policy1{}; PolicyMockT policy2{}; mimicpp::BasicExpectation expectation{ + std::source_location::current(), TimesFake{.isSatisfied = true}, FinalizerT{}, PolicyRefT{std::ref(policy1)}, @@ -601,31 +664,57 @@ TEMPLATE_TEST_CASE( { REQUIRE_CALL(policy1, matches(_)) .LR_WITH(&_1 == &call) - .RETURN(mimicpp::call::SubMatchResult{true}); + .RETURN(true); + REQUIRE_CALL(policy1, describe()) + .RETURN("policy1 description"); REQUIRE_CALL(policy2, matches(_)) .LR_WITH(&_1 == &call) - .RETURN(mimicpp::call::SubMatchResult{true}); - - REQUIRE(std::holds_alternative(std::as_const(expectation).matches(call))); + .RETURN(true); + REQUIRE_CALL(policy2, describe()) + .RETURN("policy2 description"); + + const mimicpp::MatchReport matchReport = std::as_const(expectation).matches(call); + REQUIRE(matchReport.timesReport == TimesReportT{true}); + REQUIRE_THAT( + matchReport.expectationReports, + Catch::Matchers::UnorderedRangeEquals( + std::vector{ + {true, "policy1 description"}, + {true, "policy2 description"} + })); + REQUIRE(mimicpp::MatchResult::full == evaluate_match_report(matchReport)); } SECTION("When at least one not matches => no match") { - const auto [match1, match2] = GENERATE( + const auto [isMatching1, isMatching2] = GENERATE( (table)({ - {false, false}, {false, true}, {true, false}, - })); + {false, false} + })); REQUIRE_CALL(policy1, matches(_)) .LR_WITH(&_1 == &call) - .RETURN(mimicpp::call::SubMatchResult{match1}); + .RETURN(isMatching1); + REQUIRE_CALL(policy1, describe()) + .RETURN("policy1 description"); REQUIRE_CALL(policy2, matches(_)) .LR_WITH(&_1 == &call) - .RETURN(mimicpp::call::SubMatchResult{match2}); - - REQUIRE(std::holds_alternative(std::as_const(expectation).matches(call))); + .RETURN(isMatching2); + REQUIRE_CALL(policy2, describe()) + .RETURN("policy2 description"); + + const mimicpp::MatchReport matchReport = std::as_const(expectation).matches(call); + REQUIRE(matchReport.timesReport == TimesReportT{true}); + REQUIRE_THAT( + matchReport.expectationReports, + Catch::Matchers::UnorderedRangeEquals( + std::vector{ + {isMatching1, "policy1 description"}, + {isMatching2, "policy2 description"} + })); + REQUIRE(mimicpp::MatchResult::none == evaluate_match_report(matchReport)); } SECTION("When calling consume()") @@ -639,6 +728,80 @@ TEMPLATE_TEST_CASE( } } +TEST_CASE( + "mimicpp::BasicExpectation::report gathers information about the expectation.", + "[expectation]" +) +{ + namespace Matches = Catch::Matchers; + + using FinalizerPolicyT = FinalizerFake; + using TimesPolicyT = TimesFake; + + SECTION("Finalizer policy has no description.") + { + // Todo: + } + + SECTION("Times policy is queried.") + { + using TimesT = TimesFacade, UnwrapReferenceWrapper>; + + TimesMock times{}; + mimicpp::BasicExpectation< + void(), + TimesT, + FinalizerPolicyT> + expectation{ + std::source_location::current(), + TimesT{std::ref(times)}, + FinalizerPolicyT{} + }; + + REQUIRE_CALL(times, describe_state()) + .RETURN("times description"); + + const mimicpp::ExpectationReport report = expectation.report(); + REQUIRE(report.timesDescription); + REQUIRE_THAT( + *report.timesDescription, + Matches::Equals("times description")); + } + + SECTION("Expectation policies are queried.") + { + using PolicyT = PolicyFacade< + void(), + std::reference_wrapper>, + UnwrapReferenceWrapper>; + + PolicyMock policy{}; + mimicpp::BasicExpectation< + void(), + TimesPolicyT, + FinalizerPolicyT, + PolicyT> + expectation{ + std::source_location::current(), + TimesPolicyT{}, + FinalizerPolicyT{}, + PolicyT{std::ref(policy)} + }; + + REQUIRE_CALL(policy, describe()) + .RETURN("expectation description"); + + const mimicpp::ExpectationReport report = expectation.report(); + REQUIRE_THAT( + report.expectationDescriptions, + Matches::SizeIs(1)); + REQUIRE(report.expectationDescriptions[0]); + REQUIRE_THAT( + *report.expectationDescriptions[0], + Matches::Equals("expectation description")); + } +} + TEMPLATE_TEST_CASE( "mimicpp::BasicExpectation finalizer can be exchanged.", "[expectation]", @@ -660,6 +823,7 @@ TEMPLATE_TEST_CASE( FinalizerT finalizer{}; mimicpp::BasicExpectation expectation{ + std::source_location::current(), TimesFake{}, std::ref(finalizer) }; diff --git a/test/ExpectationBuilder.cpp b/test/unit-tests/ExpectationBuilder.cpp similarity index 80% rename from test/ExpectationBuilder.cpp rename to test/unit-tests/ExpectationBuilder.cpp index 13fbcea36..d7f5c3b75 100644 --- a/test/ExpectationBuilder.cpp +++ b/test/unit-tests/ExpectationBuilder.cpp @@ -1,5 +1,12 @@ -#include "mimic++/ExpectationBuilder.hpp" +// // 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 "TestReporter.hpp" + +#include "mimic++/ExpectationBuilder.hpp" + #include "TestTypes.hpp" #include @@ -8,7 +15,7 @@ #include #include #include -#include +#include using namespace mimicpp; @@ -37,6 +44,8 @@ TEST_CASE( using ScopedExpectationT = ScopedExpectation; using CallInfoT = call::info_for_signature_t; + ScopedReporter reporter{}; + auto collection = std::make_shared>(); constexpr CallInfoT call{ .args = {}, @@ -86,14 +95,18 @@ TEST_CASE( { REQUIRE_CALL(times, is_applicable()) .RETURN(true); + REQUIRE_CALL(times, describe_state()) + .RETURN(std::nullopt); REQUIRE_CALL(times, consume()); REQUIRE_NOTHROW(collection->handle_call(call)); } - SECTION("And when times is saturated.") + SECTION("And when times is inapplicable.") { REQUIRE_CALL(times, is_applicable()) .RETURN(false); + REQUIRE_CALL(times, describe_state()) + .RETURN(std::nullopt); REQUIRE_THROWS_AS( collection->handle_call(call), NonApplicableMatchError); @@ -136,7 +149,7 @@ TEST_CASE( BaseBuilderT builder{ collection, - TimesFake{}, + TimesFake{.isSatisfied = true}, expectation_policies::InitFinalize{}, std::tuple{}}; @@ -192,7 +205,11 @@ TEST_CASE( std::reference_wrapper>, UnwrapReferenceWrapper>; FinalizerT finalizer{}; - ScopedExpectationT expectation = BaseBuilderT{collection, TimesFake{}, expectation_policies::InitFinalize{}, std::tuple{}} + ScopedExpectationT expectation = BaseBuilderT{ + collection, + TimesFake{.isSatisfied = true}, + expectation_policies::InitFinalize{}, + std::tuple{}} | FinalizerPolicyT{std::ref(finalizer)}; REQUIRE_CALL(finalizer, finalize_call(_)) @@ -263,6 +280,41 @@ TEST_CASE( } } +TEST_CASE( + "ScopedExpectation forwards source_location to finalize.", + "[expectation][expectation::builder]" +) +{ + namespace Matches = Catch::Matchers; + + using SignatureT = void(); + using TimesT = TimesFake; + using FinalizerT = FinalizerFake; + using BaseBuilderT = BasicExpectationBuilder; + + const auto collection = std::make_shared>(); + + const std::source_location beforeLoc = std::source_location::current(); + ScopedExpectation expectation = BaseBuilderT{ + collection, + TimesT{.isSatisfied = true}, + FinalizerT{}, + std::tuple{} + }; + const std::source_location afterLoc = std::source_location::current(); + + const auto& inner = dynamic_cast&>( + expectation.expectation()); + REQUIRE_THAT( + inner.from().file_name(), + Matches::Equals(beforeLoc.file_name())); + REQUIRE_THAT( + inner.from().function_name(), + Matches::Equals(beforeLoc.function_name())); + REQUIRE(beforeLoc.line() < inner.from().line()); + REQUIRE(inner.from().line() < afterLoc.line()); +} + TEST_CASE( "MIMICPP_SCOPED_EXPECTATION ScopedExpectation with unique name from a builder.", "[expectation][expectation::builder]" @@ -277,19 +329,19 @@ TEST_CASE( const auto collection = std::make_shared>(); MIMICPP_SCOPED_EXPECTATION BaseBuilderT{ - collection, - TimesFake{.isSatisfied = true}, - expectation_policies::InitFinalize{}, - std::tuple{}}; + collection, + TimesFake{.isSatisfied = true}, + expectation_policies::InitFinalize{}, + std::tuple{}}; MIMICPP_SCOPED_EXPECTATION BaseBuilderT{ - collection, - TimesFake{.isSatisfied = true}, - expectation_policies::InitFinalize{}, - std::tuple{}}; + collection, + TimesFake{.isSatisfied = true}, + expectation_policies::InitFinalize{}, + std::tuple{}}; } REQUIRE_THAT( - reporter.unsatisfied_expectations(), + reporter.unfulfilled_expectations(), Catch::Matchers::IsEmpty()); } diff --git a/test/ExpectationPolicies.cpp b/test/unit-tests/ExpectationPolicies.cpp similarity index 94% rename from test/ExpectationPolicies.cpp rename to test/unit-tests/ExpectationPolicies.cpp index 8db7249ee..a2848511a 100644 --- a/test/ExpectationPolicies.cpp +++ b/test/unit-tests/ExpectationPolicies.cpp @@ -3,8 +3,10 @@ // // (See accompanying file LICENSE_1_0.txt or copy at // // https://www.boost.org/LICENSE_1_0.txt) -#include "mimic++/ExpectationPolicies.hpp" #include "TestReporter.hpp" + +#include "mimic++/ExpectationPolicies.hpp" + #include "TestTypes.hpp" #include @@ -168,6 +170,71 @@ TEST_CASE( std::runtime_error); } +TEST_CASE( + "both, expectation_policies::Times and expectation_policies::RuntimeTimes use detail::describe_times_state for their description.", + "[expectation][expectation::builder]" +) +{ + namespace Matches = Catch::Matchers; + + SECTION("When current == max.") + { + const auto [min, max] = GENERATE( + (table)({ + {0, 1}, + {0, 2}, + {3, 42}, + })); + + const auto description = expectation_policies::detail::describe_times_state( + max, + min, + max); + + REQUIRE_THAT( + description, + Matches::StartsWith("inapplicable: already saturated (matched ")); + } + + SECTION("When min <= current < max.") + { + const auto [current, min, max] = GENERATE( + (table)({ + {0, 0, 1}, + {1, 0, 2}, + {21, 3, 42}, + })); + + const auto description = expectation_policies::detail::describe_times_state( + current, + min, + max); + + REQUIRE_THAT( + description, + Matches::Matches("applicable: accepts further matches \\(matched \\d+ out of \\d+ times\\)")); + } + + SECTION("When current < min.") + { + const auto [current, min, max] = GENERATE( + (table)({ + {0, 1, 2}, + {1, 2, 5}, + {2, 3, 42}, + })); + + const auto description = expectation_policies::detail::describe_times_state( + current, + min, + max); + + REQUIRE_THAT( + description, + Matches::StartsWith("unsatisfied: matched ")); + } +} + TEMPLATE_TEST_CASE_SIG( "expectation_policies::Category checks whether the given call::Info matches.", "[expectation][expectation::policy]", @@ -188,6 +255,13 @@ TEMPLATE_TEST_CASE_SIG( REQUIRE(policy.is_satisfied()); } + SECTION("Policy description.") + { + REQUIRE_THAT( + policy.describe(), + Catch::Matchers::Equals(format::format("expect: from {} category overload", category))); + } + const CallInfoT call{ .args = {}, .fromCategory = GENERATE(ValueCategory::lvalue, ValueCategory::rvalue, ValueCategory::any), @@ -198,12 +272,7 @@ TEMPLATE_TEST_CASE_SIG( { SECTION("When call and policy category matches, success is returned.") { - const auto result = policy.matches(call); - - REQUIRE(result.matched); - REQUIRE_THAT( - result.msg.value(), - Catch::Matchers::Equals(format::format(" matches Category {}", category))); + REQUIRE(policy.matches(call)); } SECTION("Policy doesn't consume, but asserts on wrong category.") @@ -215,12 +284,7 @@ TEMPLATE_TEST_CASE_SIG( { SECTION("When call and policy category mismatch, failure is returned.") { - const auto result = policy.matches(call); - - REQUIRE(!result.matched); - REQUIRE_THAT( - result.msg.value(), - Catch::Matchers::Equals(format::format(" does not match Category {}", category))); + REQUIRE(!policy.matches(call)); } } } @@ -245,6 +309,13 @@ TEMPLATE_TEST_CASE_SIG( REQUIRE(policy.is_satisfied()); } + SECTION("Policy description.") + { + REQUIRE_THAT( + policy.describe(), + Catch::Matchers::Equals(format::format("expect: from {} qualified overload", constness))); + } + const CallInfoT call{ .args = {}, .fromCategory = GENERATE(ValueCategory::lvalue, ValueCategory::rvalue, ValueCategory::any), @@ -255,12 +326,7 @@ TEMPLATE_TEST_CASE_SIG( { SECTION("When call and policy constness matches, success is returned.") { - const auto result = policy.matches(call); - - REQUIRE(result.matched); - REQUIRE_THAT( - result.msg.value(), - Catch::Matchers::Equals(format::format(" matches Constness {}", constness))); + REQUIRE(policy.matches(call)); } SECTION("Policy doesn't consume, but asserts on wrong constness.") @@ -272,12 +338,7 @@ TEMPLATE_TEST_CASE_SIG( { SECTION("When call and policy constness mismatch, failure is returned.") { - const auto result = policy.matches(call); - - REQUIRE(!result.matched); - REQUIRE_THAT( - result.msg.value(), - Catch::Matchers::Equals(format::format(" does not match Constness {}", constness))); + REQUIRE(!policy.matches(call)); } } } @@ -378,7 +439,7 @@ TEST_CASE( }; using ProjectionT = InvocableMock; - using DescriberT = InvocableMock; + using DescriberT = InvocableMock; using MatcherT = MatcherMock; STATIC_CHECK(matcher_for); @@ -401,6 +462,20 @@ TEST_CASE( REQUIRE(std::as_const(policy).is_satisfied()); REQUIRE_NOTHROW(policy.consume(info)); + SECTION("Policy description.") + { + REQUIRE_CALL(matcher, describe()) + .RETURN("matcher description"); + REQUIRE_CALL(describer, Invoke("matcher description")) + .RETURN("expect that: matcher description"); + + const auto description = policy.describe(); + REQUIRE(description); + REQUIRE_THAT( + *description, + Catch::Matchers::Equals("expect that: matcher description")); + } + SECTION("When matched.") { REQUIRE_CALL(projection, Invoke(_)) @@ -409,19 +484,8 @@ TEST_CASE( REQUIRE_CALL(matcher, matches(_)) .LR_WITH(&_1 == &arg0) .RETURN(true); - REQUIRE_CALL(matcher, describe(_)) - .LR_WITH(&_1 == &arg0) - .RETURN("success"); - REQUIRE_CALL(describer, Invoke(_, "success", true)) - .LR_WITH(&_1 == &arg0) - .RETURN("succeeded!"); - const call::SubMatchResult result = std::as_const(policy).matches(info); - REQUIRE(result.matched); - REQUIRE(result.msg); - REQUIRE_THAT( - *result.msg, - Matches::Equals("succeeded!")); + REQUIRE(std::as_const(policy).matches(info)); } SECTION("When not matched.") @@ -432,19 +496,8 @@ TEST_CASE( REQUIRE_CALL(matcher, matches(_)) .LR_WITH(&_1 == &arg0) .RETURN(false); - REQUIRE_CALL(matcher, describe(_)) - .LR_WITH(&_1 == &arg0) - .RETURN("failed"); - REQUIRE_CALL(describer, Invoke(_, "failed", false)) - .LR_WITH(&_1 == &arg0) - .RETURN("failure!"); - const call::SubMatchResult result = std::as_const(policy).matches(info); - REQUIRE(!result.matched); - REQUIRE(result.msg); - REQUIRE_THAT( - *result.msg, - Matches::Equals("failure!")); + REQUIRE(!std::as_const(policy).matches(info)); } } @@ -481,7 +534,8 @@ TEST_CASE( expectation_policies::SideEffectAction policy{std::ref(action)}; STATIC_REQUIRE(expectation_policy_for); REQUIRE(std::as_const(policy).is_satisfied()); - REQUIRE(call::SubMatchResult{true} == std::as_const(policy).matches(info)); + REQUIRE(std::as_const(policy).matches(info)); + REQUIRE(std::optional{} == std::as_const(policy).describe()); REQUIRE_CALL(action, Invoke(_)) .LR_WITH(&info == &_1); @@ -1087,38 +1141,25 @@ TEST_CASE( REQUIRE(std::as_const(policy).is_satisfied()); REQUIRE_NOTHROW(policy.consume(info)); - SECTION("When matched.") + SECTION("Policy description.") { - REQUIRE_CALL(matcher, matches(_)) - .LR_WITH(&_1 == &arg0) - .RETURN(true); - REQUIRE_CALL(matcher, describe(_)) - .LR_WITH(&_1 == &arg0) - .RETURN("custom requirement"); + REQUIRE_CALL(matcher, describe()) + .RETURN("matcher description"); - const call::SubMatchResult result = std::as_const(policy).matches(info); - REQUIRE(result.matched); - REQUIRE(result.msg); + const auto description = policy.describe(); REQUIRE_THAT( - *result.msg, - Matches::Equals("arg[0] passed requirement: custom requirement")); + *description, + Catch::Matchers::Equals("expect: arg[0] matcher description")); } - SECTION("When not matched.") + SECTION("Policy matches().") { + const bool match = GENERATE(true, false); REQUIRE_CALL(matcher, matches(_)) .LR_WITH(&_1 == &arg0) - .RETURN(false); - REQUIRE_CALL(matcher, describe(_)) - .LR_WITH(&_1 == &arg0) - .RETURN("custom requirement"); + .RETURN(match); - const call::SubMatchResult result = std::as_const(policy).matches(info); - REQUIRE(!result.matched); - REQUIRE(result.msg); - REQUIRE_THAT( - *result.msg, - Matches::Equals("arg[0] failed requirement: custom requirement")); + REQUIRE(match == std::as_const(policy).matches(info)); } } diff --git a/test/Matcher.cpp b/test/unit-tests/Matcher.cpp similarity index 69% rename from test/Matcher.cpp rename to test/unit-tests/Matcher.cpp index 5a045e76e..a0e605850 100644 --- a/test/Matcher.cpp +++ b/test/unit-tests/Matcher.cpp @@ -57,8 +57,8 @@ TEST_CASE( MatcherPredicateMock predicate{}; PredicateMatcher matcher{ std::ref(predicate), - "Hello, {}!", - std::tuple<>{} + "Hello, World!", + "not Hello, World!" }; SECTION("When matches() is called, argument is forwarded to the predicate.") @@ -72,10 +72,9 @@ TEST_CASE( REQUIRE(result == matcher.matches(value)); } - SECTION("When describe() is called, argument is forwarded to the functional.") + SECTION("When describe() is called.") { - constexpr int value{42}; - REQUIRE("Hello, 42!" == matcher.describe(value)); + REQUIRE("Hello, World!" == matcher.describe()); } } @@ -88,7 +87,8 @@ TEST_CASE( MatcherPredicateMock predicate{}; PredicateMatcher matcher{ std::ref(predicate), - "Hello, {}!" + "Hello, World!", + "not Hello, World!" }; PredicateMatcher negatedMatcher = !std::move(matcher); @@ -105,10 +105,9 @@ TEST_CASE( REQUIRE(result == !negatedMatcher.matches(value)); } - SECTION("When describe() is called, argument is forwarded to the functional.") + SECTION("When describe() is called.") { - constexpr int value{42}; - REQUIRE("not (Hello, 42!)" == negatedMatcher.describe(value)); + REQUIRE("not Hello, World!" == negatedMatcher.describe()); } } @@ -117,7 +116,8 @@ TEST_CASE( MatcherPredicateMock predicate{}; const PredicateMatcher matcher{ std::ref(predicate), - "Hello, {}!" + "Hello, World!", + "not Hello, World!" }; PredicateMatcher negatedMatcher = !matcher; @@ -134,10 +134,9 @@ TEST_CASE( REQUIRE(result == !negatedMatcher.matches(value)); } - SECTION("When describe() is called, argument is forwarded to the functional.") + SECTION("When describe() is called.") { - constexpr int value{42}; - REQUIRE("not (Hello, 42!)" == negatedMatcher.describe(value)); + REQUIRE("not Hello, World!" == negatedMatcher.describe()); } SECTION("And original matcher is still working.") @@ -153,10 +152,9 @@ TEST_CASE( REQUIRE(result == matcher.matches(value)); } - SECTION("When describe() is called, argument is forwarded to the functional.") + SECTION("When describe() is called.") { - constexpr int value{42}; - REQUIRE("Hello, 42!" == matcher.describe(value)); + REQUIRE("Hello, World!" == matcher.describe()); } } } @@ -173,10 +171,7 @@ TEST_CASE( constexpr int value{42}; REQUIRE(matches::_.matches(value)); - REQUIRE_THAT( - matches::_.describe(value), - Catch::Matchers::EndsWith(" without constraints") - && Catch::Matchers::StartsWith("42")); + REQUIRE(std::optional{} == matches::_.describe()); } TEST_CASE( @@ -185,49 +180,40 @@ TEST_CASE( ) { const auto matcher = matches::eq(42); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("== 42")); SECTION("When target is equal.") { constexpr int target{42}; REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::EndsWith(" == 42") - && Catch::Matchers::StartsWith("42")); } SECTION("When target is not equal.") { constexpr int target{1337}; REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::EndsWith(" == 42") - && Catch::Matchers::StartsWith("1337")); } SECTION("Matcher can be inverted.") { const auto invertedMatcher = !matches::eq(42); + REQUIRE_THAT( + invertedMatcher.describe(), + Catch::Matchers::Equals("!= 42")); + SECTION("When target is equal.") { constexpr int target{42}; REQUIRE(!invertedMatcher.matches(target)); - REQUIRE_THAT( - invertedMatcher.describe(target), - Catch::Matchers::EndsWith(" == 42)") - && Catch::Matchers::StartsWith("not (42")); } SECTION("When target is not equal.") { constexpr int target{1337}; REQUIRE(invertedMatcher.matches(target)); - REQUIRE_THAT( - invertedMatcher.describe(target), - Catch::Matchers::EndsWith(" == 42)") - && Catch::Matchers::StartsWith("not (1337")); } } } @@ -239,48 +225,40 @@ TEST_CASE( { const auto matcher = matches::ne(42); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("!= 42")); + SECTION("When target is not equal.") { constexpr int target{1337}; REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::EndsWith(" != 42") - && Catch::Matchers::StartsWith("1337")); } SECTION("When target is equal.") { constexpr int target{42}; REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::EndsWith(" != 42") - && Catch::Matchers::StartsWith("42")); } SECTION("Matcher can be inverted.") { const auto invertedMatcher = !matches::ne(42); + REQUIRE_THAT( + invertedMatcher.describe(), + Catch::Matchers::Equals("== 42")); + SECTION("When target is not equal.") { constexpr int target{1337}; REQUIRE(!invertedMatcher.matches(target)); - REQUIRE_THAT( - invertedMatcher.describe(target), - Catch::Matchers::EndsWith(" != 42)") - && Catch::Matchers::StartsWith("not (1337")); } SECTION("When target is equal.") { constexpr int target{42}; REQUIRE(invertedMatcher.matches(target)); - REQUIRE_THAT( - invertedMatcher.describe(target), - Catch::Matchers::EndsWith(" != 42)") - && Catch::Matchers::StartsWith("not (42")); } } } @@ -292,44 +270,40 @@ TEST_CASE( { const auto matcher = matches::lt(42); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("< 42")); + SECTION("When target is less.") { const int target = GENERATE(std::numeric_limits::min(), -1, 0, 1, 41); REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals(format::format("{} < 42", target))); } SECTION("When target is not less.") { const int target = GENERATE(42, 43, std::numeric_limits::max()); REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals(format::format("{} < 42", target))); } SECTION("Matcher can be inverted.") { const auto invertedMatcher = !matches::lt(42); + REQUIRE_THAT( + invertedMatcher.describe(), + Catch::Matchers::Equals(">= 42")); + SECTION("When target is less.") { const int target = GENERATE(std::numeric_limits::min(), -1, 0, 1, 41); REQUIRE(!invertedMatcher.matches(target)); - REQUIRE_THAT( - invertedMatcher.describe(target), - Catch::Matchers::Equals(format::format("not ({} < 42)", target))); } SECTION("When target is not less.") { const int target = GENERATE(42, 43, std::numeric_limits::max()); REQUIRE(invertedMatcher.matches(target)); - REQUIRE_THAT( - invertedMatcher.describe(target), - Catch::Matchers::Equals(format::format("not ({} < 42)", target))); } } } @@ -341,44 +315,40 @@ TEST_CASE( { const auto matcher = matches::le(42); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("<= 42")); + SECTION("When target is less or equal.") { const int target = GENERATE(std::numeric_limits::min(), -1, 0, 1, 42); REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals(format::format("{} <= 42", target))); } SECTION("When target is greater.") { const int target = GENERATE(43, std::numeric_limits::max()); REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals(format::format("{} <= 42", target))); } SECTION("Matcher can be inverted.") { const auto invertedMatcher = !matches::le(42); + REQUIRE_THAT( + invertedMatcher.describe(), + Catch::Matchers::Equals("> 42")); + SECTION("When target is less or equal.") { const int target = GENERATE(std::numeric_limits::min(), -1, 0, 1, 42); REQUIRE(!invertedMatcher.matches(target)); - REQUIRE_THAT( - invertedMatcher.describe(target), - Catch::Matchers::Equals(format::format("not ({} <= 42)", target))); } SECTION("When target is greater.") { const int target = GENERATE(43, std::numeric_limits::max()); REQUIRE(invertedMatcher.matches(target)); - REQUIRE_THAT( - invertedMatcher.describe(target), - Catch::Matchers::Equals(format::format("not ({} <= 42)", target))); } } } @@ -390,44 +360,40 @@ TEST_CASE( { const auto matcher = matches::gt(42); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("> 42")); + SECTION("When target is greater.") { const int target = GENERATE(43, std::numeric_limits::max()); REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals(format::format("{} > 42", target))); } SECTION("When target is not greater.") { const int target = GENERATE(std::numeric_limits::min(), -1, 0, 1, 41, 42); REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals(format::format("{} > 42", target))); } SECTION("Matcher can be inverted.") { const auto invertedMatcher = !matches::gt(42); + REQUIRE_THAT( + invertedMatcher.describe(), + Catch::Matchers::Equals("<= 42")); + SECTION("When target is greater.") { const int target = GENERATE(43, std::numeric_limits::max()); REQUIRE(!invertedMatcher.matches(target)); - REQUIRE_THAT( - invertedMatcher.describe(target), - Catch::Matchers::Equals(format::format("not ({} > 42)", target))); } SECTION("When target is not greater.") { const int target = GENERATE(std::numeric_limits::min(), -1, 0, 1, 41, 42); REQUIRE(invertedMatcher.matches(target)); - REQUIRE_THAT( - invertedMatcher.describe(target), - Catch::Matchers::Equals(format::format("not ({} > 42)", target))); } } } @@ -439,44 +405,40 @@ TEST_CASE( { const auto matcher = matches::ge(42); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals(">= 42")); + SECTION("When target is greater or equal.") { const int target = GENERATE(42, 43, std::numeric_limits::max()); REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals(format::format("{} >= 42", target))); } SECTION("When target is less.") { const int target = GENERATE(std::numeric_limits::min(), -1, 0, 1, 41); REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals(format::format("{} >= 42", target))); } SECTION("Matcher can be inverted.") { const auto invertedMatcher = !matches::ge(42); + REQUIRE_THAT( + invertedMatcher.describe(), + Catch::Matchers::Equals("< 42")); + SECTION("When target is greater or equal.") { const int target = GENERATE(42, 43, std::numeric_limits::max()); REQUIRE(!invertedMatcher.matches(target)); - REQUIRE_THAT( - invertedMatcher.describe(target), - Catch::Matchers::Equals(format::format("not ({} >= 42)", target))); } SECTION("When target is less.") { const int target = GENERATE(std::numeric_limits::min(), -1, 0, 1, 41); REQUIRE(invertedMatcher.matches(target)); - REQUIRE_THAT( - invertedMatcher.describe(target), - Catch::Matchers::Equals(format::format("not ({} >= 42)", target))); } } } @@ -499,36 +461,42 @@ TEST_CASE( const auto matcher = matches::predicate(std::ref(predicate)); REQUIRE(expectedResult == matcher.matches(target)); REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals(format::format("{} satisfies predicate", target))); + matcher.describe(), + Catch::Matchers::Equals("passes predicate")); SECTION("When matcher is inverted.") { const auto invertedMatcher = !matches::predicate(std::ref(predicate)); + REQUIRE_THAT( + invertedMatcher.describe(), + Catch::Matchers::Equals("fails predicate")); + REQUIRE_CALL(predicate, Invoke(_)) .LR_WITH(&_1 == &target) .RETURN(expectedResult); REQUIRE(expectedResult == !invertedMatcher.matches(target)); - REQUIRE_THAT( - invertedMatcher.describe(target), - Catch::Matchers::Equals(format::format("not ({} satisfies predicate)", target))); } SECTION("Custom descriptions are supported.") { const auto customMatcher = matches::predicate( std::ref(predicate), - "custom predicate is satisfied"); + "custom predicate is passed", + "custom predicate is failed"); REQUIRE_CALL(predicate, Invoke(_)) .LR_WITH(&_1 == &target) .RETURN(expectedResult); REQUIRE(expectedResult == customMatcher.matches(target)); REQUIRE_THAT( - customMatcher.describe(target), - Catch::Matchers::Equals("custom predicate is satisfied")); + customMatcher.describe(), + Catch::Matchers::Equals("custom predicate is passed")); + + REQUIRE_THAT( + (!customMatcher).describe(), + Catch::Matchers::Equals("custom predicate is failed")); } } @@ -540,15 +508,15 @@ TEST_CASE( using trompeloeil::_; const auto matcher = matches::str::eq("Hello, World!"); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("is equal to \"Hello, World!\"")); SECTION("When target is equal, they match.") { const std::string target{"Hello, World!"}; REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("string \"Hello, World!\" is equal to \"Hello, World!\"")); } SECTION("When target is not equal, they do not match.") @@ -556,23 +524,21 @@ TEST_CASE( const std::string target{"Hello, WOrld!"}; REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("string \"Hello, WOrld!\" is equal to \"Hello, World!\"")); } SECTION("Matcher can be inverted.") { const auto invertedMatcher = !matches::str::eq("Hello, World!"); + REQUIRE_THAT( + invertedMatcher.describe(), + Catch::Matchers::Equals("is not equal to \"Hello, World!\"")); + SECTION("When target is equal, they do not match.") { const std::string target{"Hello, World!"}; REQUIRE(!invertedMatcher.matches(target)); - REQUIRE_THAT( - invertedMatcher.describe(target), - Catch::Matchers::Equals("not (string \"Hello, World!\" is equal to \"Hello, World!\")")); } SECTION("When target is not equal, they do match.") @@ -580,9 +546,6 @@ TEST_CASE( const std::string target{"Hello, WOrld!"}; REQUIRE(invertedMatcher.matches(target)); - REQUIRE_THAT( - invertedMatcher.describe(target), - Catch::Matchers::Equals("not (string \"Hello, WOrld!\" is equal to \"Hello, World!\")")); } } } @@ -598,14 +561,15 @@ TEST_CASE( { const auto matcher = matches::range::eq(std::vector{}); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("elements are { }")); + SECTION("When target is also empty, they match.") { const std::vector target{}; REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { } is equal to { }")); } SECTION("When target is not empty, they do not match.") @@ -613,9 +577,6 @@ TEST_CASE( const std::vector target{42}; REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { 42 } is equal to { }")); } } @@ -623,14 +584,15 @@ TEST_CASE( { const auto matcher = matches::range::eq(std::vector{1337, 42}); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("elements are { 1337, 42 }")); + SECTION("When target is equal, they match.") { const std::vector target{1337, 42}; REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { 1337, 42 } is equal to { 1337, 42 }")); } SECTION("When target has same elements, but in different order, they do not match.") @@ -638,9 +600,6 @@ TEST_CASE( const std::vector target{42, 1337}; REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { 42, 1337 } is equal to { 1337, 42 }")); } SECTION("When target is not equal, they do not match.") @@ -648,9 +607,6 @@ TEST_CASE( const std::vector target{42}; REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { 42 } is equal to { 1337, 42 }")); } } @@ -658,14 +614,15 @@ TEST_CASE( { const auto matcher = !matches::range::eq(std::vector{1337, 42}); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("elements are not { 1337, 42 }")); + SECTION("When target is equal, they do not match.") { const std::vector target{1337, 42}; REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("not (range { 1337, 42 } is equal to { 1337, 42 })")); } SECTION("When target has same elements, but in different order, they do match.") @@ -673,9 +630,6 @@ TEST_CASE( const std::vector target{42, 1337}; REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("not (range { 42, 1337 } is equal to { 1337, 42 })")); } SECTION("When target is not equal, they do match.") @@ -683,9 +637,6 @@ TEST_CASE( const std::vector target{42}; REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("not (range { 42 } is equal to { 1337, 42 })")); } } @@ -706,8 +657,8 @@ TEST_CASE( REQUIRE(matcher.matches(target)); REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { 1337, 42 } is equal to { 1337, 42 }")); + matcher.describe(), + Catch::Matchers::Equals("elements are { 1337, 42 }")); } } @@ -722,14 +673,15 @@ TEST_CASE( { const auto matcher = matches::range::unordered_eq(std::vector{}); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("is a permutation of { }")); + SECTION("When target is also empty, they match.") { const std::vector target{}; REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { } is permutation of { }")); } SECTION("When target is not empty, they do not match.") @@ -737,9 +689,6 @@ TEST_CASE( const std::vector target{42}; REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { 42 } is permutation of { }")); } } @@ -747,14 +696,15 @@ TEST_CASE( { const auto matcher = matches::range::unordered_eq(std::vector{1337, 42}); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("is a permutation of { 1337, 42 }")); + SECTION("When target is equal, they match.") { const std::vector target{1337, 42}; REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { 1337, 42 } is permutation of { 1337, 42 }")); } SECTION("When target has same elements, but in different order, they do match.") @@ -762,9 +712,6 @@ TEST_CASE( const std::vector target{42, 1337}; REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { 42, 1337 } is permutation of { 1337, 42 }")); } SECTION("When target is not equal, they do not match.") @@ -772,9 +719,6 @@ TEST_CASE( const std::vector target{42}; REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { 42 } is permutation of { 1337, 42 }")); } } @@ -782,14 +726,15 @@ TEST_CASE( { const auto matcher = !matches::range::unordered_eq(std::vector{1337, 42}); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("is not a permutation of { 1337, 42 }")); + SECTION("When target is equal, they do not match.") { const std::vector target{1337, 42}; REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("not (range { 1337, 42 } is permutation of { 1337, 42 })")); } SECTION("When target has same elements, but in different order, they do not match.") @@ -797,9 +742,6 @@ TEST_CASE( const std::vector target{42, 1337}; REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("not (range { 42, 1337 } is permutation of { 1337, 42 })")); } SECTION("When target is not equal, they do match.") @@ -807,9 +749,6 @@ TEST_CASE( const std::vector target{42}; REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("not (range { 42 } is permutation of { 1337, 42 })")); } } @@ -821,6 +760,10 @@ TEST_CASE( std::vector{1337, 42}, std::ref(comparator)); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("is a permutation of { 1337, 42 }")); + const std::vector target{1337, 42}; REQUIRE_CALL(comparator, Invoke(1337, 1337)) @@ -829,9 +772,6 @@ TEST_CASE( .RETURN(true); REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { 1337, 42 } is permutation of { 1337, 42 }")); } } @@ -850,22 +790,23 @@ TEST_CASE( REQUIRE(matcher.matches(target)); REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { } is sorted")); + matcher.describe(), + Catch::Matchers::Equals("is a sorted range")); } SECTION("When a non-empty range is stored.") { const auto matcher = matches::range::is_sorted(); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("is a sorted range")); + SECTION("When target is sorted, it's a match.") { const std::vector target{42, 1337}; REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { 42, 1337 } is sorted")); } SECTION("When target is not sorted, it's no match.") @@ -873,9 +814,6 @@ TEST_CASE( const std::vector target{1337, 42}; REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { 1337, 42 } is sorted")); } } @@ -883,14 +821,15 @@ TEST_CASE( { const auto matcher = !matches::range::is_sorted(); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("is an unsorted range")); + SECTION("When target is sorted, it's no match.") { const std::vector target{42, 1337}; REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("not (range { 42, 1337 } is sorted)")); } SECTION("When target is not sorted, it's a match.") @@ -898,9 +837,6 @@ TEST_CASE( const std::vector target{1337, 42}; REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("not (range { 1337, 42 } is sorted)")); } } @@ -918,8 +854,8 @@ TEST_CASE( REQUIRE(matcher.matches(target)); REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { 1337, 42 } is sorted")); + matcher.describe(), + Catch::Matchers::Equals("is a sorted range")); } } @@ -938,8 +874,8 @@ TEST_CASE( REQUIRE(matcher.matches(target)); REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { } is empty")); + matcher.describe(), + Catch::Matchers::Equals("is an empty range")); } SECTION("When a non-empty range is stored, it's no match.") @@ -950,22 +886,23 @@ TEST_CASE( REQUIRE(!matcher.matches(target)); REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { 42 } is empty")); + matcher.describe(), + Catch::Matchers::Equals("is an empty range")); } SECTION("Matcher can be inverted.") { const auto matcher = !matches::range::is_empty(); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("is not an empty range")); + SECTION("When target is empty, it's no match.") { const std::vector target{}; REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("not (range { } is empty)")); } SECTION("When a non-empty range is stored, it's a match.") @@ -973,9 +910,6 @@ TEST_CASE( const std::vector target{42}; REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("not (range { 42 } is empty)")); } } } @@ -994,8 +928,8 @@ TEST_CASE( REQUIRE(matcher.matches(target)); REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("range { 42, 1337 } has size 2")); + matcher.describe(), + Catch::Matchers::Equals("has size of 2")); } SECTION("When target has different size, it's no match.") @@ -1007,25 +941,23 @@ TEST_CASE( REQUIRE(!matcher.matches(target)); REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals( - format::format( - "range {} has size 1", - mimicpp::print(target)))); + matcher.describe(), + Catch::Matchers::Equals("has size of 1")); } SECTION("Matcher can be inverted.") { const auto matcher = !matches::range::has_size(2); + REQUIRE_THAT( + matcher.describe(), + Catch::Matchers::Equals("has different size than 2")); + SECTION("When target has the expected size, it's no match.") { const std::vector target{42, 1337}; REQUIRE(!matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("not (range { 42, 1337 } has size 2)")); } SECTION("When target has different size, it's a match.") @@ -1033,9 +965,6 @@ TEST_CASE( const std::vector target{42}; REQUIRE(matcher.matches(target)); - REQUIRE_THAT( - matcher.describe(target), - Catch::Matchers::Equals("not (range { 42 } has size 2)")); } } } diff --git a/test/Mock.cpp b/test/unit-tests/Mock.cpp similarity index 99% rename from test/Mock.cpp rename to test/unit-tests/Mock.cpp index 543db71a3..0b0891a42 100644 --- a/test/Mock.cpp +++ b/test/unit-tests/Mock.cpp @@ -3,8 +3,10 @@ // // (See accompanying file LICENSE_1_0.txt or copy at // // https://www.boost.org/LICENSE_1_0.txt) -#include "mimic++/Mock.hpp" #include "TestReporter.hpp" + +#include "mimic++/Mock.hpp" + #include "TestTypes.hpp" #include diff --git a/test/Printer.cpp b/test/unit-tests/Printer.cpp similarity index 96% rename from test/Printer.cpp rename to test/unit-tests/Printer.cpp index 0d3a39ece..167fc7deb 100644 --- a/test/Printer.cpp +++ b/test/unit-tests/Printer.cpp @@ -242,6 +242,15 @@ TEST_CASE( } } + SECTION("std::source_location has specialized printer.") + { + const std::source_location loc = std::source_location::current(); + + REQUIRE_THAT( + mimicpp::print(loc), + Catch::Matchers::Matches(".+\\[\\d+:\\d+\\], .+")); + } + 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 new file mode 100644 index 000000000..9205636ae --- /dev/null +++ b/test/unit-tests/Reporter.cpp @@ -0,0 +1,1064 @@ +// // 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++/Reporter.hpp" + +#include +#include +#include +#include + +using namespace mimicpp; + +TEST_CASE( + "CallReport::Arg is equality comparable.", + "[reporting]" +) +{ + using ArgT = CallReport::Arg; + + const ArgT first{ + .typeIndex = typeid(int), + .stateString = "42" + }; + + const auto [expectedEquality, second] = GENERATE( + (table({ + {false, {typeid(int), "1337"}}, + {false, {typeid(short), "42"}}, + {true, {typeid(int), "42"}} + }))); + + REQUIRE(expectedEquality == (first == second)); + REQUIRE(expectedEquality == (second == first)); + REQUIRE(expectedEquality == !(first != second)); + REQUIRE(expectedEquality == !(second!= first)); +} + +TEST_CASE( + "CallReport is equality comparable.", + "[reporting]" +) +{ + const CallReport first{ + .returnTypeIndex = typeid(std::string), + .argDetails = { + { + .typeIndex = typeid(int), + .stateString = "42" + } + }, + .fromLoc = std::source_location::current(), + .fromCategory = ValueCategory::any, + .fromConstness = Constness::any + }; + + SECTION("When both sides are equal, they compare equal.") + { + const CallReport second{first}; + + REQUIRE(first == second); + REQUIRE(second == first); + REQUIRE(!(first != second)); + REQUIRE(!(second != first)); + } + + SECTION("When return type differs, they compare not equal.") + { + CallReport second{first}; + + second.returnTypeIndex = GENERATE(as{}, typeid(void), typeid(std::string_view)); + + REQUIRE(first != second); + REQUIRE(second != first); + REQUIRE(!(first == second)); + REQUIRE(!(second == first)); + } + + SECTION("When category differs, they compare not equal.") + { + CallReport second{first}; + + second.fromCategory = GENERATE(ValueCategory::lvalue, ValueCategory::rvalue); + + REQUIRE(first != second); + REQUIRE(second != first); + REQUIRE(!(first == second)); + REQUIRE(!(second == first)); + } + + SECTION("When constness differs, they compare not equal.") + { + CallReport second{first}; + + second.fromConstness = GENERATE(Constness::as_const, Constness::non_const); + + REQUIRE(first != second); + REQUIRE(second != first); + REQUIRE(!(first == second)); + REQUIRE(!(second == first)); + } + + SECTION("When source location differs, they compare not equal.") + { + CallReport second{first}; + + second.fromLoc = std::source_location::current(); + + REQUIRE(first != second); + REQUIRE(second != first); + REQUIRE(!(first == second)); + REQUIRE(!(second == first)); + } + + SECTION("When source location differs, they compare not equal.") + { + CallReport second{first}; + + using ArgT = CallReport::Arg; + second.argDetails = GENERATE( + std::vector{}, + std::vector{ + (ArgT{.typeIndex = typeid(int), .stateString = "1337"}) + }, + std::vector{ + (ArgT{.typeIndex = typeid(int), .stateString = "42"}), + (ArgT{.typeIndex = typeid(int), .stateString = "1337"}) + }); + + REQUIRE(first != second); + REQUIRE(second != first); + REQUIRE(!(first == second)); + REQUIRE(!(second == first)); + } +} + +TEST_CASE( + "make_call_report generates report from call info." + "[reporting]" +) +{ + SECTION("When call info has void return type.") + { + const call::Info info{ + .args = {}, + .fromCategory = GENERATE(ValueCategory::any, ValueCategory::lvalue, ValueCategory::rvalue), + .fromConstness = GENERATE(Constness::any, Constness::as_const, Constness::non_const), + .fromSourceLocation = std::source_location::current() + }; + + const CallReport report = make_call_report(info); + + REQUIRE( + report == + CallReport{ + .returnTypeIndex = typeid(void), + .argDetails = {}, + .fromLoc = info.fromSourceLocation, + .fromCategory = info.fromCategory, + .fromConstness = info.fromConstness + }); + } + + SECTION("When call info has non-void return type.") + { + const call::Info info{ + .args = {}, + .fromCategory = GENERATE(ValueCategory::any, ValueCategory::lvalue, ValueCategory::rvalue), + .fromConstness = GENERATE(Constness::any, Constness::as_const, Constness::non_const), + .fromSourceLocation = std::source_location::current() + }; + + const CallReport report = make_call_report(info); + + REQUIRE( + report == + CallReport{ + .returnTypeIndex = typeid(int), + .argDetails = {}, + .fromLoc = info.fromSourceLocation, + .fromCategory = info.fromCategory, + .fromConstness = info.fromConstness + }); + } + + SECTION("When call info has arbitrary args.") + { + const int arg0{1337}; + double arg1{4.2}; + std::string arg2{"Hello, World!"}; + const call::Info info{ + .args = {std::ref(arg0), std::ref(arg1), std::ref(arg2)}, + .fromCategory = GENERATE(ValueCategory::any, ValueCategory::lvalue, ValueCategory::rvalue), + .fromConstness = GENERATE(Constness::any, Constness::as_const, Constness::non_const), + .fromSourceLocation = std::source_location::current() + }; + + const CallReport report = make_call_report(info); + + using ArgT = CallReport::Arg; + REQUIRE( + report == + CallReport{ + .returnTypeIndex = typeid(void), + .argDetails = { + (ArgT{typeid(const int&), "1337"}), + (ArgT{typeid(double), "4.2"}), + (ArgT{typeid(std::string), "\"Hello, World!\""}) + }, + .fromLoc = info.fromSourceLocation, + .fromCategory = info.fromCategory, + .fromConstness = info.fromConstness + }); + } +} + +TEST_CASE( + "ExpectationReport is equality comparable.", + "[reporting]" +) +{ + const ExpectationReport first{ + .sourceLocation = std::source_location::current(), + .finalizerDescription = "finalizer description", + .timesDescription = "times description", + .expectationDescriptions = { + "first expectation description" + } + }; + + SECTION("When all members are equal, reports compare equal.") + { + const ExpectationReport second{first}; + + REQUIRE(first == second); + REQUIRE(second == first); + REQUIRE(!(first != second)); + REQUIRE(!(second!= first)); + } + + SECTION("When source-location differs, reports do not compare equal.") + { + ExpectationReport second{first}; + second.sourceLocation = GENERATE( + as>{}, + std::nullopt, + std::source_location::current()); + + REQUIRE(!(first == second)); + REQUIRE(!(second == first)); + REQUIRE(first != second); + REQUIRE(second!= first); + } + + SECTION("When finalizer description differs, reports do not compare equal.") + { + ExpectationReport second{first}; + second.finalizerDescription = GENERATE( + as>{}, + std::nullopt, + "other finalizer description"); + + REQUIRE(!(first == second)); + REQUIRE(!(second == first)); + REQUIRE(first != second); + REQUIRE(second!= first); + } + + SECTION("When times description differs, reports do not compare equal.") + { + ExpectationReport second{first}; + second.timesDescription = GENERATE( + as>{}, + std::nullopt, + "other times description"); + + REQUIRE(!(first == second)); + REQUIRE(!(second == first)); + REQUIRE(first != second); + REQUIRE(second!= first); + } + + SECTION("When expectation descriptions differ, reports do not compare equal.") + { + ExpectationReport second{first}; + second.expectationDescriptions = GENERATE( + (std::vector>{}), + (std::vector>{"other expectation description"}), + (std::vector>{"expectation description", "other expectation description"})); + + REQUIRE(!(first == second)); + REQUIRE(!(second == first)); + REQUIRE(first != second); + REQUIRE(second!= first); + } +} + +TEST_CASE( + "MatchReport::Finalize is equality comparable." + "[reporting]" +) +{ + using ReportT = MatchReport::Finalize; + + const ReportT first{ + .description = "Hello, World!" + }; + + const auto [expectedEquality, second] = GENERATE( + (table({ + {false, {"not equal"}}, + {false, {std::nullopt}}, + {true, {"Hello, World!"}} + }))); + + REQUIRE(expectedEquality == (first == second)); + REQUIRE(expectedEquality == (second == first)); + REQUIRE(expectedEquality == !(first != second)); + REQUIRE(expectedEquality == !(second!= first)); +} + +TEST_CASE( + "MatchReport::Times is equality comparable." + "[reporting]" +) +{ + using ReportT = MatchReport::Times; + + const ReportT first{ + .isApplicable = true, + .description = "Hello, World!" + }; + + const auto [expectedEquality, second] = GENERATE( + (table({ + {false, {true, "not equal"}}, + {false, {true, std::nullopt}}, + {false, {false, "Hello, World!"}}, + {true, {true, "Hello, World!"}} + }))); + + REQUIRE(expectedEquality == (first == second)); + REQUIRE(expectedEquality == (second == first)); + REQUIRE(expectedEquality == !(first != second)); + REQUIRE(expectedEquality == !(second!= first)); +} + +TEST_CASE( + "MatchReport::Expectation is equality comparable." + "[reporting]" +) +{ + using ReportT = MatchReport::Expectation; + + const ReportT first{ + .isMatching = true, + .description = "Hello, World!" + }; + + const auto [expectedEquality, second] = GENERATE( + (table({ + {false, {true, "not equal"}}, + {false, {true, std::nullopt}}, + {false, {false, "Hello, World!"}}, + {true, {true, "Hello, World!"}} + }))); + + REQUIRE(expectedEquality == (first == second)); + REQUIRE(expectedEquality == (second == first)); + REQUIRE(expectedEquality == !(first != second)); + REQUIRE(expectedEquality == !(second!= first)); +} + +TEST_CASE( + "MatchReport is equality comparable.", + "[reporting]" +) +{ + const MatchReport first{ + .sourceLocation = std::source_location::current(), + .finalizeReport = {"finalize description"}, + .timesReport = {true, "times description"}, + .expectationReports = { + {true, "expectation description"} + } + }; + + SECTION("When both sides are equal, they compare equal.") + { + const MatchReport second{first}; + + REQUIRE(first == second); + REQUIRE(second == first); + REQUIRE(!(first != second)); + REQUIRE(!(second != first)); + } + + SECTION("When source-location differs, reports do not compare equal.") + { + MatchReport second{first}; + second.sourceLocation = GENERATE( + as>{}, + std::nullopt, + std::source_location::current()); + + REQUIRE(!(first == second)); + REQUIRE(!(second == first)); + REQUIRE(first != second); + REQUIRE(second!= first); + } + + SECTION("When finalize report differs, they do not compare equal.") + { + MatchReport second{first}; + + second.finalizeReport = {"other finalize description"}; + + REQUIRE(first != second); + REQUIRE(second != first); + REQUIRE(!(first == second)); + REQUIRE(!(second == first)); + } + + SECTION("When times report differs, they do not compare equal.") + { + MatchReport second{first}; + + second.timesReport = {true, "other times description"}; + + REQUIRE(first != second); + REQUIRE(second != first); + REQUIRE(!(first == second)); + REQUIRE(!(second == first)); + } + + SECTION("When expectation reports differ, they do not compare equal.") + { + MatchReport second{first}; + + using ExpectationT = MatchReport::Expectation; + second.expectationReports = GENERATE( + std::vector{}, + std::vector{ + (ExpectationT{true, "other expectation description"}) + }, + std::vector{ + (ExpectationT{true, "expectation description"}), + (ExpectationT{false, "other expectation description"}) + }); + + REQUIRE(first != second); + REQUIRE(second != first); + REQUIRE(!(first == second)); + REQUIRE(!(second == first)); + } +} + +namespace +{ + class ReporterMock + : public IReporter + { + public: + MAKE_MOCK2(report_no_matches, void(CallReport, std::vector), override); + MAKE_MOCK2(report_inapplicable_matches, void(CallReport, std::vector), override); + MAKE_MOCK2(report_full_match, void(CallReport, MatchReport), noexcept override); + MAKE_MOCK1(report_unfulfilled_expectation, void(ExpectationReport), override); + MAKE_MOCK1(report_error, void(StringT), override); + MAKE_MOCK3(report_unhandled_exception, void(CallReport, ExpectationReport, std::exception_ptr), override); + }; +} + +TEST_CASE( + "install_reporter removes the previous reporter and installs a new one.", + "[reporting]" +) +{ + install_reporter>(); + + { + auto& prevReporter = dynamic_cast&>(*detail::get_reporter()); + REQUIRE_DESTRUCTION(prevReporter); + install_reporter(); + } +} + +namespace +{ + class TestException + { + }; +} + +TEST_CASE( + "free report functions forward to the currently installed reporter.", + "[reporting]" +) +{ + install_reporter(); + auto& reporter = dynamic_cast(*detail::get_reporter()); + + const CallReport callReport{ + .returnTypeIndex = typeid(void), + .fromLoc = std::source_location::current() + }; + + const std::vector matchReports{ + {.finalizeReport = {"match1"}}, + {.finalizeReport = {"match2"}} + }; + + const ExpectationReport expectationReport{}; + + SECTION("When report_no_matches() is called.") + { + REQUIRE_CALL(reporter, report_no_matches(callReport, matchReports)) + .THROW(TestException{}); + + REQUIRE_THROWS_AS( + detail::report_no_matches( + callReport, + matchReports), + TestException); + } + + SECTION("When report_inapplicable_matches() is called.") + { + REQUIRE_CALL(reporter, report_inapplicable_matches(callReport, matchReports)) + .THROW(TestException{}); + + REQUIRE_THROWS_AS( + detail::report_inapplicable_matches( + callReport, + matchReports), + TestException); + } + + SECTION("When report_full_match() is called.") + { + REQUIRE_CALL(reporter, report_full_match(callReport, matchReports.front())); + + detail::report_full_match( + callReport, + matchReports.front()); + } + + SECTION("When report_unfulfilled_expectation() is called.") + { + REQUIRE_CALL(reporter, report_unfulfilled_expectation(expectationReport)); + + detail::report_unfulfilled_expectation( + expectationReport); + } + + SECTION("When report_error() is called.") + { + const StringT error{"Error!"}; + REQUIRE_CALL(reporter, report_error(error)); + + detail::report_error(error); + } + + SECTION("When report_unhandled_exception() is called.") + { + const std::exception_ptr exception = std::make_exception_ptr(TestException{}); + REQUIRE_CALL(reporter, report_unhandled_exception(callReport, expectationReport, exception)); + + detail::report_unhandled_exception( + callReport, + expectationReport, + exception); + } +} + +TEST_CASE( + "evaluate_match_report determines the outcome of a match report.", + "[reporting]" +) +{ + using ExpectationReportT = MatchReport::Expectation; + + SECTION("When any policy doesn't match => MatchResult::none is returned.") + { + const MatchReport report{ + .timesReport = {GENERATE(true, false)}, + .expectationReports = GENERATE( + (std::vector{{false}}), + (std::vector{{true}, {false}}), + (std::vector{{false}, {true}})) + }; + + REQUIRE(MatchResult::none == evaluate_match_report(report)); + } + + SECTION("When all policy match but times is inapplicable => MatchResult::inapplicable is returned.") + { + const MatchReport report{ + .timesReport = {false}, + .expectationReports = GENERATE( + (std::vector{}), + (std::vector{{true}}), + (std::vector{{true}, {true}})) + }; + + REQUIRE(MatchResult::inapplicable == evaluate_match_report(report)); + } + + SECTION("When all policy match and times is applicable => MatchResult::full is returned.") + { + const MatchReport report{ + .timesReport = {true}, + .expectationReports = GENERATE( + (std::vector{}), + (std::vector{{true}}), + (std::vector{{true}, {true}})) + }; + + REQUIRE(MatchResult::full == evaluate_match_report(report)); + } +} + +TEST_CASE( + "DefaultReporter throws exceptions on expectation violations.", + "[reporting]" +) +{ + DefaultReporter reporter{}; + + const CallReport callReport{ + .returnTypeIndex = typeid(void), + .fromLoc = std::source_location::current() + }; + + SECTION("When none matches are reported, UnmatchedCallT is thrown.") + { + REQUIRE_THROWS_AS( + reporter.report_no_matches( + callReport, + { + MatchReport{.timesReport = {true}, .expectationReports = {{false}}} + }), + UnmatchedCallT); + } + + SECTION("When inapplicable matches are reported, UnmatchedCallT is thrown.") + { + REQUIRE_THROWS_AS( + reporter.report_inapplicable_matches( + callReport, + {MatchReport{.timesReport = {false}}}), + UnmatchedCallT); + } + + SECTION("When match is reported, nothing is done.") + { + REQUIRE_NOTHROW( + reporter.report_full_match( + callReport, + MatchReport{.timesReport = {true}})); + } + + SECTION("When unfulfilled expectation is reported.") + { + SECTION("And when there exists no uncaught exception, UnfulfilledExpectationT is thrown.") + { + REQUIRE_THROWS_AS( + reporter.report_unfulfilled_expectation({}), + UnfulfilledExpectationT); + } + + SECTION("And when there exists an uncaught exception, nothing is done.") + { + struct helper + { + ~helper() + { + rep.report_unfulfilled_expectation({}); + } + + DefaultReporter& rep; + }; + + const auto runTest = [&] + { + helper h{reporter}; + throw 42; + }; + + REQUIRE_THROWS_AS( + runTest(), + int); + } + } + + SECTION("When error is reported") + { + SECTION("And when there exists no uncaught exception, Error is thrown.") + { + REQUIRE_THROWS_AS( + reporter.report_error({"Test"}), + Error<>); + } + + SECTION("And when there exists an uncaught exception, nothing is done.") + { + struct helper + { + ~helper() + { + rep.report_error({"Test"}); + } + + DefaultReporter& rep; + }; + + const auto runTest = [&] + { + helper h{reporter}; + throw 42; + }; + + REQUIRE_THROWS_AS( + runTest(), + int); + } + } + + SECTION("When unhandled exception is reported, nothing is done.") + { + REQUIRE_NOTHROW( + reporter.report_unhandled_exception( + callReport, + {}, + std::make_exception_ptr(std::runtime_error{"Test"}))); + } +} + +TEST_CASE( + "stringify_match_report converts the match report to text representation.", + "[report]" +) +{ + namespace Matches = Catch::Matchers; + + SECTION("When report denotes a full match.") + { + SECTION("Without any requirements.") + { + const MatchReport report{ + .sourceLocation = std::source_location::current(), + .finalizeReport = {}, + .timesReport = {true, "finalize description"}, + .expectationReports = {} + }; + + REQUIRE_THAT( + stringify_match_report(report), + Matches::Matches( + "Matched expectation: \\{\n" + "from: .+\\[\\d+:\\d+\\], .+\n" + "\\}\n")); + } + + SECTION("When contains requirements.") + { + const MatchReport report{ + .sourceLocation = std::source_location::current(), + .finalizeReport = {}, + .timesReport = {true, "finalize description"}, + .expectationReports = { + {true, "Requirement1 description"}, + {true, "Requirement2 description"} + } + }; + + REQUIRE_THAT( + stringify_match_report(report), + Matches::Matches( + "Matched expectation: \\{\n" + "from: .+\\[\\d+:\\d+\\], .+\n" + "passed:\n" + "\tRequirement1 description,\n" + "\tRequirement2 description,\n" + "\\}\n")); + } + } + + SECTION("When report denotes an inapplicable match.") + { + SECTION("Without any requirements.") + { + const MatchReport report{ + .sourceLocation = std::source_location::current(), + .finalizeReport = {}, + .timesReport = {false, "finalize description"}, + .expectationReports = {} + }; + + REQUIRE_THAT( + stringify_match_report(report), + Matches::Matches( + "Inapplicable, but otherwise matched expectation: \\{\n" + "reason: finalize description\n" + "from: .+\\[\\d+:\\d+\\], .+\n" + "\\}\n")); + } + + SECTION("When contains requirements.") + { + const MatchReport report{ + .sourceLocation = std::source_location::current(), + .finalizeReport = {}, + .timesReport = {false, "finalize description"}, + .expectationReports = { + {true, "Requirement1 description"}, + {true, "Requirement2 description"} + } + }; + + REQUIRE_THAT( + stringify_match_report(report), + Matches::Matches( + "Inapplicable, but otherwise matched expectation: \\{\n" + "reason: finalize description\n" + "from: .+\\[\\d+:\\d+\\], .+\n" + "passed:\n" + "\tRequirement1 description,\n" + "\tRequirement2 description,\n" + "\\}\n")); + } + } + + SECTION("When report denotes an unmatched report.") + { + SECTION("When contains only failed requirements.") + { + const MatchReport report{ + .sourceLocation = std::source_location::current(), + .finalizeReport = {}, + .timesReport = {true, "finalize description"}, + .expectationReports = { + {false, "Requirement1 description"}, + {false, "Requirement2 description"} + } + }; + + REQUIRE_THAT( + stringify_match_report(report), + Matches::Matches( + "Unmatched expectation: \\{\n" + "from: .+\\[\\d+:\\d+\\], .+\n" + "failed:\n" + "\tRequirement1 description,\n" + "\tRequirement2 description,\n" + "\\}\n")); + } + + SECTION("When contains only mixed requirements.") + { + const MatchReport report{ + .sourceLocation = std::source_location::current(), + .finalizeReport = {}, + .timesReport = {true, "finalize description"}, + .expectationReports = { + {true, "Requirement1 description"}, + {false, "Requirement2 description"} + } + }; + + REQUIRE_THAT( + stringify_match_report(report), + Matches::Matches( + "Unmatched expectation: \\{\n" + "from: .+\\[\\d+:\\d+\\], .+\n" + "failed:\n" + "\tRequirement2 description,\n" + "passed:\n" + "\tRequirement1 description,\n" + "\\}\n")); + } + } + + SECTION("When source location is empty, that information is omitted.") + { + const MatchReport report{ + .sourceLocation = std::nullopt, + .finalizeReport = {}, + .timesReport = {true, "finalize description"}, + .expectationReports = {} + }; + + REQUIRE_THAT( + stringify_match_report(report), + Matches::Matches( + "Matched expectation: \\{\n" + "\\}\n")); + } +} + +TEST_CASE( + "stringify_call_report converts the call report to text representation.", + "[report]" +) +{ + namespace Matches = Catch::Matchers; + + SECTION("When report without arguments is given.") + { + const CallReport report{ + .returnTypeIndex = typeid(void), + .argDetails = {}, + .fromLoc = std::source_location::current(), + .fromCategory = ValueCategory::any, + .fromConstness = Constness::any + }; + + REQUIRE_THAT( + stringify_call_report(report), + Matches::Matches( + "call from .+\\[\\d+:\\d+\\], .+\n" + "constness: any\n" + "value category: any\n" + "return type: (v|void)\n")); + } + + SECTION("When report with arguments is given.") + { + const CallReport report{ + .returnTypeIndex = typeid(int), + .argDetails = {{.typeIndex = typeid(double), .stateString = "4.2"}}, + .fromLoc = std::source_location::current(), + .fromCategory = ValueCategory::lvalue, + .fromConstness = Constness::as_const + }; + + REQUIRE_THAT( + stringify_call_report(report), + Matches::Matches( + "call from .+\\[\\d+:\\d+\\], .+\n" + "constness: const\n" + "value category: lvalue\n" + "return type: (i|int)\n" + "args:\n" + "\targ\\[0\\]: \\{\n" + "\t\ttype: (d|double),\n" + "\t\tvalue: 4.2\n" + "\t\\},\n")); + } +} + +TEST_CASE( + "stringify_expectation_report converts the match report to text representation.", + "[report]" +) +{ + namespace Matches = Catch::Matchers; + + ExpectationReport report{ + .sourceLocation = std::source_location::current(), + .finalizerDescription = "finalizer description", + .timesDescription = "times description", + .expectationDescriptions = { + "expectation1 description" + } + }; + + SECTION("When full report is given.") + { + REQUIRE_THAT( + stringify_expectation_report(std::as_const(report)), + Matches::Matches( + "Expectation report:\n" + "from: .+\\[\\d+:\\d+\\], .+\n" + "times: times description\n" + "expects:\n" + "\texpectation1 description,\n" + "finally: finalizer description\n")); + } + + SECTION("When times description is missing.") + { + report.timesDescription.reset(); + + REQUIRE_THAT( + stringify_expectation_report(std::as_const(report)), + Matches::Matches( + "Expectation report:\n" + "from: .+\\[\\d+:\\d+\\], .+\n" + "expects:\n" + "\texpectation1 description,\n" + "finally: finalizer description\n")); + } + + SECTION("When finalizer description is missing.") + { + report.finalizerDescription.reset(); + + REQUIRE_THAT( + stringify_expectation_report(std::as_const(report)), + Matches::Matches( + "Expectation report:\n" + "from: .+\\[\\d+:\\d+\\], .+\n" + "times: times description\n" + "expects:\n" + + "\texpectation1 description,\n")); + } + + SECTION("When expectation contains only empty descriptions.") + { + report.expectationDescriptions[0].reset(); + + REQUIRE_THAT( + stringify_expectation_report(std::as_const(report)), + Matches::Matches( + "Expectation report:\n" + "from: .+\\[\\d+:\\d+\\], .+\n" + "times: times description\n" + "finally: finalizer description\n")); + } + + SECTION("When expectation contains no descriptions.") + { + report.expectationDescriptions.clear(); + + REQUIRE_THAT( + stringify_expectation_report(std::as_const(report)), + Matches::Matches( + "Expectation report:\n" + "from: .+\\[\\d+:\\d+\\], .+\n" + "times: times description\n" + "finally: finalizer description\n")); + } + + SECTION("When expectation contains mixed descriptions.") + { + report.expectationDescriptions.emplace_back(std::nullopt); + + REQUIRE_THAT( + stringify_expectation_report(std::as_const(report)), + Matches::Matches( + "Expectation report:\n" + "from: .+\\[\\d+:\\d+\\], .+\n" + "times: times description\n" + "expects:\n" + "\texpectation1 description,\n" + "finally: finalizer description\n")); + } + + SECTION("When expectatin contains no source-location.") + { + report.sourceLocation.reset(); + + REQUIRE_THAT( + stringify_expectation_report(std::as_const(report)), + Matches::Equals( + "Expectation report:\n" + "times: times description\n" + "expects:\n" + "\texpectation1 description,\n" + "finally: finalizer description\n")); + } +} diff --git a/test/Sequence.cpp b/test/unit-tests/Sequence.cpp similarity index 91% rename from test/Sequence.cpp rename to test/unit-tests/Sequence.cpp index 6ebbe5a9c..8058b6938 100644 --- a/test/Sequence.cpp +++ b/test/unit-tests/Sequence.cpp @@ -3,6 +3,8 @@ // // (See accompanying file LICENSE_1_0.txt or copy at // // https://www.boost.org/LICENSE_1_0.txt) +#include "TestReporter.hpp" + #include "mimic++/Sequence.hpp" #include "mimic++/Expectation.hpp" @@ -13,8 +15,6 @@ #include #include -#include "TestReporter.hpp" - using namespace mimicpp; TEST_CASE( @@ -225,6 +225,10 @@ TEST_CASE( Sequence sequence{}; + const StringT applicableText = "applicable: Sequence element expects further matches."; + const StringT saturatedText = "inapplicable: Sequence element is already saturated."; + const StringT inapplicableText = "inapplicable: Sequence element is not the current element."; + SECTION("When sequence contains just a single expectation.") { const auto count = GENERATE(range(1, 5)); @@ -234,11 +238,17 @@ TEST_CASE( { REQUIRE(!policy.is_satisfied()); REQUIRE(policy.is_applicable()); + REQUIRE_THAT( + policy.describe_state(), + Matches::Equals(applicableText)); REQUIRE_NOTHROW(policy.consume()); } REQUIRE(policy.is_satisfied()); REQUIRE(!policy.is_applicable()); + REQUIRE_THAT( + policy.describe_state(), + Matches::Equals(saturatedText)); } SECTION("When sequence has multiple expectations, the order matters.") @@ -251,9 +261,15 @@ TEST_CASE( { REQUIRE(!policy1.is_satisfied()); REQUIRE(policy1.is_applicable()); + REQUIRE_THAT( + policy1.describe_state(), + Matches::Equals(applicableText)); REQUIRE(!policy2.is_satisfied()); REQUIRE(!policy2.is_applicable()); + REQUIRE_THAT( + policy2.describe_state(), + Matches::Equals(inapplicableText)); REQUIRE_NOTHROW(policy1.consume()); @@ -261,18 +277,30 @@ TEST_CASE( { REQUIRE(policy1.is_satisfied()); REQUIRE(!policy1.is_applicable()); + REQUIRE_THAT( + policy1.describe_state(), + Matches::Equals(saturatedText)); REQUIRE(!policy2.is_satisfied()); REQUIRE(policy2.is_applicable()); + REQUIRE_THAT( + policy2.describe_state(), + Matches::Equals(applicableText)); REQUIRE_NOTHROW(policy2.consume()); } REQUIRE(policy1.is_satisfied()); REQUIRE(!policy1.is_applicable()); + REQUIRE_THAT( + policy1.describe_state(), + Matches::Equals(saturatedText)); REQUIRE(policy2.is_satisfied()); REQUIRE(!policy2.is_applicable()); + REQUIRE_THAT( + policy2.describe_state(), + Matches::Equals(saturatedText)); } } } @@ -318,7 +346,7 @@ TEST_CASE( REQUIRE(policy2.is_satisfied()); REQUIRE(!policy2.is_applicable()); } - + SECTION("When an expectation waits for multiple sequences.") { PolicyT policy1 = expect::in_sequences({sequence1}); diff --git a/test/unit-tests/TestReporter.hpp b/test/unit-tests/TestReporter.hpp new file mode 100644 index 000000000..82c2119ef --- /dev/null +++ b/test/unit-tests/TestReporter.hpp @@ -0,0 +1,179 @@ +// // 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 "mimic++/Reporter.hpp" + +#include +#include +#include +#include +#include + +class NoMatchError +{ +}; + +class NonApplicableMatchError +{ +}; + +class TestReporter final + : public mimicpp::IReporter +{ +public: + using call_report_t = mimicpp::CallReport; + using expectation_report_t = mimicpp::ExpectationReport; + using match_report_t = mimicpp::MatchReport; + + std::vector> noMatchResults{}; + + [[noreturn]] + void report_no_matches( + call_report_t call, + std::vector matchReports + ) override + { + for (auto& exp : matchReports) + { + noMatchResults.emplace_back( + call, + std::move(exp)); + } + + throw NoMatchError{}; + } + + std::vector> inapplicableMatchResults{}; + + [[noreturn]] + void report_inapplicable_matches( + call_report_t call, + std::vector matchReports + ) override + { + for (auto& exp : matchReports) + { + inapplicableMatchResults.emplace_back( + call, + std::move(exp)); + } + + throw NonApplicableMatchError{}; + } + + std::vector> fullMatchResults{}; + + void report_full_match( + call_report_t call, + mimicpp::MatchReport matchReport + ) noexcept override + { + fullMatchResults.emplace_back( + std::move(call), + std::move(matchReport)); + } + + std::vector unfulfilledExpectations{}; + + void report_unfulfilled_expectation( + expectation_report_t expectationReport + ) override + { + unfulfilledExpectations.emplace_back(std::move(expectationReport)); + } + + std::vector errors{}; + + void report_error(mimicpp::StringT message) override + { + errors.emplace_back(std::move(message)); + } + + struct unhandled_exception_info + { + call_report_t call; + expectation_report_t expectation{}; + std::exception_ptr exception; + }; + + std::vector unhandledExceptions{}; + + void report_unhandled_exception( + call_report_t call, + expectation_report_t expectationReport, + std::exception_ptr exception + ) override + { + unhandledExceptions.emplace_back( + std::move(call), + std::move(expectationReport), + std::move(exception)); + } +}; + +class ScopedReporter +{ +public: + ~ScopedReporter() noexcept + { + mimicpp::install_reporter(); + } + + ScopedReporter() noexcept + { + mimicpp::install_reporter(); + } + + ScopedReporter(const ScopedReporter&) = delete; + ScopedReporter& operator =(const ScopedReporter&) = delete; + ScopedReporter(ScopedReporter&&) = delete; + ScopedReporter& operator =(ScopedReporter&&) = delete; + + auto& no_match_reports() noexcept + { + return reporter() + .noMatchResults; + } + + auto& inapplicable_match_reports() noexcept + { + return reporter() + .inapplicableMatchResults; + } + + auto& full_match_reports() noexcept + { + return reporter() + .fullMatchResults; + } + + auto& errors() noexcept + { + return reporter() + .errors; + } + + auto& unhandled_exceptions() noexcept + { + return reporter() + .unhandledExceptions; + } + + auto& unfulfilled_expectations() noexcept + { + return reporter() + .unfulfilledExpectations; + } + +private: + [[nodiscard]] + static const TestReporter& reporter() + { + return dynamic_cast( + *mimicpp::detail::get_reporter()); + } +}; diff --git a/test/TestTypes.hpp b/test/unit-tests/TestTypes.hpp similarity index 89% rename from test/TestTypes.hpp rename to test/unit-tests/TestTypes.hpp index 83455996d..12371713c 100644 --- a/test/TestTypes.hpp +++ b/test/unit-tests/TestTypes.hpp @@ -26,7 +26,6 @@ class PolicyFake { public: using CallInfoT = mimicpp::call::info_for_signature_t; - using SubMatchT = mimicpp::call::SubMatchResult; bool isSatisfied{}; @@ -36,14 +35,22 @@ class PolicyFake return isSatisfied; } - SubMatchT matchResult{}; + bool matchResult{}; [[nodiscard]] - constexpr SubMatchT matches(const CallInfoT& call) const noexcept + constexpr bool matches(const CallInfoT& call) const noexcept { return matchResult; } + mimicpp::StringT description{}; + + [[nodiscard]] + mimicpp::StringT describe() const + { + return description; + } + static constexpr void consume(const CallInfoT& call) noexcept { } @@ -68,7 +75,6 @@ class PolicyFacade { public: using CallT = mimicpp::call::info_for_signature_t; - using SubMatchT = mimicpp::call::SubMatchResult; Policy policy; Projection projection; @@ -81,12 +87,19 @@ class PolicyFacade } [[nodiscard]] - constexpr SubMatchT matches(const CallT& call) const noexcept + constexpr bool matches(const CallT& call) const noexcept { return std::invoke(projection, policy) .matches(call); } + [[nodiscard]] + mimicpp::StringT describe() const + { + return std::invoke(projection, policy) + .describe(); + } + constexpr void consume(const CallT& call) noexcept { std::invoke(projection, policy) @@ -116,12 +129,12 @@ class PolicyMock { public: using CallInfoT = mimicpp::call::info_for_signature_t; - using SubMatchT = mimicpp::call::SubMatchResult; static constexpr bool trompeloeil_movable_mock = true; MAKE_CONST_MOCK0(is_satisfied, bool (), noexcept); - MAKE_CONST_MOCK1(matches, SubMatchT (const CallInfoT&), noexcept); + MAKE_CONST_MOCK1(matches, bool(const CallInfoT&), noexcept); + MAKE_CONST_MOCK0(describe, mimicpp::StringT()); MAKE_MOCK1(consume, void (const CallInfoT&), noexcept); }; @@ -166,6 +179,14 @@ class TimesFake return isApplicable; } + std::optional stateDescription{}; + + [[nodiscard]] + std::optional describe_state() const + { + return stateDescription; + } + static constexpr void consume() noexcept { } @@ -174,8 +195,9 @@ class TimesFake class TimesMock { public: - MAKE_CONST_MOCK0(is_satisfied, bool (), noexcept); - MAKE_CONST_MOCK0(is_applicable, bool (), noexcept); + MAKE_CONST_MOCK0(is_satisfied, bool(), noexcept); + MAKE_CONST_MOCK0(is_applicable, bool(), noexcept); + MAKE_CONST_MOCK0(describe_state, std::optional()); MAKE_MOCK0(consume, void ()); }; @@ -200,6 +222,13 @@ class TimesFacade .is_applicable(); } + [[nodiscard]] + std::optional describe_state() const + { + return std::invoke(projection, policy) + .describe_state(); + } + constexpr void consume() noexcept { return std::invoke(projection, policy) @@ -212,7 +241,7 @@ class MatcherMock { public: MAKE_CONST_MOCK1(matches, bool(T)); - MAKE_CONST_MOCK1(describe, mimicpp::StringT(T)); + MAKE_CONST_MOCK0(describe, mimicpp::StringT()); }; template @@ -234,12 +263,11 @@ class [[maybe_unused]] MatcherFacade .matches(std::forward(target)); } - template [[nodiscard]] - constexpr mimicpp::StringT describe(T&& target) const + constexpr mimicpp::StringT describe() const { return std::invoke(m_Projection, m_Matcher) - .describe(std::forward(target)); + .describe(); } private: diff --git a/test/TypeTraits.cpp b/test/unit-tests/TypeTraits.cpp similarity index 100% rename from test/TypeTraits.cpp rename to test/unit-tests/TypeTraits.cpp diff --git a/test/Utility.cpp b/test/unit-tests/Utility.cpp similarity index 51% rename from test/Utility.cpp rename to test/unit-tests/Utility.cpp index a03f7768b..a87e94a34 100644 --- a/test/Utility.cpp +++ b/test/unit-tests/Utility.cpp @@ -4,9 +4,11 @@ // // https://www.boost.org/LICENSE_1_0.txt) #include "mimic++/Utility.hpp" +#include "mimic++/Printer.hpp" #include #include +#include #include @@ -61,3 +63,61 @@ TEMPLATE_TEST_CASE( REQUIRE(value == to_underlying(Test{value})); } } + +TEST_CASE( + "ValueCategory is formattable.", + "[utility]" +) +{ + namespace Matches = Catch::Matchers; + + SECTION("When valid ValueCategory is given.") + { + const auto [expected, category] = GENERATE( + (table)({ + {"any", ValueCategory::any}, + {"rvalue",ValueCategory::rvalue}, + {"lvalue", ValueCategory::lvalue}, + })); + + REQUIRE_THAT( + format::format("{}", category), + Matches::Equals(expected)); + } + + SECTION("When an invalid ValueCategory is given, std::invalid_argument is thrown.") + { + REQUIRE_THROWS_AS( + format::format("{}", ValueCategory{42}), + std::invalid_argument); + } +} + +TEST_CASE( + "Constness is formattable.", + "[utility]" +) +{ + namespace Matches = Catch::Matchers; + + SECTION("When valid Constness is given.") + { + const auto [expected, category] = GENERATE( + (table)({ + {"any", Constness::any}, + {"const",Constness::as_const}, + {"mutable", Constness::non_const}, + })); + + REQUIRE_THAT( + format::format("{}", category), + Matches::Equals(expected)); + } + + SECTION("When an invalid Constness is given, std::invalid_argument is thrown.") + { + REQUIRE_THROWS_AS( + format::format("{}", Constness{42}), + std::invalid_argument); + } +}