diff --git a/src/util/Assert.hpp b/src/util/Assert.hpp index 93a1d166..70782b20 100644 --- a/src/util/Assert.hpp +++ b/src/util/Assert.hpp @@ -22,6 +22,7 @@ #include "util/SourceLocation.hpp" #include +#include #include #include diff --git a/src/util/async/AnyExecutionContext.hpp b/src/util/async/AnyExecutionContext.hpp index 7baefe77..f98e6623 100644 --- a/src/util/async/AnyExecutionContext.hpp +++ b/src/util/async/AnyExecutionContext.hpp @@ -85,15 +85,15 @@ public: [[nodiscard]] auto execute(SomeHandlerWithoutStopToken auto&& fn) { - using RetType = std::decay_t; + using RetType = std::decay_t>; static_assert(not std::is_same_v); - return AnyOperation(pimpl_->execute([fn = std::forward(fn)]() -> std::any { + return AnyOperation(pimpl_->execute([fn = std::forward(fn)] mutable -> std::any { if constexpr (std::is_void_v) { - fn(); + std::invoke(std::forward(fn)); return {}; } else { - return std::make_any(fn()); + return std::make_any(std::invoke(std::forward(fn))); } })); } @@ -109,17 +109,19 @@ public: [[nodiscard]] auto execute(SomeHandlerWith auto&& fn) { - using RetType = std::decay_t()))>; + using RetType = std::decay_t>; static_assert(not std::is_same_v); - return AnyOperation(pimpl_->execute([fn = std::forward(fn)](auto stopToken) -> std::any { - if constexpr (std::is_void_v) { - fn(std::move(stopToken)); - return {}; - } else { - return std::make_any(fn(std::move(stopToken))); - } - })); + return AnyOperation( + pimpl_->execute([fn = std::forward(fn)](auto stopToken) mutable -> std::any { + if constexpr (std::is_void_v) { + std::invoke(std::forward(fn), std::move(stopToken)); + return {}; + } else { + return std::make_any(std::invoke(std::forward(fn), std::move(stopToken))); + } + }) + ); } /** @@ -134,16 +136,16 @@ public: [[nodiscard]] auto execute(SomeHandlerWith auto&& fn, SomeStdDuration auto timeout) { - using RetType = std::decay_t()))>; + using RetType = std::decay_t>; static_assert(not std::is_same_v); return AnyOperation(pimpl_->execute( - [fn = std::forward(fn)](auto stopToken) -> std::any { + [fn = std::forward(fn)](auto stopToken) mutable -> std::any { if constexpr (std::is_void_v) { - fn(std::move(stopToken)); + std::invoke(std::forward(fn), std::move(stopToken)); return {}; } else { - return std::make_any(fn(std::move(stopToken))); + return std::make_any(std::invoke(std::forward(fn), std::move(stopToken))); } }, std::chrono::duration_cast(timeout) @@ -162,17 +164,17 @@ public: [[nodiscard]] auto scheduleAfter(SomeStdDuration auto delay, SomeHandlerWith auto&& fn) { - using RetType = std::decay_t()))>; + using RetType = std::decay_t>; static_assert(not std::is_same_v); auto const millis = std::chrono::duration_cast(delay); return AnyOperation( - pimpl_->scheduleAfter(millis, [fn = std::forward(fn)](auto stopToken) -> std::any { + pimpl_->scheduleAfter(millis, [fn = std::forward(fn)](auto stopToken) mutable -> std::any { if constexpr (std::is_void_v) { - fn(std::move(stopToken)); + std::invoke(std::forward(fn), std::move(stopToken)); return {}; } else { - return std::make_any(fn(std::move(stopToken))); + return std::make_any(std::invoke(std::forward(fn), std::move(stopToken))); } }) ); @@ -191,17 +193,19 @@ public: [[nodiscard]] auto scheduleAfter(SomeStdDuration auto delay, SomeHandlerWith auto&& fn) { - using RetType = std::decay_t(), true))>; + using RetType = std::decay_t>; static_assert(not std::is_same_v); auto const millis = std::chrono::duration_cast(delay); return AnyOperation(pimpl_->scheduleAfter( - millis, [fn = std::forward(fn)](auto stopToken, auto cancelled) -> std::any { + millis, [fn = std::forward(fn)](auto stopToken, auto cancelled) mutable -> std::any { if constexpr (std::is_void_v) { - fn(std::move(stopToken), cancelled); + std::invoke(std::forward(fn), std::move(stopToken), cancelled); return {}; } else { - return std::make_any(fn(std::move(stopToken), cancelled)); + return std::make_any( + std::invoke(std::forward(fn), std::move(stopToken), cancelled) + ); } } )); @@ -217,18 +221,30 @@ public: [[nodiscard]] auto executeRepeatedly(SomeStdDuration auto interval, SomeHandlerWithoutStopToken auto&& fn) { - using RetType = std::decay_t; + using RetType = std::decay_t>; static_assert(not std::is_same_v); auto const millis = std::chrono::duration_cast(interval); return AnyOperation( // - pimpl_->executeRepeatedly(millis, [fn = std::forward(fn)] -> std::any { - fn(); + pimpl_->executeRepeatedly(millis, [fn = std::forward(fn)] mutable -> std::any { + std::invoke(std::forward(fn)); return {}; }) ); } + /** + * @brief Schedule an operation on the execution context without expectations of a result + * @note Exceptions are caught internally and `ASSERT`ed on + * + * @param fn The block of code to execute + */ + void + submit(SomeHandlerWithoutStopToken auto&& fn) + { + pimpl_->submit(std::forward(fn)); + } + /** * @brief Make a strand for this execution context * @@ -276,6 +292,7 @@ private: virtual impl::ErasedOperation scheduleAfter(std::chrono::milliseconds, std::function) = 0; virtual impl::ErasedOperation executeRepeatedly(std::chrono::milliseconds, std::function) = 0; + virtual void submit(std::function) = 0; virtual AnyStrand makeStrand() = 0; virtual void @@ -323,6 +340,12 @@ private: return ctx.executeRepeatedly(interval, std::move(fn)); } + void + submit(std::function fn) override + { + return ctx.submit(std::move(fn)); + } + AnyStrand makeStrand() override { diff --git a/src/util/async/AnyStrand.hpp b/src/util/async/AnyStrand.hpp index 170721c2..d60db3a3 100644 --- a/src/util/async/AnyStrand.hpp +++ b/src/util/async/AnyStrand.hpp @@ -64,16 +64,16 @@ public: [[nodiscard]] auto execute(SomeHandlerWithoutStopToken auto&& fn) { - using RetType = std::decay_t; + using RetType = std::decay_t>; static_assert(not std::is_same_v); return AnyOperation( // - pimpl_->execute([fn = std::forward(fn)]() -> std::any { + pimpl_->execute([fn = std::forward(fn)] mutable -> std::any { if constexpr (std::is_void_v) { - fn(); + std::invoke(std::forward(fn)); return {}; } else { - return std::make_any(fn()); + return std::make_any(std::invoke(std::forward(fn))); } }) ); @@ -88,16 +88,16 @@ public: [[nodiscard]] auto execute(SomeHandlerWith auto&& fn) { - using RetType = std::decay_t()))>; + using RetType = std::decay_t>; static_assert(not std::is_same_v); return AnyOperation( // - pimpl_->execute([fn = std::forward(fn)](auto stopToken) -> std::any { + pimpl_->execute([fn = std::forward(fn)](auto stopToken) mutable -> std::any { if constexpr (std::is_void_v) { - fn(std::move(stopToken)); + std::invoke(std::forward(fn), std::move(stopToken)); return {}; } else { - return std::make_any(fn(std::move(stopToken))); + return std::make_any(std::invoke(std::forward(fn), std::move(stopToken))); } }) ); @@ -113,17 +113,19 @@ public: [[nodiscard]] auto execute(SomeHandlerWith auto&& fn, SomeStdDuration auto timeout) { - using RetType = std::decay_t()))>; + using RetType = std::decay_t>; static_assert(not std::is_same_v); return AnyOperation( // pimpl_->execute( - [fn = std::forward(fn)](auto stopToken) -> std::any { + [fn = std::forward(fn)](auto stopToken) mutable -> std::any { if constexpr (std::is_void_v) { - fn(std::move(stopToken)); + std::invoke(std::forward(fn), std::move(stopToken)); return {}; } else { - return std::make_any(fn(std::move(stopToken))); + return std::make_any( + std::invoke(std::forward(fn), std::move(stopToken)) + ); } }, std::chrono::duration_cast(timeout) @@ -141,18 +143,30 @@ public: [[nodiscard]] auto executeRepeatedly(SomeStdDuration auto interval, SomeHandlerWithoutStopToken auto&& fn) { - using RetType = std::decay_t; + using RetType = std::decay_t>; static_assert(not std::is_same_v); auto const millis = std::chrono::duration_cast(interval); return AnyOperation( // - pimpl_->executeRepeatedly(millis, [fn = std::forward(fn)] -> std::any { - fn(); + pimpl_->executeRepeatedly(millis, [fn = std::forward(fn)] mutable -> std::any { + std::invoke(std::forward(fn)); return {}; }) ); } + /** + * @brief Schedule an operation on the execution context without expectations of a result + * @note Exceptions are caught internally and `ASSERT`ed on + * + * @param fn The block of code to execute + */ + void + submit(SomeHandlerWithoutStopToken auto&& fn) + { + pimpl_->submit(std::forward(fn)); + } + private: struct Concept { virtual ~Concept() = default; @@ -165,6 +179,7 @@ private: [[nodiscard]] virtual impl::ErasedOperation execute(std::function) = 0; [[nodiscard]] virtual impl::ErasedOperation executeRepeatedly(std::chrono::milliseconds, std::function) = 0; + virtual void submit(std::function) = 0; }; template @@ -194,6 +209,12 @@ private: { return strand.executeRepeatedly(interval, std::move(fn)); } + + void + submit(std::function fn) override + { + return strand.submit(std::move(fn)); + } }; private: diff --git a/src/util/async/README.md b/src/util/async/README.md index c01a2869..770529e5 100644 --- a/src/util/async/README.md +++ b/src/util/async/README.md @@ -91,11 +91,14 @@ Scheduled operations can be aborted by calling ### Error handling -By default, exceptions that happen during the execution of user-provided code are caught and returned in the error channel of `std::expected` as an instance of the `ExecutionError` struct. The user can then extract the error message by calling `what()` or directly accessing the `message` member. +For APIs that return an Operation, by default, exceptions that happen during the execution of user-provided code are caught and returned in the error channel of `std::expected` as an instance of the `ExecutionError` struct. The user can then extract the error message by calling `what()` or directly accessing the `message` member. +In the `submit` API however, exceptions are caught and `ASSERT`ed on. ### Returned value -If the user-provided lambda returns anything but `void`, the type and value will propagate through the operation object and can be received by calling `get` which will block until a value or an error is available. +For `submit` API the return type is always `void`. + +For other APIs, if the user-provided lambda returns anything but `void`, the type and value will propagate through the operation object and can be received by calling `get` which will block until a value or an error is available. The `wait` member function can be used when the user just wants to wait for the value to become available but not necessarily getting at the value just yet. @@ -122,6 +125,12 @@ This section provides some examples. For more examples take a look at `Execution ### Regular operation +#### One shot tasks + +```cpp +ctx.submit([]() { /* do something */ }); +``` + #### Awaiting and reading values ```cpp diff --git a/src/util/async/context/BasicExecutionContext.hpp b/src/util/async/context/BasicExecutionContext.hpp index 288e9314..5fab8fdf 100644 --- a/src/util/async/context/BasicExecutionContext.hpp +++ b/src/util/async/context/BasicExecutionContext.hpp @@ -138,7 +138,8 @@ class BasicExecutionContext { public: /** @brief Whether operations on this execution context are noexcept */ - static constexpr bool kIS_NOEXCEPT = noexcept(ErrorHandlerType::wrap([](auto&) { throw 0; })); + static constexpr bool kIS_NOEXCEPT = noexcept(ErrorHandlerType::wrap([](auto&) { throw 0; })) and + noexcept(ErrorHandlerType::catchAndAssert([] { throw 0; })); using ContextHolderType = ContextType; @@ -209,17 +210,17 @@ public: delay, std::forward(fn), timeout ); } else { - using FnRetType = std::decay_t()))>; + using FnRetType = std::decay_t>; return ScheduledOperation( impl::extractAssociatedExecutor(*this), delay, [this, timeout, fn = std::forward(fn)](auto) mutable { return this->execute( - [fn = std::forward(fn)](auto stopToken) { + [fn = std::forward(fn)](auto stopToken) mutable { if constexpr (std::is_void_v) { - fn(std::move(stopToken)); + std::invoke(std::forward(fn), std::move(stopToken)); } else { - return fn(std::move(stopToken)); + return std::invoke(std::forward(fn), std::move(stopToken)); } }, timeout @@ -249,18 +250,18 @@ public: delay, std::forward(fn), timeout ); } else { - using FnRetType = std::decay_t(), true))>; + using FnRetType = std::decay_t>; return ScheduledOperation( impl::extractAssociatedExecutor(*this), delay, [this, timeout, fn = std::forward(fn)](auto ec) mutable { return this->execute( [fn = std::forward(fn), - isAborted = (ec == boost::asio::error::operation_aborted)](auto stopToken) { + isAborted = (ec == boost::asio::error::operation_aborted)](auto stopToken) mutable { if constexpr (std::is_void_v) { - fn(std::move(stopToken), isAborted); + std::invoke(std::forward(fn), std::move(stopToken), isAborted); } else { - return fn(std::move(stopToken), isAborted); + return std::invoke(std::forward(fn), std::move(stopToken), isAborted); } }, timeout @@ -310,12 +311,12 @@ public: [[maybe_unused]] auto timeoutHandler = impl::getTimeoutHandleIfNeeded(TimerContextProvider::getContext(*this), timeout, stopSource); - using FnRetType = std::decay_t()))>; + using FnRetType = std::decay_t>; if constexpr (std::is_void_v) { - fn(std::move(stopToken)); + std::invoke(std::forward(fn), std::move(stopToken)); outcome.setValue(); } else { - outcome.setValue(fn(std::move(stopToken))); + outcome.setValue(std::invoke(std::forward(fn), std::move(stopToken))); } }) ); @@ -350,17 +351,29 @@ public: context_, impl::outcomeForHandler(fn), ErrorHandlerType::wrap([fn = std::forward(fn)](auto& outcome) mutable { - using FnRetType = std::decay_t; + using FnRetType = std::decay_t>; if constexpr (std::is_void_v) { - fn(); + std::invoke(std::forward(fn)); outcome.setValue(); } else { - outcome.setValue(fn()); + outcome.setValue(std::invoke(std::forward(fn))); } }) ); } + /** + * @brief Schedule an operation on the execution context without expectations of a result + * @note Exceptions are caught internally and `ASSERT`ed on + * + * @param fn The block of code to execute + */ + void + submit(SomeHandlerWithoutStopToken auto&& fn) noexcept(kIS_NOEXCEPT) + { + DispatcherType::post(context_, ErrorHandlerType::catchAndAssert(fn)); + } + /** * @brief Create a strand for this execution context * diff --git a/src/util/async/context/impl/Execution.hpp b/src/util/async/context/impl/Execution.hpp index 8743aa07..020773ba 100644 --- a/src/util/async/context/impl/Execution.hpp +++ b/src/util/async/context/impl/Execution.hpp @@ -30,68 +30,89 @@ namespace util::async::impl { struct SpawnDispatchStrategy { - template + template [[nodiscard]] static auto - dispatch(ContextType& ctx, OutcomeType&& outcome, auto&& fn) + dispatch(ContextType& ctx, OutcomeType&& outcome, FnType&& fn) { auto op = outcome.getOperation(); util::spawn( ctx.getExecutor(), - [outcome = std::forward(outcome), - fn = std::forward(fn)](auto yield) mutable { + [outcome = std::forward(outcome), fn = std::forward(fn)](auto yield) mutable { if constexpr (SomeStoppableOutcome) { auto& stopSource = outcome.getStopSource(); - fn(outcome, stopSource, stopSource[yield]); + std::invoke(std::forward(fn), outcome, stopSource, stopSource[yield]); } else { - fn(outcome); + std::invoke(std::forward(fn), outcome); } } ); return op; } + + template + static void + post(ContextType& ctx, FnType&& fn) + { + util::spawn(ctx.getExecutor(), [fn = std::forward(fn)](auto) mutable { + std::invoke(std::forward(fn)); + }); + } }; struct PostDispatchStrategy { - template + template [[nodiscard]] static auto - dispatch(ContextType& ctx, OutcomeType&& outcome, auto&& fn) + dispatch(ContextType& ctx, OutcomeType&& outcome, FnType&& fn) { auto op = outcome.getOperation(); boost::asio::post( - ctx.getExecutor(), - [outcome = std::forward(outcome), fn = std::forward(fn)]() mutable { + ctx.getExecutor(), [outcome = std::forward(outcome), fn = std::forward(fn)]() mutable { if constexpr (SomeStoppableOutcome) { auto& stopSource = outcome.getStopSource(); - fn(outcome, stopSource, stopSource.getToken()); + std::invoke(std::forward(fn), outcome, stopSource, stopSource.getToken()); } else { - fn(outcome); + std::invoke(std::forward(fn), outcome); } } ); return op; } + + template + static void + post(ContextType& ctx, FnType&& fn) + { + boost::asio::post(ctx.getExecutor(), std::forward(fn)); + } }; struct SyncDispatchStrategy { - template + template [[nodiscard]] static auto - dispatch([[maybe_unused]] ContextType& ctx, OutcomeType outcome, auto&& fn) + dispatch([[maybe_unused]] ContextType& ctx, OutcomeType outcome, FnType&& fn) { auto op = outcome.getOperation(); if constexpr (SomeStoppableOutcome) { auto& stopSource = outcome.getStopSource(); - fn(outcome, stopSource, stopSource.getToken()); + std::invoke(std::forward(fn), outcome, stopSource, stopSource.getToken()); } else { - fn(outcome); + std::invoke(std::forward(fn), outcome); } return op; } + + template + static void + post([[maybe_unused]] ContextType& ctx, FnType&& fn) + { + std::invoke(std::forward(fn)); + } }; } // namespace util::async::impl diff --git a/src/util/async/context/impl/Strand.hpp b/src/util/async/context/impl/Strand.hpp index cb0a38d8..dc060a32 100644 --- a/src/util/async/context/impl/Strand.hpp +++ b/src/util/async/context/impl/Strand.hpp @@ -81,12 +81,12 @@ public: TimerContextProvider::getContext(parentContext_.get()), timeout, stopSource ); - using FnRetType = std::decay_t()))>; + using FnRetType = std::decay_t>; if constexpr (std::is_void_v) { - fn(std::move(stopToken)); + std::invoke(std::forward(fn), std::move(stopToken)); outcome.setValue(); } else { - outcome.setValue(fn(std::move(stopToken))); + outcome.setValue(std::invoke(std::forward(fn), std::move(stopToken))); } }) ); @@ -108,12 +108,12 @@ public: context_, impl::outcomeForHandler(fn), ErrorHandlerType::wrap([fn = std::forward(fn)](auto& outcome) mutable { - using FnRetType = std::decay_t; + using FnRetType = std::decay_t>; if constexpr (std::is_void_v) { - fn(); + std::invoke(std::forward(fn)); outcome.setValue(); } else { - outcome.setValue(fn()); + outcome.setValue(std::invoke(std::forward(fn))); } }) ); @@ -128,6 +128,12 @@ public: return RepeatedOperation(impl::extractAssociatedExecutor(*this), interval, std::forward(fn)); } } + + void + submit(SomeHandlerWithoutStopToken auto&& fn) noexcept(kIS_NOEXCEPT) + { + DispatcherType::post(context_, ErrorHandlerType::catchAndAssert(fn)); + } }; } // namespace util::async::impl diff --git a/src/util/async/context/impl/Utils.hpp b/src/util/async/context/impl/Utils.hpp index 75280d7a..f722f836 100644 --- a/src/util/async/context/impl/Utils.hpp +++ b/src/util/async/context/impl/Utils.hpp @@ -29,6 +29,7 @@ #include #include +#include namespace util::async::impl { @@ -61,12 +62,12 @@ template outcomeForHandler(auto&& fn) { if constexpr (SomeHandlerWith) { - using FnRetType = decltype(fn(std::declval())); + using FnRetType = std::decay_t>; using RetType = std::expected; return StoppableOutcome(); } else { - using FnRetType = decltype(fn()); + using FnRetType = std::decay_t>; using RetType = std::expected; return Outcome(); diff --git a/src/util/async/impl/ErrorHandling.hpp b/src/util/async/impl/ErrorHandling.hpp index 8507c625..c7002b7d 100644 --- a/src/util/async/impl/ErrorHandling.hpp +++ b/src/util/async/impl/ErrorHandling.hpp @@ -19,6 +19,7 @@ #pragma once +#include "util/Assert.hpp" #include "util/async/Concepts.hpp" #include "util/async/Error.hpp" @@ -38,7 +39,7 @@ struct DefaultErrorHandler { return [fn = std::forward(fn)](SomeOutcome auto& outcome, Args&&... args) mutable { try { - fn(outcome, std::forward(args)...); + std::invoke(std::forward(fn), outcome, std::forward(args)...); } catch (std::exception const& e) { outcome.setValue( std::unexpected(ExecutionError{fmt::format("{}", std::this_thread::get_id()), e.what()}) @@ -50,6 +51,20 @@ struct DefaultErrorHandler { } }; } + + [[nodiscard]] static auto + catchAndAssert(auto&& fn) noexcept // note this is a lie when used with MockAssert (use MockAssertNoThrow) + { + return [fn = std::forward(fn)] mutable { + try { + std::invoke(std::forward(fn)); + } catch (std::exception const& e) { + ASSERT(false, "Exception caught: {}", e.what()); + } catch (...) { + ASSERT(false, "Unknown exception caught"); + } + }; + } }; struct NoErrorHandler { @@ -58,6 +73,12 @@ struct NoErrorHandler { { return std::forward(fn); } + + [[nodiscard]] static constexpr auto + catchAndAssert(auto&& fn) + { + return std::forward(fn); + } }; } // namespace util::async::impl diff --git a/tests/common/data/cassandra/FakesAndMocks.hpp b/tests/common/data/cassandra/FakesAndMocks.hpp index 4a40f3ab..f6d8c3a9 100644 --- a/tests/common/data/cassandra/FakesAndMocks.hpp +++ b/tests/common/data/cassandra/FakesAndMocks.hpp @@ -124,6 +124,6 @@ struct FakeRetryPolicy { void retry(Fn&& fn) { - fn(); + std::invoke(std::forward(fn)); } }; diff --git a/tests/common/util/MockAssert.hpp b/tests/common/util/MockAssert.hpp index 7820a1c3..c493de35 100644 --- a/tests/common/util/MockAssert.hpp +++ b/tests/common/util/MockAssert.hpp @@ -28,7 +28,6 @@ #include namespace common::util { - class WithMockAssert : virtual public testing::Test { public: struct MockAssertException { @@ -48,30 +47,42 @@ public: ~WithMockAssertNoThrow() override; }; +namespace impl { +template +struct MockGuard { + T mock; + ~MockGuard() + { + ::util::impl::OnAssert::resetAction(); + } +}; +} // namespace impl + } // namespace common::util -#define EXPECT_CLIO_ASSERT_FAIL_WITH_MESSAGE(statement, message_regex) \ - if (dynamic_cast(this) != nullptr) { \ - EXPECT_THROW( \ - { \ - try { \ - statement; \ - } catch (common::util::WithMockAssert::MockAssertException const& e) { \ - EXPECT_THAT(e.message, testing::ContainsRegex(message_regex)); \ - throw; \ - } \ - }, \ - common::util::WithMockAssert::MockAssertException \ - ); \ - } else if (dynamic_cast(this) != nullptr) { \ - testing::StrictMock> callMock; \ - ::util::impl::OnAssert::setAction([&callMock](std::string_view m) { callMock.Call(m); }); \ - EXPECT_CALL(callMock, Call(testing::ContainsRegex(message_regex))); \ - statement; \ - ::util::impl::OnAssert::resetAction(); \ - } else { \ - std::cerr << "EXPECT_CLIO_ASSERT_FAIL_WITH_MESSAGE() can be used only inside test body" << std::endl; \ - std::terminate(); \ +#define EXPECT_CLIO_ASSERT_FAIL_WITH_MESSAGE(statement, message_regex) \ + if (dynamic_cast(this) != nullptr) { \ + EXPECT_THROW( \ + { \ + try { \ + statement; \ + } catch (common::util::WithMockAssert::MockAssertException const& e) { \ + EXPECT_THAT(e.message, testing::ContainsRegex(message_regex)); \ + throw; \ + } \ + }, \ + common::util::WithMockAssert::MockAssertException \ + ); \ + } else if (dynamic_cast(this) != nullptr) { \ + using MockGuardType = \ + common::util::impl::MockGuard>>; \ + auto mockGuard = std::make_shared(); \ + ::util::impl::OnAssert::setAction([mockGuard](std::string_view m) { mockGuard->mock.Call(m); }); \ + EXPECT_CALL(mockGuard->mock, Call(testing::ContainsRegex(message_regex))); \ + statement; \ + } else { \ + std::cerr << "EXPECT_CLIO_ASSERT_FAIL_WITH_MESSAGE() can be used only inside test body" << std::endl; \ + std::terminate(); \ } #define EXPECT_CLIO_ASSERT_FAIL(statement) EXPECT_CLIO_ASSERT_FAIL_WITH_MESSAGE(statement, ".*") diff --git a/tests/common/util/MockExecutionContext.hpp b/tests/common/util/MockExecutionContext.hpp index 41e5bc14..f3f51f27 100644 --- a/tests/common/util/MockExecutionContext.hpp +++ b/tests/common/util/MockExecutionContext.hpp @@ -84,6 +84,7 @@ struct MockExecutionContext { (std::chrono::milliseconds, std::function), () ); + MOCK_METHOD(void, submit, (std::function), ()); MOCK_METHOD(MockStrand const&, makeStrand, (), ()); MOCK_METHOD(void, stop, (), (const)); diff --git a/tests/common/util/MockStrand.hpp b/tests/common/util/MockStrand.hpp index 85c95e1c..8a49ab25 100644 --- a/tests/common/util/MockStrand.hpp +++ b/tests/common/util/MockStrand.hpp @@ -69,4 +69,5 @@ struct MockStrand { (std::chrono::milliseconds, std::function), (const) ); + MOCK_METHOD(void, submit, (std::function), (const)); }; diff --git a/tests/unit/util/async/AsyncExecutionContextTests.cpp b/tests/unit/util/async/AsyncExecutionContextTests.cpp index 29166633..9bc25c3c 100644 --- a/tests/unit/util/async/AsyncExecutionContextTests.cpp +++ b/tests/unit/util/async/AsyncExecutionContextTests.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include "util/MockAssert.hpp" #include "util/Profiler.hpp" #include "util/async/Operation.hpp" #include "util/async/context/BasicExecutionContext.hpp" @@ -32,12 +33,13 @@ #include #include #include +#include using namespace util::async; using ::testing::Types; template -struct ExecutionContextTests : public ::testing::Test { +struct ExecutionContextTests : common::util::WithMockAssertNoThrow { using ExecutionContextType = T; ExecutionContextType ctx{2}; @@ -238,6 +240,32 @@ TYPED_TEST(ExecutionContextTests, repeatingOperationForceInvoke) EXPECT_EQ(callCount, 0uz); } +TYPED_TEST(ExecutionContextTests, submit) +{ + EXPECT_CLIO_ASSERT_FAIL(this->ctx.submit([] -> void { throw 0; })); + + std::atomic_uint32_t count = 0; + std::binary_semaphore sem{0}; + + static constexpr auto kNUM_SUBMISSIONS = 1024; + + for (auto i = 1; i <= kNUM_SUBMISSIONS; ++i) { + if (i == kNUM_SUBMISSIONS) { + this->ctx.submit([&count, &sem] { + ++count; + sem.release(); + }); + } else { + this->ctx.submit([&count] { ++count; }); + } + } + + sem.acquire(); + + // order is not guaranteed (see `strandSubmit` below) + ASSERT_EQ(count, static_cast(kNUM_SUBMISSIONS)); +} + TYPED_TEST(ExecutionContextTests, strandMove) { auto strand = this->ctx.makeStrand(); @@ -328,6 +356,35 @@ TYPED_TEST(ExecutionContextTests, strandedRepeatingOperationForceInvoke) EXPECT_EQ(callCount, 0uz); } +TYPED_TEST(ExecutionContextTests, strandSubmit) +{ + auto strand = this->ctx.makeStrand(); + EXPECT_CLIO_ASSERT_FAIL(strand.submit([] -> void { throw 0; })); + + std::vector results; + std::binary_semaphore sem{0}; + + static constexpr auto kNUM_SUBMISSIONS = 1024; + + for (auto i = 1; i <= kNUM_SUBMISSIONS; ++i) { + if (i == kNUM_SUBMISSIONS) { + strand.submit([&results, &sem, i] { + results.push_back(i); + sem.release(); + }); + } else { + strand.submit([&results, i] { results.push_back(i); }); + } + } + + sem.acquire(); + + ASSERT_EQ(results.size(), static_cast(kNUM_SUBMISSIONS)); + for (int i = 0; i < kNUM_SUBMISSIONS; ++i) { + EXPECT_EQ(results[i], i + 1); + } +} + TYPED_TEST(AsyncExecutionContextTests, executeAutoAborts) { auto value = 0;