diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5a4e85a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third_party/googletest"] + path = third_party/googletest + url = https://github.com/google/googletest.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 0735b48..804e706 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,8 @@ math(EXPR MIND_VOSCILLATOR_FREQ_MS "1000 / ${MIND_VOSCILLATOR_PERIOD_MS}") # World thread configuration option(WORLD_USE_BODY_THREAD "Use body thread for world component instead of separate world thread" OFF) +# Test configuration +option(ENABLE_TESTS "Enable building tests" OFF) # Configure config.h configure_file( @@ -56,6 +58,10 @@ if(NOT DL_LIBRARY) message(FATAL_ERROR "Dynamic linking library (libdl/libldl) not found") endif() +# Add third-party dependencies +if(ENABLE_TESTS) + add_subdirectory(third_party) +endif() # Add core components add_subdirectory(smocore) add_subdirectory(commonLibs) @@ -74,6 +80,12 @@ target_link_libraries(salmanoff # Add all registered DAPSS targets as dependencies add_all_daps_dependencies() +# Add tests if enabled +if(ENABLE_TESTS) + enable_testing() + add_subdirectory(tests) +endif() + install(TARGETS salmanoff DESTINATION bin) # Install device configuration files (preprocessed .daps files) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..7cf9fbc --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,16 @@ +# Create a test executable for qutex +add_executable(qutex_tests smocore/qutex_tests.cpp) + +# Link against Google Test and the smocore library +target_link_libraries(qutex_tests + gtest_main + smocore + ${Boost_LIBRARIES} + ${DL_LIBRARY} +) + +# Ensure Google Test is built before our test executable +add_dependencies(qutex_tests gtest_main) + +# Add the test to CTest +add_test(NAME qutex_tests COMMAND qutex_tests) diff --git a/tests/smocore/qutex_tests.cpp b/tests/smocore/qutex_tests.cpp new file mode 100644 index 0000000..d45d1b1 --- /dev/null +++ b/tests/smocore/qutex_tests.cpp @@ -0,0 +1,347 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace smo { + +// Mock implementation of LockerAndInvokerBase for testing +class MockLockerAndInvoker : public LockerAndInvokerBase { +public: + explicit MockLockerAndInvoker(const void* addr) + : LockerAndInvokerBase(addr), awakened(false) {} + + bool awakened; + Qutex* registeredQutex = nullptr; + List::iterator queueIterator; + + List::iterator getLockvokerIteratorForQutex(Qutex& qutex) override { + registeredQutex = &qutex; + queueIterator = qutex.registerInQueue(std::shared_ptr(this)); + return queueIterator; + } + + void awaken(bool forceAwaken = false) override { + awakened = true; + } +}; + +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 + } + + Qutex 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 + auto it1 = 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 + auto it1 = qutex.registerInQueue(mock1); + auto it2 = qutex.registerInQueue(mock2); + auto it3 = 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 + auto it1 = qutex.registerInQueue(mock1); + auto it2 = 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 + auto it1 = qutex.registerInQueue(mock1); + auto it2 = qutex.registerInQueue(mock2); + auto it3 = qutex.registerInQueue(mock3); + auto it4 = 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 + auto it1 = qutex.registerInQueue(mock1); + auto it2 = qutex.registerInQueue(mock2); + auto it3 = qutex.registerInQueue(mock3); + auto it4 = qutex.registerInQueue(mock4); + auto it5 = qutex.registerInQueue(mock5); + + // Create one more mock + int addr6 = 6; + auto mock6 = std::make_shared(&addr6); + auto it6 = 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 + auto it1 = 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 + auto it1 = qutex.registerInQueue(mock1); + + // Set as owned first + qutex.isOwned = true; + + // Backoff should not rotate and should awaken the front item + mock1->awakened = false; + qutex.backoff(*mock1, 1); + + EXPECT_FALSE(qutex.isOwned); + EXPECT_EQ(qutex.queue.size(), 1); + // 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 + auto it1 = qutex.registerInQueue(mock1); + auto it2 = qutex.registerInQueue(mock2); + auto it3 = 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 + auto it1 = qutex.registerInQueue(mock1); + auto it2 = 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 + auto it1 = qutex.registerInQueue(mock1); + auto it2 = qutex.registerInQueue(mock2); + + // Set as owned + qutex.isOwned = true; + + // 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 with empty queue +TEST_F(QutexTest, ReleaseEmptyQueue) { + // Set as owned + qutex.isOwned = true; + + // Release with empty queue should just set isOwned to false + qutex.release(); + + EXPECT_FALSE(qutex.isOwned); + 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 + auto it1 = 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 diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt new file mode 100644 index 0000000..175a76f --- /dev/null +++ b/third_party/CMakeLists.txt @@ -0,0 +1,2 @@ +# Add Google Test as a subdirectory +add_subdirectory(googletest googletest) diff --git a/third_party/googletest b/third_party/googletest new file mode 160000 index 0000000..50b8600 --- /dev/null +++ b/third_party/googletest @@ -0,0 +1 @@ +Subproject commit 50b8600c63c5487e901e2845a0f64d384a65f75d