2019-01-22 03:59:38 +01:00
|
|
|
#include <cpp-utils/thread/LeftRight.h>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
using cpputils::LeftRight;
|
|
|
|
using std::vector;
|
|
|
|
|
|
|
|
TEST(LeftRightTest, givenInt_whenWritingAndReading_thenChangesArePresent) {
|
|
|
|
LeftRight<int> obj;
|
|
|
|
|
|
|
|
obj.write([] (auto& obj) {obj = 5;});
|
|
|
|
int read = obj.read([] (auto& obj) {return obj;});
|
|
|
|
EXPECT_EQ(5, read);
|
|
|
|
|
|
|
|
// check changes are also present in background copy
|
|
|
|
obj.write([] (auto&) {}); // this switches to the background copy
|
|
|
|
read = obj.read([] (auto& obj) {return obj;});
|
|
|
|
EXPECT_EQ(5, read);
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST(LeftRightTest, givenVector_whenWritingAndReading_thenChangesArePresent) {
|
|
|
|
LeftRight<vector<int>> obj;
|
|
|
|
|
|
|
|
obj.write([] (auto& obj) {obj.push_back(5);});
|
|
|
|
vector<int> read = obj.read([] (auto& obj) {return obj;});
|
|
|
|
EXPECT_EQ((vector<int>{5}), read);
|
|
|
|
|
|
|
|
obj.write([] (auto& obj) {obj.push_back(6);});
|
|
|
|
read = obj.read([] (auto& obj) {return obj;});
|
|
|
|
EXPECT_EQ((vector<int>{5, 6}), read);
|
|
|
|
}
|
|
|
|
|
2019-02-02 22:03:14 +01:00
|
|
|
TEST(LeftRightTest, givenVector_whenWritingReturnsValue_thenValueIsReturned) {
|
|
|
|
LeftRight<vector<int>> obj;
|
|
|
|
|
|
|
|
auto a = obj.write([] (auto&) -> int {return 5;});
|
|
|
|
static_assert(std::is_same<int, decltype(a)>::value, "");
|
|
|
|
EXPECT_EQ(5, a);
|
|
|
|
}
|
|
|
|
|
2019-01-22 03:59:38 +01:00
|
|
|
TEST(LeftRightTest, readsCanBeConcurrent) {
|
|
|
|
LeftRight<int> obj;
|
2019-02-02 22:03:14 +01:00
|
|
|
std::atomic<int> num_running_readers{0};
|
2019-01-22 03:59:38 +01:00
|
|
|
|
|
|
|
std::thread reader1([&] () {
|
|
|
|
obj.read([&] (auto&) {
|
|
|
|
++num_running_readers;
|
|
|
|
while(num_running_readers.load() < 2) {}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
std::thread reader2([&] () {
|
|
|
|
obj.read([&] (auto&) {
|
|
|
|
++num_running_readers;
|
|
|
|
while(num_running_readers.load() < 2) {}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// the threads only finish after both entered the read function.
|
|
|
|
// if LeftRight didn't allow concurrency, this would cause a deadlock.
|
|
|
|
reader1.join();
|
|
|
|
reader2.join();
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST(LeftRightTest, writesCanBeConcurrentWithReads_readThenWrite) {
|
|
|
|
LeftRight<int> obj;
|
|
|
|
std::atomic<bool> reader_running{false};
|
|
|
|
std::atomic<bool> writer_running{false};
|
|
|
|
|
|
|
|
std::thread reader([&] () {
|
|
|
|
obj.read([&] (auto&) {
|
|
|
|
reader_running = true;
|
|
|
|
while(!writer_running.load()) {}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
std::thread writer([&] () {
|
|
|
|
// run read first, write second
|
|
|
|
while (!reader_running.load()) {}
|
|
|
|
|
|
|
|
obj.write([&] (auto&) {
|
|
|
|
writer_running = true;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// the threads only finish after both entered the read function.
|
|
|
|
// if LeftRight didn't allow concurrency, this would cause a deadlock.
|
|
|
|
reader.join();
|
|
|
|
writer.join();
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST(LeftRightTest, writesCanBeConcurrentWithReads_writeThenRead) {
|
|
|
|
LeftRight<int> obj;
|
|
|
|
std::atomic<bool> writer_running{false};
|
|
|
|
std::atomic<bool> reader_running{false};
|
|
|
|
|
|
|
|
std::thread writer([&] () {
|
|
|
|
obj.read([&] (auto&) {
|
|
|
|
writer_running = true;
|
|
|
|
while(!reader_running.load()) {}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
std::thread reader([&] () {
|
|
|
|
// run write first, read second
|
|
|
|
while (!writer_running.load()) {}
|
|
|
|
|
|
|
|
obj.read([&] (auto&) {
|
|
|
|
reader_running = true;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// the threads only finish after both entered the read function.
|
|
|
|
// if LeftRight didn't allow concurrency, this would cause a deadlock.
|
|
|
|
writer.join();
|
|
|
|
reader.join();
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST(LeftRightTest, writesCannotBeConcurrentWithWrites) {
|
|
|
|
LeftRight<int> obj;
|
|
|
|
std::atomic<bool> first_writer_started{false};
|
|
|
|
std::atomic<bool> first_writer_finished{false};
|
|
|
|
|
|
|
|
std::thread writer1([&] () {
|
|
|
|
obj.write([&] (auto&) {
|
|
|
|
first_writer_started = true;
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
|
|
|
first_writer_finished = true;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
std::thread writer2([&] () {
|
|
|
|
// make sure the other writer runs first
|
|
|
|
while (!first_writer_started.load()) {}
|
|
|
|
|
|
|
|
obj.write([&] (auto&) {
|
|
|
|
// expect the other writer finished before this one starts
|
|
|
|
EXPECT_TRUE(first_writer_finished.load());
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
writer1.join();
|
|
|
|
writer2.join();
|
|
|
|
}
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
class MyException : std::exception {};
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST(LeftRightTest, whenReadThrowsException_thenThrowsThrough) {
|
|
|
|
LeftRight<int> obj;
|
|
|
|
|
|
|
|
EXPECT_THROW(
|
|
|
|
obj.read([](auto&) {throw MyException();}),
|
|
|
|
MyException
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST(LeftRightTest, whenWriteThrowsException_thenThrowsThrough) {
|
|
|
|
LeftRight<int> obj;
|
|
|
|
|
|
|
|
EXPECT_THROW(
|
|
|
|
obj.write([](auto&) {throw MyException();}),
|
|
|
|
MyException
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-02-02 22:03:14 +01:00
|
|
|
TEST(LeftRightTest, givenInt_whenWriteThrowsExceptionOnFirstCall_thenResetsToOldState) {
|
2019-01-22 03:59:38 +01:00
|
|
|
LeftRight<int> obj;
|
|
|
|
|
|
|
|
obj.write([](auto& obj) {obj = 5;});
|
|
|
|
|
|
|
|
EXPECT_THROW(
|
|
|
|
obj.write([](auto& obj) {
|
|
|
|
obj = 6;
|
|
|
|
throw MyException();
|
|
|
|
}),
|
|
|
|
MyException
|
|
|
|
);
|
|
|
|
|
|
|
|
// check reading it returns old value
|
|
|
|
int read = obj.read([] (auto& obj) {return obj;});
|
|
|
|
EXPECT_EQ(5, read);
|
|
|
|
|
|
|
|
// check changes are also present in background copy
|
|
|
|
obj.write([] (auto&) {}); // this switches to the background copy
|
|
|
|
read = obj.read([] (auto& obj) {return obj;});
|
|
|
|
EXPECT_EQ(5, read);
|
|
|
|
}
|
|
|
|
|
2019-02-02 22:03:14 +01:00
|
|
|
// note: each write is executed twice, on the foreground and background copy.
|
|
|
|
// We need to test a thrown exception in either call is handled correctly.
|
|
|
|
TEST(LeftRightTest, givenInt_whenWriteThrowsExceptionOnSecondCall_thenKeepsNewState) {
|
|
|
|
LeftRight<int> obj;
|
|
|
|
|
|
|
|
obj.write([](auto& obj) {obj = 5;});
|
|
|
|
bool write_called = false;
|
|
|
|
|
|
|
|
EXPECT_THROW(
|
|
|
|
obj.write([&](auto& obj) {
|
|
|
|
obj = 6;
|
|
|
|
if (write_called) {
|
|
|
|
// this is the second time the write callback is executed
|
|
|
|
throw MyException();
|
|
|
|
} else {
|
|
|
|
write_called = true;
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
MyException
|
|
|
|
);
|
|
|
|
|
|
|
|
// check reading it returns new value
|
|
|
|
int read = obj.read([] (auto& obj) {return obj;});
|
|
|
|
EXPECT_EQ(6, read);
|
|
|
|
|
|
|
|
// check changes are also present in background copy
|
|
|
|
obj.write([] (auto&) {}); // this switches to the background copy
|
|
|
|
read = obj.read([] (auto& obj) {return obj;});
|
|
|
|
EXPECT_EQ(6, read);
|
|
|
|
}
|
|
|
|
|
2019-01-22 03:59:38 +01:00
|
|
|
TEST(LeftRightTest, givenVector_whenWriteThrowsException_thenResetsToOldState) {
|
|
|
|
LeftRight<vector<int>> obj;
|
|
|
|
|
|
|
|
obj.write([](auto& obj) {obj.push_back(5);});
|
|
|
|
|
|
|
|
EXPECT_THROW(
|
|
|
|
obj.write([](auto& obj) {
|
|
|
|
obj.push_back(6);
|
|
|
|
throw MyException();
|
|
|
|
}),
|
|
|
|
MyException
|
|
|
|
);
|
|
|
|
|
|
|
|
// check reading it returns old value
|
|
|
|
vector<int> read = obj.read([] (auto& obj) {return obj;});
|
|
|
|
EXPECT_EQ((vector<int>{5}), read);
|
|
|
|
|
|
|
|
// check changes are also present in background copy
|
|
|
|
obj.write([] (auto&) {}); // this switches to the background copy
|
|
|
|
read = obj.read([] (auto& obj) {return obj;});
|
|
|
|
EXPECT_EQ((vector<int>{5}), read);
|
|
|
|
}
|