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 /src/Progmasoft/Mock.cpp | |
| 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.
Diffstat (limited to 'src/Progmasoft/Mock.cpp')
| -rw-r--r-- | src/Progmasoft/Mock.cpp | 203 |
1 files changed, 203 insertions, 0 deletions
diff --git a/src/Progmasoft/Mock.cpp b/src/Progmasoft/Mock.cpp new file mode 100644 index 00000000..2897838c --- /dev/null +++ b/src/Progmasoft/Mock.cpp @@ -0,0 +1,203 @@ +// Copyright (c) 2026 Progmasoft. +// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 + +#include <Progmasoft/Catch3/Interfaces/IResultCapture.hpp> +#include <Progmasoft/Catch3/Internal/AssertionHandler.hpp> +#include <Progmasoft/Catch3/Internal/CompilerCapabilities.hpp> +#include <Progmasoft/Catch3/Internal/Context.hpp> +#include <Progmasoft/Catch3/Mock.hpp> +#include <algorithm> +#include <cstdio> +#include <cstdlib> +#include <exception> + +namespace Progmasoft::Catch3::Detail +{ + namespace + { + constexpr auto kMacro = "CATCH3_EXPECT_CALL"; + + /// Whether a test case is running that a report can belong to. + [[nodiscard]] bool + InTestCase() + { + const auto *capture = Catch::getCurrentContext().getResultCapture(); + return capture != nullptr && !capture->getCurrentTestName().empty(); + } + + void + Report(const Catch::SourceLineInfo &line, + const std::string &mock, + Catch::ResultWas::OfType result, + std::string message, + Catch::ResultDisposition::Flags disposition) + { + Catch::AssertionHandler handler(kMacro, line, mock, disposition); + handler.handleMessage(result, std::move(message)); + handler.complete(); + } + + void + WriteToStandardError(const Catch::SourceLineInfo &line, + const std::string &mock, + const std::string &message) + { + std::fprintf(stderr, + "%s:%zu: %s: %s\n", + line.file, + line.line, + mock.c_str(), + message.c_str()); + } + } // namespace + + void + MockSequence::Add(const std::shared_ptr<MockExpectationState> &step) + { + steps.push_back(step); + } + + std::string + MockSequence::Enter(const MockExpectationState &step) + { + const auto position + = std::find_if(steps.begin() + static_cast<std::ptrdiff_t>(current), + steps.end(), + [&step](const auto &candidate) { + return candidate.lock().get() == &step; + }); + std::string undone; + if (position == steps.end()) + { + return undone; + } + const auto target = static_cast<std::size_t>(position - steps.begin()); + for (; current < target; ++current) + { + const auto earlier = steps[current].lock(); + if (earlier == nullptr) + { + continue; + } + if (!earlier->Satisfied()) + { + undone += (undone.empty() ? "call out of order: " : "\n and ") + + step.Description + " came before " + + earlier->Description; + } + earlier->Retired = true; + } + return undone; + } + + std::unique_lock<std::recursive_mutex> + LockMocks() + { + static std::recursive_mutex mutex; + return std::unique_lock<std::recursive_mutex>{ mutex }; + } + + void + ReportMockFailure(const Catch::SourceLineInfo &line, + const std::string &mock, + std::string message, + MockReport report) + { + if (!InTestCase()) + { + WriteToStandardError(line, mock, message); + return; + } + // An aborting run throws out of any failure, and a destructor must + // not let that through. + CATCH_TRY + { + Report(line, + mock, + Catch::ResultWas::ExplicitFailure, + std::move(message), + Catch::ResultDisposition::ContinueOnFailure); + } + CATCH_CATCH_ALL + { + if (report == MockReport::kContinue) + { + std::rethrow_exception(std::current_exception()); + } + } + } + + void + AbortMockCall(const Catch::SourceLineInfo &line, + const std::string &mock, + std::string message) + { + if (InTestCase()) + { + Report(line, + mock, + Catch::ResultWas::ExplicitFailure, + std::move(message), + Catch::ResultDisposition::Normal); + } + else + { + WriteToStandardError(line, mock, message); + } + std::abort(); + } + + void + ReportMockSuccess(const Catch::SourceLineInfo &line, + const std::string &mock, + std::string message) + { + if (InTestCase()) + { + Report(line, + mock, + Catch::ResultWas::Ok, + std::move(message), + Catch::ResultDisposition::ContinueOnFailure); + } + } + + std::string + DescribeCalls(std::size_t minimum, std::size_t maximum) + { + const auto calls = [](std::size_t count) { + return std::to_string(count) + (count == 1 ? " call" : " calls"); + }; + if (minimum == 0 && maximum == kUnlimitedCalls) + { + return "any number of calls"; + } + if (minimum == maximum) + { + return maximum == 0 ? "no calls" : "exactly " + calls(minimum); + } + if (maximum == kUnlimitedCalls) + { + return "at least " + calls(minimum); + } + if (minimum == 0) + { + return "at most " + calls(maximum); + } + return std::to_string(minimum) + " to " + calls(maximum); + } + + std::string + DescribeCount(std::size_t calls) + { + switch (calls) + { + case 0: + return "never called"; + case 1: + return "called once"; + default: + return "called " + std::to_string(calls) + " times"; + } + } +} // namespace Progmasoft::Catch3::Detail |