Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/rpc/handlers/ServerInfo.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "rpc/common/Specs.hpp"
#include "rpc/common/Types.hpp"
#include "util/Assert.hpp"
#include "util/Concepts.hpp"
#include "util/build/Build.hpp"

#include <boost/json/conversion.hpp>
Expand Down Expand Up @@ -42,8 +43,9 @@ namespace rpc {
* @brief Contains common functionality for handling the `server_info` command
*
* @tparam CountersType The type of the counters
* @tparam ClockType Clock used for the output time and the ledger age
*/
template <typename CountersType>
template <typename CountersType, util::SomeSystemClock ClockType = std::chrono::system_clock>
class BaseServerInfoHandler {
static constexpr auto kBackendCountersKey = "backend_counters";

Expand Down Expand Up @@ -100,7 +102,7 @@ class BaseServerInfoHandler {
std::optional<AdminSection> adminSection = std::nullopt;
std::string completeLedgers;
uint32_t loadFactor = 1u;
std::chrono::time_point<std::chrono::system_clock> time = std::chrono::system_clock::now();
std::chrono::time_point<std::chrono::system_clock> time = ClockType::now();
std::chrono::seconds uptime = {};
std::string clioVersion = util::build::getClioVersionString();
std::string xrplVersion = xrpl::BuildInfo::getVersionString();
Expand Down Expand Up @@ -189,8 +191,7 @@ class BaseServerInfoHandler {
return Error{Status{RippledError::RpcInternal}};

auto output = Output{};
auto const sinceEpoch =
duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
auto const sinceEpoch = duration_cast<seconds>(output.info.time.time_since_epoch()).count();
auto const age = static_cast<int32_t>(sinceEpoch) -
static_cast<int32_t>(lgrInfo->closeTime.time_since_epoch().count()) -
static_cast<int32_t>(kRippleEpochStart);
Expand Down
10 changes: 10 additions & 0 deletions src/util/Concepts.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#include <algorithm>
#include <array>
#include <chrono>
#include <concepts>
#include <cstddef>
#include <string_view>
#include <type_traits>
Expand All @@ -14,6 +16,14 @@ namespace util {
template <typename T>
concept SomeNumberType = std::is_arithmetic_v<T> && !std::is_same_v<T, bool> && !std::is_const_v<T>;

/**
* @brief Specifies a clock that reports the current time as a system clock time point
*/
template <typename T>
concept SomeSystemClock = requires {
{ T::now() } -> std::same_as<std::chrono::system_clock::time_point>;
};

/**
* @brief Checks that the list of given values contains no duplicates
*
Expand Down
56 changes: 56 additions & 0 deletions tests/common/util/TestConstantClock.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#pragma once

#include <chrono>
#include <cstddef>
#include <cstdint>

/**
* @brief A clock that always reports the same instant and counts how often it was read.
*
* Satisfies util::SomeSystemClock, so it can stand in for std::chrono::system_clock in any
* component templated on a clock. The call count makes "the clock was sampled exactly once"
* an assertable property, and the fixed instant makes time-derived output deterministic.
*
* The counter is process-wide: reset it in the fixture constructor of every suite that reads it.
*/
class TestConstantClock {
public:
/** @brief The instant now() reports, as a Unix timestamp in seconds. */
static constexpr std::uint32_t kNowUnix = 1'700'000'000u;

/** @brief The instant now() reports. */
static constexpr std::chrono::system_clock::time_point kNow{std::chrono::seconds{kNowUnix}};

/**
* @brief Report the fixed instant and count the read
*
* @return kNow
*/
static std::chrono::system_clock::time_point
now()
{
++callCounter;
return kNow;
}

/**
* @brief How often now() has been called since the last reset
*
* @return The call count
*/
static std::size_t
callCount()
{
return callCounter;
}

/** @brief Set the call count back to zero. */
static void
resetCounter()
{
callCounter = 0;
}

private:
static inline std::size_t callCounter = 0;
};
112 changes: 72 additions & 40 deletions tests/unit/rpc/handlers/ServerInfoTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "util/MockETLServiceTestFixture.hpp"
#include "util/MockLoadBalancer.hpp"
#include "util/MockSubscriptionManager.hpp"
#include "util/TestConstantClock.hpp"
#include "util/TestObject.hpp"

#include <boost/json/object.hpp>
Expand All @@ -17,6 +18,8 @@
#include <boost/json/value_to.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/protocol/LedgerHeader.h>

#include <chrono>
#include <optional>
Expand All @@ -26,21 +29,50 @@ using namespace rpc;
using namespace data;
using namespace testing;

using TestServerInfoHandler = BaseServerInfoHandler<MockCounters>;

namespace {

constexpr auto kLedgerHash = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652";
constexpr auto kClientIp = "1.1.1.1";
constexpr auto kNowUnix = TestConstantClock::kNowUnix;

using TestServerInfoHandler = BaseServerInfoHandler<MockCounters, TestConstantClock>;

} // namespace

struct RPCServerInfoHandlerTest : HandlerBaseTest, MockLoadBalancerTest, MockCountersTest {
RPCServerInfoHandlerTest()
{
TestConstantClock::resetCounter();
backend_->setRange(10, 30);
}

template <typename Callback>
void
runNormalRequest(xrpl::LedgerHeader const& ledgerHeader, Callback callback)
{
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
EXPECT_CALL(*backend_, doFetchLedgerObject)
.WillOnce(Return(createLegacyFeeSettingBlob(1, 2, 3, 4, 0)));
EXPECT_CALL(*mockLoadBalancerPtr_, forwardToRippled(_, Eq(kClientIp), false, _))
.WillOnce(Return(std::unexpected{rpc::ClioError::EtlInvalidResponse}));
EXPECT_CALL(*mockCountersPtr_, uptime).WillOnce(Return(std::chrono::seconds{1234}));
EXPECT_CALL(*mockETLServicePtr_, isAmendmentBlocked).WillOnce(Return(false));

auto const handler = AnyHandler{TestServerInfoHandler{
backend_,
mockSubscriptionManagerPtr_,
mockLoadBalancerPtr_,
mockETLServicePtr_,
*mockCountersPtr_
}};

runSpawn([&](auto yield) {
callback(
handler.process(boost::json::parse("{}"), Context{yield, {}, false, kClientIp})
);
});
}

static void
validateNormalOutput(rpc::ReturnType const& output)
{
Expand All @@ -56,6 +88,12 @@ struct RPCServerInfoHandlerTest : HandlerBaseTest, MockLoadBalancerTest, MockCou
EXPECT_TRUE(info.contains("libxrpl_version"));
EXPECT_TRUE(info.contains("validated_ledger"));
EXPECT_TRUE(info.contains("time"));
EXPECT_EQ(
boost::json::value_to<std::string>(info.at("time")),
xrpl::to_string(
std::chrono::time_point_cast<std::chrono::microseconds>(TestConstantClock::kNow)
)
);
EXPECT_TRUE(info.contains("uptime"));

auto const& validated = info.at("validated_ledger").as_object();
Expand Down Expand Up @@ -134,6 +172,7 @@ TEST_F(RPCServerInfoHandlerTest, NoLedgerHeaderErrorsOutWithInternal)
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "internal");
EXPECT_EQ(err.at("error_message").as_string(), "Internal error.");
EXPECT_EQ(TestConstantClock::callCount(), 0u);
});
}

Expand All @@ -159,42 +198,14 @@ TEST_F(RPCServerInfoHandlerTest, NoFeesErrorsOutWithInternal)
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "internal");
EXPECT_EQ(err.at("error_message").as_string(), "Internal error.");
EXPECT_EQ(TestConstantClock::callCount(), 0u);
});
}

TEST_F(RPCServerInfoHandlerTest, DefaultOutputIsPresent)
{
MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get();
MockCounters const* rawCountersPtr = mockCountersPtr_.get();
MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();

auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));

auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);
EXPECT_CALL(*backend_, doFetchLedgerObject).WillOnce(Return(feeBlob));

EXPECT_CALL(
*rawBalancerPtr, forwardToRippled(testing::_, testing::Eq(kClientIp), false, testing::_)
)
.WillOnce(Return(std::unexpected{rpc::ClioError::EtlInvalidResponse}));

EXPECT_CALL(*rawCountersPtr, uptime).WillOnce(Return(std::chrono::seconds{1234}));

EXPECT_CALL(*rawETLServicePtr, isAmendmentBlocked).WillOnce(Return(false));

auto const handler = AnyHandler{TestServerInfoHandler{
backend_,
mockSubscriptionManagerPtr_,
mockLoadBalancerPtr_,
mockETLServicePtr_,
*mockCountersPtr_
}};

runSpawn([&](auto yield) {
auto const req = boost::json::parse("{}");
auto const output = handler.process(req, Context{yield, {}, false, kClientIp});

auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
runNormalRequest(ledgerHeader, [&](auto const& output) {
validateNormalOutput(output);

// no admin section present by default
Expand All @@ -205,13 +216,34 @@ TEST_F(RPCServerInfoHandlerTest, DefaultOutputIsPresent)
});
}

TEST_F(RPCServerInfoHandlerTest, SamplesTheClockOnceForTimeAndAge)
{
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
runNormalRequest(ledgerHeader, [&](auto const& output) {
ASSERT_TRUE(output);
EXPECT_EQ(TestConstantClock::callCount(), 1u);
});
}

TEST_F(RPCServerInfoHandlerTest, FutureLedgerCloseTimeReportsZeroAge)
{
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix + 5);
runNormalRequest(ledgerHeader, [&](auto const& output) {
ASSERT_TRUE(output);
auto const& result = output.result.value().as_object();
auto const& info = result.at("info").as_object();
auto const& validated = info.at("validated_ledger").as_object();
EXPECT_EQ(validated.at("age").as_uint64(), 0u);
});
}

TEST_F(RPCServerInfoHandlerTest, AmendmentBlockedIsPresentIfSet)
{
MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get();
MockCounters const* rawCountersPtr = mockCountersPtr_.get();
MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();

auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));

auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);
Expand Down Expand Up @@ -252,7 +284,7 @@ TEST_F(RPCServerInfoHandlerTest, CorruptionDetectedIsPresentIfSet)
MockCounters const* rawCountersPtr = mockCountersPtr_.get();
MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();

auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));

auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);
Expand Down Expand Up @@ -292,7 +324,7 @@ TEST_F(RPCServerInfoHandlerTest, CacheReportsEnabledFlagCorrectly)
MockLoadBalancer* rawBalancerPtr = mockLoadBalancerPtr_.get();
MockCounters const* rawCountersPtr = mockCountersPtr_.get();

auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
EXPECT_CALL(*backend_, fetchLedgerBySequence).Times(2).WillRepeatedly(Return(ledgerHeader));

auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);
Expand Down Expand Up @@ -350,7 +382,7 @@ TEST_F(RPCServerInfoHandlerTest, AdminSectionPresentWhenAdminFlagIsSet)
MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();

auto const empty = boost::json::object{};
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));

auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);
Expand Down Expand Up @@ -393,7 +425,7 @@ TEST_F(RPCServerInfoHandlerTest, BackendCountersPresentWhenRequestWithParam)
MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();

auto const empty = boost::json::object{};
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));

auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);
Expand Down Expand Up @@ -443,7 +475,7 @@ TEST_F(RPCServerInfoHandlerTest, RippledForwardedValuesPresent)
MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();

auto const empty = boost::json::object{};
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));

auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);
Expand Down Expand Up @@ -497,7 +529,7 @@ TEST_F(RPCServerInfoHandlerTest, RippledForwardedValuesMissingNoExceptionThrown)
MockETLService const* rawETLServicePtr = mockETLServicePtr_.get();

auto const empty = boost::json::object{};
auto const ledgerHeader = createLedgerHeader(kLedgerHash, 30, 3); // 3 seconds old
auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix - 3);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));

auto const feeBlob = createLegacyFeeSettingBlob(1, 2, 3, 4, 0);
Expand Down
Loading