#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl::PeerFinder { namespace { class LivecacheTest : public ::testing::Test { protected: static beast::Journal journal() { return beast::Journal{TestSink::instance()}; } static beast::IP::Endpoint endpoint(std::uint16_t index, bool v4 = true) { auto const port = static_cast(10000 + index); if (v4) { auto bytes = beast::IP::AddressV4::bytes_type{ {54, static_cast((index / 256) % 256), static_cast(index % 256), 1}}; return beast::IP::Endpoint{beast::IP::Address{beast::IP::AddressV4{bytes}}, port}; } auto bytes = beast::IP::AddressV6::bytes_type{ {0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, static_cast((index / 256) % 256), static_cast(index % 256), 1}}; return beast::IP::Endpoint{beast::IP::Address{beast::IP::AddressV6{bytes}}, port}; } void addEndpoint(beast::IP::Endpoint const& ep, std::uint32_t hops = 0) { cache_.insert(Endpoint{ep, hops}); } TestStopwatch clock_; Livecache<> cache_{clock_, journal()}; }; } // namespace TEST_F(LivecacheTest, basic_insert) { EXPECT_TRUE(cache_.empty()); for (auto i = 0; i < 10; ++i) addEndpoint(endpoint(i, true)); EXPECT_FALSE(cache_.empty()); EXPECT_EQ(cache_.size(), 10u); for (auto i = 10; i < 20; ++i) addEndpoint(endpoint(i, false)); EXPECT_FALSE(cache_.empty()); EXPECT_EQ(cache_.size(), 20u); } TEST_F(LivecacheTest, insert_update_keeps_lowest_hop_count) { auto const ep1 = Endpoint{endpoint(1), 2}; cache_.insert(ep1); ASSERT_EQ(cache_.size(), 1u); EXPECT_EQ((cache_.hops.begin() + 2)->begin()->hops, 2u); auto const ep2 = Endpoint{ep1.address, 4}; cache_.insert(ep2); EXPECT_EQ(cache_.size(), 1u); EXPECT_EQ((cache_.hops.begin() + 2)->begin()->hops, 2u); auto const ep3 = Endpoint{ep1.address, 2}; cache_.insert(ep3); EXPECT_EQ(cache_.size(), 1u); EXPECT_EQ((cache_.hops.begin() + 2)->begin()->hops, 2u); auto const ep4 = Endpoint{ep1.address, 1}; cache_.insert(ep4); EXPECT_EQ(cache_.size(), 1u); EXPECT_EQ((cache_.hops.begin() + 1)->begin()->hops, 1u); } TEST_F(LivecacheTest, hop_iterators_support_const_reverse_and_move_back) { auto const ep1 = Endpoint{endpoint(1), 1}; auto const ep2 = Endpoint{endpoint(2), 1}; cache_.insert(ep1); cache_.insert(ep2); auto hop = *(cache_.hops.begin() + 1); ASSERT_NE(hop.begin(), hop.end()); ASSERT_NE(hop.cbegin(), hop.cend()); ASSERT_NE(hop.rbegin(), hop.rend()); ASSERT_NE(hop.crbegin(), hop.crend()); auto const firstAddress = hop.begin()->address; hop.moveBack(hop.begin()); EXPECT_EQ(hop.rbegin()->address, firstAddress); auto const& constHops = cache_.hops; EXPECT_NE(constHops.begin(), constHops.end()); EXPECT_NE(constHops.cbegin(), constHops.cend()); EXPECT_NE(constHops.rbegin(), constHops.rend()); EXPECT_NE(constHops.crbegin(), constHops.crend()); auto const constHop = *(constHops.cbegin() + 1); EXPECT_EQ(std::distance(constHop.begin(), constHop.end()), 2); EXPECT_EQ(std::distance(constHop.cbegin(), constHop.cend()), 2); EXPECT_EQ(std::distance(constHop.rbegin(), constHop.rend()), 2); EXPECT_EQ(std::distance(constHop.crbegin(), constHop.crend()), 2); } TEST_F(LivecacheTest, on_write_reports_entries_and_expiration) { cache_.insert(Endpoint{endpoint(1), 1}); cache_.insert(Endpoint{endpoint(2), Tuning::kMaxHops + 1}); JsonPropertyStream stream; { beast::PropertyStream::Map map(stream); cache_.onWrite(map); } auto const& top = stream.top(); EXPECT_EQ(top["size"].asUInt(), 2u); EXPECT_FALSE(top["hist"].asString().empty()); ASSERT_TRUE(top.isMember("entries")); ASSERT_EQ(top["entries"].size(), 2u); auto const& entry = top["entries"][json::UInt{0}]; EXPECT_TRUE(entry.isMember("hops")); EXPECT_TRUE(entry.isMember("address")); EXPECT_TRUE(entry.isMember("expires")); } TEST_F(LivecacheTest, expire_removes_entries_after_ttl) { using namespace std::chrono_literals; cache_.insert(Endpoint{endpoint(1), 1}); ASSERT_EQ(cache_.size(), 1u); cache_.expire(); EXPECT_EQ(cache_.size(), 1u); clock_.advance(Tuning::kLiveCacheSecondsToLive - 1s); cache_.expire(); EXPECT_EQ(cache_.size(), 1u); clock_.advance(1s); cache_.expire(); EXPECT_TRUE(cache_.empty()); } TEST_F(LivecacheTest, expire_removes_multiple_entries_after_ttl) { using namespace std::chrono_literals; cache_.insert(Endpoint{endpoint(1), 1}); cache_.insert(Endpoint{endpoint(2), 2}); clock_.advance(Tuning::kLiveCacheSecondsToLive); cache_.expire(); EXPECT_TRUE(cache_.empty()); } TEST_F(LivecacheTest, histogram_counts_all_entries) { constexpr auto kNumEndpoints = 40; for (auto i = 0; i < kNumEndpoints; ++i) { addEndpoint(endpoint(static_cast(i)), xrpl::randInt()); } auto const histogram = cache_.hops.histogram(); ASSERT_FALSE(histogram.empty()); std::vector values; boost::split(values, histogram, boost::algorithm::is_any_of(",")); auto sum = 0; for (auto const& value : values) { auto const count = boost::lexical_cast(boost::trim_copy(value)); sum += count; EXPECT_GE(count, 0); } EXPECT_EQ(sum, kNumEndpoints); } TEST_F(LivecacheTest, shuffle_preserves_bucket_contents) { for (auto i = 0; i < 100; ++i) { addEndpoint(endpoint(static_cast(i)), xrpl::randInt(Tuning::kMaxHops + 1)); } using AtHop = std::vector; using AllHops = std::array; auto const compareEndpoint = [](Endpoint const& lhs, Endpoint const& rhs) { return rhs.hops < lhs.hops || (rhs.hops == lhs.hops && rhs.address < lhs.address); }; auto const sameEndpoint = [](Endpoint const& lhs, Endpoint const& rhs) { return lhs.hops == rhs.hops && lhs.address == rhs.address; }; auto const sameEndpoints = [&sameEndpoint](std::vector const& lhs, std::vector const& rhs) { return lhs.size() == rhs.size() && std::equal(lhs.begin(), lhs.end(), rhs.begin(), sameEndpoint); }; AllHops before; AllHops beforeSorted; for (auto i = std::make_pair(0, cache_.hops.begin()); i.second != cache_.hops.end(); ++i.first, ++i.second) { std::ranges::copy(*i.second, std::back_inserter(before[i.first])); std::ranges::copy(*i.second, std::back_inserter(beforeSorted[i.first])); std::ranges::sort(beforeSorted[i.first], compareEndpoint); } cache_.hops.shuffle(); AllHops after; AllHops afterSorted; for (auto i = std::make_pair(0, cache_.hops.begin()); i.second != cache_.hops.end(); ++i.first, ++i.second) { std::ranges::copy(*i.second, std::back_inserter(after[i.first])); std::ranges::copy(*i.second, std::back_inserter(afterSorted[i.first])); std::ranges::sort(afterSorted[i.first], compareEndpoint); } auto allBucketsKeptOriginalOrder = true; for (auto i = 0u; i < before.size(); ++i) { EXPECT_EQ(before[i].size(), after[i].size()); allBucketsKeptOriginalOrder = allBucketsKeptOriginalOrder && sameEndpoints(before[i], after[i]); EXPECT_TRUE(sameEndpoints(beforeSorted[i], afterSorted[i])); } EXPECT_FALSE(allBucketsKeptOriginalOrder); } } // namespace xrpl::PeerFinder