diff --git a/src/rpc/handlers/ServerInfo.hpp b/src/rpc/handlers/ServerInfo.hpp index 00303cc51..64e6209c4 100644 --- a/src/rpc/handlers/ServerInfo.hpp +++ b/src/rpc/handlers/ServerInfo.hpp @@ -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 @@ -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 +template class BaseServerInfoHandler { static constexpr auto kBackendCountersKey = "backend_counters"; @@ -100,7 +102,7 @@ class BaseServerInfoHandler { std::optional adminSection = std::nullopt; std::string completeLedgers; uint32_t loadFactor = 1u; - std::chrono::time_point time = std::chrono::system_clock::now(); + std::chrono::time_point time = ClockType::now(); std::chrono::seconds uptime = {}; std::string clioVersion = util::build::getClioVersionString(); std::string xrplVersion = xrpl::BuildInfo::getVersionString(); @@ -189,8 +191,7 @@ class BaseServerInfoHandler { return Error{Status{RippledError::RpcInternal}}; auto output = Output{}; - auto const sinceEpoch = - duration_cast(system_clock::now().time_since_epoch()).count(); + auto const sinceEpoch = duration_cast(output.info.time.time_since_epoch()).count(); auto const age = static_cast(sinceEpoch) - static_cast(lgrInfo->closeTime.time_since_epoch().count()) - static_cast(kRippleEpochStart); diff --git a/src/util/Concepts.hpp b/src/util/Concepts.hpp index 115ba522d..09b9786a7 100644 --- a/src/util/Concepts.hpp +++ b/src/util/Concepts.hpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include #include @@ -14,6 +16,14 @@ namespace util { template concept SomeNumberType = std::is_arithmetic_v && !std::is_same_v && !std::is_const_v; +/** + * @brief Specifies a clock that reports the current time as a system clock time point + */ +template +concept SomeSystemClock = requires { + { T::now() } -> std::same_as; +}; + /** * @brief Checks that the list of given values contains no duplicates * diff --git a/tests/common/util/TestConstantClock.hpp b/tests/common/util/TestConstantClock.hpp new file mode 100644 index 000000000..b68fc161b --- /dev/null +++ b/tests/common/util/TestConstantClock.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include + +/** + * @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; +}; diff --git a/tests/unit/rpc/handlers/ServerInfoTests.cpp b/tests/unit/rpc/handlers/ServerInfoTests.cpp index c1e7cbdfc..c5c879150 100644 --- a/tests/unit/rpc/handlers/ServerInfoTests.cpp +++ b/tests/unit/rpc/handlers/ServerInfoTests.cpp @@ -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 @@ -17,6 +18,8 @@ #include #include #include +#include +#include #include #include @@ -26,21 +29,50 @@ using namespace rpc; using namespace data; using namespace testing; -using TestServerInfoHandler = BaseServerInfoHandler; - namespace { constexpr auto kLedgerHash = "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652"; constexpr auto kClientIp = "1.1.1.1"; +constexpr auto kNowUnix = TestConstantClock::kNowUnix; + +using TestServerInfoHandler = BaseServerInfoHandler; } // namespace struct RPCServerInfoHandlerTest : HandlerBaseTest, MockLoadBalancerTest, MockCountersTest { RPCServerInfoHandlerTest() { + TestConstantClock::resetCounter(); backend_->setRange(10, 30); } + template + 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) { @@ -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(info.at("time")), + xrpl::to_string( + std::chrono::time_point_cast(TestConstantClock::kNow) + ) + ); EXPECT_TRUE(info.contains("uptime")); auto const& validated = info.at("validated_ledger").as_object(); @@ -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); }); } @@ -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 @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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);