diff options
| author | Leitwolf11 | 2026-11-01 20:05:56 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-11-01 20:05:56 +0300 |
| commit | 8603d31fd8c1a72e30cb444f278d7a0592383ba7 (patch) | |
| tree | 496d26ff659e92cdf4e6fe27a4aa47bf52818052 | |
| parent | 2aea9dafd283d69bec459ea0d495c25d1f6a0f94 (diff) | |
| download | catch3-feature/mocking.tar.gz | |
Answer the review of the mocksfeature/mocking
A report no longer throws, so a noexcept mock survives an aborting run.
A call that has nothing to return still leaves the test case, which ends
the process in a noexcept function; the documentation says so.
RunContext clears the result capture when it goes, so a mock of static
lifetime finds none and writes to standard error.
Decision owns its source line, since Verify can free the expectation
while a call is on its way. Moving a result out takes the lock. Vector
matchers refer to their vector, so they are rejected at compile time
like composed ones. A null text expectation accepts only null, and
integers compare by value, so 256 no longer accepts an unsigned char.
| -rw-r--r-- | docs/progmasoft-mocking.md | 10 | ||||
| -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-- | src/Progmasoft/Catch3/Internal/RunContext.cpp | 5 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3/Mock.hpp | 137 | ||||
| -rw-r--r-- | src/Progmasoft/Mock.cpp | 15 | ||||
| -rw-r--r-- | tests/Progmasoft/Mock.tests.cpp | 75 | ||||
| -rw-r--r-- | tests/Progmasoft/RunnerFixture.cpp | 9 |
9 files changed, 212 insertions, 50 deletions
diff --git a/docs/progmasoft-mocking.md b/docs/progmasoft-mocking.md index 98c372ec..f79a537e 100644 --- a/docs/progmasoft-mocking.md +++ b/docs/progmasoft-mocking.md @@ -67,14 +67,15 @@ 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 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, so it does not compile here; put it in a -`Where`. +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: @@ -91,7 +92,8 @@ 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`. +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. 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/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 index f100041c..59ad8bb9 100644 --- a/src/Progmasoft/Catch3/Mock.hpp +++ b/src/Progmasoft/Catch3/Mock.hpp @@ -6,6 +6,7 @@ #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> @@ -13,6 +14,7 @@ #include <limits> #include <memory> #include <mutex> +#include <optional> #include <string> #include <tuple> #include <type_traits> @@ -87,22 +89,16 @@ namespace Progmasoft::Catch3 [[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. + /// 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, - MockReport report); + std::string message); /// Fails the running test case and leaves it, for a call nothing can - /// return from. + /// return from. In a `noexcept` function that ends the process. [[noreturn]] void AbortMockCall(const Catch::SourceLineInfo &line, const std::string &mock, @@ -170,6 +166,66 @@ namespace Progmasoft::Catch3 { }; + /// 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 @@ -221,6 +277,10 @@ namespace Progmasoft::Catch3 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(); @@ -235,6 +295,23 @@ namespace Progmasoft::Catch3 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) { @@ -250,12 +327,14 @@ namespace Progmasoft::Catch3 std::move(description) }; } else if constexpr (std::is_integral_v<Argument> - && std::is_integral_v<Stored>) + && 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 auto value = static_cast<Argument>(expected); + const Stored value = expected; return { [value](const Argument &argument) { - return argument == value; + return SameInteger(argument, value); }, Catch::Detail::stringify(value) }; } @@ -383,6 +462,8 @@ namespace Progmasoft::Catch3 auto held = std::make_shared<Result>(std::forward<Value>(value)); return Does([held](Arguments...) -> Result { + // Calls can come from several threads. + const auto lock = Detail::LockMocks(); return std::move(*held); }); } @@ -526,7 +607,7 @@ namespace Progmasoft::Catch3 // and what it did not get to call says nothing more. if (std::uncaught_exceptions() <= exceptions) { - VerifyAll(Detail::MockReport::kInDestructor); + VerifyAll(); } } @@ -541,7 +622,7 @@ namespace Progmasoft::Catch3 void Verify() { - VerifyAll(Detail::MockReport::kContinue); + VerifyAll(); } Result @@ -549,7 +630,7 @@ namespace Progmasoft::Catch3 { const Decision decision = Decide( std::tuple<const std::decay_t<Arguments> &...>{ arguments... }); - if (decision.line == nullptr) + if (!decision.line) { return Refuse(decision.message); } @@ -557,8 +638,7 @@ namespace Progmasoft::Catch3 { Detail::ReportMockFailure(*decision.line, name, - decision.message, - Detail::MockReport::kContinue); + decision.message); } if (decision.action != nullptr) { @@ -596,7 +676,7 @@ namespace Progmasoft::Catch3 struct Decision final { // Empty when the call is refused. - const Catch::SourceLineInfo *line = nullptr; + std::optional<Catch::SourceLineInfo> line; std::shared_ptr<std::function<Result(Arguments...)>> action; std::string message; }; @@ -687,7 +767,7 @@ namespace Progmasoft::Catch3 auto &state = *expectation.state; ++state.Calls; Decision decision; - decision.line = &state.Line; + decision.line = state.Line; decision.action = expectation.action; for (const auto &sequence : state.Sequences) { @@ -732,17 +812,11 @@ namespace Progmasoft::Catch3 const auto line = FirstLine(); if constexpr (std::is_void_v<Result>) { - Detail::ReportMockFailure(line, - name, - message, - Detail::MockReport::kContinue); + Detail::ReportMockFailure(line, name, message); } else if constexpr (std::is_default_constructible_v<Result>) { - Detail::ReportMockFailure(line, - name, - message, - Detail::MockReport::kContinue); + Detail::ReportMockFailure(line, name, message); return Result{}; } else @@ -762,7 +836,7 @@ namespace Progmasoft::Catch3 } void - VerifyAll(Detail::MockReport report) + VerifyAll() { std::vector<std::unique_ptr<Expectation>> checked; { @@ -786,8 +860,7 @@ namespace Progmasoft::Catch3 "unmet expectation: " + state.Description + " needs " + Detail::DescribeCalls(state.Minimum, state.Maximum) - + ", " + Detail::DescribeCount(state.Calls), - report); + + ", " + Detail::DescribeCount(state.Calls)); } } } diff --git a/src/Progmasoft/Mock.cpp b/src/Progmasoft/Mock.cpp index b1364450..bc7945f5 100644 --- a/src/Progmasoft/Mock.cpp +++ b/src/Progmasoft/Mock.cpp @@ -9,7 +9,6 @@ #include <algorithm> #include <cstdio> #include <cstdlib> -#include <exception> namespace Progmasoft::Catch3::Detail { @@ -101,16 +100,15 @@ namespace Progmasoft::Catch3::Detail void ReportMockFailure(const Catch::SourceLineInfo &line, const std::string &mock, - std::string message, - MockReport report) + std::string message) { if (!InTestCase()) { WriteToStandardError(line, mock, message); return; } - // An aborting run throws out of any failure, and a destructor must - // not let that through. + // An aborting run throws out of any failure. The run still stops, at + // its next assertion. CATCH_TRY { Report(line, @@ -120,12 +118,7 @@ namespace Progmasoft::Catch3::Detail Catch::ResultDisposition::ContinueOnFailure); } CATCH_CATCH_ALL - { - if (report == MockReport::kContinue) - { - std::rethrow_exception(std::current_exception()); - } - } + {} } void diff --git a/tests/Progmasoft/Mock.tests.cpp b/tests/Progmasoft/Mock.tests.cpp index 0329bc41..a5213e51 100644 --- a/tests/Progmasoft/Mock.tests.cpp +++ b/tests/Progmasoft/Mock.tests.cpp @@ -5,6 +5,7 @@ #include <Progmasoft/Catch3/Matchers/String.hpp> #include <Progmasoft/Catch3/Mock.hpp> #include <Progmasoft/Catch3/TestMacros.hpp> +#include <atomic> #include <functional> #include <map> #include <memory> @@ -242,6 +243,80 @@ TEST_CASE("Strings match by value, whatever their type", "[progmasoft][mock]") size(3); } +TEST_CASE("A null text expectation accepts only null", "[progmasoft][mock]") +{ + const char *const missing = nullptr; + CATCH3_MOCK_FUNCTION(pointer, int(const char *)); + CATCH3_MOCK_FUNCTION(text, int(const std::string &)); + CATCH3_EXPECT_CALL(pointer)(missing).Returns(1); + CATCH3_EXPECT_CALL(pointer)("text").Returns(2); + CATCH3_EXPECT_CALL(text)(missing).AnyTimes().Returns(1); + CATCH3_EXPECT_CALL(text)(Any).Returns(2); + + CHECK(pointer(nullptr) == 1); + CHECK(pointer("text") == 2); + CHECK(text("") == 2); +} + +TEST_CASE("Integers match by value, whatever their types", "[progmasoft][mock]") +{ + CATCH3_MOCK_FUNCTION(narrow, int(unsigned char)); + CATCH3_MOCK_FUNCTION(unsignedOne, int(unsigned)); + CATCH3_MOCK_FUNCTION(signedOne, int(int)); + CATCH3_MOCK_FUNCTION(flag, int(bool)); + CATCH3_EXPECT_CALL(narrow)(256).AnyTimes().Returns(1); + CATCH3_EXPECT_CALL(narrow)(Any).Returns(2); + CATCH3_EXPECT_CALL(unsignedOne)(-1).AnyTimes().Returns(1); + CATCH3_EXPECT_CALL(unsignedOne)(7).Returns(2); + CATCH3_EXPECT_CALL(unsignedOne)(Any).Returns(3); + CATCH3_EXPECT_CALL(signedOne)(4294967295U).AnyTimes().Returns(1); + CATCH3_EXPECT_CALL(signedOne)(Any).Returns(2); + CATCH3_EXPECT_CALL(flag)(true).Returns(1); + + CHECK(narrow(0) == 2); + CHECK(unsignedOne(7U) == 2); + CHECK(unsignedOne(4294967295U) == 3); + CHECK(signedOne(-1) == 2); + CHECK(flag(true) == 1); +} + +TEST_CASE("Calls from other threads share a result that can only move", + "[progmasoft][mock]") +{ + CATCH3_MOCK_FUNCTION(take, std::unique_ptr<int>()); + CATCH3_EXPECT_CALL(take)().Times(400).Returns(std::make_unique<int>(1)); + std::atomic<int> taken{ 0 }; + std::vector<std::thread> threads; + for (int thread = 0; thread < 4; ++thread) + { + threads.emplace_back([&take, &taken] { + for (int call = 0; call < 100; ++call) + { + if (take() != nullptr) + { + ++taken; + } + } + }); + } + for (auto &thread : threads) + { + thread.join(); + } + CHECK(taken == 1); +} + +TEST_CASE("A mock can outlive the run", "[progmasoft][mock]") +{ + const Scratch scratch; + const auto run + = RunFixture(scratch, { "--no-history", "[mock-exit]", "--jobs=1" }); + INFO(run.output); + CHECK(ExitCode(run) == 0); + CHECK(run.Test("fixture mock outlives the run")["outcome"].AsString() + == "passed"); +} + TEST_CASE("Calls from other threads are counted", "[progmasoft][mock]") { CATCH3_MOCK_FUNCTION(tick, int()); diff --git a/tests/Progmasoft/RunnerFixture.cpp b/tests/Progmasoft/RunnerFixture.cpp index 2796912f..3a61d05c 100644 --- a/tests/Progmasoft/RunnerFixture.cpp +++ b/tests/Progmasoft/RunnerFixture.cpp @@ -251,6 +251,15 @@ TEST_CASE("fixture mock left by a failure", "[fixture][mock]") FAIL("the test's own failure"); } +TEST_CASE("fixture mock outlives the run", "[fixture][mock-exit]") +{ + static auto *mock = new Progmasoft::Catch3::MockFunction<void()>{ "late" }; + CATCH3_EXPECT_CALL (*mock)(); + std::atexit([] { + delete mock; + }); +} + TEST_CASE("fixture skips", "[fixture][skip]") { SKIP("not on this machine"); |