diff options
| author | Leitwolf | 2026-11-01 21:19:33 +0300 |
|---|---|---|
| committer | GitHub | 2026-11-01 21:19:33 +0300 |
| commit | 5cbd481125b881a4c2eed74050be9899eb3f23a7 (patch) | |
| tree | 496d26ff659e92cdf4e6fe27a4aa47bf52818052 | |
| parent | 26a1588ae3ae183ae5c900caf3c294bdcb3f8855 (diff) | |
| parent | 8603d31fd8c1a72e30cb444f278d7a0592383ba7 (diff) | |
| download | catch3-5cbd481125b881a4c2eed74050be9899eb3f23a7.tar.gz | |
Merge pull request #30 from Progmasoft/feature/mocking
Add mocks that report through the assertion path
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | docs/Readme.md | 1 | ||||
| -rw-r--r-- | docs/progmasoft-mocking.md | 142 | ||||
| -rw-r--r-- | docs/progmasoft-roadmap.md | 11 | ||||
| -rw-r--r-- | docs/why-catch3.md | 3 | ||||
| -rw-r--r-- | extras/catch_amalgamated.cpp | 7 | ||||
| -rw-r--r-- | extras/catch_amalgamated.hpp | 2 | ||||
| -rw-r--r-- | extras/catch_amalgamated_third_party.hpp | 2 | ||||
| -rw-r--r-- | premake5.lua | 1 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3.hpp | 1 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3/Internal/RunContext.cpp | 5 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3/Mock.hpp | 1029 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3/meson.build | 1 | ||||
| -rw-r--r-- | src/Progmasoft/Mock.cpp | 197 | ||||
| -rw-r--r-- | src/Progmasoft/meson.build | 1 | ||||
| -rw-r--r-- | tests/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | tests/Progmasoft/BUILD.bazel | 1 | ||||
| -rw-r--r-- | tests/Progmasoft/Mock.tests.cpp | 405 | ||||
| -rw-r--r-- | tests/Progmasoft/RunnerFixture.cpp | 76 | ||||
| -rw-r--r-- | tests/meson.build | 1 | ||||
| -rw-r--r-- | xmake.lua | 1 |
22 files changed, 1884 insertions, 9 deletions
@@ -25,6 +25,9 @@ serialization. tests, keeps a history that drives time limits, ordering, incremental runs and replay, selects and shards tests, and writes console, JUnit, JSON and trace reports. +- **Mocks.** [Mocks](docs/progmasoft-mocking.md) of virtual interfaces and of + free functions behind a seam check call counts, arguments and order, and + report through the same assertions as the rest of a test. - **Deterministic property testing.** Generate values from a reproducible seed, check a predicate over multiple trials, and shrink a failing input to a smaller counterexample. @@ -96,6 +99,7 @@ their package, target, header and pkg-config names differ. - [Catch3 additions: properties, snapshots, results, and XML](docs/progmasoft-catch3.md) - [Project scope, compatibility policy, and limitations](docs/why-catch3.md) - [The Catch3 runner](docs/progmasoft-runner.md) +- [Mocking](docs/progmasoft-mocking.md) - [Tutorial](docs/tutorial.md) - [Catch2-compatible API reference](docs/Readme.md) - [Catch2 upstream project and source history](https://github.com/catchorg/Catch2) diff --git a/docs/Readme.md b/docs/Readme.md index 3da1a8ca..15fe412d 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -38,6 +38,7 @@ Catch2 release. **Running:** * [The Catch3 runner](progmasoft-runner.md#top) +* [Mocking](progmasoft-mocking.md#top) * [Command line reference](command-line.md#top) * [Running specific section/generator](filtering-execution-path.md#top) diff --git a/docs/progmasoft-mocking.md b/docs/progmasoft-mocking.md new file mode 100644 index 00000000..f79a537e --- /dev/null +++ b/docs/progmasoft-mocking.md @@ -0,0 +1,142 @@ +<a id="top"></a> + +# Mocking + +`<Progmasoft/Catch3/Mock.hpp>` replaces a dependency of the code under test +with a mock that checks how it is called. Mocks report through Catch's +assertion path: an unexpected call, a call out of order and an unmet +expectation fail the test case that owns the mock, at the line that declared +the expectation, in every report the runner writes. + +```cpp +#include <Progmasoft/Catch3/Mock.hpp> +#include <Progmasoft/Catch3/TestMacros.hpp> + +class Store +{ +public: + virtual ~Store() = default; + virtual int Get(const std::string &key) const = 0; + virtual void Put(const std::string &key, int value) = 0; +}; + +class MockStore final : public Store +{ +public: + CATCH3_MOCK_METHOD(int, Get, (const std::string &), const override); + CATCH3_MOCK_METHOD(void, Put, (const std::string &, int), override); +}; + +TEST_CASE("A score goes up by one") +{ + MockStore store; + CATCH3_EXPECT_CALL(store.GetMock)("ada").Returns(41); + CATCH3_EXPECT_CALL(store.PutMock)("ada", 42); + + AddPoint(store, "ada"); +} +``` + +A mock verifies itself in its destructor, so nothing has to be called at the +end of the test case. When the test case is already being left by a failure or +a skip, the expectations it did not reach are not reported on top of it. +`Verify()` checks the expectations earlier and forgets them, for a test case +that goes on with new ones. + +## Declaring mocks + +`CATCH3_MOCK_METHOD(result, name, (types), specifiers)` declares the member +function `name` and the mock `nameMock` beside it. The specifiers are whatever +follows the parameters: `const`, `noexcept`, `override`. Up to 8 parameters are +supported, and a type with a comma in it, such as `std::map<std::string, int>`, +needs an alias. + +Overloads need mocks with names of their own: + +```cpp +CATCH3_MOCK_OVERLOAD(void, Put, PutOne, (const std::string &, int), override); +CATCH3_MOCK_OVERLOAD(void, Put, PutAll, (const Scores &), override); +``` + +`CATCH3_MOCK_FUNCTION(name, signature)` declares a `MockFunction` on its own. + +## Setting expectations + +`CATCH3_EXPECT_CALL(mock)(arguments...)` expects one call. Without arguments it +accepts any arguments; otherwise each parameter takes one of these: + +| Written as | Accepts | +| --- | --- | +| a value | what compares equal to it; strings compare by value, also as `const char *`, and integers by value whatever their types, so 256 never accepts an `unsigned char` | +| a Catch matcher, such as `StartsWith("a")` | what the matcher accepts | +| `Any` | anything | +| `Where(predicate, "description")` | what the predicate accepts | + +A composition such as `StartsWith("a") && EndsWith("z")` refers to temporaries +that are gone when the mock is called, and Catch's vector matchers, such as +`Equals(vector)`, refer to their vector. Neither compiles here; put them in a +`Where`. A matcher of your own has to hold what it compares with. + +The result of `CATCH3_EXPECT_CALL` takes these, in any order: + +| Member | Meaning | +| --- | --- | +| `Times(n)`, `Between(a, b)`, `AtLeast(n)`, `AtMost(n)`, `AnyTimes()`, `Never()` | how many calls it wants; one by default | +| `Returns(value)` | the result of each call; a type that cannot be copied moves out on the first one, and a reference result refers to `value` | +| `Does(function)` | runs `function` with the arguments, and returns what it returns; it keeps its state between calls | +| `Throws(exception)` | throws `exception` from each call | +| `InSequence(sequence)` | see below | + +A call goes to the first expectation, in the order they were declared, that +accepts its arguments and can take another call. A call none of them accepts +fails the test case and returns a value-initialized result. So does a call they +accept when all of them are used up. When the result cannot be made that way, a +reference for example, the test case ends there; the same happens to an +expectation with no `Returns` or `Does`. A `noexcept` function cannot leave the +test case that way, so such a call ends the process. + +A mock with no expectation for a call treats it as a failure. An expectation +with `AnyTimes()` is how to allow calls that do not matter to the test. + +## Sequences + +Expectations in a `Sequence` have to be met in the order they were declared in, +across mocks: + +```cpp +Sequence order; +CATCH3_EXPECT_CALL(file.OpenMock)().InSequence(order); +CATCH3_EXPECT_CALL(file.WriteMock)(Any).AtLeast(1).InSequence(order); +CATCH3_EXPECT_CALL(log.FlushMock)().InSequence(order); +``` + +A step that is passed takes no more calls. A step reached before the ones ahead +of it have their calls fails the test case and names them. + +## Free functions + +Code that reaches a free function through a seam, a `std::function` or a +function pointer it calls through, can be pointed at a mock. `ScopedSeam` puts +the seam back at the end of its scope: + +```cpp +CATCH3_MOCK_FUNCTION(clock, long()); +CATCH3_EXPECT_CALL(clock)().Returns(50L); + +const ScopedSeam seam(clockSeam, clock.AsFunction()); +CHECK(SecondsSince(20) == 30); +``` + +The mock has to outlive what `AsFunction` returns. A function pointer can only +take a function without state, so it reaches a mock of static lifetime through +one. + +## Threads + +Mocks count and order calls correctly when several threads call them. Reports +go through Catch's assertions, which are not safe across threads unless +`CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS` is set, so a failure in a +thread other than the test's needs that setting. + +Outside of a test case, such as in the destructor of a global, there is nothing +to fail: failures go to standard error. diff --git a/docs/progmasoft-roadmap.md b/docs/progmasoft-roadmap.md index 5ab04264..81a8956f 100644 --- a/docs/progmasoft-roadmap.md +++ b/docs/progmasoft-roadmap.md @@ -21,7 +21,7 @@ The steps are ordered by what each one builds on. | --- | --- | --- | | 1 | Native property testing: choice-sequence shrinking, combinators, state machines, statistics, targeted search, example database, differential and metamorphic oracles | done; see [Native property testing](progmasoft-properties.md#top) | | 2 | The Catch3 runner | done; see [The Catch3 runner](progmasoft-runner.md#top) | -| 3 | Mocking | planned | +| 3 | Mocking | done; see [Mocking](progmasoft-mocking.md#top) | | 4 | Per-test code coverage | planned | | 5 | Change-impact test selection and test prioritization | planned | | 6 | Mutation testing | planned | @@ -56,10 +56,11 @@ Catch3 runner replaced Catch2's in every test binary: ## 3. Mocking -Mocks that report through Catch2's assertion path: expectations with call -counts, argument matchers built on Catch2's matchers, sequences, and actions, -for virtual interfaces and for free functions behind a seam. Unexpected calls -and unmet expectations fail the test case that owns the mock. +Mocks report through Catch2's assertion path: expectations with call counts, +argument matchers built on Catch2's matchers, sequences, and actions, for +virtual interfaces and for free functions behind a seam. Unexpected calls and +unmet expectations fail the test case that owns the mock. See +[Mocking](progmasoft-mocking.md#top). ## 4. Per-test code coverage diff --git a/docs/why-catch3.md b/docs/why-catch3.md index 120295f0..871b6efc 100644 --- a/docs/why-catch3.md +++ b/docs/why-catch3.md @@ -20,13 +20,14 @@ Catch2's runner with [its own](progmasoft-runner.md#top). | C++ baseline | C++17; the headers work in C++17 and later | The same | | Randomness | Catch2's existing behavior is unchanged | A specified SplitMix64 sequence provides deterministic trial seeds and unbiased bounded integer sampling | | Generated values | No change to existing Catch2 generators | Strategies for numbers, text, collections and structures with `Map`, `Filter` and `FlatMap`; shrinking of recorded choices, so every combination shrinks; replay tokens and an example database; state machines, `Classify`/`Cover` statistics, targeted search, and differential and metamorphic oracles | +| Mocks | None | [`CATCH3_MOCK_METHOD`, `CATCH3_EXPECT_CALL`](progmasoft-mocking.md#top) and `Sequence`, reporting through the assertion path | | Build identity | One library: the `Catch3` CMake package with `Catch3::Catch3` and `Catch3::Catch3WithMain`, `//:catch3` in Bazel, `catch3` in Meson and pkg-config | The same library | | Source terms | Catch2-originated source retains its upstream BSL-1.0 terms | Progmasoft-authored additions state their own SPDX license in each file | Properties take any number of generated values. Shrinking is deterministic and bounded, but local rather than globally minimal. Parallel trials do not exist yet; [the roadmap](progmasoft-roadmap.md#top) -orders them with mocking, per-test coverage, test selection, mutation testing +orders them with per-test coverage, test selection, mutation testing and test generation. ## Compatibility policy diff --git a/extras/catch_amalgamated.cpp b/extras/catch_amalgamated.cpp index 78782e44..fb73d0b2 100644 --- a/extras/catch_amalgamated.cpp +++ b/extras/catch_amalgamated.cpp @@ -7,7 +7,7 @@ // SPDX-License-Identifier: BSL-1.0 // Catch3 v0.3.0.1, on the Catch2 v3.16.1 engine -// Generated: 2026-11-01 16:09:52.594301 +// Generated: 2026-11-01 20:05:03.089807 // ---------------------------------------------------------- // This file is an amalgamation of multiple different files. // You probably shouldn't edit it directly. @@ -5189,6 +5189,11 @@ namespace Catch { RunContext::~RunContext() { updateTotalsFromAtomics(); m_reporter->testRunEnded(TestRunStats(m_runInfo, m_totals, aborting())); + // What outlives the run, such as a mock of static lifetime, must not + // find a capture that is gone. + if ( getCurrentContext().getResultCapture() == this ) { + getCurrentMutableContext().setResultCapture( nullptr ); + } } Totals RunContext::runTest(TestCaseHandle const& testCase) { diff --git a/extras/catch_amalgamated.hpp b/extras/catch_amalgamated.hpp index ed4340cf..2572e065 100644 --- a/extras/catch_amalgamated.hpp +++ b/extras/catch_amalgamated.hpp @@ -7,7 +7,7 @@ // SPDX-License-Identifier: BSL-1.0 // Catch3 v0.3.0.1, on the Catch2 v3.16.1 engine -// Generated: 2026-11-01 16:09:52.447462 +// Generated: 2026-11-01 20:05:02.974375 // ---------------------------------------------------------- // This file is an amalgamation of multiple different files. // You probably shouldn't edit it directly. diff --git a/extras/catch_amalgamated_third_party.hpp b/extras/catch_amalgamated_third_party.hpp index f76ef223..77980d07 100644 --- a/extras/catch_amalgamated_third_party.hpp +++ b/extras/catch_amalgamated_third_party.hpp @@ -1,5 +1,5 @@ // Catch3 v0.3.0.1: yyjson and pugixml for catch_amalgamated.cpp -// Generated: 2026-11-01 16:09:52.737649 +// Generated: 2026-11-01 20:05:03.275425 // ---------------------------------------------------------- // This file is an amalgamation of src/Progmasoft/Runner/Yyjson.inl, // src/Progmasoft/PugiXml.inl and the libraries they include from diff --git a/premake5.lua b/premake5.lua index 7b917cfd..d4c55c9e 100644 --- a/premake5.lua +++ b/premake5.lua @@ -47,6 +47,7 @@ project "Catch3" "src/Progmasoft/Results.cpp", "src/Progmasoft/XmlWriter.cpp", "src/Progmasoft/ChoiceShrinker.cpp", + "src/Progmasoft/Mock.cpp", "src/Progmasoft/StrategyContext.cpp", "src/Progmasoft/PropertyEngine.cpp", "src/Progmasoft/Snapshot.cpp", diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 87b19651..43754fff 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -343,6 +343,7 @@ set(PROGMASOFT_RUNNER_FILES # Property testing and snapshots. set(PROGMASOFT_LIBRARY_FILES "${CATCH_DIR}/src/Progmasoft/ChoiceShrinker.cpp" + "${CATCH_DIR}/src/Progmasoft/Mock.cpp" "${CATCH_DIR}/src/Progmasoft/StrategyContext.cpp" "${CATCH_DIR}/src/Progmasoft/PropertyEngine.cpp" "${CATCH_DIR}/src/Progmasoft/Snapshot.cpp" diff --git a/src/Progmasoft/Catch3.hpp b/src/Progmasoft/Catch3.hpp index cc2b0632..2e04782a 100644 --- a/src/Progmasoft/Catch3.hpp +++ b/src/Progmasoft/Catch3.hpp @@ -14,6 +14,7 @@ #include <Progmasoft/Catch3/Generators/Text.hpp> #include <Progmasoft/Catch3/Generators/Tuple.hpp> #include <Progmasoft/Catch3/Generators/Vector.hpp> +#include <Progmasoft/Catch3/Mock.hpp> #include <Progmasoft/Catch3/Property.hpp> #include <Progmasoft/Catch3/Random.hpp> #include <Progmasoft/Catch3/Relations.hpp> diff --git a/src/Progmasoft/Catch3/Internal/RunContext.cpp b/src/Progmasoft/Catch3/Internal/RunContext.cpp index e86be2e4..30af4533 100644 --- a/src/Progmasoft/Catch3/Internal/RunContext.cpp +++ b/src/Progmasoft/Catch3/Internal/RunContext.cpp @@ -356,6 +356,11 @@ namespace Catch { RunContext::~RunContext() { updateTotalsFromAtomics(); m_reporter->testRunEnded(TestRunStats(m_runInfo, m_totals, aborting())); + // What outlives the run, such as a mock of static lifetime, must not + // find a capture that is gone. + if ( getCurrentContext().getResultCapture() == this ) { + getCurrentMutableContext().setResultCapture( nullptr ); + } } Totals RunContext::runTest(TestCaseHandle const& testCase) { diff --git a/src/Progmasoft/Catch3/Mock.hpp b/src/Progmasoft/Catch3/Mock.hpp new file mode 100644 index 00000000..59ad8bb9 --- /dev/null +++ b/src/Progmasoft/Catch3/Mock.hpp @@ -0,0 +1,1029 @@ +// Copyright (c) 2026 Progmasoft. +// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 +#pragma once + +#include <Progmasoft/Catch3/Internal/Enforce.hpp> +#include <Progmasoft/Catch3/Internal/SourceLineInfo.hpp> +#include <Progmasoft/Catch3/Matchers/Matchers.hpp> +#include <Progmasoft/Catch3/Matchers/Templated.hpp> +#include <Progmasoft/Catch3/Matchers/Vector.hpp> +#include <Progmasoft/Catch3/ToString.hpp> +#include <cstddef> +#include <exception> +#include <functional> +#include <limits> +#include <memory> +#include <mutex> +#include <optional> +#include <string> +#include <tuple> +#include <type_traits> +#include <utility> +#include <vector> + +namespace Progmasoft::Catch3 +{ + template<typename Signature> + class MockExpectation; + template<typename Signature> + class MockFunction; + + namespace Detail + { + inline constexpr std::size_t kUnlimitedCalls + = std::numeric_limits<std::size_t>::max(); + + class MockSequence; + + /// What the checks need to know of an expectation, whatever the + /// signature of its mock. + struct MockExpectationState final + { + std::string Description; + Catch::SourceLineInfo Line; + std::size_t Minimum = 1; + std::size_t Maximum = 1; + std::size_t Calls = 0; + bool Retired = false; + std::vector<std::shared_ptr<MockSequence>> Sequences; + + MockExpectationState(std::string description, + const Catch::SourceLineInfo &line) + : Description(std::move(description)) + , Line(line) + {} + + [[nodiscard]] bool + Satisfied() const noexcept + { + return Calls >= Minimum; + } + + /// Whether the expectation can take one more call. + [[nodiscard]] bool + Available() const noexcept + { + return !Retired && Calls < Maximum; + } + }; + + /// The steps of a sequence. It does not keep them alive: a step whose + /// mock is gone was verified with that mock. + class MockSequence final + { + public: + void + Add(const std::shared_ptr<MockExpectationState> &step); + + /// Moves to `step`, retiring the steps before it. Returns what + /// was left undone, empty when the call came in order. + [[nodiscard]] std::string + Enter(const MockExpectationState &step); + + private: + std::vector<std::weak_ptr<MockExpectationState>> steps; + std::size_t current = 0; + }; + + /// All mocks share one lock, since a sequence can span them. + [[nodiscard]] std::unique_lock<std::recursive_mutex> + LockMocks(); + + /// Fails the running test case at `line` and never throws, since a + /// mock can stand in for a `noexcept` function or be in a destructor. + /// Outside of a test case the message goes to standard error. + void + ReportMockFailure(const Catch::SourceLineInfo &line, + const std::string &mock, + std::string message); + + /// Fails the running test case and leaves it, for a call nothing can + /// return from. In a `noexcept` function that ends the process. + [[noreturn]] void + AbortMockCall(const Catch::SourceLineInfo &line, + const std::string &mock, + std::string message); + + /// Counts a met expectation as a passed assertion. + void + ReportMockSuccess(const Catch::SourceLineInfo &line, + const std::string &mock, + std::string message); + + /// "exactly 2 calls", "at least 1 call", "any number of calls". + [[nodiscard]] std::string + DescribeCalls(std::size_t minimum, std::size_t maximum); + + /// "called once" and the like, for a report. + [[nodiscard]] std::string + DescribeCount(std::size_t calls); + + template<std::size_t Index, typename Signature> + struct MockArgumentOf; + + template<std::size_t Index, typename... Arguments> + struct MockArgumentOf<Index, void(Arguments...)> + { + using Type = std::tuple_element_t<Index, std::tuple<Arguments...>>; + }; + + /// The type of a parameter of a mocked method. + template<std::size_t Index, typename Signature> + using MockArgument = typename MockArgumentOf<Index, Signature>::Type; + + template<typename T> + struct IsComposedMatcher: std::false_type + { + }; + template<typename T> + struct IsComposedMatcher<Catch::Matchers::Detail::MatchAllOf<T>> + : std::true_type + { + }; + template<typename T> + struct IsComposedMatcher<Catch::Matchers::Detail::MatchAnyOf<T>> + : std::true_type + { + }; + template<typename T> + struct IsComposedMatcher<Catch::Matchers::Detail::MatchNotOf<T>> + : std::true_type + { + }; + template<typename... Ts> + struct IsComposedMatcher< + Catch::Matchers::Detail::MatchAllOfGeneric<Ts...>>: std::true_type + { + }; + template<typename... Ts> + struct IsComposedMatcher< + Catch::Matchers::Detail::MatchAnyOfGeneric<Ts...>>: std::true_type + { + }; + template<typename T> + struct IsComposedMatcher<Catch::Matchers::Detail::MatchNotOfGeneric<T>> + : std::true_type + { + }; + + /// Catch's vector matchers refer to what they compare with. + template<typename T> + struct HoldsReference: std::false_type + { + }; + template<typename T, typename Alloc> + struct HoldsReference< + Catch::Matchers::VectorContainsElementMatcher<T, Alloc>> + : std::true_type + { + }; + template<typename T, typename AllocComp, typename AllocMatch> + struct HoldsReference< + Catch::Matchers::ContainsMatcher<T, AllocComp, AllocMatch>> + : std::true_type + { + }; + template<typename T, typename AllocComp, typename AllocMatch> + struct HoldsReference< + Catch::Matchers::EqualsMatcher<T, AllocComp, AllocMatch>> + : std::true_type + { + }; + template<typename T, typename AllocComp, typename AllocMatch> + struct HoldsReference< + Catch::Matchers::ApproxMatcher<T, AllocComp, AllocMatch>> + : std::true_type + { + }; + template<typename T, typename AllocComp, typename AllocMatch> + struct HoldsReference< + Catch::Matchers::UnorderedEqualsMatcher<T, AllocComp, AllocMatch>> + : std::true_type + { + }; + + /// Whether two integers have the same value, whatever their types. + template<typename Left, typename Right> + [[nodiscard]] constexpr bool + SameInteger(Left left, Right right) noexcept + { + if constexpr (std::is_signed_v<Left> == std::is_signed_v<Right>) + { + return left == right; + } + else if constexpr (std::is_signed_v<Left>) + { + return left >= 0 + && static_cast<std::make_unsigned_t<Left>>(left) + == right; + } + else + { + return right >= 0 + && left + == static_cast<std::make_unsigned_t<Right>>( + right); + } + } + + /// How one argument of a call is judged. + template<typename Argument> + struct ArgumentMatcher final + { + std::function<bool(const Argument &)> Test; + std::string Description; + }; + } // namespace Detail + + /// Accepts any value of an argument. + struct AnyArgument final + {}; + inline constexpr AnyArgument Any{}; + + /// Accepts the values `predicate` accepts; `description` names it in + /// reports. + template<typename Predicate> + struct WhereArgument final + { + Predicate Test; + std::string Description; + }; + + template<typename Predicate> + [[nodiscard]] WhereArgument<std::decay_t<Predicate>> + Where(Predicate &&predicate, std::string description) + { + return { std::forward<Predicate>(predicate), std::move(description) }; + } + + namespace Detail + { + template<typename Argument, typename Expected> + [[nodiscard]] ArgumentMatcher<Argument> + MakeArgumentMatcher(Expected &&expected) + { + using Stored = std::decay_t<Expected>; + if constexpr (std::is_same_v<Stored, AnyArgument>) + { + return { [](const Argument &) { + return true; + }, + "any" }; + } + else if constexpr (std::is_base_of_v< + Catch::Matchers::MatcherUntypedBase, + Stored>) + { + static_assert(!IsComposedMatcher<Stored>::value, + "A composed matcher refers to temporaries that " + "are gone when the mock is called; use Where."); + static_assert(!HoldsReference<Stored>::value, + "A vector matcher refers to its vector, which " + "can be gone when the mock is called; use " + "Where."); + auto matcher = std::make_shared<Stored>( + std::forward<Expected>(expected)); + auto description = matcher->toString(); + return { [matcher](const Argument &value) { + return matcher->match(value); + }, + std::move(description) }; + } + else if constexpr (!std::is_null_pointer_v<Stored> + && std::is_constructible_v<std::string, Stored> + && std::is_constructible_v<std::string, + Argument>) + { + // Strings are compared by value, also as `const char *`. + if constexpr (std::is_pointer_v<Stored>) + { + if (expected == nullptr) + { + return { []([[maybe_unused]] const Argument &value) { + if constexpr (std::is_pointer_v<Argument>) + { + return value == nullptr; + } + else + { + return false; + } + }, + "nullptr" }; + } + } + std::string text{ std::forward<Expected>(expected) }; + auto description = Catch::Detail::stringify(text); + return { [text = std::move(text)](const Argument &value) { + if constexpr (std::is_pointer_v<Argument>) + { + return value != nullptr && text == value; + } + else + { + return text == value; + } + }, + std::move(description) }; + } + else if constexpr (std::is_integral_v<Argument> + && std::is_integral_v<Stored> + && !std::is_same_v<Argument, bool> + && !std::is_same_v<Stored, bool>) + { + // A literal is an int, whatever the type of the parameter. + const Stored value = expected; + return { [value](const Argument &argument) { + return SameInteger(argument, value); + }, + Catch::Detail::stringify(value) }; + } + else + { + auto description = Catch::Detail::stringify(expected); + return { [value = std::forward<Expected>(expected)]( + const Argument &argument) { + return argument == value; + }, + std::move(description) }; + } + } + + template<typename Argument, typename Predicate> + [[nodiscard]] ArgumentMatcher<Argument> + MakeArgumentMatcher(WhereArgument<Predicate> where) + { + return { std::move(where.Test), std::move(where.Description) }; + } + } // namespace Detail + + /// Calls that have to come in the order their expectations were declared + /// in, across mocks. + class Sequence final + { + public: + Sequence() + : state(std::make_shared<Detail::MockSequence>()) + {} + + private: + template<typename Signature> + friend class MockExpectation; + + std::shared_ptr<Detail::MockSequence> state; + }; + + /// One expected kin |