Files
rippled/src/tests/libxrpl/beast/insight/Unit.cpp
Pratik Mankawde b068a6f327 style: Rename gtest test cases to snake_case
develop's new fix-gtest-names pre-commit hook requires a snake_case test-case
name. CI runs it with --all-files, so the existing CamelCase names fail the
check on every branch that carries these files.
2026-09-23 19:04:56 +01:00

175 lines
6.0 KiB
C++

/**
* GTest unit tests for beast::insight::Unit and its plumbing.
*
* A metric's unit decides two things that are invisible at the call site: the
* name suffix the exporter appends, and which bucket ladder the histogram
* view applies. Getting it wrong is silent -- a byte count declared as
* milliseconds still records, still exports, still draws a graph, and the
* graph is wrong. So each hop the unit has to survive is asserted here
* rather than left to inspection.
*
* The hop that matters most is the group wrapper. Call sites reach a
* collector through Groups, so a unit that reaches OTelCollector correctly
* but is dropped by the group prefixing layer would pass a naive test while
* failing in production.
*/
#include <xrpl/beast/insight/Unit.h>
#include <xrpl/beast/insight/Event.h>
#include <xrpl/beast/insight/EventImpl.h>
#include <xrpl/beast/insight/Groups.h>
#include <xrpl/beast/insight/NullCollector.h>
#include <gtest/gtest.h>
#include <chrono>
#include <cstdint>
#include <memory>
#include <vector>
namespace beast::insight {
namespace {
/**
* An EventImpl that records what it was notified with.
*
* Needed because every shipped implementation either discards the sample
* (NullCollector) or sends it somewhere external. Asserting the recorded
* value proves the raw-integral path preserves it, rather than only proving
* that notify() can be called without crashing.
*/
class RecordingEventImpl : public EventImpl
{
public:
explicit RecordingEventImpl(Unit unit) : EventImpl(unit)
{
}
void
notify(value_type const& value) override
{
samples.push_back(value);
}
/**
* Every value passed to notify(), in call order.
*/
std::vector<value_type> samples;
};
} // namespace
// The unit code is a contract with the collector's Prometheus exporter: it
// derives the exported name suffix from this string. Assert the exact codes,
// not merely that they differ.
TEST(InsightUnit, otel_code_is_the_ucum_code_for_each_unit)
{
EXPECT_STREQ(otelUnitCode(Unit::Millis), "ms");
EXPECT_STREQ(otelUnitCode(Unit::Bytes), "By");
}
// The description is what an operator reads in the metric catalogue, so a
// byte-valued instrument must not describe itself as a duration.
TEST(InsightUnit, description_matches_what_the_unit_actually_measures)
{
EXPECT_STREQ(otelUnitDescription(Unit::Millis), "Duration in ms");
EXPECT_STREQ(otelUnitDescription(Unit::Bytes), "Size in bytes");
}
TEST(InsightUnit, default_event_unit_is_millis_for_backward_compatibility)
{
// Every pre-existing makeEvent(name) call site records a duration, so the
// one-argument overload must keep meaning milliseconds.
auto const collector = NullCollector::make();
auto const event = collector->makeEvent("legacy");
ASSERT_NE(event.impl(), nullptr);
EXPECT_EQ(event.impl()->unit(), Unit::Millis);
}
TEST(InsightUnit, make_event_carries_the_requested_unit_to_the_impl)
{
auto const collector = NullCollector::make();
auto const event = collector->makeEvent("size", Unit::Bytes);
ASSERT_NE(event.impl(), nullptr);
EXPECT_EQ(event.impl()->unit(), Unit::Bytes);
}
TEST(InsightUnit, prefixed_make_event_carries_the_unit)
{
auto const collector = NullCollector::make();
auto const event = collector->makeEvent("rpc", "size", Unit::Bytes);
ASSERT_NE(event.impl(), nullptr);
EXPECT_EQ(event.impl()->unit(), Unit::Bytes);
}
TEST(InsightUnit, group_wrapper_forwards_the_unit_along_with_the_prefix)
{
// ServerHandler creates its events through a Group, not through the
// collector directly. If the group's makeEvent override forwards only the
// name, the unit silently reverts to milliseconds and the byte histogram
// inherits the latency ladder again.
auto const collector = NullCollector::make();
auto const groups = makeGroups(collector);
auto const event = groups->get("rpc")->makeEvent("size", Unit::Bytes);
ASSERT_NE(event.impl(), nullptr);
EXPECT_EQ(event.impl()->unit(), Unit::Bytes);
}
TEST(InsightUnit, group_wrapper_still_defaults_to_millis)
{
auto const collector = NullCollector::make();
auto const groups = makeGroups(collector);
auto const event = groups->get("rpc")->makeEvent("time");
ASSERT_NE(event.impl(), nullptr);
EXPECT_EQ(event.impl()->unit(), Unit::Millis);
}
TEST(InsightUnit, raw_integral_notify_preserves_the_value_exactly)
{
// The byte path must not be rounded or scaled on its way through the
// duration-typed storage field.
auto const impl = std::make_shared<RecordingEventImpl>(Unit::Bytes);
Event const event(impl);
event.notify(std::uint64_t{4096});
event.notify(std::uint64_t{0});
event.notify(std::uint64_t{1'048'577});
ASSERT_EQ(impl->samples.size(), 3U);
EXPECT_EQ(impl->samples[0].count(), 4096);
EXPECT_EQ(impl->samples[1].count(), 0);
EXPECT_EQ(impl->samples[2].count(), 1'048'577);
}
TEST(InsightUnit, duration_notify_still_rounds_up_to_whole_milliseconds)
{
// Pre-existing behaviour, asserted so the new overload cannot quietly
// change it: Event applies ceil to whole milliseconds, which is why
// sub-millisecond resolution is impossible on this path.
auto const impl = std::make_shared<RecordingEventImpl>(Unit::Millis);
Event const event(impl);
event.notify(std::chrono::microseconds{40});
event.notify(std::chrono::microseconds{1'000});
event.notify(std::chrono::milliseconds{7});
ASSERT_EQ(impl->samples.size(), 3U);
EXPECT_EQ(impl->samples[0].count(), 1) << "40us must round up to 1ms, not down to 0";
EXPECT_EQ(impl->samples[1].count(), 1);
EXPECT_EQ(impl->samples[2].count(), 7);
}
TEST(InsightUnit, notify_on_a_null_event_is_safe_for_both_overloads)
{
// A default-constructed Event has no impl. Both overloads must be no-ops
// rather than dereferencing null.
Event const none;
ASSERT_EQ(none.impl(), nullptr);
EXPECT_NO_THROW(none.notify(std::uint64_t{4096}));
EXPECT_NO_THROW(none.notify(std::chrono::milliseconds{5}));
}
} // namespace beast::insight