mirror of
https://github.com/XRPLF/clio.git
synced 2025-12-06 17:27:58 +00:00
@@ -140,6 +140,7 @@ target_sources(
|
||||
util/async/AnyStrandTests.cpp
|
||||
util/async/AsyncExecutionContextTests.cpp
|
||||
util/BatchingTests.cpp
|
||||
util/BlockingCacheTests.cpp
|
||||
util/ConceptsTests.cpp
|
||||
util/CoroutineGroupTests.cpp
|
||||
util/LedgerUtilsTests.cpp
|
||||
|
||||
@@ -645,7 +645,7 @@ struct LoadBalancerForwardToRippledErrorTestBundle {
|
||||
std::string testName;
|
||||
rpc::ClioError firstSourceError;
|
||||
rpc::ClioError secondSourceError;
|
||||
rpc::ClioError responseExpectedError;
|
||||
rpc::CombinedError responseExpectedError;
|
||||
};
|
||||
|
||||
struct LoadBalancerForwardToRippledErrorTests
|
||||
@@ -776,7 +776,7 @@ TEST_F(LoadBalancerForwardToRippledTests, commandLineMissing)
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
EXPECT_EQ(
|
||||
loadBalancer->forwardToRippled(request, clientIP_, false, yield).error(),
|
||||
rpc::ClioError::RpcCommandIsMissing
|
||||
rpc::CombinedError{rpc::ClioError::RpcCommandIsMissing}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -672,7 +672,7 @@ struct LoadBalancerForwardToRippledErrorNgTestBundle {
|
||||
std::string testName;
|
||||
rpc::ClioError firstSourceError;
|
||||
rpc::ClioError secondSourceError;
|
||||
rpc::ClioError responseExpectedError;
|
||||
rpc::CombinedError responseExpectedError;
|
||||
};
|
||||
|
||||
struct LoadBalancerForwardToRippledErrorNgTests
|
||||
@@ -803,7 +803,7 @@ TEST_F(LoadBalancerForwardToRippledNgTests, commandLineMissing)
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
EXPECT_EQ(
|
||||
loadBalancer->forwardToRippled(request, clientIP_, false, yield).error(),
|
||||
rpc::ClioError::RpcCommandIsMissing
|
||||
rpc::CombinedError{rpc::ClioError::RpcCommandIsMissing}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ TEST_P(RPCEngineFlowParameterTest, Test)
|
||||
if (testBundle.response.has_value()) {
|
||||
EXPECT_EQ(*response, testBundle.response.value());
|
||||
} else {
|
||||
EXPECT_TRUE(*status == testBundle.status.value());
|
||||
EXPECT_EQ(*status, testBundle.status.value());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -295,7 +295,7 @@ TEST_F(RPCEngineTest, ThrowDatabaseError)
|
||||
auto const res = engine->buildResponse(ctx);
|
||||
auto const status = std::get_if<rpc::Status>(&res.response);
|
||||
ASSERT_TRUE(status != nullptr);
|
||||
EXPECT_TRUE(*status == Status{RippledError::rpcTOO_BUSY});
|
||||
EXPECT_EQ(*status, Status{RippledError::rpcTOO_BUSY});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -327,7 +327,7 @@ TEST_F(RPCEngineTest, ThrowException)
|
||||
auto const res = engine->buildResponse(ctx);
|
||||
auto const status = std::get_if<rpc::Status>(&res.response);
|
||||
ASSERT_TRUE(status != nullptr);
|
||||
EXPECT_TRUE(*status == Status{RippledError::rpcINTERNAL});
|
||||
EXPECT_EQ(*status, Status{RippledError::rpcINTERNAL});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -453,7 +453,7 @@ TEST_P(RPCEngineCacheParameterTest, Test)
|
||||
|
||||
auto const res = engine->buildResponse(ctx);
|
||||
auto const response = std::get_if<boost::json::object>(&res.response);
|
||||
EXPECT_TRUE(*response == boost::json::parse(R"JSON({ "computed": "world_50"})JSON").as_object());
|
||||
EXPECT_EQ(*response, boost::json::parse(R"JSON({ "computed": "world_50"})JSON").as_object());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -498,7 +498,8 @@ TEST_F(RPCEngineTest, NotCacheIfErrorHappen)
|
||||
|
||||
auto const res = engine->buildResponse(ctx);
|
||||
auto const error = std::get_if<rpc::Status>(&res.response);
|
||||
EXPECT_TRUE(*error == rpc::Status{"Very custom error"});
|
||||
ASSERT_NE(error, nullptr);
|
||||
EXPECT_EQ(*error, rpc::Status{"Very custom error"});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
252
tests/unit/util/BlockingCacheTests.cpp
Normal file
252
tests/unit/util/BlockingCacheTests.cpp
Normal file
@@ -0,0 +1,252 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2025, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "util/AsioContextTestFixture.hpp"
|
||||
#include "util/BlockingCache.hpp"
|
||||
#include "util/NameGenerator.hpp"
|
||||
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
using testing::MockFunction;
|
||||
using testing::Return;
|
||||
using testing::StrictMock;
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
|
||||
struct BlockingCacheTest : SyncAsioContextTest {
|
||||
using ErrorType = std::string;
|
||||
using ValueType = int;
|
||||
using Cache = util::BlockingCache<ValueType, ErrorType>;
|
||||
using MockUpdater = StrictMock<MockFunction<std::expected<ValueType, ErrorType>(boost::asio::yield_context)>>;
|
||||
using MockVerifier = StrictMock<MockFunction<bool(ValueType const&)>>;
|
||||
|
||||
std::unique_ptr<Cache> cache = std::make_unique<Cache>();
|
||||
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<ValueType, ErrorType> 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<Cache>(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<BlockingCacheWaitTestBundle> {};
|
||||
|
||||
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<ValueType, ErrorType> {
|
||||
boost::asio::spawn(yield, waitingCoroutine);
|
||||
if (GetParam().updateSuccessful) {
|
||||
return value;
|
||||
}
|
||||
return std::unexpected{error};
|
||||
});
|
||||
|
||||
if (GetParam().updateSuccessful)
|
||||
EXPECT_CALL(mockVerifier, Call(value)).WillOnce(Return(GetParam().verifierAccepts));
|
||||
|
||||
runSpawnWithTimeout(std::chrono::seconds{1}, [&](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<Cache>(value);
|
||||
ASSERT_EQ(cache->state(), Cache::State::HasValue);
|
||||
cache->invalidate();
|
||||
EXPECT_EQ(cache->state(), Cache::State::NoValue);
|
||||
}
|
||||
|
||||
TEST_F(BlockingCacheTest, UpdateFromTwoCoroutinesHappensOnlyOnes)
|
||||
{
|
||||
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<ValueType, ErrorType> {
|
||||
boost::asio::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);
|
||||
};
|
||||
|
||||
runSpawnWithTimeout(std::chrono::seconds{1}, [&](boost::asio::yield_context yield) {
|
||||
boost::asio::spawn(yield, updatingCoroutine);
|
||||
});
|
||||
}
|
||||
@@ -17,53 +17,292 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "rpc/Errors.hpp"
|
||||
#include "util/AsioContextTestFixture.hpp"
|
||||
#include "util/MockAssert.hpp"
|
||||
#include "util/ResponseExpirationCache.hpp"
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
|
||||
using namespace util;
|
||||
using testing::MockFunction;
|
||||
using testing::Return;
|
||||
using testing::StrictMock;
|
||||
|
||||
struct ResponseExpirationCacheTests : public ::testing::Test {
|
||||
protected:
|
||||
ResponseExpirationCache cache_{std::chrono::seconds{100}, {"key"}};
|
||||
boost::json::object object_{{"key", "value"}};
|
||||
struct ResponseExpirationCacheTest : SyncAsioContextTest {
|
||||
using MockUpdater = StrictMock<MockFunction<
|
||||
std::expected<ResponseExpirationCache::EntryData, ResponseExpirationCache::Error>(boost::asio::yield_context)>>;
|
||||
using MockVerifier = StrictMock<MockFunction<bool(ResponseExpirationCache::EntryData const&)>>;
|
||||
|
||||
std::string const cmd = "server_info";
|
||||
boost::json::object const obj = {{"some key", "some value"}};
|
||||
MockUpdater mockUpdater;
|
||||
MockVerifier mockVerifier;
|
||||
};
|
||||
|
||||
TEST_F(ResponseExpirationCacheTests, PutAndGetNotExpired)
|
||||
TEST_F(ResponseExpirationCacheTest, ShouldCacheDeterminesIfCommandIsCacheable)
|
||||
{
|
||||
EXPECT_FALSE(cache_.get("key").has_value());
|
||||
std::unordered_set<std::string> cmds = {cmd, "account_info"};
|
||||
ResponseExpirationCache cache{std::chrono::seconds(10), cmds};
|
||||
|
||||
cache_.put("key", object_);
|
||||
auto result = cache_.get("key");
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(*result, object_);
|
||||
result = cache_.get("key2");
|
||||
ASSERT_FALSE(result.has_value());
|
||||
for (auto const& c : cmds) {
|
||||
EXPECT_TRUE(cache.shouldCache(c));
|
||||
}
|
||||
|
||||
cache_.put("key2", object_);
|
||||
result = cache_.get("key2");
|
||||
ASSERT_FALSE(result.has_value());
|
||||
EXPECT_FALSE(cache.shouldCache("account_tx"));
|
||||
EXPECT_FALSE(cache.shouldCache("ledger"));
|
||||
EXPECT_FALSE(cache.shouldCache("submit"));
|
||||
EXPECT_FALSE(cache.shouldCache(""));
|
||||
}
|
||||
|
||||
TEST_F(ResponseExpirationCacheTests, Invalidate)
|
||||
TEST_F(ResponseExpirationCacheTest, ShouldCacheEmptySetMeansNothingCacheable)
|
||||
{
|
||||
cache_.put("key", object_);
|
||||
cache_.invalidate();
|
||||
EXPECT_FALSE(cache_.get("key").has_value());
|
||||
std::unordered_set<std::string> const emptyCmds;
|
||||
ResponseExpirationCache cache{std::chrono::seconds(10), emptyCmds};
|
||||
|
||||
EXPECT_FALSE(cache.shouldCache("server_info"));
|
||||
EXPECT_FALSE(cache.shouldCache("account_info"));
|
||||
EXPECT_FALSE(cache.shouldCache("any_command"));
|
||||
EXPECT_FALSE(cache.shouldCache(""));
|
||||
}
|
||||
|
||||
TEST_F(ResponseExpirationCacheTests, GetExpired)
|
||||
TEST_F(ResponseExpirationCacheTest, ShouldCacheCaseMatchingIsRequired)
|
||||
{
|
||||
ResponseExpirationCache cache{std::chrono::milliseconds{1}, {"key"}};
|
||||
auto const response = boost::json::object{{"key", "value"}};
|
||||
std::unordered_set<std::string> const specificCmds = {cmd};
|
||||
ResponseExpirationCache cache{std::chrono::seconds(10), specificCmds};
|
||||
|
||||
cache.put("key", response);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{2});
|
||||
|
||||
auto const result = cache.get("key");
|
||||
EXPECT_FALSE(result);
|
||||
EXPECT_TRUE(cache.shouldCache(cmd));
|
||||
EXPECT_FALSE(cache.shouldCache("SERVER_INFO"));
|
||||
EXPECT_FALSE(cache.shouldCache("Server_Info"));
|
||||
}
|
||||
|
||||
TEST_F(ResponseExpirationCacheTest, GetOrUpdateNoValueInCacheCallsUpdaterAndVerifier)
|
||||
{
|
||||
ResponseExpirationCache cache{std::chrono::seconds(10), {cmd}};
|
||||
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
EXPECT_CALL(mockUpdater, Call)
|
||||
.WillOnce(Return(ResponseExpirationCache::EntryData{
|
||||
.lastUpdated = std::chrono::steady_clock::now(),
|
||||
.response = obj,
|
||||
}));
|
||||
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(true));
|
||||
|
||||
auto result =
|
||||
cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result.value(), obj);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ResponseExpirationCacheTest, GetOrUpdateExpiredValueInCacheCallsUpdaterAndVerifier)
|
||||
{
|
||||
ResponseExpirationCache cache{std::chrono::milliseconds(1), {cmd}};
|
||||
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
boost::json::object const expiredObject = {{"some key", "expired value"}};
|
||||
EXPECT_CALL(mockUpdater, Call)
|
||||
.WillOnce(Return(ResponseExpirationCache::EntryData{
|
||||
.lastUpdated = std::chrono::steady_clock::now(),
|
||||
.response = expiredObject,
|
||||
}));
|
||||
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(true));
|
||||
|
||||
auto result =
|
||||
cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result.value(), expiredObject);
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
|
||||
EXPECT_CALL(mockUpdater, Call)
|
||||
.WillOnce(Return(
|
||||
ResponseExpirationCache::EntryData{.lastUpdated = std::chrono::steady_clock::now(), .response = obj}
|
||||
));
|
||||
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(true));
|
||||
|
||||
result = cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result.value(), obj);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ResponseExpirationCacheTest, GetOrUpdateCachedValueNotExpiredDoesNotCallUpdaterOrVerifier)
|
||||
{
|
||||
ResponseExpirationCache cache{std::chrono::seconds(10), {cmd}};
|
||||
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
// First call to populate cache
|
||||
EXPECT_CALL(mockUpdater, Call)
|
||||
.WillOnce(Return(ResponseExpirationCache::EntryData{
|
||||
.lastUpdated = std::chrono::steady_clock::now(),
|
||||
.response = obj,
|
||||
}));
|
||||
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(true));
|
||||
|
||||
auto result =
|
||||
cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result.value(), obj);
|
||||
|
||||
// Second call should use cached value and not call updater/verifier
|
||||
result = cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result.value(), obj);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ResponseExpirationCacheTest, GetOrUpdateHandlesErrorFromUpdater)
|
||||
{
|
||||
ResponseExpirationCache cache{std::chrono::seconds(10), {cmd}};
|
||||
|
||||
ResponseExpirationCache::Error const error{
|
||||
.status = rpc::Status{rpc::ClioError::EtlConnectionError}, .warnings = {}
|
||||
};
|
||||
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
EXPECT_CALL(mockUpdater, Call).WillOnce(Return(std::unexpected(error)));
|
||||
|
||||
auto result =
|
||||
cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
|
||||
|
||||
ASSERT_FALSE(result.has_value());
|
||||
EXPECT_EQ(result.error(), error);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ResponseExpirationCacheTest, GetOrUpdateVerifierRejection)
|
||||
{
|
||||
ResponseExpirationCache cache{std::chrono::seconds(10), {cmd}};
|
||||
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
EXPECT_CALL(mockUpdater, Call)
|
||||
.WillOnce(Return(ResponseExpirationCache::EntryData{
|
||||
.lastUpdated = std::chrono::steady_clock::now(),
|
||||
.response = obj,
|
||||
}));
|
||||
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(false));
|
||||
|
||||
auto result =
|
||||
cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result.value(), obj);
|
||||
|
||||
boost::json::object const anotherObj = {{"some key", "another value"}};
|
||||
EXPECT_CALL(mockUpdater, Call)
|
||||
.WillOnce(Return(ResponseExpirationCache::EntryData{
|
||||
.lastUpdated = std::chrono::steady_clock::now(),
|
||||
.response = anotherObj,
|
||||
}));
|
||||
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(true));
|
||||
|
||||
result = cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result.value(), anotherObj);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ResponseExpirationCacheTest, GetOrUpdateMultipleConcurrentUpdates)
|
||||
{
|
||||
ResponseExpirationCache cache{std::chrono::seconds(10), {cmd}};
|
||||
bool waitingCoroutineFinished = false;
|
||||
|
||||
auto waitingCoroutine = [&](boost::asio::yield_context yield) {
|
||||
auto result =
|
||||
cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result.value(), obj);
|
||||
waitingCoroutineFinished = true;
|
||||
};
|
||||
|
||||
EXPECT_CALL(mockUpdater, Call)
|
||||
.WillOnce(
|
||||
[this, &waitingCoroutine](boost::asio::yield_context yield
|
||||
) -> std::expected<ResponseExpirationCache::EntryData, ResponseExpirationCache::Error> {
|
||||
boost::asio::spawn(yield, waitingCoroutine);
|
||||
return ResponseExpirationCache::EntryData{
|
||||
.lastUpdated = std::chrono::steady_clock::now(),
|
||||
.response = obj,
|
||||
};
|
||||
}
|
||||
);
|
||||
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(true));
|
||||
|
||||
runSpawnWithTimeout(std::chrono::seconds{1}, [&](boost::asio::yield_context yield) {
|
||||
auto result =
|
||||
cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result.value(), obj);
|
||||
ASSERT_FALSE(waitingCoroutineFinished);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ResponseExpirationCacheTest, InvalidateForcesRefresh)
|
||||
{
|
||||
ResponseExpirationCache cache{std::chrono::seconds(10), {cmd}};
|
||||
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
boost::json::object oldObject = {{"some key", "old value"}};
|
||||
EXPECT_CALL(mockUpdater, Call)
|
||||
.WillOnce(Return(ResponseExpirationCache::EntryData{
|
||||
.lastUpdated = std::chrono::steady_clock::now(),
|
||||
.response = oldObject,
|
||||
}));
|
||||
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(true));
|
||||
|
||||
auto result =
|
||||
cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result.value(), oldObject);
|
||||
|
||||
cache.invalidate();
|
||||
|
||||
EXPECT_CALL(mockUpdater, Call)
|
||||
.WillOnce(Return(ResponseExpirationCache::EntryData{
|
||||
.lastUpdated = std::chrono::steady_clock::now(),
|
||||
.response = obj,
|
||||
}));
|
||||
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(true));
|
||||
|
||||
result = cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result.value(), obj);
|
||||
});
|
||||
}
|
||||
|
||||
struct ResponseExpirationCacheAssertTest : common::util::WithMockAssert, ResponseExpirationCacheTest {};
|
||||
|
||||
TEST_F(ResponseExpirationCacheAssertTest, NonCacheableCommandThrowsAssertion)
|
||||
{
|
||||
ResponseExpirationCache cache{std::chrono::seconds(10), {cmd}};
|
||||
|
||||
ASSERT_FALSE(cache.shouldCache("non_cacheable_command"));
|
||||
|
||||
runSpawn([&](boost::asio::yield_context yield) {
|
||||
EXPECT_CLIO_ASSERT_FAIL({
|
||||
[[maybe_unused]]
|
||||
auto const v = cache.getOrUpdate(
|
||||
yield, "non_cacheable_command", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction()
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user