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

summaryrefslogtreecommitdiffhomepage
path: root/src/Progmasoft/Mock.cpp
diff options
context:
space:
mode:
authorLeitwolf2026-11-01 21:19:33 +0300
committerGitHub2026-11-01 21:19:33 +0300
commit5cbd481125b881a4c2eed74050be9899eb3f23a7 (patch)
tree496d26ff659e92cdf4e6fe27a4aa47bf52818052 /src/Progmasoft/Mock.cpp
parent26a1588ae3ae183ae5c900caf3c294bdcb3f8855 (diff)
parent8603d31fd8c1a72e30cb444f278d7a0592383ba7 (diff)
downloadcatch3-5cbd481125b881a4c2eed74050be9899eb3f23a7.tar.gz
Merge pull request #30 from Progmasoft/feature/mocking
Add mocks that report through the assertion path
Diffstat (limited to 'src/Progmasoft/Mock.cpp')
-rw-r--r--src/Progmasoft/Mock.cpp197
1 files changed, 197 insertions, 0 deletions
diff --git a/src/Progmasoft/Mock.cpp b/src/Progmasoft/Mock.cpp
new file mode 100644
index 00000000..bc7945f5
--- /dev/null
+++ b/src/Progmasoft/Mock.cpp
@@ -0,0 +1,197 @@
+// 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>
+
+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()
+ {
+ // Never destroyed: a mock of static lifetime can go after it would.
+ static auto *mutex = new std::recursive_mutex;
+ return std::unique_lock<std::recursive_mutex>{ *mutex };
+ }
+
+ void
+ ReportMockFailure(const Catch::SourceLineInfo &line,
+ const std::string &mock,
+ std::string message)
+ {
+ if (!InTestCase())
+ {
+ WriteToStandardError(line, mock, message);
+ return;
+ }
+ // An aborting run throws out of any failure. The run still stops, at
+ // its next assertion.
+ CATCH_TRY
+ {
+ Report(line,
+ mock,
+ Catch::ResultWas::ExplicitFailure,
+ std::move(message),
+ Catch::ResultDisposition::ContinueOnFailure);
+ }
+ CATCH_CATCH_ALL
+ {}
+ }
+
+ 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