simplify piprotectedvariable
This commit is contained in:
@@ -4,22 +4,22 @@
|
||||
//! \~english Thread-safe variable
|
||||
//! \~russian Потокобезопасная переменная
|
||||
/*
|
||||
PIP - Platform Independent Primitives
|
||||
Thread-safe variable
|
||||
Ivan Pelipenko peri4ko@yandex.ru
|
||||
PIP - Platform Independent Primitives
|
||||
Thread-safe variable
|
||||
Ivan Pelipenko peri4ko@yandex.ru
|
||||
|
||||
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 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.
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
You should have received a copy of the GNU Lesser General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef PIPROTECTEDVARIABLE_H
|
||||
@@ -39,16 +39,22 @@ public:
|
||||
//! \~russian Указателеподобная обертка, возвращаемая \a getRef(), пока защищенное значение остается заблокированным.
|
||||
class PIP_EXPORT Pointer {
|
||||
friend class PIProtectedVariable<T>;
|
||||
NO_COPY_CLASS(Pointer);
|
||||
|
||||
public:
|
||||
//! \~english Copies wrapper state for access to the same protected value.
|
||||
//! \~russian Копирует состояние обертки для доступа к тому же защищенному значению.
|
||||
Pointer(const Pointer & v): pv(v.pv), counter(v.counter + 1) {}
|
||||
//! \~\english Move constructor - transfers ownership of the lock.
|
||||
//! \~russian Конструктор перемещения - передает владение блокировкой.
|
||||
Pointer(Pointer && other) noexcept: pv(other.pv), ownsLock(other.ownsLock) { other.ownsLock = false; }
|
||||
|
||||
//! \~english Destroys wrapper and releases the mutex when it owns the original lock.
|
||||
//! \~russian Уничтожает обертку и освобождает мьютекс, когда она владеет исходной блокировкой.
|
||||
|
||||
//! \~\english Move assignment is deleted - Pointer can only be moved once.
|
||||
//! \~russian Оператор перемещения удален - Pointer можно переместить только один раз.
|
||||
Pointer & operator=(Pointer &&) = delete;
|
||||
|
||||
//! \~\english Destroys wrapper and releases the mutex.
|
||||
//! \~russian Уничтожает обертку и освобождает мьютекс.
|
||||
~Pointer() {
|
||||
if (counter == 0) pv.mutex.unlock();
|
||||
if (ownsLock) pv.mutex.unlock();
|
||||
}
|
||||
|
||||
//! \~english Returns pointer access to the protected value.
|
||||
@@ -61,10 +67,10 @@ public:
|
||||
|
||||
private:
|
||||
Pointer() = delete;
|
||||
Pointer(PIProtectedVariable<T> & v): pv(v) {}
|
||||
Pointer(PIProtectedVariable<T> & v): pv(v), ownsLock(true) {}
|
||||
|
||||
PIProtectedVariable<T> & pv;
|
||||
int counter = 0;
|
||||
bool ownsLock = true;
|
||||
};
|
||||
|
||||
//! \~english Replaces the protected value with \a v.
|
||||
|
||||
@@ -37,3 +37,4 @@ pip_test(piobject)
|
||||
pip_test(client_server pip_client_server)
|
||||
pip_test(io)
|
||||
pip_test(system)
|
||||
pip_test(thread)
|
||||
|
||||
936
tests/thread/piprotectedvariable_test.cpp
Normal file
936
tests/thread/piprotectedvariable_test.cpp
Normal file
@@ -0,0 +1,936 @@
|
||||
//! \~\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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "pimutex.h"
|
||||
#include "piprotectedvariable.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <climits>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
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<int> 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<int> pv;
|
||||
pv.set(42);
|
||||
EXPECT_EQ(pv.get(), 42);
|
||||
}
|
||||
|
||||
TEST(PIProtectedVariable_Basic, SetWithDifferentTypes) {
|
||||
PIProtectedVariable<int> pvInt;
|
||||
PIProtectedVariable<double> pvDouble;
|
||||
PIProtectedVariable<string> 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<int> 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<TestStruct> 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<int> pv;
|
||||
pv = 999;
|
||||
EXPECT_EQ(pv.get(), 999);
|
||||
}
|
||||
|
||||
TEST(PIProtectedVariable_Basic, OperatorAssignmentReturnsReference) {
|
||||
PIProtectedVariable<int> pv;
|
||||
PIProtectedVariable<int> & 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<int> pv;
|
||||
pv.set(0); // Set to zero explicitly
|
||||
EXPECT_EQ(pv.get(), 0);
|
||||
|
||||
PIProtectedVariable<string> pvStr;
|
||||
pvStr.set(""); // Set to empty explicitly
|
||||
EXPECT_TRUE(pvStr.get().empty());
|
||||
}
|
||||
|
||||
TEST(PIProtectedVariable_Basic, SetWithMoveSemantics) {
|
||||
PIProtectedVariable<string> 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<int> pv;
|
||||
atomic<bool> start(false);
|
||||
|
||||
vector<thread> 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<int> pv;
|
||||
pv.set(42);
|
||||
atomic<int> readCount(0);
|
||||
atomic<int> wrongCount(0);
|
||||
atomic<bool> start(false);
|
||||
|
||||
vector<thread> 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<int> pv;
|
||||
pv.set(0); // Initialize with a valid value
|
||||
atomic<int> counter(0);
|
||||
atomic<int> readCount(0);
|
||||
atomic<int> invalidReads(0);
|
||||
atomic<bool> 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<thread> 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<int> pv;
|
||||
pv.set(0); // Initialize with a valid value
|
||||
atomic<int> writeCount(0);
|
||||
atomic<int> readCount(0);
|
||||
atomic<bool> done(false);
|
||||
atomic<bool> start(false);
|
||||
|
||||
// Multiple writers
|
||||
vector<thread> 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<thread> 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<int> 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<TestStruct> 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<string> pv;
|
||||
pv.set("original");
|
||||
|
||||
auto ptr = pv.getRef();
|
||||
*ptr = "modified";
|
||||
|
||||
EXPECT_EQ(pv.get(), "modified");
|
||||
}
|
||||
|
||||
TEST(PIProtectedVariable_Pointer, OperatorDerefAllowsModification) {
|
||||
PIProtectedVariable<int> pv;
|
||||
pv.set(100);
|
||||
|
||||
auto ptr = pv.getRef();
|
||||
(*ptr) += 50;
|
||||
|
||||
EXPECT_EQ(pv.get(), 150);
|
||||
}
|
||||
|
||||
TEST(PIProtectedVariable_Pointer, MutexLockedWhenPointerCreated) {
|
||||
PIProtectedVariable<int> pv;
|
||||
pv.set(42);
|
||||
|
||||
atomic<bool> gotRef(false);
|
||||
atomic<bool> 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<int> 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<string> pv;
|
||||
pv.set("Hello");
|
||||
|
||||
{
|
||||
auto ptr = pv.getRef();
|
||||
*ptr += " World";
|
||||
}
|
||||
|
||||
EXPECT_EQ(pv.get(), "Hello World");
|
||||
}
|
||||
|
||||
TEST(PIProtectedVariable_Pointer, PointerWithComplexTypes) {
|
||||
struct ComplexType {
|
||||
vector<int> data;
|
||||
map<string, int> mapping;
|
||||
|
||||
ComplexType() {
|
||||
data = {1, 2, 3, 4, 5};
|
||||
mapping["one"] = 1;
|
||||
mapping["two"] = 2;
|
||||
}
|
||||
};
|
||||
|
||||
PIProtectedVariable<ComplexType> 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<int> 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<int> pv;
|
||||
pv.set(42);
|
||||
|
||||
atomic<int> 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<int> pv;
|
||||
pv.set(42);
|
||||
|
||||
atomic<bool> 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<int> 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<int> 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<int> 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<int> data;
|
||||
string name;
|
||||
|
||||
ComplexType(): data{1, 2, 3}, name("test") {}
|
||||
};
|
||||
|
||||
PIProtectedVariable<ComplexType> 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<int> 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<int> 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<int> 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<int> 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<int> 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<int> 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<int> pv;
|
||||
atomic<int> counter(0);
|
||||
atomic<bool> start(false);
|
||||
const int NUM_THREADS = 50;
|
||||
|
||||
vector<thread> 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<int> pvInt;
|
||||
PIProtectedVariable<double> pvDouble;
|
||||
PIProtectedVariable<bool> pvBool;
|
||||
PIProtectedVariable<string> pvString;
|
||||
PIProtectedVariable<void *> 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<int> 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<string> pvStr;
|
||||
PIProtectedVariable<vector<int>> pvVec;
|
||||
|
||||
pvStr.set("");
|
||||
EXPECT_TRUE(pvStr.get().empty());
|
||||
|
||||
pvVec.set(vector<int>());
|
||||
EXPECT_TRUE(pvVec.get().empty());
|
||||
}
|
||||
|
||||
TEST(PIProtectedVariable_EdgeCases, PointerLifetime) {
|
||||
PIProtectedVariable<int> 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<MovesOnly> 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<int> 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<int> buffer;
|
||||
atomic<bool> done(false);
|
||||
atomic<int> produced(0);
|
||||
atomic<int> consumed(0);
|
||||
atomic<bool> 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<int> config;
|
||||
config.set(100);
|
||||
|
||||
atomic<int> totalReads(0);
|
||||
atomic<int> wrongReads(0);
|
||||
atomic<bool> start(false);
|
||||
atomic<bool> done(false);
|
||||
|
||||
// Readers
|
||||
vector<thread> 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);
|
||||
}
|
||||
Reference in New Issue
Block a user