mirror of
https://github.com/latentPrion/libspinscale.git
synced 2026-06-23 11:38:33 +00:00
Tests: Move qutex and nursery tests into Libspinscale repo
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
[submodule "googletest"]
|
||||
path = googletest
|
||||
url = https://github.com/google/googletest.git
|
||||
+25
-10
@@ -1,6 +1,8 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(libspinscale VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
# Set C++ standard
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@@ -148,16 +150,29 @@ install(DIRECTORY include/spinscale
|
||||
FILES_MATCHING PATTERN "*.h"
|
||||
)
|
||||
|
||||
if(BUILD_TESTING AND TARGET GTest::gtest_main)
|
||||
add_executable(spinscale_env_kv_store_tests
|
||||
tests/env_kv_store_test.cpp
|
||||
)
|
||||
target_link_libraries(spinscale_env_kv_store_tests PRIVATE
|
||||
spinscale
|
||||
GTest::gtest_main
|
||||
)
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(spinscale_env_kv_store_tests)
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(_libspinscaleTestsDefault ON)
|
||||
else()
|
||||
set(_libspinscaleTestsDefault OFF)
|
||||
if(DEFINED ENABLE_TESTS AND ENABLE_TESTS)
|
||||
set(_libspinscaleTestsDefault ON)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
option(LIBSPINSCALE_BUILD_TESTS "Build libspinscale unit tests"
|
||||
${_libspinscaleTestsDefault})
|
||||
|
||||
if(LIBSPINSCALE_BUILD_TESTS)
|
||||
if(NOT TARGET gtest AND NOT TARGET gtest_main)
|
||||
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
|
||||
add_subdirectory(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/googletest
|
||||
${CMAKE_CURRENT_BINARY_DIR}/googletest
|
||||
EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
|
||||
enable_testing()
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
install(FILES include/boostAsioLinkageFix.h
|
||||
|
||||
Submodule
+1
Submodule googletest added at 7140cd416c
@@ -0,0 +1,37 @@
|
||||
add_executable(spinscale_env_kv_store_tests
|
||||
env_kv_store_test.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(spinscale_env_kv_store_tests PRIVATE
|
||||
spinscale
|
||||
gtest_main
|
||||
)
|
||||
|
||||
add_dependencies(spinscale_env_kv_store_tests gtest_main)
|
||||
add_test(NAME spinscale_env_kv_store_tests
|
||||
COMMAND spinscale_env_kv_store_tests)
|
||||
|
||||
add_executable(qutex_tests
|
||||
cps/qutex_tests.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(qutex_tests PRIVATE
|
||||
spinscale
|
||||
gtest_main
|
||||
)
|
||||
|
||||
add_dependencies(qutex_tests gtest_main)
|
||||
add_test(NAME qutex_tests COMMAND qutex_tests)
|
||||
|
||||
add_executable(nonViralTaskNursery_tests
|
||||
co/nonViralTaskNursery_tests.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(nonViralTaskNursery_tests PRIVATE
|
||||
spinscale
|
||||
gtest_main
|
||||
)
|
||||
|
||||
add_dependencies(nonViralTaskNursery_tests gtest_main)
|
||||
add_test(NAME nonViralTaskNursery_tests
|
||||
COMMAND nonViralTaskNursery_tests)
|
||||
@@ -0,0 +1,657 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <coroutine>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <gtest/gtest.h>
|
||||
#include <thread>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
|
||||
#include <spinscale/co/invokers.h>
|
||||
#include <spinscale/co/nonViralTaskNursery.h>
|
||||
#include <spinscale/syncCancelerForAsyncWork.h>
|
||||
|
||||
namespace {
|
||||
|
||||
struct ResumeGate
|
||||
{
|
||||
std::coroutine_handle<> waitingHandle;
|
||||
|
||||
bool await_ready() const noexcept
|
||||
{ return false; }
|
||||
|
||||
bool await_suspend(std::coroutine_handle<> callerHandle) noexcept
|
||||
{
|
||||
waitingHandle = callerHandle;
|
||||
return true;
|
||||
}
|
||||
|
||||
void await_resume() const noexcept
|
||||
{}
|
||||
};
|
||||
|
||||
sscl::co::NonViralNonPostingInvoker immediateCompleteCReq(
|
||||
std::exception_ptr &exceptionPtr,
|
||||
std::function<void()> completion)
|
||||
{
|
||||
(void)exceptionPtr;
|
||||
(void)completion;
|
||||
co_return;
|
||||
}
|
||||
|
||||
sscl::co::NonViralNonPostingInvoker throwingCompleteCReq(
|
||||
std::exception_ptr &exceptionPtr,
|
||||
std::function<void()> completion)
|
||||
{
|
||||
(void)exceptionPtr;
|
||||
(void)completion;
|
||||
throw std::runtime_error("nursery test failure");
|
||||
co_return;
|
||||
}
|
||||
|
||||
sscl::co::NonViralNonPostingInvoker suspendUntilResumeCReq(
|
||||
std::exception_ptr &exceptionPtr,
|
||||
std::function<void()> completion,
|
||||
ResumeGate &gate)
|
||||
{
|
||||
(void)exceptionPtr;
|
||||
(void)completion;
|
||||
co_await gate;
|
||||
co_return;
|
||||
}
|
||||
|
||||
sscl::co::NonViralNonPostingInvoker cancelAwareSuspendCReq(
|
||||
std::exception_ptr &exceptionPtr,
|
||||
std::function<void()> completion,
|
||||
sscl::SyncCancelerForAsyncWork &canceler,
|
||||
ResumeGate &gate)
|
||||
{
|
||||
(void)exceptionPtr;
|
||||
(void)completion;
|
||||
|
||||
while (!canceler.isCancellationRequested())
|
||||
{
|
||||
co_await gate;
|
||||
}
|
||||
|
||||
co_return;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class NonViralTaskNurseryTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
nursery.openAdmission();
|
||||
}
|
||||
|
||||
sscl::co::NonViralTaskNursery nursery;
|
||||
ResumeGate gate;
|
||||
ResumeGate gate2;
|
||||
};
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, GetNewSlotLeaseFillCommitRetires)
|
||||
{
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
lease.fillSlot(
|
||||
[&lease]()
|
||||
{
|
||||
return immediateCompleteCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda());
|
||||
});
|
||||
lease.commit();
|
||||
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
EXPECT_EQ(nursery.unsettledCount(), 0U);
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, UncommittedLeaseReleasesReservation)
|
||||
{
|
||||
EXPECT_EQ(nursery.unsettledCount(), 0U);
|
||||
{
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
(void)lease;
|
||||
}
|
||||
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, CloseAdmissionRejectsNewLeases)
|
||||
{
|
||||
nursery.closeAdmission();
|
||||
|
||||
EXPECT_THROW(nursery.getNewSlotLease(), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, SetOnSettledHookRejectsAfterFillSlot)
|
||||
{
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
lease.fillSlot(
|
||||
[&lease]()
|
||||
{
|
||||
return immediateCompleteCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda());
|
||||
});
|
||||
|
||||
EXPECT_THROW(
|
||||
lease.setOnSettledHook([](std::exception_ptr &) {}),
|
||||
std::runtime_error);
|
||||
lease.commit();
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, AsyncAwaitFiresOnDrain)
|
||||
{
|
||||
std::atomic<bool> drained{false};
|
||||
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
lease.fillSlot(
|
||||
[&lease]()
|
||||
{
|
||||
return immediateCompleteCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda());
|
||||
});
|
||||
lease.commit();
|
||||
|
||||
nursery.closeAdmission();
|
||||
nursery.asyncAwaitAllSettlements(
|
||||
[&drained]()
|
||||
{
|
||||
drained.store(true, std::memory_order_release);
|
||||
});
|
||||
|
||||
EXPECT_TRUE(drained.load(std::memory_order_acquire));
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, AsyncAwaitRejectsWhenAdmissionOpen)
|
||||
{
|
||||
EXPECT_THROW(nursery.asyncAwaitAllSettlements([]() {}), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, SecondDrainWaiterThrows)
|
||||
{
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
lease.fillSlot(
|
||||
[&lease, this]()
|
||||
{
|
||||
return suspendUntilResumeCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda(),
|
||||
gate);
|
||||
});
|
||||
lease.commit();
|
||||
|
||||
nursery.closeAdmission();
|
||||
|
||||
bool firstWaiterRegistered = false;
|
||||
nursery.asyncAwaitAllSettlements(
|
||||
[&firstWaiterRegistered]()
|
||||
{
|
||||
firstWaiterRegistered = true;
|
||||
});
|
||||
|
||||
EXPECT_FALSE(firstWaiterRegistered);
|
||||
EXPECT_THROW(
|
||||
nursery.asyncAwaitAllSettlements([]() {}),
|
||||
std::runtime_error);
|
||||
|
||||
if (gate.waitingHandle)
|
||||
{
|
||||
gate.waitingHandle.resume();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, SyncAwaitNestedRun)
|
||||
{
|
||||
boost::asio::io_context ioContext;
|
||||
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
lease.fillSlot(
|
||||
[&lease, this]()
|
||||
{
|
||||
return suspendUntilResumeCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda(),
|
||||
gate);
|
||||
});
|
||||
lease.commit();
|
||||
|
||||
std::thread awaitThread(
|
||||
[this, &ioContext]()
|
||||
{
|
||||
nursery.closeAdmission();
|
||||
nursery.syncAwaitAllSettlements(ioContext);
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
|
||||
ASSERT_TRUE(static_cast<bool>(gate.waitingHandle));
|
||||
gate.waitingHandle.resume();
|
||||
|
||||
awaitThread.join();
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, RequestCancelOnAllDoesNotDestroyInvokers)
|
||||
{
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
lease.getSyncCanceler().startAcceptingWork();
|
||||
lease.fillSlot(
|
||||
[&lease, this]()
|
||||
{
|
||||
return suspendUntilResumeCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda(),
|
||||
gate);
|
||||
});
|
||||
lease.commit();
|
||||
|
||||
EXPECT_EQ(nursery.unsettledCount(), 1U);
|
||||
nursery.requestCancelOnAll();
|
||||
EXPECT_EQ(nursery.unsettledCount(), 1U);
|
||||
|
||||
ASSERT_TRUE(static_cast<bool>(gate.waitingHandle));
|
||||
gate.waitingHandle.resume();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, RequestCancelOnAllStopsCanceler)
|
||||
{
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
lease.getSyncCanceler().startAcceptingWork();
|
||||
lease.fillSlot(
|
||||
[&lease, this]()
|
||||
{
|
||||
return cancelAwareSuspendCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda(),
|
||||
lease.getSyncCanceler(),
|
||||
gate);
|
||||
});
|
||||
lease.commit();
|
||||
|
||||
nursery.requestCancelOnAll();
|
||||
EXPECT_TRUE(lease.getSyncCanceler().isCancellationRequested());
|
||||
|
||||
ASSERT_TRUE(static_cast<bool>(gate.waitingHandle));
|
||||
gate.waitingHandle.resume();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, ExceptionPtrRecorded)
|
||||
{
|
||||
std::exception_ptr captured;
|
||||
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
lease.fillSlot(
|
||||
[&captured, &lease]()
|
||||
{
|
||||
std::exception_ptr &exceptionStorage =
|
||||
lease.getExceptionStorage();
|
||||
auto invoker = throwingCompleteCReq(
|
||||
exceptionStorage,
|
||||
lease.getCallerLambda());
|
||||
captured = exceptionStorage;
|
||||
return invoker;
|
||||
});
|
||||
lease.commit();
|
||||
|
||||
EXPECT_TRUE(captured != nullptr);
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, LaunchSugar)
|
||||
{
|
||||
auto handle = nursery.launch(
|
||||
[](sscl::co::NonViralTaskNursery::Slot::Lease &lease)
|
||||
{
|
||||
return immediateCompleteCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda());
|
||||
});
|
||||
|
||||
EXPECT_TRUE(handle == handle);
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, LaunchWithOnSettledHook)
|
||||
{
|
||||
std::atomic<bool> hookRan{false};
|
||||
|
||||
nursery.launch(
|
||||
[](sscl::co::NonViralTaskNursery::Slot::Lease &lease)
|
||||
{
|
||||
return immediateCompleteCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda());
|
||||
},
|
||||
[&hookRan](std::exception_ptr &)
|
||||
{
|
||||
hookRan.store(true, std::memory_order_release);
|
||||
});
|
||||
|
||||
EXPECT_TRUE(hookRan.load(std::memory_order_acquire));
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, HandleStability)
|
||||
{
|
||||
auto handle = nursery.launch(
|
||||
[](sscl::co::NonViralTaskNursery::Slot::Lease &lease)
|
||||
{
|
||||
return immediateCompleteCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda());
|
||||
});
|
||||
|
||||
sscl::co::NonViralTaskNursery::Slot::Handle copy = handle;
|
||||
EXPECT_TRUE(handle == copy);
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, CommitWithoutFillSlotThrows)
|
||||
{
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
|
||||
EXPECT_THROW(lease.commit(), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, DoubleCommitThrows)
|
||||
{
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
lease.fillSlot(
|
||||
[&lease]()
|
||||
{
|
||||
return immediateCompleteCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda());
|
||||
});
|
||||
lease.commit();
|
||||
|
||||
EXPECT_THROW(lease.commit(), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, FillSlotTwiceThrows)
|
||||
{
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
lease.fillSlot(
|
||||
[&lease, this]()
|
||||
{
|
||||
return suspendUntilResumeCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda(),
|
||||
gate);
|
||||
});
|
||||
|
||||
EXPECT_THROW(
|
||||
lease.fillSlot(
|
||||
[&lease]()
|
||||
{
|
||||
return immediateCompleteCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda());
|
||||
}),
|
||||
std::runtime_error);
|
||||
|
||||
if (gate.waitingHandle) {
|
||||
gate.waitingHandle.resume();
|
||||
}
|
||||
|
||||
lease.commit();
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, SyncAwaitRejectsWhenAdmissionOpen)
|
||||
{
|
||||
boost::asio::io_context ioContext;
|
||||
|
||||
EXPECT_THROW(
|
||||
nursery.syncAwaitAllSettlements(ioContext),
|
||||
std::runtime_error);
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, SyncAwaitRejectsStoppedIoContext)
|
||||
{
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
lease.fillSlot(
|
||||
[&lease, this]()
|
||||
{
|
||||
return suspendUntilResumeCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda(),
|
||||
gate);
|
||||
});
|
||||
lease.commit();
|
||||
|
||||
nursery.closeAdmission();
|
||||
|
||||
boost::asio::io_context ioContext;
|
||||
ioContext.stop();
|
||||
|
||||
EXPECT_THROW(
|
||||
nursery.syncAwaitAllSettlements(ioContext),
|
||||
std::runtime_error);
|
||||
|
||||
if (gate.waitingHandle) {
|
||||
gate.waitingHandle.resume();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, SyncAwaitReturnsImmediatelyWhenDrained)
|
||||
{
|
||||
boost::asio::io_context ioContext;
|
||||
|
||||
nursery.closeAdmission();
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
|
||||
nursery.syncAwaitAllSettlements(ioContext);
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, UnsettledCountTracksInFlightTasks)
|
||||
{
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
lease.fillSlot(
|
||||
[&lease, this]()
|
||||
{
|
||||
return suspendUntilResumeCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda(),
|
||||
gate);
|
||||
});
|
||||
lease.commit();
|
||||
|
||||
EXPECT_EQ(nursery.unsettledCount(), 1U);
|
||||
EXPECT_FALSE(nursery.allSettled());
|
||||
|
||||
if (gate.waitingHandle) {
|
||||
gate.waitingHandle.resume();
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
EXPECT_EQ(nursery.unsettledCount(), 0U);
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, MultipleTasksDrainTogether)
|
||||
{
|
||||
std::atomic<bool> drained{false};
|
||||
|
||||
auto lease1 = nursery.getNewSlotLease();
|
||||
lease1.fillSlot(
|
||||
[&lease1, this]()
|
||||
{
|
||||
return suspendUntilResumeCReq(
|
||||
lease1.getExceptionStorage(),
|
||||
lease1.getCallerLambda(),
|
||||
gate);
|
||||
});
|
||||
lease1.commit();
|
||||
|
||||
auto lease2 = nursery.getNewSlotLease();
|
||||
lease2.fillSlot(
|
||||
[&lease2, this]()
|
||||
{
|
||||
return suspendUntilResumeCReq(
|
||||
lease2.getExceptionStorage(),
|
||||
lease2.getCallerLambda(),
|
||||
gate2);
|
||||
});
|
||||
lease2.commit();
|
||||
|
||||
EXPECT_EQ(nursery.unsettledCount(), 2U);
|
||||
|
||||
nursery.closeAdmission();
|
||||
nursery.asyncAwaitAllSettlements(
|
||||
[&drained]()
|
||||
{
|
||||
drained.store(true, std::memory_order_release);
|
||||
});
|
||||
|
||||
EXPECT_FALSE(drained.load(std::memory_order_acquire));
|
||||
|
||||
if (gate.waitingHandle) {
|
||||
gate.waitingHandle.resume();
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
EXPECT_FALSE(drained.load(std::memory_order_acquire));
|
||||
|
||||
if (gate2.waitingHandle) {
|
||||
gate2.waitingHandle.resume();
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
EXPECT_TRUE(drained.load(std::memory_order_acquire));
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, OnSettledHookRunsAtRetirement)
|
||||
{
|
||||
std::atomic<bool> hookRan{false};
|
||||
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
lease.setOnSettledHook(
|
||||
[&hookRan](std::exception_ptr &)
|
||||
{
|
||||
hookRan.store(true, std::memory_order_release);
|
||||
});
|
||||
lease.fillSlot(
|
||||
[&lease]()
|
||||
{
|
||||
return immediateCompleteCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda());
|
||||
});
|
||||
lease.commit();
|
||||
|
||||
EXPECT_TRUE(hookRan.load(std::memory_order_acquire));
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, OnSettledHookSeesRetiredSlot)
|
||||
{
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
lease.setOnSettledHook(
|
||||
[this](std::exception_ptr &)
|
||||
{
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
EXPECT_EQ(nursery.unsettledCount(), 0U);
|
||||
});
|
||||
lease.fillSlot(
|
||||
[&lease]()
|
||||
{
|
||||
return immediateCompleteCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda());
|
||||
});
|
||||
lease.commit();
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, DuplicateRetireThrows)
|
||||
{
|
||||
std::function<void()> completion;
|
||||
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
lease.fillSlot(
|
||||
[&completion, &lease]()
|
||||
{
|
||||
completion = lease.getCallerLambda();
|
||||
return immediateCompleteCReq(
|
||||
lease.getExceptionStorage(),
|
||||
completion);
|
||||
});
|
||||
lease.commit();
|
||||
|
||||
ASSERT_TRUE(static_cast<bool>(completion));
|
||||
EXPECT_THROW(completion(), std::runtime_error);
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, MovedLeaseTransfersReleaseObligation)
|
||||
{
|
||||
EXPECT_EQ(nursery.unsettledCount(), 0U);
|
||||
{
|
||||
auto lease = nursery.getNewSlotLease();
|
||||
auto movedLease = std::move(lease);
|
||||
(void)movedLease;
|
||||
}
|
||||
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
EXPECT_EQ(nursery.unsettledCount(), 0U);
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, LaunchAssignsDistinctHandles)
|
||||
{
|
||||
auto handle1 = nursery.launch(
|
||||
[this](sscl::co::NonViralTaskNursery::Slot::Lease &lease)
|
||||
{
|
||||
return suspendUntilResumeCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda(),
|
||||
gate);
|
||||
});
|
||||
|
||||
auto handle2 = nursery.launch(
|
||||
[this](sscl::co::NonViralTaskNursery::Slot::Lease &lease)
|
||||
{
|
||||
return suspendUntilResumeCReq(
|
||||
lease.getExceptionStorage(),
|
||||
lease.getCallerLambda(),
|
||||
gate2);
|
||||
});
|
||||
|
||||
EXPECT_NE(handle1, handle2);
|
||||
EXPECT_EQ(nursery.unsettledCount(), 2U);
|
||||
|
||||
if (gate.waitingHandle) {
|
||||
gate.waitingHandle.resume();
|
||||
}
|
||||
|
||||
if (gate2.waitingHandle) {
|
||||
gate2.waitingHandle.resume();
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
EXPECT_TRUE(nursery.allSettled());
|
||||
}
|
||||
|
||||
TEST_F(NonViralTaskNurseryTest, AdmissionIsOpenReflectsCloseAndOpen)
|
||||
{
|
||||
EXPECT_TRUE(nursery.admissionIsOpen());
|
||||
|
||||
nursery.closeAdmission();
|
||||
EXPECT_FALSE(nursery.admissionIsOpen());
|
||||
|
||||
nursery.openAdmission();
|
||||
EXPECT_TRUE(nursery.admissionIsOpen());
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <spinscale/cps/qutex.h>
|
||||
#include <spinscale/cps/lockerAndInvokerBase.h>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
namespace smo {
|
||||
|
||||
// Mock implementation of LockerAndInvokerBase for testing
|
||||
class MockLockerAndInvoker : public sscl::cps::LockerAndInvokerBase {
|
||||
public:
|
||||
explicit MockLockerAndInvoker(const void* addr)
|
||||
: sscl::cps::LockerAndInvokerBase(addr), awakened(false) {}
|
||||
|
||||
bool awakened;
|
||||
mutable sscl::cps::Qutex* registeredQutex = nullptr;
|
||||
mutable sscl::cps::LockerAndInvokerBase::List::iterator queueIterator;
|
||||
|
||||
sscl::cps::LockerAndInvokerBase::List::iterator
|
||||
getLockvokerIteratorForQutex(sscl::cps::Qutex& qutex) const override
|
||||
{
|
||||
registeredQutex = &qutex;
|
||||
|
||||
for (auto it = qutex.queue.begin(); it != qutex.queue.end(); ++it)
|
||||
{
|
||||
if ((**it) == *this)
|
||||
{
|
||||
queueIterator = it;
|
||||
return it;
|
||||
}
|
||||
}
|
||||
|
||||
throw std::runtime_error(
|
||||
"MockLockerAndInvoker: not registered in qutex queue");
|
||||
}
|
||||
|
||||
void awaken(bool forceAwaken = false) override
|
||||
{
|
||||
(void)forceAwaken;
|
||||
awakened = true;
|
||||
}
|
||||
|
||||
size_t getLockSetSize() const override
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
sscl::cps::Qutex& getLockAt(size_t index) const override
|
||||
{
|
||||
if (index != 0 || registeredQutex == nullptr)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"MockLockerAndInvoker: invalid lock index or no registered qutex");
|
||||
}
|
||||
|
||||
return *registeredQutex;
|
||||
}
|
||||
};
|
||||
|
||||
class QutexTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Create mock lockvokers with unique addresses
|
||||
mock1 = std::make_shared<MockLockerAndInvoker>(&addr1);
|
||||
mock2 = std::make_shared<MockLockerAndInvoker>(&addr2);
|
||||
mock3 = std::make_shared<MockLockerAndInvoker>(&addr3);
|
||||
mock4 = std::make_shared<MockLockerAndInvoker>(&addr4);
|
||||
mock5 = std::make_shared<MockLockerAndInvoker>(&addr5);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// Clean up
|
||||
}
|
||||
|
||||
sscl::cps::Qutex qutex{"test-qutex"};
|
||||
std::shared_ptr<MockLockerAndInvoker> mock1, mock2, mock3, mock4, mock5;
|
||||
|
||||
// Unique addresses for testing
|
||||
int addr1 = 1;
|
||||
int addr2 = 2;
|
||||
int addr3 = 3;
|
||||
int addr4 = 4;
|
||||
int addr5 = 5;
|
||||
};
|
||||
|
||||
// Test basic queue registration and unregistration
|
||||
TEST_F(QutexTest, QueueRegistrationAndUnregistration) {
|
||||
// Register mock1 in queue
|
||||
auto it1 = qutex.registerInQueue(mock1);
|
||||
EXPECT_EQ(qutex.queue.size(), 1);
|
||||
EXPECT_FALSE(qutex.isOwned);
|
||||
|
||||
// Register mock2 in queue
|
||||
auto it2 = qutex.registerInQueue(mock2);
|
||||
EXPECT_EQ(qutex.queue.size(), 2);
|
||||
|
||||
// Unregister mock1
|
||||
qutex.unregisterFromQueue(it1);
|
||||
EXPECT_EQ(qutex.queue.size(), 1);
|
||||
|
||||
// Unregister mock2
|
||||
qutex.unregisterFromQueue(it2);
|
||||
EXPECT_EQ(qutex.queue.size(), 0);
|
||||
}
|
||||
|
||||
// Test single lock acquisition when queue is empty
|
||||
TEST_F(QutexTest, SingleLockAcquisitionEmptyQueue) {
|
||||
// Register mock1
|
||||
(void)qutex.registerInQueue(mock1);
|
||||
|
||||
// Try to acquire with nRequiredLocks = 1
|
||||
bool acquired = qutex.tryAcquire(*mock1, 1);
|
||||
EXPECT_TRUE(acquired);
|
||||
EXPECT_TRUE(qutex.isOwned);
|
||||
}
|
||||
|
||||
// Test single lock acquisition when at front of queue
|
||||
TEST_F(QutexTest, SingleLockAcquisitionAtFront) {
|
||||
// Register multiple lockvokers
|
||||
(void)qutex.registerInQueue(mock1);
|
||||
(void)qutex.registerInQueue(mock2);
|
||||
(void)qutex.registerInQueue(mock3);
|
||||
|
||||
// mock1 should be at front, mock3 at back
|
||||
EXPECT_EQ(qutex.queue.front().get(), mock1.get());
|
||||
EXPECT_EQ(qutex.queue.back().get(), mock3.get());
|
||||
|
||||
// mock1 (at front) should succeed
|
||||
bool acquired = qutex.tryAcquire(*mock1, 1);
|
||||
EXPECT_TRUE(acquired);
|
||||
EXPECT_TRUE(qutex.isOwned);
|
||||
|
||||
// mock2 (not at front) should fail
|
||||
qutex.isOwned = false; // Reset for testing
|
||||
bool acquired2 = qutex.tryAcquire(*mock2, 1);
|
||||
EXPECT_FALSE(acquired2);
|
||||
}
|
||||
|
||||
// Test single lock acquisition failure when not at front
|
||||
TEST_F(QutexTest, SingleLockAcquisitionNotAtFront) {
|
||||
// Register multiple lockvokers
|
||||
(void)qutex.registerInQueue(mock1);
|
||||
(void)qutex.registerInQueue(mock2);
|
||||
|
||||
// mock2 (not at front) should fail
|
||||
bool acquired = qutex.tryAcquire(*mock2, 1);
|
||||
EXPECT_FALSE(acquired);
|
||||
EXPECT_FALSE(qutex.isOwned);
|
||||
}
|
||||
|
||||
// Test multi-lock acquisition (nRequiredLocks > 1)
|
||||
TEST_F(QutexTest, MultiLockAcquisition) {
|
||||
// Register 4 lockvokers
|
||||
(void)qutex.registerInQueue(mock1);
|
||||
(void)qutex.registerInQueue(mock2);
|
||||
(void)qutex.registerInQueue(mock3);
|
||||
(void)qutex.registerInQueue(mock4);
|
||||
|
||||
// For nRequiredLocks = 2, need to be in top 50% (top 2 out of 4)
|
||||
// mock1 (position 1) should succeed
|
||||
bool acquired1 = qutex.tryAcquire(*mock1, 2);
|
||||
EXPECT_TRUE(acquired1);
|
||||
|
||||
// Reset for next test
|
||||
qutex.isOwned = false;
|
||||
|
||||
// mock2 (position 2) should succeed
|
||||
bool acquired2 = qutex.tryAcquire(*mock2, 2);
|
||||
EXPECT_TRUE(acquired2);
|
||||
|
||||
// Reset for next test
|
||||
qutex.isOwned = false;
|
||||
|
||||
// mock3 (position 3) should fail (in bottom 50%)
|
||||
bool acquired3 = qutex.tryAcquire(*mock3, 2);
|
||||
EXPECT_FALSE(acquired3);
|
||||
|
||||
// Reset for next test
|
||||
qutex.isOwned = false;
|
||||
|
||||
// mock4 (position 4) should fail (in bottom 50%)
|
||||
bool acquired4 = qutex.tryAcquire(*mock4, 2);
|
||||
EXPECT_FALSE(acquired4);
|
||||
}
|
||||
|
||||
// Test multi-lock acquisition with 3 required locks
|
||||
TEST_F(QutexTest, MultiLockAcquisitionThreeLocks) {
|
||||
// Register 6 lockvokers
|
||||
(void)qutex.registerInQueue(mock1);
|
||||
(void)qutex.registerInQueue(mock2);
|
||||
(void)qutex.registerInQueue(mock3);
|
||||
(void)qutex.registerInQueue(mock4);
|
||||
(void)qutex.registerInQueue(mock5);
|
||||
|
||||
// Create one more mock
|
||||
int addr6 = 6;
|
||||
auto mock6 = std::make_shared<MockLockerAndInvoker>(&addr6);
|
||||
(void)qutex.registerInQueue(mock6);
|
||||
|
||||
// For nRequiredLocks = 3, need to be in top 66% (top 4 out of 6)
|
||||
// Positions 1, 2, 3, 4 should succeed
|
||||
// Positions 5, 6 should fail
|
||||
|
||||
bool acquired1 = qutex.tryAcquire(*mock1, 3);
|
||||
EXPECT_TRUE(acquired1);
|
||||
qutex.isOwned = false;
|
||||
|
||||
bool acquired2 = qutex.tryAcquire(*mock2, 3);
|
||||
EXPECT_TRUE(acquired2);
|
||||
qutex.isOwned = false;
|
||||
|
||||
bool acquired3 = qutex.tryAcquire(*mock3, 3);
|
||||
EXPECT_TRUE(acquired3);
|
||||
qutex.isOwned = false;
|
||||
|
||||
bool acquired4 = qutex.tryAcquire(*mock4, 3);
|
||||
EXPECT_TRUE(acquired4);
|
||||
qutex.isOwned = false;
|
||||
|
||||
bool acquired5 = qutex.tryAcquire(*mock5, 3);
|
||||
EXPECT_FALSE(acquired5);
|
||||
|
||||
qutex.isOwned = false;
|
||||
bool acquired6 = qutex.tryAcquire(*mock6, 3);
|
||||
EXPECT_FALSE(acquired6);
|
||||
}
|
||||
|
||||
// Test acquisition failure when already owned
|
||||
TEST_F(QutexTest, AcquisitionFailureWhenOwned) {
|
||||
// Register mock1
|
||||
(void)qutex.registerInQueue(mock1);
|
||||
|
||||
// Manually set as owned
|
||||
qutex.isOwned = true;
|
||||
|
||||
// Try to acquire should fail
|
||||
bool acquired = qutex.tryAcquire(*mock1, 1);
|
||||
EXPECT_FALSE(acquired);
|
||||
EXPECT_TRUE(qutex.isOwned);
|
||||
}
|
||||
|
||||
// Test backoff with single item (should not rotate)
|
||||
TEST_F(QutexTest, BackoffSingleItem) {
|
||||
// Register only one lockvoker
|
||||
(void)qutex.registerInQueue(mock1);
|
||||
|
||||
// Set as owned first
|
||||
qutex.isOwned = true;
|
||||
|
||||
// nRequiredLocks > 1 avoids the "front item with nRequiredLocks==1" guard
|
||||
mock1->awakened = false;
|
||||
qutex.backoff(*mock1, 2);
|
||||
|
||||
EXPECT_FALSE(qutex.isOwned);
|
||||
EXPECT_EQ(qutex.queue.size(), 1u);
|
||||
// Should not awaken since there's only one item
|
||||
EXPECT_FALSE(mock1->awakened);
|
||||
}
|
||||
|
||||
// Test backoff with multiple items and rotation
|
||||
TEST_F(QutexTest, BackoffWithRotation) {
|
||||
// Register multiple lockvokers
|
||||
(void)qutex.registerInQueue(mock1);
|
||||
(void)qutex.registerInQueue(mock2);
|
||||
(void)qutex.registerInQueue(mock3);
|
||||
|
||||
// Set as owned first
|
||||
qutex.isOwned = true;
|
||||
|
||||
// mock1 should be at front initially
|
||||
EXPECT_EQ(qutex.queue.front().get(), mock1.get());
|
||||
|
||||
// Backoff from mock1 (at front) with nRequiredLocks = 2
|
||||
mock2->awakened = false;
|
||||
qutex.backoff(*mock1, 2);
|
||||
|
||||
// mock1 should have been rotated to position 2
|
||||
// mock2 should now be at front
|
||||
EXPECT_EQ(qutex.queue.front().get(), mock2.get());
|
||||
EXPECT_FALSE(qutex.isOwned);
|
||||
|
||||
// mock2 should have been awakened
|
||||
EXPECT_TRUE(mock2->awakened);
|
||||
}
|
||||
|
||||
// Test backoff with rotation to back when queue smaller than nRequiredLocks
|
||||
TEST_F(QutexTest, BackoffRotationToBack) {
|
||||
// Register only 2 lockvokers
|
||||
(void)qutex.registerInQueue(mock1);
|
||||
(void)qutex.registerInQueue(mock2);
|
||||
|
||||
// Set as owned first
|
||||
qutex.isOwned = true;
|
||||
|
||||
// mock1 should be at front initially
|
||||
EXPECT_EQ(qutex.queue.front().get(), mock1.get());
|
||||
EXPECT_EQ(qutex.queue.back().get(), mock2.get());
|
||||
|
||||
// Backoff from mock1 with nRequiredLocks = 5 (larger than queue size)
|
||||
mock2->awakened = false;
|
||||
qutex.backoff(*mock1, 5);
|
||||
|
||||
// mock1 should have been moved to the back
|
||||
EXPECT_EQ(qutex.queue.front().get(), mock2.get());
|
||||
EXPECT_EQ(qutex.queue.back().get(), mock1.get());
|
||||
EXPECT_FALSE(qutex.isOwned);
|
||||
|
||||
// mock2 should have been awakened
|
||||
EXPECT_TRUE(mock2->awakened);
|
||||
}
|
||||
|
||||
// Test release functionality
|
||||
TEST_F(QutexTest, Release) {
|
||||
// Register multiple lockvokers
|
||||
(void)qutex.registerInQueue(mock1);
|
||||
(void)qutex.registerInQueue(mock2);
|
||||
|
||||
ASSERT_TRUE(qutex.tryAcquire(*mock1, 1));
|
||||
|
||||
// Release should set isOwned to false and awaken front item
|
||||
mock1->awakened = false;
|
||||
qutex.release();
|
||||
|
||||
EXPECT_FALSE(qutex.isOwned);
|
||||
EXPECT_TRUE(mock1->awakened);
|
||||
}
|
||||
|
||||
// Test release without a prior acquire is rejected
|
||||
TEST_F(QutexTest, ReleaseWithoutAcquireThrows) {
|
||||
qutex.isOwned = true;
|
||||
|
||||
EXPECT_THROW(qutex.release(), std::runtime_error);
|
||||
EXPECT_TRUE(qutex.queue.empty());
|
||||
}
|
||||
|
||||
// Test exception when trying to acquire from empty queue
|
||||
TEST_F(QutexTest, ExceptionOnEmptyQueueAcquisition) {
|
||||
// Don't register any lockvokers
|
||||
EXPECT_THROW(qutex.tryAcquire(*mock1, 1), std::runtime_error);
|
||||
}
|
||||
|
||||
// Test exception when backoff called on empty queue
|
||||
TEST_F(QutexTest, ExceptionOnEmptyQueueBackoff) {
|
||||
// Don't register any lockvokers
|
||||
EXPECT_THROW(qutex.backoff(*mock1, 1), std::runtime_error);
|
||||
}
|
||||
|
||||
// Test edge case: single lockvoker with multiple required locks
|
||||
TEST_F(QutexTest, SingleLockvokerMultipleRequiredLocks) {
|
||||
// Register only one lockvoker
|
||||
(void)qutex.registerInQueue(mock1);
|
||||
|
||||
// Should succeed regardless of nRequiredLocks when only one item
|
||||
bool acquired = qutex.tryAcquire(*mock1, 5);
|
||||
EXPECT_TRUE(acquired);
|
||||
EXPECT_TRUE(qutex.isOwned);
|
||||
}
|
||||
|
||||
// Test unregistration without locking
|
||||
TEST_F(QutexTest, UnregistrationWithoutLocking) {
|
||||
// Register lockvoker
|
||||
auto it1 = qutex.registerInQueue(mock1);
|
||||
EXPECT_EQ(qutex.queue.size(), 1);
|
||||
|
||||
// Unregister without locking
|
||||
qutex.unregisterFromQueue(it1, false);
|
||||
EXPECT_EQ(qutex.queue.size(), 0);
|
||||
}
|
||||
|
||||
} // namespace smo
|
||||
Reference in New Issue
Block a user