From 6ddad54985b89320f6ee03ca1344be317dbcf4bf Mon Sep 17 00:00:00 2001 From: Alex Kremer Date: Wed, 29 Jul 2026 23:54:46 +0100 Subject: [PATCH] chore: Move lexical cast tests to gtest (#7873) --- include/xrpl/beast/core/LexicalCast.h | 16 +- src/test/beast/LexicalCast_test.cpp | 280 ------------------- src/tests/libxrpl/beast/LexicalCast.cpp | 339 ++++++++++++++++++++++++ 3 files changed, 347 insertions(+), 288 deletions(-) delete mode 100644 src/test/beast/LexicalCast_test.cpp create mode 100644 src/tests/libxrpl/beast/LexicalCast.cpp diff --git a/include/xrpl/beast/core/LexicalCast.h b/include/xrpl/beast/core/LexicalCast.h index 7cf21892bd..288c5d6673 100644 --- a/include/xrpl/beast/core/LexicalCast.h +++ b/include/xrpl/beast/core/LexicalCast.h @@ -58,7 +58,7 @@ struct LexicalCast "beast::LexicalCast can only be used with integral types"); template - bool + constexpr bool operator()(Integral& out, std::string_view in) const requires(std::is_integral_v && !std::is_same_v) { @@ -110,7 +110,7 @@ struct LexicalCast> { explicit LexicalCast() = default; - bool + constexpr bool operator()(Out& out, boost::core::basic_string_view in) const { return LexicalCast()(out, in); @@ -123,7 +123,7 @@ struct LexicalCast { explicit LexicalCast() = default; - bool + constexpr bool operator()(Out& out, std::string in) const { return LexicalCast()(out, in); @@ -136,7 +136,7 @@ struct LexicalCast { explicit LexicalCast() = default; - bool + constexpr bool operator()(Out& out, char const* in) const { XRPL_ASSERT(in, "beast::detail::LexicalCast(char const*) : non-null input"); @@ -151,7 +151,7 @@ struct LexicalCast { explicit LexicalCast() = default; - bool + constexpr bool operator()(Out& out, char* in) const { XRPL_ASSERT(in, "beast::detail::LexicalCast(char*) : non-null input"); @@ -177,7 +177,7 @@ struct BadLexicalCast : public std::bad_cast * @return `false` if there was a parsing or range error */ template -bool +constexpr bool lexicalCastChecked(Out& out, In in) { return detail::LexicalCast()(out, in); @@ -191,7 +191,7 @@ lexicalCastChecked(Out& out, In in) * @return The new type. */ template -Out +constexpr Out lexicalCastThrow(In in) { if (Out out; lexicalCastChecked(out, in)) @@ -207,7 +207,7 @@ lexicalCastThrow(In in) * @return The new type. */ template -Out +constexpr Out lexicalCast(In in, Out defaultValue = Out()) { if (Out out; lexicalCastChecked(out, in)) diff --git a/src/test/beast/LexicalCast_test.cpp b/src/test/beast/LexicalCast_test.cpp deleted file mode 100644 index b1d37daab8..0000000000 --- a/src/test/beast/LexicalCast_test.cpp +++ /dev/null @@ -1,280 +0,0 @@ -#include -#include -#include - -#include -#include -#include -#include - -namespace beast { - -class LexicalCast_test : public unit_test::Suite -{ -public: - template - static IntType - nextRandomInt(xor_shift_engine& r) - { - return static_cast(r()); - } - - template - void - testInteger(IntType in) - { - std::string s; - auto out = static_cast(~in); // Ensure out != in - - expect(lexicalCastChecked(s, in)); - expect(lexicalCastChecked(out, s)); - expect(out == in); - } - - template - void - testIntegers(xor_shift_engine& r) - { - { - std::stringstream ss; - ss << "random " << typeid(IntType).name(); - testcase(ss.str()); - - for (int i = 0; i < 1000; ++i) - { - auto const value = nextRandomInt(r); - testInteger(value); - } - } - - { - std::stringstream ss; - ss << "numeric_limits <" << typeid(IntType).name() << ">"; - testcase(ss.str()); - - testInteger(std::numeric_limits::min()); - testInteger(std::numeric_limits::max()); - } - } - - void - testPathologies() - { - testcase("pathologies"); - try - { - lexicalCastThrow("\xef\xbc\x91\xef\xbc\x90"); // utf-8 encoded - } - catch (BadLexicalCast const&) - { - pass(); - } - } - - template - void - tryBadConvert(std::string const& s) - { - T out; - expect(!lexicalCastChecked(out, s), s); - } - - void - testConversionOverflows() - { - testcase("conversion overflows"); - - tryBadConvert("99999999999999999999"); - tryBadConvert("4294967300"); - tryBadConvert("75821"); - } - - void - testConversionUnderflows() - { - testcase("conversion underflows"); - - tryBadConvert("-1"); - - tryBadConvert("-99999999999999999999"); - tryBadConvert("-4294967300"); - tryBadConvert("-75821"); - } - - template - bool - tryEdgeCase(std::string const& s) - { - T ret; - - bool const result = lexicalCastChecked(ret, s); - - if (!result) - return false; - - return s == std::to_string(ret); - } - - void - testEdgeCases() - { - testcase("conversion edge cases"); - - expect(tryEdgeCase("18446744073709551614")); - expect(tryEdgeCase("18446744073709551615")); - expect(!tryEdgeCase("18446744073709551616")); - - expect(tryEdgeCase("9223372036854775806")); - expect(tryEdgeCase("9223372036854775807")); - expect(!tryEdgeCase("9223372036854775808")); - - expect(tryEdgeCase("-9223372036854775807")); - expect(tryEdgeCase("-9223372036854775808")); - expect(!tryEdgeCase("-9223372036854775809")); - - expect(tryEdgeCase("4294967294")); - expect(tryEdgeCase("4294967295")); - expect(!tryEdgeCase("4294967296")); - - expect(tryEdgeCase("2147483646")); - expect(tryEdgeCase("2147483647")); - expect(!tryEdgeCase("2147483648")); - - expect(tryEdgeCase("-2147483647")); - expect(tryEdgeCase("-2147483648")); - expect(!tryEdgeCase("-2147483649")); - - expect(tryEdgeCase("65534")); - expect(tryEdgeCase("65535")); - expect(!tryEdgeCase("65536")); - - expect(tryEdgeCase("32766")); - expect(tryEdgeCase("32767")); - expect(!tryEdgeCase("32768")); - - expect(tryEdgeCase("-32767")); - expect(tryEdgeCase("-32768")); - expect(!tryEdgeCase("-32769")); - } - - template - void - testThrowConvert(std::string const& s, bool success) - { - bool result = !success; - T out; - - try - { - out = lexicalCastThrow(s); - result = true; - } - catch (BadLexicalCast const&) - { - result = false; - } - - expect(result == success, s); - } - - void - testThrowingConversions() - { - testcase("throwing conversion"); - - testThrowConvert("99999999999999999999", false); - testThrowConvert("9223372036854775806", true); - - testThrowConvert("4294967290", true); - testThrowConvert("42949672900", false); - testThrowConvert("429496729000", false); - testThrowConvert("4294967290000", false); - - testThrowConvert("5294967295", false); - testThrowConvert("-2147483644", true); - - testThrowConvert("66666", false); - testThrowConvert("-5711", true); - } - - void - testZero() - { - testcase("zero conversion"); - - { - std::int32_t out = 0; - - expect(lexicalCastChecked(out, "-0"), "0"); - expect(lexicalCastChecked(out, "0"), "0"); - expect(lexicalCastChecked(out, "+0"), "0"); - } - - { - std::uint32_t out = 0; - - expect(!lexicalCastChecked(out, "-0"), "0"); - expect(lexicalCastChecked(out, "0"), "0"); - expect(lexicalCastChecked(out, "+0"), "0"); - } - } - - void - testEntireRange() - { - testcase("entire range"); - - std::int32_t i = std::numeric_limits::min(); - std::string const empty; - - while (i <= std::numeric_limits::max()) - { - auto const j = static_cast(i); - - auto actual = std::to_string(j); - - auto result = lexicalCast(j, empty); - - expect(result == actual, actual + " (string to integer)"); - - if (result == actual) - { - auto number = lexicalCast(result); - - if (number != j) - expect(false, actual + " (integer to string)"); - } - - i++; - } - } - - void - run() override - { - std::int64_t const seedValue = 50; - - xor_shift_engine r(seedValue); - - testIntegers(r); - testIntegers(r); - testIntegers(r); - testIntegers(r); - testIntegers(r); - testIntegers(r); - testIntegers(r); - testIntegers(r); - - testPathologies(); - testConversionOverflows(); - testConversionUnderflows(); - testThrowingConversions(); - testZero(); - testEdgeCases(); - testEntireRange(); - } -}; - -BEAST_DEFINE_TESTSUITE(LexicalCast, beast, beast); - -} // namespace beast diff --git a/src/tests/libxrpl/beast/LexicalCast.cpp b/src/tests/libxrpl/beast/LexicalCast.cpp new file mode 100644 index 0000000000..d18af4e1cd --- /dev/null +++ b/src/tests/libxrpl/beast/LexicalCast.cpp @@ -0,0 +1,339 @@ +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast { +namespace { + +template +[[nodiscard]] constexpr bool +parses(std::string_view text) +{ + T out{}; + return lexicalCastChecked(out, text); +} + +template +[[nodiscard]] constexpr T +parsed(std::string_view text) +{ + T out{}; + return lexicalCastChecked(out, text) ? out : T{}; +} + +template +constexpr T kMax = std::numeric_limits::max(); + +template +constexpr T kMin = std::numeric_limits::min(); + +template +constexpr T kUnderMax = kMax - 1; + +template +constexpr T kOverMin = kMin + 1; + +// Comfortably inside the range, not boundary values. +constexpr auto kNearMax32 = kMax - 5; +constexpr auto kNearMin32 = kMin + 4; +constexpr auto kUnderInt64Max = uint64_t{kMax} - 1; +constexpr auto kInRangeInt16 = int16_t{-5711}; + +// No wider integer type can hold these, so ToString cannot produce them. +constexpr auto kAboveUint64Max = "18446744073709551616"; +constexpr auto kBelowInt64Min = "-9223372036854775809"; + +// Out of range for every integer type we test. +constexpr auto kTwentyNines = "99999999999999999999"; +constexpr auto kNegativeTwentyNines = "-99999999999999999999"; + +// Arbitrary values chosen to sit well outside a type's range, not just over it. +constexpr auto kAboveUint16Max = "75821"; +constexpr auto kBelowInt16Min = "-75821"; +constexpr auto kAboveInt32Max = "5294967295"; +constexpr auto kAboveInt16Max = "66666"; + +constexpr auto kPositiveInt32 = int32_t{42}; +constexpr auto kNegativeInt32 = int32_t{-42}; + +constexpr auto kPositiveInt32Text = "+42"; +constexpr auto kNegativeInt32Text = "-42"; + +constexpr auto kNegativeOne = "-1"; +constexpr auto kNegativeZero = "-0"; +constexpr auto kBareZero = "0"; +constexpr auto kPositiveZero = "+0"; + +// Full-width digits one and zero, not ASCII ones. +constexpr std::string_view kFullWidthDigits = "\xef\xbc\x91\xef\xbc\x90"; + +// The decimal text of a value, usable in a constant expression. +template +struct ToString +{ + std::array buffer{}; + std::size_t length{}; + + constexpr explicit ToString(T value) + { + auto const result = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value); + length = static_cast(result.ptr - buffer.data()); + } + + constexpr + operator std::string_view() const + { + return {buffer.data(), length}; + } +}; + +template +constexpr auto kMaxText = ToString{kMax}; + +template +constexpr auto kUnderMaxText = ToString{kUnderMax}; + +template +constexpr auto kOverMaxText = ToString{Wider{kMax} + 1}; + +template +constexpr auto kMinText = ToString{kMin}; + +template +constexpr auto kOverMinText = ToString{kOverMin}; + +template +constexpr auto kUnderMinText = ToString{Wider{kMin} - 1}; + +constexpr auto kOverUint32MaxText = ToString{uint64_t{kMax} + 5}; +constexpr auto kNegatedOverUint32MaxText = ToString{-(int64_t{kMax} + 5)}; + +// lexicalCastThrow deduces its input type, so the text has to be an explicit +// string_view rather than a ToString. +template +[[nodiscard]] constexpr T +castThrow(Value value) +{ + return lexicalCastThrow(std::string_view{ToString{value}}); +} + +template +[[nodiscard]] bool +roundTrips(std::string_view text) +{ + T out{}; + return lexicalCastChecked(out, text) && std::to_string(out) == text; +} + +template +void +expectRoundTrip(T value) +{ + SCOPED_TRACE(::testing::Message() << "value: " << value); + + auto const text = lexicalCast(value); + EXPECT_EQ(text, std::to_string(value)); + + auto decoded = static_cast(~value); // ensure decoded != value + EXPECT_TRUE(lexicalCastChecked(decoded, text)); + EXPECT_EQ(decoded, value); +} + +} // namespace + +// int/unsigned/short/unsigned short are covered by the list below — they are +// these exact types everywhere we build. +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); + +using IntegerTypes = ::testing::Types< // + int16_t, + uint16_t, + int32_t, + uint32_t, + int64_t, + uint64_t>; + +struct IntegerTypeNames +{ + template + static std::string + // NOLINTNEXTLINE(readability-identifier-naming) - required by gtest + GetName(int) + { + return (std::is_signed_v ? "int" : "uint") + std::to_string(sizeof(T) * 8) + "_t"; + } +}; + +template +class LexicalCastIntegers : public ::testing::Test +{ +}; + +TYPED_TEST_SUITE(LexicalCastIntegers, IntegerTypes, IntegerTypeNames); + +TYPED_TEST(LexicalCastIntegers, round_trips_random_values) +{ + static constexpr auto kSampleCount = 1000uz; + + xor_shift_engine r{50}; // seeded per test so a failure reproduces on its own + + for (auto i = 0uz; i < kSampleCount; ++i) + expectRoundTrip(static_cast(r())); +} + +TYPED_TEST(LexicalCastIntegers, round_trips_numeric_limits) +{ + expectRoundTrip(std::numeric_limits::min()); + expectRoundTrip(std::numeric_limits::max()); +} + +TEST(LexicalCast, round_trips_every_int16_value) +{ + for (int32_t i = kMin; i <= kMax; ++i) + { + auto const value = static_cast(i); + + // ASSERT, or a broken cast reports all 65536 iterations. + auto const text = lexicalCast(value); + ASSERT_EQ(text, std::to_string(value)); + ASSERT_EQ(lexicalCast(text), value); + } +} + +TEST(LexicalCast, rejects_overflow) +{ + static_assert(not parses(kOverUint32MaxText)); + static_assert(not parses(kTwentyNines)); + static_assert(not parses(kAboveUint16Max)); +} + +TEST(LexicalCast, rejects_underflow) +{ + static_assert(not parses(kNegativeOne)); + static_assert(not parses(kNegatedOverUint32MaxText)); + static_assert(not parses(kNegativeTwentyNines)); + static_assert(not parses(kBelowInt16Min)); +} + +TEST(LexicalCast, accepts_up_to_the_maximum) +{ + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kOverMaxText)); + + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kOverMaxText)); + + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kOverMaxText)); + + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kOverMaxText)); + + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kOverMaxText)); + + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kAboveUint64Max)); +} + +TEST(LexicalCast, accepts_down_to_the_minimum) +{ + static_assert(parsed(kOverMinText) == kOverMin); + static_assert(parsed(kMinText) == kMin); + static_assert(not parses(kUnderMinText)); + + static_assert(parsed(kOverMinText) == kOverMin); + static_assert(parsed(kMinText) == kMin); + static_assert(not parses(kUnderMinText)); + + static_assert(parsed(kOverMinText) == kOverMin); + static_assert(parsed(kMinText) == kMin); + static_assert(not parses(kBelowInt64Min)); +} + +TEST(LexicalCast, limits_round_trip_through_to_string) +{ + EXPECT_TRUE(roundTrips(kMaxText)); + EXPECT_TRUE(roundTrips(kMaxText)); + EXPECT_TRUE(roundTrips(kMinText)); + EXPECT_TRUE(roundTrips(kMaxText)); + EXPECT_TRUE(roundTrips(kMinText)); + EXPECT_TRUE(roundTrips(kMaxText)); + EXPECT_TRUE(roundTrips(kMinText)); +} + +TEST(LexicalCast, accepts_signed_zero_in_every_form) +{ + static_assert(parsed(kNegativeZero) == 0); + static_assert(parsed(kBareZero) == 0); + static_assert(parsed(kPositiveZero) == 0); +} + +TEST(LexicalCast, rejects_negative_zero_when_unsigned) +{ + static_assert(not parses(kNegativeZero)); + static_assert(parsed(kBareZero) == 0); + static_assert(parsed(kPositiveZero) == 0); +} + +TEST(LexicalCast, accepts_char_pointer_and_std_string_input) +{ + int32_t fromLiteral = 0; + EXPECT_TRUE(lexicalCastChecked(fromLiteral, kPositiveInt32Text)); + EXPECT_EQ(fromLiteral, kPositiveInt32); + + int32_t fromString = 0; + EXPECT_TRUE(lexicalCastChecked(fromString, std::string{kNegativeInt32Text})); + EXPECT_EQ(fromString, kNegativeInt32); +} + +TEST(LexicalCast, throwing_cast_returns_in_range_values) +{ + static_assert(castThrow(kUnderInt64Max) == kUnderInt64Max); + static_assert(castThrow(kNearMax32) == kNearMax32); + static_assert(castThrow(kNearMin32) == kNearMin32); + static_assert(castThrow(kInRangeInt16) == kInRangeInt16); +} + +TEST(LexicalCast, throwing_cast_throws_on_out_of_range) +{ + EXPECT_THROW(lexicalCastThrow(kTwentyNines), BadLexicalCast); + + // kNearMax32 with digits appended, so each is further past uint32_t's range. + for (auto const scale : {10, 100, 1000}) + { + auto const tooBig = ToString{uint64_t{kNearMax32} * scale}; + EXPECT_THROW(lexicalCastThrow(std::string_view{tooBig}), BadLexicalCast); + } + + EXPECT_THROW(lexicalCastThrow(kAboveInt32Max), BadLexicalCast); + EXPECT_THROW(lexicalCastThrow(kAboveInt16Max), BadLexicalCast); +} + +// Full-width digits, not ASCII ones. +TEST(LexicalCast, throwing_cast_throws_on_utf8_digits) +{ + EXPECT_THROW(lexicalCastThrow(kFullWidthDigits), BadLexicalCast); +} + +} // namespace beast