From 5602ab81eb7fb1f177ea3e79e9dabaf6186444ad Mon Sep 17 00:00:00 2001 From: snowyukitty <270071858+snowyukitty@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:26:21 +0900 Subject: [PATCH 1/2] fix: Use a single timestamp in server_info --- src/rpc/handlers/ServerInfo.hpp | 9 +- tests/unit/rpc/handlers/ServerInfoTests.cpp | 125 +++++++++++++------- 2 files changed, 90 insertions(+), 44 deletions(-) diff --git a/src/rpc/handlers/ServerInfo.hpp b/src/rpc/handlers/ServerInfo.hpp index 00303cc51..759b5f731 100644 --- a/src/rpc/handlers/ServerInfo.hpp +++ b/src/rpc/handlers/ServerInfo.hpp @@ -42,8 +42,10 @@ namespace rpc { * @brief Contains common functionality for handling the `server_info` command * * @tparam CountersType The type of the counters + * @tparam ClockType Clock used for output time and ledger age; `now()` returns + * `std::chrono::system_clock::time_point` */ -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/tests/unit/rpc/handlers/ServerInfoTests.cpp b/tests/unit/rpc/handlers/ServerInfoTests.cpp index c1e7cbdfc..f773d32b4 100644 --- a/tests/unit/rpc/handlers/ServerInfoTests.cpp +++ b/tests/unit/rpc/handlers/ServerInfoTests.cpp @@ -17,8 +17,11 @@ #include #include #include +#include +#include #include +#include #include #include @@ -26,21 +29,62 @@ 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 = 1'700'000'000u; +constexpr auto kNow = std::chrono::system_clock::time_point{std::chrono::seconds{kNowUnix}}; + +struct TestClock { + static inline std::size_t callCount = 0; + + static std::chrono::system_clock::time_point + now() + { + ++callCount; + return kNow; + } +}; + +using TestServerInfoHandler = BaseServerInfoHandler; } // namespace struct RPCServerInfoHandlerTest : HandlerBaseTest, MockLoadBalancerTest, MockCountersTest { RPCServerInfoHandlerTest() { + TestClock::callCount = 0; 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 +100,10 @@ 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(kNow)) + ); EXPECT_TRUE(info.contains("uptime")); auto const& validated = info.at("validated_ledger").as_object(); @@ -134,6 +182,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(TestClock::callCount, 0u); }); } @@ -159,42 +208,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(TestClock::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 +226,37 @@ 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(TestClock::callCount, 1u); + }); +} + +TEST_F(RPCServerInfoHandlerTest, FutureLedgerCloseTimeReportsZeroAge) +{ + auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix + 5); + runNormalRequest(ledgerHeader, [&](auto const& output) { + ASSERT_TRUE(output); + auto const& validated = output.result.value() + .as_object() + .at("info") + .as_object() + .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 +297,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 +337,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 +395,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 +438,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 +488,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 +542,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); From f4e5e9c9100395cf8206e5a5a9451d40c6902f09 Mon Sep 17 00:00:00 2001 From: snowyukitty <270071858+snowyukitty@users.noreply.github.com> Date: Fri, 31 Jul 2026 07:58:54 +0900 Subject: [PATCH 2/2] test: use intermediate references in future close-time assertion --- tests/unit/rpc/handlers/ServerInfoTests.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/unit/rpc/handlers/ServerInfoTests.cpp b/tests/unit/rpc/handlers/ServerInfoTests.cpp index f773d32b4..ccc444be2 100644 --- a/tests/unit/rpc/handlers/ServerInfoTests.cpp +++ b/tests/unit/rpc/handlers/ServerInfoTests.cpp @@ -240,12 +240,9 @@ TEST_F(RPCServerInfoHandlerTest, FutureLedgerCloseTimeReportsZeroAge) auto const ledgerHeader = createLedgerHeaderWithUnixTime(kLedgerHash, 30, kNowUnix + 5); runNormalRequest(ledgerHeader, [&](auto const& output) { ASSERT_TRUE(output); - auto const& validated = output.result.value() - .as_object() - .at("info") - .as_object() - .at("validated_ledger") - .as_object(); + 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); }); }