diff options
| author | Leitwolf11 | 2026-11-01 19:04:52 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-11-01 19:04:52 +0300 |
| commit | c51d15273e87267215e5877edc268e7db354187b (patch) | |
| tree | bc314b97cbb793ea136ceabecc3d565240ae7cf2 | |
| parent | 26a1588ae3ae183ae5c900caf3c294bdcb3f8855 (diff) | |
| download | catch3-c51d15273e87267215e5877edc268e7db354187b.tar.gz | |
Add mocks that report through the assertion path
Step 3 of the roadmap. MockFunction checks the calls of one function
against its expectations; CATCH3_MOCK_METHOD declares one for a virtual
member, and CATCH3_MOCK_FUNCTION and ScopedSeam serve free functions
behind a seam.
Expectations take values, Catch matchers, Any or Where; count calls with
Times, Between, AtLeast, AtMost, AnyTimes and Never; answer with Returns,
Does or Throws; and order calls across mocks with Sequence. A failure
goes through AssertionHandler at the line of the expectation, so every
report of the runner shows it. Met expectations count as passed
assertions. A test case that is already being left does not get unmet
expectations on top of its own failure.
Composed matchers refer to temporaries, so they are rejected at compile
time rather than left dangling. A call that fails returns a
value-initialized result, or leaves the test case when no such result
exists.
The failure paths are tested through the runner fixture, reading the
messages from its JSON report.
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | docs/Readme.md | 1 | ||||
| -rw-r--r-- | docs/progmasoft-mocking.md | 140 | ||||
| -rw-r--r-- | docs/progmasoft-roadmap.md | 11 | ||||
| -rw-r--r-- | docs/why-catch3.md | 3 | ||||
| -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/Mock.hpp | 955 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3/meson.build | 1 | ||||
| -rw-r--r-- | src/Progmasoft/Mock.cpp | 203 | ||||
| -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 | 310 | ||||
| -rw-r--r-- | tests/Progmasoft/RunnerFixture.cpp | 67 | ||||
| -rw-r--r-- | tests/meson.build | 1 | ||||
| -rw-r--r-- | xmake.lua | 1 |
18 files changed, 1697 insertions, 6 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..98c372ec --- /dev/null +++ b/docs/progmasoft-mocking.md @@ -0,0 +1,140 @@ +<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 *` | +| 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, so it does not compile here; put it in a +`Where`. + +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 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/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/Mock.hpp b/src/Progmasoft/Catch3/Mock.hpp new file mode 100644 index 00000000..1e077ac8 --- /dev/null +++ b/src/Progmasoft/Catch3/Mock.hpp @@ -0,0 +1,955 @@ +// 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/ToString.hpp> +#include <cstddef> +#include <exception> +#include <functional> +#include <limits> +#include <memory> +#include <mutex> +#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(); + + enum class MockReport : unsigned char + { + kContinue, + kInDestructor, + }; + + /// Fails the running test case at `line`. Outside of a test case + /// the message goes to standard error. + void + ReportMockFailure(const Catch::SourceLineInfo &line, + const std::string &mock, + std::string message, + MockReport report); + + /// Fails the running test case and leaves it, for a call nothing can + /// return from. + [[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 + { + }; + + /// 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."); + 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 *`. + 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>) + { + // A literal is an int, whatever the type of the parameter. + const auto value = static_cast<Argument>(expected); + return { [value](const Argument &argument) { + return 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 kind of call of a mock. It expects one call until told + /// otherwise. + template<typename Result, typename... Arguments> + class MockExpectation<Result(Arguments...)> final + { + public: + MockExpectation & + Times(std::size_t count) + { + return Between(count, count); + } + + MockExpectation & + AtLeast(std::size_t count) + { + return Between(count, Detail::kUnlimitedCalls); + } + + MockExpectation & + AtMost(std::size_t count) + { + return Between(0, count); + } + + MockExpectation & + AnyTimes() + { + return Between(0, Detail::kUnlimitedCalls); + } + + MockExpectation & + Never() + { + return Between(0, 0); + } + + MockExpectation & + Between(std::size_t minimum, std::size_t maximum) + { + CATCH_ENFORCE(minimum <= maximum, + "A mock expectation needs its minimum at most its " + "maximum"); + const auto lock = Detail::LockMocks(); + state->Minimum = minimum; + state->Maximum = maximum; + return *this; + } + + /// Takes this step in `sequence`, after the ones declared before it. + MockExpectation & + InSequence(const Sequence &sequence) + { + const auto lock = Detail::LockMocks(); + sequence.state->Add(state); + state->Sequences.push_back(sequence.state); + return *this; + } + + /// Returns `value`. A type that cannot be copied is moved out on the + /// first call; a reference result refers to `value`, which has to + /// outlive the mock. + template<typename Value> + MockExpectation & + Returns(Value &&value) + { + static_assert(!std::is_void_v<Result>, + "A mock of a void function returns nothing."); + if constexpr (std::is_reference_v<Result>) + { + static_assert(std::is_lvalue_reference_v<Value>, + "A reference result needs an object that " + "outlives the mock."); + auto *target = &value; + return Does([target](Arguments...) -> Result { + return static_cast<Result>(*target); + }); + } + else if constexpr (std::is_copy_constructible_v<Result>) + { + return Does([held = Result(std::forward<Value>(value))]( + Arguments...) -> Result { + return held; + }); + } + else + { + auto held + = std::make_shared<Result>(std::forward<Value>(value)); + return Does([held](Arguments...) -> Result { + return std::move(*held); + }); + } + } + + /// Runs `action` with the arguments of each call; what it returns is + /// what the call returns. + template<typename Action> + MockExpectation & + Does(Action &&work) + { + const auto lock = Detail::LockMocks(); + action = std::make_shared<std::function<Result(Arguments...)>>( + std::forward<Action>(work)); + return *this; + } + + /// Throws `exception` from each call. + template<typename Exception> + MockExpectation & + Throws(Exception exception) + { + return Does( + [exception = std::move(exception)](Arguments...) -> Result { + Catch::throw_exception(exception); + }); + } + + /// What the arguments of a call have to match. + using Matchers + = std::tuple<Detail::ArgumentMatcher<std::decay_t<Arguments>>...>; + + private: + template<typename Signature> + friend class MockFunction; + + MockExpectation(std::string description, + const Catch::SourceLineInfo &line, + std::unique_ptr<Matchers> accepted) + : state(std::make_shared<Detail::MockExpectationState>( + std::move(description), + line)) + , matchers(std::move(accepted)) + {} + + template<std::size_t... Indices> + [[nodiscard]] bool + Accepts([[maybe_unused]] const std::tuple< + const std::decay_t<Arguments> &...> &values, + std::index_sequence<Indices...>) const + { + return (std::get<Indices>(*matchers).Test(std::get<Indices>(values)) + && ...); + } + + [[nodiscard]] bool + Accepts( + const std::tuple<const std::decay_t<Arguments> &...> &values) const + { + return matchers == nullptr + || Accepts(values, std::index_sequence_for<Arguments...>{}); + } + + std::shared_ptr<Detail::MockExpectationState> state; + // Empty when any arguments will do. + std::unique_ptr<Matchers> matchers; + std::shared_ptr<std::function<Result(Arguments...)>> action; + }; + + /// A mock of one function: a callable that checks its calls against the + /// expectations set on it and fails the running test case when they do + /// not hold. It is what `CATCH3_MOCK_METHOD` declares, and on its own it + /// stands in for a free function behind a seam. Its destructor verifies + /// it. + template<typename Result, typename... Arguments> + class MockFunction<Result(Arguments...)> final + { + using Expectation = MockExpectation<Result(Arguments...)>; + + public: + /// Declares an expectation at `line`; called with what the arguments + /// have to match. See `CATCH3_EXPECT_CALL`. + class ExpectationStart final + { + public: + template<typename... Expected> + Expectation & + operator()(Expected &&...expected) + { + constexpr auto arguments = sizeof...(Argu |