Files
rippled/src/tests/libxrpl/telemetry/ValidationTracker.cpp
Pratik Mankawde 7ab34d2e37 fix(telemetry): resolve phase-7 clang-tidy findings
- OTelCollector::formatName made static (no member state); braces added;
  <cctype> added for std::tolower.
- Telemetry.cpp getMeter(): braces around single-statement if.
- GetMeter.cpp: drop unused <opentelemetry/metrics/meter.h> and
  sync_instruments.h; add <xrpl/beast/utility/Journal.h> and <memory>;
  make sdkProvider const.
- ValidationTracker.h: add <cstddef> for std::size_t.
- detail/ValidationTracker.cpp: use std::views::reverse range-based loop;
  add <ranges>.
- test ValidationTracker.cpp: use auto for cast result.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 15:09:43 +01:00

285 lines
9.8 KiB
C++

/** @file ValidationTracker.cpp
Unit tests for xrpl::telemetry::ValidationTracker.
*/
#include <xrpld/telemetry/ValidationTracker.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/Protocol.h>
#include <gtest/gtest.h>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <thread>
using namespace xrpl;
using namespace xrpl::telemetry;
/// Helper to create a unique uint256 from an integer seed.
static uint256
makeHash(std::uint64_t n)
{
return uint256(n);
}
/// Test fixture providing a fresh ValidationTracker per test.
class ValidationTrackerTest : public ::testing::Test
{
protected:
ValidationTracker tracker_;
};
// ---------------------------------------------------------------
// 1. Normal agreement
// Record both our validation and network validation for the
// same hash, then reconcile after the grace period elapses.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, NormalAgreement)
{
auto const hash = makeHash(1);
LedgerIndex const seq = 100;
tracker_.recordOurValidation(hash, seq);
tracker_.recordNetworkValidation(hash, seq);
// Immediately after recording, nothing is reconciled yet
// (grace period has not elapsed).
tracker_.reconcile();
EXPECT_EQ(tracker_.totalValidationsSent(), 1u);
EXPECT_EQ(tracker_.totalValidationsChecked(), 1u);
// Wait for the grace period (8 seconds) to elapse, then reconcile.
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
EXPECT_EQ(tracker_.totalAgreements(), 1u);
EXPECT_EQ(tracker_.totalMissed(), 0u);
EXPECT_EQ(tracker_.agreements1h(), 1u);
EXPECT_EQ(tracker_.missed1h(), 0u);
EXPECT_DOUBLE_EQ(tracker_.agreementPct1h(), 100.0);
}
// ---------------------------------------------------------------
// 2. Missed validation
// Only the network validates; we never do. After grace period
// the event should be reconciled as a miss.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, MissedValidation)
{
auto const hash = makeHash(2);
LedgerIndex const seq = 200;
tracker_.recordNetworkValidation(hash, seq);
// Wait for grace period then reconcile.
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
EXPECT_EQ(tracker_.totalAgreements(), 0u);
EXPECT_EQ(tracker_.totalMissed(), 1u);
EXPECT_EQ(tracker_.agreements1h(), 0u);
EXPECT_EQ(tracker_.missed1h(), 1u);
EXPECT_DOUBLE_EQ(tracker_.agreementPct1h(), 0.0);
}
// ---------------------------------------------------------------
// 3. Late repair
// Network validates first, grace period elapses (miss), then
// our validation arrives within the 5-minute repair window and
// the miss is flipped to an agreement.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, LateRepair)
{
auto const hash = makeHash(3);
LedgerIndex const seq = 300;
// Network validates, but we do not (yet).
tracker_.recordNetworkValidation(hash, seq);
// Grace period elapses -- reconciled as a miss.
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
EXPECT_EQ(tracker_.totalMissed(), 1u);
EXPECT_EQ(tracker_.totalAgreements(), 0u);
EXPECT_EQ(tracker_.missed1h(), 1u);
// Late arrival of our validation (within repair window).
tracker_.recordOurValidation(hash, seq);
tracker_.reconcile();
// Miss should be repaired to agreement.
EXPECT_EQ(tracker_.totalAgreements(), 1u);
EXPECT_EQ(tracker_.totalMissed(), 0u);
EXPECT_EQ(tracker_.agreements1h(), 1u);
EXPECT_EQ(tracker_.missed1h(), 0u);
EXPECT_DOUBLE_EQ(tracker_.agreementPct1h(), 100.0);
}
// ---------------------------------------------------------------
// 4. Empty window returns 0%
// When no events have been recorded the percentage methods
// must return 0.0, not NaN or any other value.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, EmptyWindowReturnsZero)
{
EXPECT_DOUBLE_EQ(tracker_.agreementPct1h(), 0.0);
EXPECT_DOUBLE_EQ(tracker_.agreementPct24h(), 0.0);
EXPECT_EQ(tracker_.agreements1h(), 0u);
EXPECT_EQ(tracker_.missed1h(), 0u);
EXPECT_EQ(tracker_.agreements24h(), 0u);
EXPECT_EQ(tracker_.missed24h(), 0u);
EXPECT_EQ(tracker_.totalAgreements(), 0u);
EXPECT_EQ(tracker_.totalMissed(), 0u);
EXPECT_EQ(tracker_.totalValidationsSent(), 0u);
EXPECT_EQ(tracker_.totalValidationsChecked(), 0u);
}
// ---------------------------------------------------------------
// 5. Grace period boundary
// Events recorded less than 8 seconds ago must NOT be
// reconciled. Verify that an immediate reconcile is a no-op.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, GracePeriodBoundary)
{
auto const hash = makeHash(5);
LedgerIndex const seq = 500;
tracker_.recordOurValidation(hash, seq);
tracker_.recordNetworkValidation(hash, seq);
// Reconcile immediately -- grace period has not elapsed.
tracker_.reconcile();
// Nothing should be reconciled yet.
EXPECT_EQ(tracker_.totalAgreements(), 0u);
EXPECT_EQ(tracker_.totalMissed(), 0u);
EXPECT_EQ(tracker_.agreements1h(), 0u);
EXPECT_EQ(tracker_.missed1h(), 0u);
// Lifetime send/check counters should still be incremented.
EXPECT_EQ(tracker_.totalValidationsSent(), 1u);
EXPECT_EQ(tracker_.totalValidationsChecked(), 1u);
}
// ---------------------------------------------------------------
// 6. Max pending events -- trimming
// Add more than kMaxPendingEvents (1000) events. After
// reconciliation and a second reconcile pass the pending map
// should be trimmed. Lifetime totals must remain consistent.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, MaxPendingEventsTrimming)
{
constexpr std::size_t kCount = 1100;
for (std::size_t i = 0; i < kCount; ++i)
{
auto const hash = makeHash(i + 1);
auto const seq = static_cast<LedgerIndex>(i + 1);
tracker_.recordOurValidation(hash, seq);
tracker_.recordNetworkValidation(hash, seq);
}
EXPECT_EQ(tracker_.totalValidationsSent(), kCount);
EXPECT_EQ(tracker_.totalValidationsChecked(), kCount);
// Wait for grace period so all events can be reconciled.
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
// All events should be reconciled as agreements.
EXPECT_EQ(tracker_.totalAgreements(), kCount);
EXPECT_EQ(tracker_.totalMissed(), 0u);
// Reconcile again to trigger pending eviction / trimming.
// The pending map should be trimmed, but totals remain correct.
tracker_.reconcile();
EXPECT_EQ(tracker_.totalAgreements(), kCount);
EXPECT_EQ(tracker_.totalMissed(), 0u);
}
// ---------------------------------------------------------------
// 7. Multiple distinct ledgers -- mixed results
// Record a mix of agreements and misses to verify that window
// counts and percentages are computed correctly.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, MixedAgreementsAndMisses)
{
// 3 agreements: both sides validate.
for (int i = 1; i <= 3; ++i)
{
auto const hash = makeHash(static_cast<std::uint64_t>(i));
tracker_.recordOurValidation(hash, static_cast<LedgerIndex>(i));
tracker_.recordNetworkValidation(hash, static_cast<LedgerIndex>(i));
}
// 2 misses: only network validates.
for (int i = 4; i <= 5; ++i)
{
auto const hash = makeHash(static_cast<std::uint64_t>(i));
tracker_.recordNetworkValidation(hash, static_cast<LedgerIndex>(i));
}
// Wait for grace period then reconcile.
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
EXPECT_EQ(tracker_.totalAgreements(), 3u);
EXPECT_EQ(tracker_.totalMissed(), 2u);
EXPECT_EQ(tracker_.agreements1h(), 3u);
EXPECT_EQ(tracker_.missed1h(), 2u);
// 3 out of 5 = 60%
EXPECT_DOUBLE_EQ(tracker_.agreementPct1h(), 60.0);
}
// ---------------------------------------------------------------
// 8. Duplicate recording for same hash
// Recording the same hash multiple times should not create
// duplicate pending entries or double-count totals beyond the
// per-call increments.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, DuplicateRecordingSameHash)
{
auto const hash = makeHash(42);
LedgerIndex const seq = 42;
// Record our validation twice for the same hash.
tracker_.recordOurValidation(hash, seq);
tracker_.recordOurValidation(hash, seq);
tracker_.recordNetworkValidation(hash, seq);
// Each call increments the lifetime counter.
EXPECT_EQ(tracker_.totalValidationsSent(), 2u);
EXPECT_EQ(tracker_.totalValidationsChecked(), 1u);
// But only one pending event exists, so only one agreement.
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
EXPECT_EQ(tracker_.totalAgreements(), 1u);
EXPECT_EQ(tracker_.totalMissed(), 0u);
}
// ---------------------------------------------------------------
// 9. Only-we-validated scenario
// We validate but the network does not. After grace period
// this should be a miss (not an agreement).
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, OnlyWeValidated)
{
auto const hash = makeHash(99);
LedgerIndex const seq = 99;
tracker_.recordOurValidation(hash, seq);
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
EXPECT_EQ(tracker_.totalAgreements(), 0u);
EXPECT_EQ(tracker_.totalMissed(), 1u);
EXPECT_EQ(tracker_.missed1h(), 1u);
EXPECT_DOUBLE_EQ(tracker_.agreementPct1h(), 0.0);
}