From 5b81ea893c87d767d616e5b74876928b6a7c1786 Mon Sep 17 00:00:00 2001 From: Hayodea Hekol Date: Tue, 9 Jun 2026 05:50:28 -0400 Subject: [PATCH] Tests: Add sscl Nursery tests. We'll evetually move these into sscl. --- tests/CMakeLists.txt | 14 + .../nonViralTaskNursery_tests.cpp | 637 ++++++++++++++++++ 2 files changed, 651 insertions(+) create mode 100644 tests/libspinscale/nonViralTaskNursery_tests.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3bea28e..83e5378 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -30,3 +30,17 @@ add_dependencies(stagingBuffer_tests gtest_main) # Add the test to CTest add_test(NAME stagingBuffer_tests COMMAND stagingBuffer_tests) + +# Create a test executable for NonViralTaskNursery +add_executable(nonViralTaskNursery_tests + libspinscale/nonViralTaskNursery_tests.cpp) + +target_link_libraries(nonViralTaskNursery_tests + gtest_main + spinscale + ${Boost_LIBRARIES} +) + +add_dependencies(nonViralTaskNursery_tests gtest_main) + +add_test(NAME nonViralTaskNursery_tests COMMAND nonViralTaskNursery_tests) diff --git a/tests/libspinscale/nonViralTaskNursery_tests.cpp b/tests/libspinscale/nonViralTaskNursery_tests.cpp new file mode 100644 index 0000000..078a442 --- /dev/null +++ b/tests/libspinscale/nonViralTaskNursery_tests.cpp @@ -0,0 +1,637 @@ +#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::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, 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]() + { + 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, OnSettledHookExceptionStillRetiresSlot) +{ + auto lease = nursery.getNewSlotLease(); + lease.setOnSettledHook( + []() + { + throw std::runtime_error("onSettled hook failure"); + }); + lease.fillSlot( + [&lease]() + { + return immediateCompleteCReq( + lease.getExceptionStorage(), + lease.getCallerLambda()); + }); + lease.commit(); + + EXPECT_TRUE(nursery.allSettled()); + EXPECT_EQ(nursery.unsettledCount(), 0U); +} + +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()); +}