//! \~\file piprotectedvariable_test.cpp //! \~\brief Unit tests for PIProtectedVariable class /* PIP - Platform Independent Primitives Unit tests for PIProtectedVariable This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . */ #include "pimutex.h" #include "piprotectedvariable.h" #include "gtest/gtest.h" #include #include #include #include #include #include using namespace std; namespace { constexpr int THREAD_COUNT = 10; constexpr int ITERATIONS_PER_THREAD = 1000; constexpr int WAIT_THREAD_TIME_MS = 1000; } // anonymous namespace //! \~\brief Basic functionality tests //! \~\english Tests for basic constructor, set, get, and operator= functionality //! \~\russian Тесты базовой функциональности конструктора, set, get и оператора= TEST(PIProtectedVariable_Basic, ConstructorCreatesValidObject) { PIProtectedVariable pv; // Note: PIProtectedVariable does not value-initialize its internal var, // so we just verify that the object is constructible and we can set/get values pv.set(42); EXPECT_EQ(pv.get(), 42); } TEST(PIProtectedVariable_Basic, SetCorrectlySetsValue) { PIProtectedVariable pv; pv.set(42); EXPECT_EQ(pv.get(), 42); } TEST(PIProtectedVariable_Basic, SetWithDifferentTypes) { PIProtectedVariable pvInt; PIProtectedVariable pvDouble; PIProtectedVariable pvString; pvInt.set(123); pvDouble.set(3.14159); pvString.set("Hello, World!"); EXPECT_EQ(pvInt.get(), 123); EXPECT_DOUBLE_EQ(pvDouble.get(), 3.14159); EXPECT_EQ(pvString.get(), "Hello, World!"); } TEST(PIProtectedVariable_Basic, GetReturnsCopy) { PIProtectedVariable pv; pv.set(100); int val1 = pv.get(); int val2 = pv.get(); // Modifying copies should not affect the protected variable val1 = 200; val2 = 300; EXPECT_EQ(pv.get(), 100); EXPECT_EQ(val1, 200); EXPECT_EQ(val2, 300); } TEST(PIProtectedVariable_Basic, GetReturnsCopyForObjectTypes) { struct TestStruct { int x; int y; TestStruct(): x(0), y(0) {} TestStruct(int a, int b): x(a), y(b) {} }; PIProtectedVariable pv; pv.set(TestStruct(10, 20)); TestStruct val = pv.get(); val.x = 100; val.y = 200; // Original should be unchanged TestStruct original = pv.get(); EXPECT_EQ(original.x, 10); EXPECT_EQ(original.y, 20); // Verify the copy was modified EXPECT_EQ(val.x, 100); EXPECT_EQ(val.y, 200); } TEST(PIProtectedVariable_Basic, OperatorAssignmentWorks) { PIProtectedVariable pv; pv = 999; EXPECT_EQ(pv.get(), 999); } TEST(PIProtectedVariable_Basic, OperatorAssignmentReturnsReference) { PIProtectedVariable pv; PIProtectedVariable & ref = (pv = 42); EXPECT_EQ(&ref, &pv); EXPECT_EQ(pv.get(), 42); } TEST(PIProtectedVariable_Basic, EmptyDefaultConstructedVariable) { // Note: PIProtectedVariable does not value-initialize its internal var // We can only test that it's constructible and works after setting values PIProtectedVariable pv; pv.set(0); // Set to zero explicitly EXPECT_EQ(pv.get(), 0); PIProtectedVariable pvStr; pvStr.set(""); // Set to empty explicitly EXPECT_TRUE(pvStr.get().empty()); } TEST(PIProtectedVariable_Basic, SetWithMoveSemantics) { PIProtectedVariable pv; string movable = "This is a movable string"; pv.set(std::move(movable)); EXPECT_EQ(pv.get(), "This is a movable string"); } //! \~\brief Thread safety tests //! \~\english Tests for concurrent access safety //! \~\russian Тесты потокобезопасности при конкурентном доступе TEST(PIProtectedVariable_ThreadSafety, ConcurrentSetOperations) { PIProtectedVariable pv; atomic start(false); vector threads; for (int i = 0; i < THREAD_COUNT; ++i) { threads.emplace_back([&pv, &start, i]() { while (!start.load()) { this_thread::yield(); } for (int j = 0; j < ITERATIONS_PER_THREAD; ++j) { pv.set(i * ITERATIONS_PER_THREAD + j); } }); } start.store(true); for (auto & t: threads) { t.join(); } // The final value should be one of the values set by the threads // We can't predict which one, but it should be valid int finalVal = pv.get(); EXPECT_GE(finalVal, 0); EXPECT_LT(finalVal, THREAD_COUNT * ITERATIONS_PER_THREAD); } TEST(PIProtectedVariable_ThreadSafety, ConcurrentGetOperations) { PIProtectedVariable pv; pv.set(42); atomic readCount(0); atomic wrongCount(0); atomic start(false); vector threads; for (int i = 0; i < THREAD_COUNT; ++i) { threads.emplace_back([&pv, &readCount, &wrongCount, &start]() { while (!start.load()) { this_thread::yield(); } for (int j = 0; j < ITERATIONS_PER_THREAD; ++j) { int val = pv.get(); readCount++; if (val != 42) { wrongCount++; } } }); } start.store(true); for (auto & t: threads) { t.join(); } EXPECT_EQ(readCount, THREAD_COUNT * ITERATIONS_PER_THREAD); EXPECT_EQ(wrongCount, 0); } TEST(PIProtectedVariable_ThreadSafety, MixedReadWriteOperations) { PIProtectedVariable pv; pv.set(0); // Initialize with a valid value atomic counter(0); atomic readCount(0); atomic invalidReads(0); atomic start(false); const int NUM_WRITER_ITERATIONS = 1000; // Writer thread thread writer([&pv, &counter, &start]() { while (!start.load()) { this_thread::yield(); } for (int i = 0; i < NUM_WRITER_ITERATIONS; ++i) { int val = counter.fetch_add(1) + 1; pv.set(val); } }); // Reader threads vector readers; for (int i = 0; i < THREAD_COUNT; ++i) { readers.emplace_back([&pv, &readCount, &invalidReads, &start, NUM_WRITER_ITERATIONS]() { while (!start.load()) { this_thread::yield(); } for (int j = 0; j < NUM_WRITER_ITERATIONS; ++j) { int val = pv.get(); readCount++; // Verify value is within expected range (0 to NUM_WRITER_ITERATIONS) if (val < 0 || val > NUM_WRITER_ITERATIONS) { invalidReads++; } } }); } start.store(true); writer.join(); for (auto & t: readers) { t.join(); } // Verify that counter reached expected value EXPECT_EQ(counter, NUM_WRITER_ITERATIONS); // Verify that all reads were valid EXPECT_EQ(readCount, THREAD_COUNT * NUM_WRITER_ITERATIONS); EXPECT_EQ(invalidReads, 0); } TEST(PIProtectedVariable_ThreadSafety, HeavyConcurrentAccess) { PIProtectedVariable pv; pv.set(0); // Initialize with a valid value atomic writeCount(0); atomic readCount(0); atomic done(false); atomic start(false); // Multiple writers vector writers; for (int i = 0; i < THREAD_COUNT / 2; ++i) { writers.emplace_back([&pv, &writeCount, &done, &start, i]() { while (!start.load()) { this_thread::yield(); } while (!done.load()) { pv.set(i * 1000 + writeCount.fetch_add(1)); } }); } // Multiple readers vector readers; for (int i = 0; i < THREAD_COUNT / 2; ++i) { readers.emplace_back([&pv, &readCount, &done, &start]() { while (!start.load()) { this_thread::yield(); } while (!done.load()) { int val = pv.get(); readCount++; // Verify value is non-negative (writers only write positive values) if (val < 0) { FAIL() << "Invalid value read: " << val; } } }); } start.store(true); // Run for a short time this_thread::sleep_for(chrono::milliseconds(500)); done.store(true); for (auto & t: writers) { t.join(); } for (auto & t: readers) { t.join(); } EXPECT_GT(writeCount, 0); EXPECT_GT(readCount, 0); } //! \~\brief Pointer wrapper tests //! \~\english Tests for getRef() and Pointer wrapper functionality //! \~\russian Тесты обертки getRef() и Pointer TEST(PIProtectedVariable_Pointer, GetRefReturnsValidPointer) { PIProtectedVariable pv; pv.set(42); auto ptr = pv.getRef(); EXPECT_NE(&(*ptr), nullptr); EXPECT_EQ(*ptr, 42); } TEST(PIProtectedVariable_Pointer, OperatorArrowWorks) { struct TestStruct { int x = 10; int y = 20; int getValue() const { return x + y; } }; PIProtectedVariable pv; pv.set(TestStruct()); auto ptr = pv.getRef(); EXPECT_EQ(ptr->x, 10); EXPECT_EQ(ptr->y, 20); EXPECT_EQ(ptr->getValue(), 30); } TEST(PIProtectedVariable_Pointer, OperatorDerefWorks) { PIProtectedVariable pv; pv.set("original"); auto ptr = pv.getRef(); *ptr = "modified"; EXPECT_EQ(pv.get(), "modified"); } TEST(PIProtectedVariable_Pointer, OperatorDerefAllowsModification) { PIProtectedVariable pv; pv.set(100); auto ptr = pv.getRef(); (*ptr) += 50; EXPECT_EQ(pv.get(), 150); } TEST(PIProtectedVariable_Pointer, MutexLockedWhenPointerCreated) { PIProtectedVariable pv; pv.set(42); atomic gotRef(false); atomic start(false); // Start blocker thread but don't let it run yet thread blocker([&pv, &gotRef, &start]() { while (!start.load()) { this_thread::yield(); } auto p = pv.getRef(); gotRef.store(true); }); // Get the lock in main thread auto ptr = pv.getRef(); // Now let blocker try to acquire start.store(true); // Give the thread time to try to acquire the lock this_thread::sleep_for(chrono::milliseconds(100)); // The blocker should NOT have acquired the lock yet EXPECT_FALSE(gotRef.load()); // Release the first pointer by letting it go out of scope ptr.~Pointer(); blocker.join(); EXPECT_TRUE(gotRef.load()); } TEST(PIProtectedVariable_Pointer, MutexUnlockedWhenPointerDestroyed) { PIProtectedVariable pv; pv.set(42); { auto ptr = pv.getRef(); // ptr holds the lock (*ptr) = 100; } // ptr destroyed here, lock released // Now we should be able to get another reference immediately auto ptr2 = pv.getRef(); EXPECT_EQ(*ptr2, 100); } TEST(PIProtectedVariable_Pointer, PointerModifiesProtectedValue) { PIProtectedVariable pv; pv.set("Hello"); { auto ptr = pv.getRef(); *ptr += " World"; } EXPECT_EQ(pv.get(), "Hello World"); } TEST(PIProtectedVariable_Pointer, PointerWithComplexTypes) { struct ComplexType { vector data; map mapping; ComplexType() { data = {1, 2, 3, 4, 5}; mapping["one"] = 1; mapping["two"] = 2; } }; PIProtectedVariable pv; pv.set(ComplexType()); auto ptr = pv.getRef(); ptr->data.push_back(6); ptr->mapping["three"] = 3; EXPECT_EQ(pv.get().data.size(), 6u); EXPECT_EQ(pv.get().mapping.size(), 3u); EXPECT_EQ(pv.get().mapping["three"], 3); } //! \~\brief Move semantics tests //! \~\english Tests for move constructor and lock ownership transfer //! \~\russian Тесты конструктора перемещения и передачи владения блокировкой TEST(PIProtectedVariable_Move, MoveConstructorWorks) { PIProtectedVariable pv; pv.set(42); auto ptr1 = pv.getRef(); auto ptr2 = std::move(ptr1); // ptr2 should have access EXPECT_EQ(*ptr2, 42); *ptr2 = 100; EXPECT_EQ(pv.get(), 100); } TEST(PIProtectedVariable_Move, MoveConstructorTransfersLockOwnership) { PIProtectedVariable pv; pv.set(42); atomic unlockCount(0); // We can't directly test unlock count without modifying the class, // but we can test the behavior: after move, only the target should unlock auto ptr1 = pv.getRef(); // ptr1 holds the lock bool canAcquire = false; thread tryAcquire([&pv, &canAcquire]() { auto p = pv.getRef(); canAcquire = true; }); this_thread::sleep_for(chrono::milliseconds(50)); EXPECT_FALSE(canAcquire); // Lock still held by ptr1 // Move ptr1 to ptr2 auto ptr2 = std::move(ptr1); // ptr2 should still hold the lock this_thread::sleep_for(chrono::milliseconds(50)); EXPECT_FALSE(canAcquire); // Lock still held (now by ptr2) // Destroy ptr2 ptr2.~Pointer(); tryAcquire.join(); EXPECT_TRUE(canAcquire); // Now lock is free } TEST(PIProtectedVariable_Move, SourceDoesNotUnlockAfterMove) { PIProtectedVariable pv; pv.set(42); atomic deadlockOccurred(false); // If source unlocked after move, we would get a double-unlock error // or undefined behavior. This test verifies that moving works correctly. auto ptr1 = pv.getRef(); (*ptr1) = 100; // Move ptr1 auto ptr2 = std::move(ptr1); // ptr2 should work correctly (*ptr2) = 200; EXPECT_EQ(pv.get(), 200); // When ptr1 is destroyed, it should NOT unlock (ownsLock should be false) // When ptr2 is destroyed, it SHOULD unlock // This should complete without any errors } TEST(PIProtectedVariable_Move, TargetUnlocksInDestructor) { PIProtectedVariable pv; pv.set(42); bool lockReleased = false; { auto ptr1 = pv.getRef(); auto ptr2 = std::move(ptr1); // ptr2 holds the lock, ptr1 does not } // ptr2 destroyed here, should release lock // If we get here without deadlock, the lock was properly released auto ptr3 = pv.getRef(); lockReleased = true; EXPECT_TRUE(lockReleased); } TEST(PIProtectedVariable_Move, UnlockCalledExactlyOnceAfterMove) { PIProtectedVariable pv; pv.set(42); // Test that after move: // 1. Source Pointer's destructor does NOT call unlock() // 2. Target Pointer's destructor DOES call unlock() // 3. Total unlock() calls = 1 (not 2!) { auto ptr1 = pv.getRef(); auto ptr2 = std::move(ptr1); // ptr1.ownsLock should be false // ptr2.ownsLock should be true // Both go out of scope // Only ptr2 should unlock } // If we get here without issues, exactly one unlock happened EXPECT_EQ(pv.get(), 42); } TEST(PIProtectedVariable_Move, MoveWithRVODisabled) { // This test should be compiled with -fno-elide-constructors // to ensure move constructor is actually called PIProtectedVariable pv; pv.set(42); auto createAndMove = [&pv]() -> decltype(pv.getRef()) { auto ptr = pv.getRef(); return std::move(ptr); }; auto ptr = createAndMove(); *ptr = 100; EXPECT_EQ(pv.get(), 100); } TEST(PIProtectedVariable_Move, MoveConstructorWithComplexTypes) { struct ComplexType { vector data; string name; ComplexType(): data{1, 2, 3}, name("test") {} }; PIProtectedVariable pv; pv.set(ComplexType()); auto ptr1 = pv.getRef(); ptr1->data.push_back(4); ptr1->name = "modified"; auto ptr2 = std::move(ptr1); ptr2->data.push_back(5); EXPECT_EQ(pv.get().data.size(), 5u); EXPECT_EQ(pv.get().name, "modified"); } //! \~\brief Copy prevention tests //! \~\english Tests that verify copy operations are deleted //! \~\russian Тесты проверки что операции копирования удалены TEST(PIProtectedVariable_CopyPrevention, CopyConstructorDeleted) { PIProtectedVariable pv; pv.set(42); auto ptr = pv.getRef(); // This should not compile: // auto ptr2 = ptr; // Copy constructor is deleted // We verify this at compile time by ensuring the code compiles // without attempting to copy EXPECT_EQ(*ptr, 42); } TEST(PIProtectedVariable_CopyPrevention, CopyAssignmentDeleted) { PIProtectedVariable pv; pv.set(42); auto ptr1 = pv.getRef(); auto ptr2 = pv.getRef(); // This will block until ptr1 is released // This should not compile: // ptr1 = ptr2; // Copy assignment is deleted (*ptr2) = 100; EXPECT_EQ(pv.get(), 100); } TEST(PIProtectedVariable_CopyPrevention, MoveAssignmentDeleted) { PIProtectedVariable pv; pv.set(42); auto ptr1 = pv.getRef(); auto ptr2 = pv.getRef(); // This will block until ptr1 is released // This should not compile: // ptr1 = std::move(ptr2); // Move assignment is deleted (*ptr2) = 100; EXPECT_EQ(pv.get(), 100); } TEST(PIProtectedVariable_CopyPrevention, CompileTimeCheck) { // Compile-time verification that copy operations are deleted // These static_assert statements will fail to compile if copy operations exist PIProtectedVariable pv; // The fact that this file compiles proves that: // 1. Pointer copy constructor is deleted // 2. Pointer copy assignment is deleted // 3. Pointer move assignment is deleted EXPECT_TRUE(true); // Placeholder assertion } //! \~\brief Edge case tests //! \~\english Tests for edge cases and exception safety //! \~\russian Тесты граничных случаев и исключительной безопасности TEST(PIProtectedVariable_EdgeCases, MultipleGetRefCallsSequential) { PIProtectedVariable pv; pv.set(0); for (int i = 0; i < 100; ++i) { auto ptr = pv.getRef(); (*ptr)++; } EXPECT_EQ(pv.get(), 100); } TEST(PIProtectedVariable_EdgeCases, NestedGetRefInSeparateScopes) { PIProtectedVariable pv; pv.set(0); { auto ptr1 = pv.getRef(); (*ptr1) += 10; } { auto ptr2 = pv.getRef(); (*ptr2) += 20; } { auto ptr3 = pv.getRef(); (*ptr3) += 30; } EXPECT_EQ(pv.get(), 60); } TEST(PIProtectedVariable_EdgeCases, LargeNumberOfThreads) { PIProtectedVariable pv; atomic counter(0); atomic start(false); const int NUM_THREADS = 50; vector threads; for (int i = 0; i < NUM_THREADS; ++i) { threads.emplace_back([&pv, &counter, &start]() { while (!start.load()) { this_thread::yield(); } for (int j = 0; j < 100; ++j) { int val = counter.fetch_add(1); pv.set(val); int readVal = pv.get(); EXPECT_GE(readVal, 0); } }); } start.store(true); for (auto & t: threads) { t.join(); } EXPECT_EQ(counter, NUM_THREADS * 100); } TEST(PIProtectedVariable_EdgeCases, ValueInitializationOfDifferentTypes) { // Note: PIProtectedVariable does not value-initialize its internal var // We test that setting and getting works for different types PIProtectedVariable pvInt; PIProtectedVariable pvDouble; PIProtectedVariable pvBool; PIProtectedVariable pvString; PIProtectedVariable pvVoidPtr; pvInt.set(0); pvDouble.set(0.0); pvBool.set(false); pvString.set(""); pvVoidPtr.set(nullptr); EXPECT_EQ(pvInt.get(), 0); EXPECT_DOUBLE_EQ(pvDouble.get(), 0.0); EXPECT_FALSE(pvBool.get()); EXPECT_TRUE(pvString.get().empty()); EXPECT_EQ(pvVoidPtr.get(), nullptr); } TEST(PIProtectedVariable_EdgeCases, NegativeAndExtremeValues) { PIProtectedVariable pv; pv.set(INT_MIN); EXPECT_EQ(pv.get(), INT_MIN); pv.set(INT_MAX); EXPECT_EQ(pv.get(), INT_MAX); pv.set(-1); EXPECT_EQ(pv.get(), -1); pv.set(0); EXPECT_EQ(pv.get(), 0); } TEST(PIProtectedVariable_EdgeCases, EmptyStringAndContainer) { PIProtectedVariable pvStr; PIProtectedVariable> pvVec; pvStr.set(""); EXPECT_TRUE(pvStr.get().empty()); pvVec.set(vector()); EXPECT_TRUE(pvVec.get().empty()); } TEST(PIProtectedVariable_EdgeCases, PointerLifetime) { PIProtectedVariable pv; pv.set(42); int * rawPtr = nullptr; { auto ptr = pv.getRef(); rawPtr = &(*ptr); // ptr destroyed here } // rawPtr still points to valid memory (pv.var) // but we shouldn't modify through it without holding the lock EXPECT_EQ(*rawPtr, 42); } //! \~\brief Exception safety tests //! \~\english Tests for exception safety //! \~\russian Тесты исключительной безопасности TEST(PIProtectedVariable_ExceptionSafety, SetWithMoveDoesNotLeaveInBadState) { struct MovesOnly { int value; MovesOnly(): value(0) {} MovesOnly(int v): value(v) {} MovesOnly(const MovesOnly &) = delete; // Copy is deleted MovesOnly(MovesOnly &&) noexcept = default; MovesOnly & operator=(const MovesOnly &) = delete; MovesOnly & operator=(MovesOnly &&) noexcept = default; }; PIProtectedVariable pv; pv.set(MovesOnly(42)); // Use getRef to access the value since get() requires copy auto ptr = pv.getRef(); EXPECT_EQ(ptr->value, 42); } TEST(PIProtectedVariable_ExceptionSafety, GetRefDoesNotLeakLockOnException) { PIProtectedVariable pv; pv.set(0); bool exceptionThrown = false; try { auto ptr = pv.getRef(); (*ptr) = 100; // ptr goes out of scope normally, lock released } catch (...) { exceptionThrown = true; } EXPECT_FALSE(exceptionThrown); EXPECT_EQ(pv.get(), 100); // If lock was leaked, this would deadlock auto ptr2 = pv.getRef(); EXPECT_EQ(*ptr2, 100); } //! \~\brief Integration tests //! \~\english Integration tests combining multiple features //! \~\russian Интеграционные тесты, сочетающие несколько функций TEST(PIProtectedVariable_Integration, ProducerConsumerPattern) { PIProtectedVariable buffer; atomic done(false); atomic produced(0); atomic consumed(0); atomic start(false); const int NUM_ITEMS = 1000; // Producer thread producer([&]() { while (!start.load()) { this_thread::yield(); } for (int i = 0; i < NUM_ITEMS; ++i) { auto ptr = buffer.getRef(); *ptr = i; produced++; } done.store(true); }); // Consumer - consumes exactly NUM_ITEMS thread consumer([&]() { while (!start.load()) { this_thread::yield(); } for (int i = 0; i < NUM_ITEMS; ++i) { // Wait for producer to produce something while (produced.load() <= consumed.load() && !done.load()) { this_thread::yield(); } auto ptr = buffer.getRef(); int val = *ptr; consumed++; (void)val; // Suppress unused warning } }); start.store(true); producer.join(); consumer.join(); EXPECT_EQ(produced, NUM_ITEMS); EXPECT_EQ(consumed, NUM_ITEMS); } TEST(PIProtectedVariable_Integration, ReadMostlyPattern) { PIProtectedVariable config; config.set(100); atomic totalReads(0); atomic wrongReads(0); atomic start(false); atomic done(false); // Readers vector readers; for (int i = 0; i < THREAD_COUNT; ++i) { readers.emplace_back([&]() { while (!start.load()) { this_thread::yield(); } while (!done.load()) { int val = config.get(); totalReads++; if (val < 100 || val > 200) { wrongReads++; } } }); } // Writer thread writer([&]() { while (!start.load()) { this_thread::yield(); } for (int i = 0; i < 100; ++i) { config.set(100 + i); this_thread::sleep_for(chrono::milliseconds(10)); } done.store(true); }); start.store(true); writer.join(); for (auto & t: readers) { t.join(); } EXPECT_GT(totalReads, 0); EXPECT_EQ(wrongReads, 0); }