diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8cf8b5e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "googletest"] + path = googletest + url = https://github.com/google/googletest.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f2f847..6e502e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/googletest b/googletest new file mode 160000 index 0000000..7140cd4 --- /dev/null +++ b/googletest @@ -0,0 +1 @@ +Subproject commit 7140cd416cecd7462a8aae488024abeee55598e4 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..bfeeccf --- /dev/null +++ b/tests/CMakeLists.txt @@ -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) diff --git a/tests/co/nonViralTaskNursery_tests.cpp b/tests/co/nonViralTaskNursery_tests.cpp new file mode 100644 index 0000000..4fdf7bb --- /dev/null +++ b/tests/co/nonViralTaskNursery_tests.cpp @@ -0,0 +1,657 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +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 completion) +{ + (void)exceptionPtr; + (void)completion; + co_return; +} + +sscl::co::NonViralNonPostingInvoker throwingCompleteCReq( + std::exception_ptr &exceptionPtr, + std::function completion) +{ + (void)exceptionPtr; + (void)completion; + throw std::runtime_error("nursery test failure"); + co_return; +} + +sscl::co::NonViralNonPostingInvoker suspendUntilResumeCReq( + std::exception_ptr &exceptionPtr, + std::function completion, + ResumeGate &gate) +{ + (void)exceptionPtr; + (void)completion; + co_await gate; + co_return; +} + +sscl::co::NonViralNonPostingInvoker cancelAwareSuspendCReq( + std::exception_ptr &exceptionPtr, + std::function 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 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(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(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(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 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 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 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 completion; + + auto lease = nursery.getNewSlotLease(); + lease.fillSlot( + [&completion, &lease]() + { + completion = lease.getCallerLambda(); + return immediateCompleteCReq( + lease.getExceptionStorage(), + completion); + }); + lease.commit(); + + ASSERT_TRUE(static_cast(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()); +} diff --git a/tests/cps/qutex_tests.cpp b/tests/cps/qutex_tests.cpp new file mode 100644 index 0000000..bf33a01 --- /dev/null +++ b/tests/cps/qutex_tests.cpp @@ -0,0 +1,373 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +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(&addr1); + mock2 = std::make_shared(&addr2); + mock3 = std::make_shared(&addr3); + mock4 = std::make_shared(&addr4); + mock5 = std::make_shared(&addr5); + } + + void TearDown() override { + // Clean up + } + + sscl::cps::Qutex qutex{"test-qutex"}; + std::shared_ptr 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(&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