#include "util/AsioContextTestFixture.hpp" #include "util/BlockingCache.hpp" #include "util/NameGenerator.hpp" #include "util/Spawn.hpp" #include #include #include using testing::MockFunction; using testing::Return; using testing::StrictMock; #include #include #include struct BlockingCacheTest : SyncAsioContextTest { using ErrorType = std::string; using ValueType = int; using Cache = util::BlockingCache; using MockUpdater = StrictMock(boost::asio::yield_context)>>; using MockVerifier = StrictMock>; std::unique_ptr cache = std::make_unique(); MockUpdater mockUpdater; MockVerifier mockVerifier; int const value = 42; std::string error = "some error"; }; TEST_F(BlockingCacheTest, asyncGet_NoValueCacheUpdateSuccess) { EXPECT_CALL(mockUpdater, Call).WillOnce(Return(value)); EXPECT_CALL(mockVerifier, Call(value)).WillOnce(Return(true)); runSpawn([&](boost::asio::yield_context yield) { auto result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction()); ASSERT_TRUE(result.has_value()); EXPECT_EQ(result.value(), 42); }); } TEST_F(BlockingCacheTest, asyncGet_NoValueCacheUpdateFailure) { EXPECT_CALL(mockUpdater, Call).WillOnce(Return(std::unexpected{error})); runSpawn([&](boost::asio::yield_context yield) { auto result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction()); ASSERT_FALSE(result.has_value()); EXPECT_EQ(result.error(), error); }); } TEST_F(BlockingCacheTest, asyncGet_NoValueCacheUpdateSuccessButVerifierRejects) { runSpawn([&](boost::asio::yield_context yield) { std::expected result; { EXPECT_CALL(mockUpdater, Call).WillOnce(Return(value)); EXPECT_CALL(mockVerifier, Call(value)).WillOnce(Return(false)); result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction()); ASSERT_TRUE(result.has_value()); EXPECT_EQ(result.value(), value); } int const newValue = 24; { EXPECT_CALL(mockUpdater, Call).WillOnce(Return(newValue)); EXPECT_CALL(mockVerifier, Call(newValue)).WillOnce(Return(true)); result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction()); ASSERT_TRUE(result.has_value()); EXPECT_EQ(result.value(), newValue); } result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction()); ASSERT_TRUE(result.has_value()); EXPECT_EQ(result.value(), newValue); }); } TEST_F(BlockingCacheTest, asyncGet_HasValueCacheReturnsValue) { cache = std::make_unique(value); runSpawn([&](boost::asio::yield_context yield) { auto result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction()); ASSERT_TRUE(result.has_value()); EXPECT_EQ(result.value(), value); }); } struct BlockingCacheWaitTestBundle { bool updateSuccessful; bool verifierAccepts; std::string testName; }; struct BlockingCacheWaitTest : BlockingCacheTest, testing::WithParamInterface {}; TEST_P(BlockingCacheWaitTest, WaitForUpdate) { bool waitingCoroutineFinished = false; auto waitingCoroutine = [&](boost::asio::yield_context yield) { auto result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction()); if (GetParam().updateSuccessful) { ASSERT_TRUE(result.has_value()); EXPECT_EQ(result.value(), value); } else { ASSERT_FALSE(result.has_value()); EXPECT_EQ(result.error(), error); } waitingCoroutineFinished = true; }; EXPECT_CALL(mockUpdater, Call) .WillOnce( [this, &waitingCoroutine]( boost::asio::yield_context yield ) -> std::expected { util::spawn(yield, waitingCoroutine); if (GetParam().updateSuccessful) { return value; } return std::unexpected{error}; } ); if (GetParam().updateSuccessful) EXPECT_CALL(mockVerifier, Call(value)).WillOnce(Return(GetParam().verifierAccepts)); runSpawn([&](boost::asio::yield_context yield) { auto result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction()); if (GetParam().updateSuccessful) { ASSERT_TRUE(result.has_value()); EXPECT_EQ(result.value(), value); } else { ASSERT_FALSE(result.has_value()); EXPECT_EQ(result.error(), error); } ASSERT_FALSE(waitingCoroutineFinished); }); } INSTANTIATE_TEST_SUITE_P( BlockingCacheTest, BlockingCacheWaitTest, testing::Values( BlockingCacheWaitTestBundle{ .updateSuccessful = true, .verifierAccepts = true, .testName = "UpdateSucceedsVerifierAccepts" }, BlockingCacheWaitTestBundle{ .updateSuccessful = true, .verifierAccepts = false, .testName = "UpdateSucceedsVerifierRejects" }, BlockingCacheWaitTestBundle{ .updateSuccessful = false, .verifierAccepts = false, .testName = "UpdateFails" } ), tests::util::kNAME_GENERATOR ); TEST_F(BlockingCacheTest, InvalidateWhenStateIsNoValue) { ASSERT_EQ(cache->state(), Cache::State::NoValue); cache->invalidate(); ASSERT_EQ(cache->state(), Cache::State::NoValue); } TEST_F(BlockingCacheTest, InvalidateWhenStateIsUpdating) { EXPECT_CALL(mockUpdater, Call).WillOnce([this](auto&&) { EXPECT_EQ(cache->state(), Cache::State::Updating); cache->invalidate(); EXPECT_EQ(cache->state(), Cache::State::Updating); return value; }); EXPECT_CALL(mockVerifier, Call(value)).WillOnce(Return(true)); runSpawn([&](boost::asio::yield_context yield) { auto result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction()); ASSERT_TRUE(result.has_value()); ASSERT_EQ(result.value(), value); ASSERT_EQ(cache->state(), Cache::State::HasValue); }); } TEST_F(BlockingCacheTest, InvalidateWhenStateIsHasValue) { cache = std::make_unique(value); ASSERT_EQ(cache->state(), Cache::State::HasValue); cache->invalidate(); EXPECT_EQ(cache->state(), Cache::State::NoValue); } TEST_F(BlockingCacheTest, UpdateFromTwoCoroutinesHappensOnlyOnce) { auto waitingCoroutine = [&](boost::asio::yield_context yield) { auto result = cache->update(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction()); ASSERT_TRUE(result.has_value()); ASSERT_EQ(result.value(), value); }; EXPECT_CALL(mockUpdater, Call) .WillOnce( [this, &waitingCoroutine]( boost::asio::yield_context yield ) -> std::expected { util::spawn(yield, waitingCoroutine); return value; } ); EXPECT_CALL(mockVerifier, Call(value)).WillOnce(Return(true)); auto updatingCoroutine = [&](boost::asio::yield_context yield) { auto const result = cache->update(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction()); EXPECT_TRUE(result.has_value()); ASSERT_EQ(result.value(), value); }; runSpawn([&](boost::asio::yield_context yield) { util::spawn(yield, updatingCoroutine); }); }