mirror of
https://github.com/latentPrion/libspinscale.git
synced 2026-06-23 19:48:32 +00:00
Tests: Add all tests from the coro creation repo
We went back and brought along all the tests we implemented while we were building the new coro framework.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
#ifndef SPINSCALE_TEST_SUPPORT_GROUP_ASSERTIONS_H
|
||||
#define SPINSCALE_TEST_SUPPORT_GROUP_ASSERTIONS_H
|
||||
|
||||
#include <exception>
|
||||
#include <string>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <spinscale/co/group.h>
|
||||
|
||||
namespace sscl::tests {
|
||||
|
||||
template <typename Invoker>
|
||||
int completedIntValue(Invoker &invoker)
|
||||
{
|
||||
if (invoker.completedReturnValues().myExceptionPtr) {
|
||||
std::rethrow_exception(
|
||||
invoker.completedReturnValues().myExceptionPtr);
|
||||
}
|
||||
|
||||
return invoker.completedReturnValues().myReturnValue;
|
||||
}
|
||||
|
||||
inline void expectCompletedSettlement(
|
||||
const sscl::co::Group::SettlementDescriptor &descriptor)
|
||||
{
|
||||
EXPECT_EQ(
|
||||
descriptor.type,
|
||||
sscl::co::Group::SettlementDescriptor::TypeE::COMPLETED);
|
||||
}
|
||||
|
||||
template <typename Invoker>
|
||||
void expectCompletedIntSettlement(
|
||||
const sscl::co::Group::SettlementDescriptor &descriptor,
|
||||
int expectedValue)
|
||||
{
|
||||
ASSERT_EQ(
|
||||
descriptor.type,
|
||||
sscl::co::Group::SettlementDescriptor::TypeE::COMPLETED);
|
||||
EXPECT_EQ(completedIntValue(descriptor.invokerAs<Invoker>()), expectedValue);
|
||||
}
|
||||
|
||||
inline void expectExceptionSettlement(
|
||||
const sscl::co::Group::SettlementDescriptor &descriptor)
|
||||
{
|
||||
EXPECT_EQ(
|
||||
descriptor.type,
|
||||
sscl::co::Group::SettlementDescriptor::TypeE::EXCEPTION_THROWN);
|
||||
EXPECT_TRUE(descriptor.calleeException != nullptr);
|
||||
}
|
||||
|
||||
inline void expectRuntimeErrorSettlement(
|
||||
const sscl::co::Group::SettlementDescriptor &descriptor,
|
||||
const std::string &expectedMessage)
|
||||
{
|
||||
ASSERT_EQ(
|
||||
descriptor.type,
|
||||
sscl::co::Group::SettlementDescriptor::TypeE::EXCEPTION_THROWN);
|
||||
ASSERT_TRUE(descriptor.calleeException != nullptr);
|
||||
|
||||
try {
|
||||
std::rethrow_exception(descriptor.calleeException);
|
||||
}
|
||||
catch (const std::runtime_error &runtimeError) {
|
||||
EXPECT_EQ(std::string(runtimeError.what()), expectedMessage);
|
||||
return;
|
||||
}
|
||||
catch (...) {
|
||||
FAIL() << "Expected std::runtime_error settlement.";
|
||||
}
|
||||
}
|
||||
|
||||
inline void expectIntExceptionSettlement(
|
||||
const sscl::co::Group::SettlementDescriptor &descriptor,
|
||||
int expectedValue)
|
||||
{
|
||||
ASSERT_EQ(
|
||||
descriptor.type,
|
||||
sscl::co::Group::SettlementDescriptor::TypeE::EXCEPTION_THROWN);
|
||||
ASSERT_TRUE(descriptor.calleeException != nullptr);
|
||||
|
||||
try {
|
||||
std::rethrow_exception(descriptor.calleeException);
|
||||
}
|
||||
catch (int caughtValue) {
|
||||
EXPECT_EQ(caughtValue, expectedValue);
|
||||
return;
|
||||
}
|
||||
catch (...) {
|
||||
FAIL() << "Expected int exception settlement.";
|
||||
}
|
||||
}
|
||||
|
||||
inline void expectEmptyGroupError(
|
||||
const std::runtime_error &runtimeError)
|
||||
{
|
||||
constexpr const char *expectedMessage =
|
||||
"co_await: Group has no member invokers; call add() before awaiting";
|
||||
EXPECT_EQ(std::string(runtimeError.what()), expectedMessage);
|
||||
}
|
||||
|
||||
} // namespace sscl::tests
|
||||
|
||||
#endif // SPINSCALE_TEST_SUPPORT_GROUP_ASSERTIONS_H
|
||||
@@ -0,0 +1,413 @@
|
||||
#include <support/threadHarness.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
namespace sscl::tests {
|
||||
|
||||
struct DedicatedIoThread::StartupState
|
||||
{
|
||||
std::mutex mutex;
|
||||
std::condition_variable condition;
|
||||
std::thread::id osThreadId;
|
||||
std::exception_ptr startupException;
|
||||
bool allowInitialization = false;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *callerThreadName = "test:caller";
|
||||
constexpr const char *calleeThreadName = "test:callee";
|
||||
constexpr const char *alternateThreadName = "test:alternate";
|
||||
constexpr const char *bodyThreadName = "test:body";
|
||||
constexpr const char *worldThreadName = "test:world";
|
||||
constexpr const char *legThreadName = "test:leg";
|
||||
|
||||
void runDedicatedThread(
|
||||
const std::shared_ptr<DedicatedIoThread::StartupState> &state,
|
||||
const sscl::PuppeteerThread::EntryFnArguments &args)
|
||||
{
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state->mutex);
|
||||
state->condition.wait(
|
||||
lock,
|
||||
[&state]() { return state->allowInitialization; });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
args.usableBeforeJolt.initializeTls();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(state->mutex);
|
||||
state->osThreadId = std::this_thread::get_id();
|
||||
state->initialized = true;
|
||||
}
|
||||
|
||||
state->condition.notify_all();
|
||||
|
||||
args.usableBeforeJolt.getIoContext().restart();
|
||||
args.usableBeforeJolt.getIoContext().run();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(state->mutex);
|
||||
state->startupException = std::current_exception();
|
||||
state->initialized = true;
|
||||
}
|
||||
|
||||
state->condition.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string threadRoleName(PostingThreadRole role)
|
||||
{
|
||||
switch (role)
|
||||
{
|
||||
case PostingThreadRole::CALLER:
|
||||
return callerThreadName;
|
||||
case PostingThreadRole::CALLEE:
|
||||
return calleeThreadName;
|
||||
case PostingThreadRole::ALTERNATE:
|
||||
return alternateThreadName;
|
||||
case PostingThreadRole::BODY:
|
||||
return bodyThreadName;
|
||||
case PostingThreadRole::WORLD:
|
||||
return worldThreadName;
|
||||
case PostingThreadRole::LEG:
|
||||
return legThreadName;
|
||||
}
|
||||
|
||||
throw std::runtime_error("Unknown PostingThreadRole");
|
||||
}
|
||||
|
||||
void IoContextPump::pumpUntilIdle(
|
||||
boost::asio::io_context &ioContext,
|
||||
std::chrono::milliseconds idleTimeout,
|
||||
std::chrono::milliseconds totalTimeout)
|
||||
{
|
||||
const auto totalDeadline =
|
||||
std::chrono::steady_clock::now() + totalTimeout;
|
||||
auto lastProgress = std::chrono::steady_clock::now();
|
||||
|
||||
while (std::chrono::steady_clock::now() < totalDeadline)
|
||||
{
|
||||
if (ioContext.poll_one() > 0)
|
||||
{
|
||||
lastProgress = std::chrono::steady_clock::now();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std::chrono::steady_clock::now() - lastProgress >= idleTimeout) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
ThreadBoundComponent::ThreadBoundComponent()
|
||||
: sscl::pptr::PuppeteerComponent(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
void ThreadBoundComponent::handleLoopExceptionHook()
|
||||
{
|
||||
loopException = std::current_exception();
|
||||
}
|
||||
|
||||
DedicatedIoThread::DedicatedIoThread(PostingThreadRole roleIn)
|
||||
: role(roleIn),
|
||||
startupState(std::make_shared<StartupState>()),
|
||||
component(),
|
||||
thread(std::make_shared<sscl::PuppeteerThread>(
|
||||
static_cast<sscl::ThreadId>(roleIn),
|
||||
threadRoleName(roleIn),
|
||||
[state = startupState](
|
||||
const sscl::PuppeteerThread::EntryFnArguments &args)
|
||||
{
|
||||
runDedicatedThread(state, args);
|
||||
},
|
||||
component,
|
||||
nullptr))
|
||||
{
|
||||
component.thread = thread;
|
||||
releaseStartupBarrier();
|
||||
waitUntilInitialized();
|
||||
}
|
||||
|
||||
DedicatedIoThread::~DedicatedIoThread()
|
||||
{
|
||||
stopAndJoin();
|
||||
}
|
||||
|
||||
boost::asio::io_context &DedicatedIoThread::ioContext()
|
||||
{
|
||||
return thread->getIoContext();
|
||||
}
|
||||
|
||||
sscl::ThreadId DedicatedIoThread::threadId() const noexcept
|
||||
{
|
||||
return static_cast<sscl::ThreadId>(role);
|
||||
}
|
||||
|
||||
std::thread::id DedicatedIoThread::osThreadId() const
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(startupState->mutex);
|
||||
return startupState->osThreadId;
|
||||
}
|
||||
|
||||
std::shared_ptr<sscl::PuppeteerThread> DedicatedIoThread::componentThread() const
|
||||
{
|
||||
return thread;
|
||||
}
|
||||
|
||||
void DedicatedIoThread::stopAndJoin()
|
||||
{
|
||||
if (!thread) {
|
||||
return;
|
||||
}
|
||||
|
||||
releaseStartupBarrier();
|
||||
thread->getIoContext().stop();
|
||||
|
||||
if (thread->thread.joinable()) {
|
||||
thread->thread.join();
|
||||
}
|
||||
|
||||
thread.reset();
|
||||
}
|
||||
|
||||
void DedicatedIoThread::releaseStartupBarrier()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(startupState->mutex);
|
||||
startupState->allowInitialization = true;
|
||||
}
|
||||
|
||||
startupState->condition.notify_all();
|
||||
}
|
||||
|
||||
void DedicatedIoThread::waitUntilInitialized()
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(startupState->mutex);
|
||||
const bool initialized = startupState->condition.wait_for(
|
||||
lock,
|
||||
defaultPostingTaskTimeout,
|
||||
[this]() { return startupState->initialized; });
|
||||
|
||||
if (!initialized) {
|
||||
throw std::runtime_error("Timed out waiting for test thread startup");
|
||||
}
|
||||
|
||||
std::exception_ptr startupException = startupState->startupException;
|
||||
lock.unlock();
|
||||
|
||||
if (startupException) {
|
||||
std::rethrow_exception(startupException);
|
||||
}
|
||||
}
|
||||
|
||||
void ThreadRegistry::registerThread(
|
||||
PostingThreadRole role,
|
||||
DedicatedIoThread &thread)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(registryMutex());
|
||||
threadsByRole()[role] = &thread;
|
||||
}
|
||||
|
||||
void ThreadRegistry::unregisterThread(PostingThreadRole role)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(registryMutex());
|
||||
threadsByRole().erase(role);
|
||||
}
|
||||
|
||||
boost::asio::io_context &ThreadRegistry::ioContext(PostingThreadRole role)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(registryMutex());
|
||||
auto iterator = threadsByRole().find(role);
|
||||
|
||||
if (iterator == threadsByRole().end()) {
|
||||
throw std::runtime_error(
|
||||
"No test thread registered for " + threadRoleName(role));
|
||||
}
|
||||
|
||||
return iterator->second->ioContext();
|
||||
}
|
||||
|
||||
std::thread::id ThreadRegistry::osThreadId(PostingThreadRole role)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(registryMutex());
|
||||
auto iterator = threadsByRole().find(role);
|
||||
|
||||
if (iterator == threadsByRole().end()) {
|
||||
throw std::runtime_error(
|
||||
"No test thread registered for " + threadRoleName(role));
|
||||
}
|
||||
|
||||
return iterator->second->osThreadId();
|
||||
}
|
||||
|
||||
std::mutex &ThreadRegistry::registryMutex()
|
||||
{
|
||||
static std::mutex mutex;
|
||||
return mutex;
|
||||
}
|
||||
|
||||
std::map<PostingThreadRole, DedicatedIoThread *> &
|
||||
ThreadRegistry::threadsByRole()
|
||||
{
|
||||
static std::map<PostingThreadRole, DedicatedIoThread *> threads;
|
||||
return threads;
|
||||
}
|
||||
|
||||
PostingThreadSet::PostingThreadSet()
|
||||
: callerThread(PostingThreadRole::CALLER),
|
||||
calleeThread(PostingThreadRole::CALLEE),
|
||||
alternateThread(PostingThreadRole::ALTERNATE),
|
||||
bodyThread(PostingThreadRole::BODY),
|
||||
worldThread(PostingThreadRole::WORLD),
|
||||
legThread(PostingThreadRole::LEG)
|
||||
{
|
||||
ThreadRegistry::registerThread(PostingThreadRole::CALLER, callerThread);
|
||||
ThreadRegistry::registerThread(PostingThreadRole::CALLEE, calleeThread);
|
||||
ThreadRegistry::registerThread(PostingThreadRole::ALTERNATE, alternateThread);
|
||||
ThreadRegistry::registerThread(PostingThreadRole::BODY, bodyThread);
|
||||
ThreadRegistry::registerThread(PostingThreadRole::WORLD, worldThread);
|
||||
ThreadRegistry::registerThread(PostingThreadRole::LEG, legThread);
|
||||
|
||||
sscl::ComponentThread::setPuppeteerThreadId(
|
||||
static_cast<sscl::ThreadId>(PostingThreadRole::CALLER));
|
||||
sscl::ComponentThread::setPuppeteerThread(callerThread.componentThread());
|
||||
}
|
||||
|
||||
PostingThreadSet::~PostingThreadSet()
|
||||
{
|
||||
ThreadRegistry::unregisterThread(PostingThreadRole::CALLER);
|
||||
ThreadRegistry::unregisterThread(PostingThreadRole::CALLEE);
|
||||
ThreadRegistry::unregisterThread(PostingThreadRole::ALTERNATE);
|
||||
ThreadRegistry::unregisterThread(PostingThreadRole::BODY);
|
||||
ThreadRegistry::unregisterThread(PostingThreadRole::WORLD);
|
||||
ThreadRegistry::unregisterThread(PostingThreadRole::LEG);
|
||||
|
||||
sscl::ComponentThread::setPuppeteerThread(nullptr);
|
||||
}
|
||||
|
||||
DedicatedIoThread &PostingThreadSet::thread(PostingThreadRole role)
|
||||
{
|
||||
switch (role)
|
||||
{
|
||||
case PostingThreadRole::CALLER:
|
||||
return callerThread;
|
||||
case PostingThreadRole::CALLEE:
|
||||
return calleeThread;
|
||||
case PostingThreadRole::ALTERNATE:
|
||||
return alternateThread;
|
||||
case PostingThreadRole::BODY:
|
||||
return bodyThread;
|
||||
case PostingThreadRole::WORLD:
|
||||
return worldThread;
|
||||
case PostingThreadRole::LEG:
|
||||
return legThread;
|
||||
}
|
||||
|
||||
throw std::runtime_error("Unknown PostingThreadRole");
|
||||
}
|
||||
|
||||
DedicatedIoThread &PostingThreadSet::caller()
|
||||
{
|
||||
return callerThread;
|
||||
}
|
||||
|
||||
DedicatedIoThread &PostingThreadSet::callee()
|
||||
{
|
||||
return calleeThread;
|
||||
}
|
||||
|
||||
DedicatedIoThread &PostingThreadSet::alternate()
|
||||
{
|
||||
return alternateThread;
|
||||
}
|
||||
|
||||
DedicatedIoThread &PostingThreadSet::body()
|
||||
{
|
||||
return bodyThread;
|
||||
}
|
||||
|
||||
DedicatedIoThread &PostingThreadSet::world()
|
||||
{
|
||||
return worldThread;
|
||||
}
|
||||
|
||||
DedicatedIoThread &PostingThreadSet::leg()
|
||||
{
|
||||
return legThread;
|
||||
}
|
||||
|
||||
void CrossThreadTrace::recordConstructionThread()
|
||||
{
|
||||
record(constructionThreadId);
|
||||
}
|
||||
|
||||
void CrossThreadTrace::recordCalleeExecutionThread()
|
||||
{
|
||||
record(calleeExecutionThreadId);
|
||||
}
|
||||
|
||||
void CrossThreadTrace::recordFinalSuspendThread()
|
||||
{
|
||||
record(finalSuspendThreadId);
|
||||
}
|
||||
|
||||
void CrossThreadTrace::recordAwaitResumeThread()
|
||||
{
|
||||
record(awaitResumeThreadId);
|
||||
}
|
||||
|
||||
void CrossThreadTrace::recordCompletionCallbackThread()
|
||||
{
|
||||
record(completionCallbackThreadId);
|
||||
}
|
||||
|
||||
std::thread::id CrossThreadTrace::constructionThread() const
|
||||
{
|
||||
return read(constructionThreadId);
|
||||
}
|
||||
|
||||
std::thread::id CrossThreadTrace::calleeExecutionThread() const
|
||||
{
|
||||
return read(calleeExecutionThreadId);
|
||||
}
|
||||
|
||||
std::thread::id CrossThreadTrace::finalSuspendThread() const
|
||||
{
|
||||
return read(finalSuspendThreadId);
|
||||
}
|
||||
|
||||
std::thread::id CrossThreadTrace::awaitResumeThread() const
|
||||
{
|
||||
return read(awaitResumeThreadId);
|
||||
}
|
||||
|
||||
std::thread::id CrossThreadTrace::completionCallbackThread() const
|
||||
{
|
||||
return read(completionCallbackThreadId);
|
||||
}
|
||||
|
||||
void CrossThreadTrace::record(std::thread::id &slot)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
slot = std::this_thread::get_id();
|
||||
}
|
||||
|
||||
std::thread::id CrossThreadTrace::read(const std::thread::id &slot) const
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
return slot;
|
||||
}
|
||||
|
||||
} // namespace sscl::tests
|
||||
@@ -0,0 +1,362 @@
|
||||
#ifndef SPINSCALE_TEST_SUPPORT_THREAD_HARNESS_H
|
||||
#define SPINSCALE_TEST_SUPPORT_THREAD_HARNESS_H
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
|
||||
#include <spinscale/co/invokers.h>
|
||||
#include <spinscale/co/postingPromise.h>
|
||||
#include <spinscale/component.h>
|
||||
#include <spinscale/componentThread.h>
|
||||
|
||||
namespace sscl::tests {
|
||||
|
||||
constexpr std::chrono::milliseconds defaultIdleTimeout{800};
|
||||
constexpr std::chrono::milliseconds defaultTotalTimeout{10000};
|
||||
constexpr std::chrono::milliseconds defaultPostingTaskTimeout{10000};
|
||||
|
||||
enum class PostingThreadRole : sscl::ThreadId
|
||||
{
|
||||
CALLER = 70,
|
||||
CALLEE = 71,
|
||||
ALTERNATE = 72,
|
||||
BODY = 73,
|
||||
WORLD = 74,
|
||||
LEG = 75,
|
||||
};
|
||||
|
||||
std::string threadRoleName(PostingThreadRole role);
|
||||
|
||||
class IoContextPump
|
||||
{
|
||||
public:
|
||||
static void pumpUntilIdle(
|
||||
boost::asio::io_context &ioContext,
|
||||
std::chrono::milliseconds idleTimeout = defaultIdleTimeout,
|
||||
std::chrono::milliseconds totalTimeout = defaultTotalTimeout);
|
||||
|
||||
template <typename Predicate>
|
||||
static bool pumpUntil(
|
||||
boost::asio::io_context &ioContext,
|
||||
Predicate &&predicate,
|
||||
std::chrono::milliseconds idleTimeout = defaultIdleTimeout,
|
||||
std::chrono::milliseconds totalTimeout = defaultTotalTimeout)
|
||||
{
|
||||
const auto totalDeadline =
|
||||
std::chrono::steady_clock::now() + totalTimeout;
|
||||
auto lastProgress = std::chrono::steady_clock::now();
|
||||
|
||||
while (std::chrono::steady_clock::now() < totalDeadline)
|
||||
{
|
||||
if (std::invoke(predicate)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ioContext.poll_one() > 0)
|
||||
{
|
||||
lastProgress = std::chrono::steady_clock::now();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std::chrono::steady_clock::now() - lastProgress >= idleTimeout) {
|
||||
return std::invoke(predicate);
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
|
||||
return std::invoke(predicate);
|
||||
}
|
||||
};
|
||||
|
||||
class ThreadBoundComponent final
|
||||
: public sscl::pptr::PuppeteerComponent
|
||||
{
|
||||
public:
|
||||
ThreadBoundComponent();
|
||||
void handleLoopExceptionHook() override;
|
||||
|
||||
std::exception_ptr loopException;
|
||||
};
|
||||
|
||||
class DedicatedIoThread
|
||||
{
|
||||
public:
|
||||
explicit DedicatedIoThread(PostingThreadRole role);
|
||||
~DedicatedIoThread();
|
||||
|
||||
DedicatedIoThread(const DedicatedIoThread &) = delete;
|
||||
DedicatedIoThread &operator=(const DedicatedIoThread &) = delete;
|
||||
DedicatedIoThread(DedicatedIoThread &&) = delete;
|
||||
DedicatedIoThread &operator=(DedicatedIoThread &&) = delete;
|
||||
|
||||
boost::asio::io_context &ioContext();
|
||||
sscl::ThreadId threadId() const noexcept;
|
||||
std::thread::id osThreadId() const;
|
||||
std::shared_ptr<sscl::PuppeteerThread> componentThread() const;
|
||||
|
||||
void stopAndJoin();
|
||||
|
||||
struct StartupState;
|
||||
|
||||
template <typename Function>
|
||||
void post(Function &&function)
|
||||
{
|
||||
boost::asio::post(
|
||||
ioContext(),
|
||||
std::forward<Function>(function));
|
||||
}
|
||||
|
||||
template <typename Function>
|
||||
auto runSync(Function &&function)
|
||||
-> std::invoke_result_t<Function &>
|
||||
{
|
||||
using Result = std::invoke_result_t<Function &>;
|
||||
|
||||
if (std::this_thread::get_id() == osThreadId()) {
|
||||
if constexpr (std::is_void_v<Result>) {
|
||||
std::invoke(function);
|
||||
return;
|
||||
} else {
|
||||
return std::invoke(function);
|
||||
}
|
||||
}
|
||||
|
||||
auto promise = std::make_shared<std::promise<Result>>();
|
||||
auto future = promise->get_future();
|
||||
|
||||
post(
|
||||
[promise, function = std::forward<Function>(function)]() mutable
|
||||
{
|
||||
try
|
||||
{
|
||||
if constexpr (std::is_void_v<Result>)
|
||||
{
|
||||
std::invoke(function);
|
||||
promise->set_value();
|
||||
}
|
||||
else
|
||||
{
|
||||
promise->set_value(std::invoke(function));
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
promise->set_exception(std::current_exception());
|
||||
}
|
||||
});
|
||||
|
||||
return future.get();
|
||||
}
|
||||
|
||||
private:
|
||||
void releaseStartupBarrier();
|
||||
void waitUntilInitialized();
|
||||
|
||||
PostingThreadRole role;
|
||||
std::shared_ptr<StartupState> startupState;
|
||||
ThreadBoundComponent component;
|
||||
std::shared_ptr<sscl::PuppeteerThread> thread;
|
||||
};
|
||||
|
||||
class ThreadRegistry
|
||||
{
|
||||
public:
|
||||
static void registerThread(
|
||||
PostingThreadRole role,
|
||||
DedicatedIoThread &thread);
|
||||
static void unregisterThread(PostingThreadRole role);
|
||||
static boost::asio::io_context &ioContext(PostingThreadRole role);
|
||||
static std::thread::id osThreadId(PostingThreadRole role);
|
||||
|
||||
private:
|
||||
static std::mutex ®istryMutex();
|
||||
static std::map<PostingThreadRole, DedicatedIoThread *> &threadsByRole();
|
||||
};
|
||||
|
||||
template <PostingThreadRole role>
|
||||
struct PostingThreadTag
|
||||
{
|
||||
static boost::asio::io_context &io_context()
|
||||
{
|
||||
return ThreadRegistry::ioContext(role);
|
||||
}
|
||||
};
|
||||
|
||||
template <PostingThreadRole role, typename T>
|
||||
using RolePostingPromise =
|
||||
sscl::co::TaggedPostingPromise<T, PostingThreadTag<role>>;
|
||||
|
||||
template <PostingThreadRole role>
|
||||
struct RolePostingPromiseTemplate
|
||||
{
|
||||
template <typename T>
|
||||
using Type = RolePostingPromise<role, T>;
|
||||
};
|
||||
|
||||
template <PostingThreadRole role, typename T>
|
||||
using RoleViralPostingInvoker =
|
||||
sscl::co::ViralPostingInvoker<
|
||||
RolePostingPromiseTemplate<role>::template Type,
|
||||
T>;
|
||||
|
||||
template <PostingThreadRole role>
|
||||
using RoleNonViralPostingInvoker =
|
||||
sscl::co::NonViralPostingInvoker<
|
||||
RolePostingPromiseTemplate<role>::template Type>;
|
||||
|
||||
class PostingThreadSet
|
||||
{
|
||||
public:
|
||||
PostingThreadSet();
|
||||
~PostingThreadSet();
|
||||
|
||||
PostingThreadSet(const PostingThreadSet &) = delete;
|
||||
PostingThreadSet &operator=(const PostingThreadSet &) = delete;
|
||||
PostingThreadSet(PostingThreadSet &&) = delete;
|
||||
PostingThreadSet &operator=(PostingThreadSet &&) = delete;
|
||||
|
||||
DedicatedIoThread &thread(PostingThreadRole role);
|
||||
DedicatedIoThread &caller();
|
||||
DedicatedIoThread &callee();
|
||||
DedicatedIoThread &alternate();
|
||||
DedicatedIoThread &body();
|
||||
DedicatedIoThread &world();
|
||||
DedicatedIoThread &leg();
|
||||
|
||||
private:
|
||||
DedicatedIoThread callerThread;
|
||||
DedicatedIoThread calleeThread;
|
||||
DedicatedIoThread alternateThread;
|
||||
DedicatedIoThread bodyThread;
|
||||
DedicatedIoThread worldThread;
|
||||
DedicatedIoThread legThread;
|
||||
};
|
||||
|
||||
class CrossThreadTrace
|
||||
{
|
||||
public:
|
||||
void recordConstructionThread();
|
||||
void recordCalleeExecutionThread();
|
||||
void recordFinalSuspendThread();
|
||||
void recordAwaitResumeThread();
|
||||
void recordCompletionCallbackThread();
|
||||
|
||||
std::thread::id constructionThread() const;
|
||||
std::thread::id calleeExecutionThread() const;
|
||||
std::thread::id finalSuspendThread() const;
|
||||
std::thread::id awaitResumeThread() const;
|
||||
std::thread::id completionCallbackThread() const;
|
||||
|
||||
private:
|
||||
void record(std::thread::id &slot);
|
||||
std::thread::id read(const std::thread::id &slot) const;
|
||||
|
||||
mutable std::mutex mutex;
|
||||
std::thread::id constructionThreadId;
|
||||
std::thread::id calleeExecutionThreadId;
|
||||
std::thread::id finalSuspendThreadId;
|
||||
std::thread::id awaitResumeThreadId;
|
||||
std::thread::id completionCallbackThreadId;
|
||||
};
|
||||
|
||||
template <typename InvokerFactory>
|
||||
void runNonViralPostingTask(
|
||||
DedicatedIoThread &callerThread,
|
||||
InvokerFactory &&invokerFactory,
|
||||
std::chrono::milliseconds timeout = defaultPostingTaskTimeout)
|
||||
{
|
||||
using Factory = std::decay_t<InvokerFactory>;
|
||||
using Invoker = std::invoke_result_t<
|
||||
Factory &, std::exception_ptr &, std::function<void()>>;
|
||||
|
||||
struct TaskState
|
||||
{
|
||||
explicit TaskState(Factory factoryIn)
|
||||
: factory(std::move(factoryIn))
|
||||
{}
|
||||
|
||||
Factory factory;
|
||||
std::exception_ptr coroutineException;
|
||||
std::exception_ptr taskException;
|
||||
std::optional<Invoker> invoker;
|
||||
std::mutex mutex;
|
||||
std::condition_variable condition;
|
||||
bool completed = false;
|
||||
};
|
||||
|
||||
auto taskState = std::make_shared<TaskState>(
|
||||
std::forward<InvokerFactory>(invokerFactory));
|
||||
|
||||
callerThread.post(
|
||||
[taskState]()
|
||||
{
|
||||
auto completeTask = [taskState]()
|
||||
{
|
||||
taskState->taskException = taskState->coroutineException;
|
||||
taskState->invoker.reset();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(taskState->mutex);
|
||||
taskState->completed = true;
|
||||
}
|
||||
|
||||
taskState->condition.notify_one();
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
taskState->invoker.emplace(
|
||||
std::invoke(
|
||||
taskState->factory,
|
||||
taskState->coroutineException,
|
||||
std::move(completeTask)));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(taskState->mutex);
|
||||
taskState->taskException = std::current_exception();
|
||||
taskState->completed = true;
|
||||
}
|
||||
|
||||
taskState->condition.notify_one();
|
||||
}
|
||||
});
|
||||
|
||||
std::unique_lock<std::mutex> lock(taskState->mutex);
|
||||
const bool completed = taskState->condition.wait_for(
|
||||
lock,
|
||||
timeout,
|
||||
[&taskState]() { return taskState->completed; });
|
||||
|
||||
if (!completed) {
|
||||
throw std::runtime_error("Timed out waiting for posting coroutine task");
|
||||
}
|
||||
|
||||
std::exception_ptr taskException = taskState->taskException;
|
||||
lock.unlock();
|
||||
|
||||
if (taskException) {
|
||||
std::rethrow_exception(taskException);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sscl::tests
|
||||
|
||||
#endif // SPINSCALE_TEST_SUPPORT_THREAD_HARNESS_H
|
||||
@@ -0,0 +1,161 @@
|
||||
#ifndef SPINSCALE_TEST_SUPPORT_TIMER_AWAITERS_H
|
||||
#define SPINSCALE_TEST_SUPPORT_TIMER_AWAITERS_H
|
||||
|
||||
#include <coroutine>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <boost/asio/deadline_timer.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/date_time/posix_time/posix_time_types.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
namespace sscl::tests {
|
||||
|
||||
using SharedDeadlineTimer = std::shared_ptr<boost::asio::deadline_timer>;
|
||||
|
||||
class CancelableDeadlineTimerRegistry
|
||||
{
|
||||
public:
|
||||
void clear()
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
timersByLabel.clear();
|
||||
}
|
||||
|
||||
void registerTimer(
|
||||
int labelMilliseconds,
|
||||
const SharedDeadlineTimer &timer)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
timersByLabel[labelMilliseconds] = timer;
|
||||
}
|
||||
|
||||
void cancel(int labelMilliseconds)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
const auto iterator = timersByLabel.find(labelMilliseconds);
|
||||
|
||||
if (iterator == timersByLabel.end()) {
|
||||
throw std::runtime_error(
|
||||
"No cancelable deadline_timer registered for label "
|
||||
+ std::to_string(labelMilliseconds));
|
||||
}
|
||||
|
||||
const SharedDeadlineTimer timer = iterator->second.lock();
|
||||
|
||||
if (!timer) {
|
||||
throw std::runtime_error(
|
||||
"Cancelable deadline_timer expired before cancel for label "
|
||||
+ std::to_string(labelMilliseconds));
|
||||
}
|
||||
|
||||
timer->cancel();
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mutex;
|
||||
std::unordered_map<int, std::weak_ptr<boost::asio::deadline_timer>>
|
||||
timersByLabel;
|
||||
};
|
||||
|
||||
struct DeadlineTimerAwaiter
|
||||
{
|
||||
DeadlineTimerAwaiter(
|
||||
boost::asio::io_context &ioContext,
|
||||
int delayMilliseconds)
|
||||
: timer(std::make_shared<boost::asio::deadline_timer>(ioContext))
|
||||
{
|
||||
start(delayMilliseconds);
|
||||
}
|
||||
|
||||
DeadlineTimerAwaiter(
|
||||
SharedDeadlineTimer sharedTimer,
|
||||
int delayMilliseconds)
|
||||
: timer(std::move(sharedTimer))
|
||||
{
|
||||
start(delayMilliseconds);
|
||||
}
|
||||
|
||||
bool await_ready() const noexcept
|
||||
{ return waitCompleted; }
|
||||
|
||||
bool await_suspend(std::coroutine_handle<> handle) noexcept
|
||||
{
|
||||
resumeHandle = handle;
|
||||
return !waitCompleted;
|
||||
}
|
||||
|
||||
boost::system::error_code await_resume() const noexcept
|
||||
{ return completionErrorCode; }
|
||||
|
||||
private:
|
||||
void start(int delayMilliseconds)
|
||||
{
|
||||
timer->expires_from_now(
|
||||
boost::posix_time::milliseconds(delayMilliseconds));
|
||||
timer->async_wait(
|
||||
[this](const boost::system::error_code &errorCode)
|
||||
{
|
||||
completionErrorCode = errorCode;
|
||||
waitCompleted = true;
|
||||
if (resumeHandle) {
|
||||
resumeHandle.resume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SharedDeadlineTimer timer;
|
||||
boost::system::error_code completionErrorCode;
|
||||
bool waitCompleted = false;
|
||||
std::coroutine_handle<> resumeHandle;
|
||||
};
|
||||
|
||||
struct RegisteredDeadlineTimerAwaiter
|
||||
{
|
||||
RegisteredDeadlineTimerAwaiter(
|
||||
boost::asio::io_context &ioContext,
|
||||
int delayMilliseconds,
|
||||
int registrationLabelMilliseconds,
|
||||
CancelableDeadlineTimerRegistry ®istry)
|
||||
: timer(std::make_shared<boost::asio::deadline_timer>(ioContext))
|
||||
{
|
||||
registry.registerTimer(registrationLabelMilliseconds, timer);
|
||||
waiter.emplace(timer, delayMilliseconds);
|
||||
}
|
||||
|
||||
bool await_ready() const noexcept
|
||||
{ return waiter->await_ready(); }
|
||||
|
||||
bool await_suspend(std::coroutine_handle<> handle) noexcept
|
||||
{ return waiter->await_suspend(handle); }
|
||||
|
||||
boost::system::error_code await_resume() const noexcept
|
||||
{ return waiter->await_resume(); }
|
||||
|
||||
SharedDeadlineTimer timer;
|
||||
std::optional<DeadlineTimerAwaiter> waiter;
|
||||
};
|
||||
|
||||
inline void throwIfTimerWaitFailed(
|
||||
const boost::system::error_code &waitError)
|
||||
{
|
||||
if (waitError) {
|
||||
throw std::runtime_error(
|
||||
"deadline_timer wait failed: " + waitError.message());
|
||||
}
|
||||
}
|
||||
|
||||
inline bool timerWasCanceled(const boost::system::error_code &waitError)
|
||||
{
|
||||
return waitError == boost::asio::error::operation_aborted;
|
||||
}
|
||||
|
||||
} // namespace sscl::tests
|
||||
|
||||
#endif // SPINSCALE_TEST_SUPPORT_TIMER_AWAITERS_H
|
||||
Reference in New Issue
Block a user