Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLeitwolf2026-11-01 21:19:33 +0300
committerGitHub2026-11-01 21:19:33 +0300
commit5cbd481125b881a4c2eed74050be9899eb3f23a7 (patch)
tree496d26ff659e92cdf4e6fe27a4aa47bf52818052
parent26a1588ae3ae183ae5c900caf3c294bdcb3f8855 (diff)
parent8603d31fd8c1a72e30cb444f278d7a0592383ba7 (diff)
downloadcatch3-5cbd481125b881a4c2eed74050be9899eb3f23a7.tar.gz
Merge pull request #30 from Progmasoft/feature/mocking
Add mocks that report through the assertion path
-rw-r--r--README.md4
-rw-r--r--docs/Readme.md1
-rw-r--r--docs/progmasoft-mocking.md142
-rw-r--r--docs/progmasoft-roadmap.md11
-rw-r--r--docs/why-catch3.md3
-rw-r--r--extras/catch_amalgamated.cpp7
-rw-r--r--extras/catch_amalgamated.hpp2
-rw-r--r--extras/catch_amalgamated_third_party.hpp2
-rw-r--r--premake5.lua1
-rw-r--r--src/CMakeLists.txt1
-rw-r--r--src/Progmasoft/Catch3.hpp1
-rw-r--r--src/Progmasoft/Catch3/Internal/RunContext.cpp5
-rw-r--r--src/Progmasoft/Catch3/Mock.hpp1029
-rw-r--r--src/Progmasoft/Catch3/meson.build1
-rw-r--r--src/Progmasoft/Mock.cpp197
-rw-r--r--src/Progmasoft/meson.build1
-rw-r--r--tests/CMakeLists.txt1
-rw-r--r--tests/Progmasoft/BUILD.bazel1
-rw-r--r--tests/Progmasoft/Mock.tests.cpp405
-rw-r--r--tests/Progmasoft/RunnerFixture.cpp76
-rw-r--r--tests/meson.build1
-rw-r--r--xmake.lua1
22 files changed, 1884 insertions, 9 deletions
diff --git a/README.md b/README.md
index 14ee4bdf..510c1fe4 100644
--- a/README.md
+++ b/README.md
@@ -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