From f5e63f8a91c8b11c594887b68252bbe8b6f0b7cc Mon Sep 17 00:00:00 2001 From: Marek Foss Date: Mon, 6 Jul 2026 21:48:36 +0200 Subject: [PATCH] test: Migrate basics Beast tests to GTest (#7136) Co-authored-by: Alex Kremer --- .../scripts/levelization/results/ordering.txt | 1 - src/test/basics/Buffer_test.cpp | 268 ---- src/test/basics/FileUtilities_test.cpp | 64 - src/test/basics/IOUAmount_test.cpp | 276 ----- src/test/basics/IntrusiveShared_test.cpp | 879 ------------- src/test/basics/KeyCache_test.cpp | 82 -- src/test/basics/StringUtilities_test.cpp | 309 ----- src/test/basics/TaggedCache_test.cpp | 251 ---- src/test/basics/Units_test.cpp | 344 ------ src/test/basics/XRPAmount_test.cpp | 326 ----- src/test/basics/base58_test.cpp | 440 ------- src/test/basics/base_uint_test.cpp | 376 ------ src/test/basics/join_test.cpp | 83 -- src/tests/libxrpl/basics/Buffer.cpp | 260 ++++ src/tests/libxrpl/basics/FileUtilities.cpp | 94 ++ src/tests/libxrpl/basics/IOUAmount.cpp | 243 ++++ src/tests/libxrpl/basics/IntrusiveShared.cpp | 844 +++++++++++++ src/tests/libxrpl/basics/KeyCache.cpp | 81 ++ .../libxrpl/basics/Number.cpp} | 1084 +++++++++-------- src/tests/libxrpl/basics/StringUtilities.cpp | 293 +++++ src/tests/libxrpl/basics/TaggedCache.cpp | 246 ++++ src/tests/libxrpl/basics/Units.cpp | 328 +++++ src/tests/libxrpl/basics/XRPAmount.cpp | 295 +++++ src/tests/libxrpl/basics/base58.cpp | 438 +++++++ src/tests/libxrpl/basics/base_uint_test.cpp | 360 ++++++ .../libxrpl/basics/hardened_hash.cpp} | 57 +- src/tests/libxrpl/basics/join.cpp | 80 ++ 27 files changed, 4145 insertions(+), 4257 deletions(-) delete mode 100644 src/test/basics/Buffer_test.cpp delete mode 100644 src/test/basics/FileUtilities_test.cpp delete mode 100644 src/test/basics/IOUAmount_test.cpp delete mode 100644 src/test/basics/IntrusiveShared_test.cpp delete mode 100644 src/test/basics/KeyCache_test.cpp delete mode 100644 src/test/basics/StringUtilities_test.cpp delete mode 100644 src/test/basics/TaggedCache_test.cpp delete mode 100644 src/test/basics/Units_test.cpp delete mode 100644 src/test/basics/XRPAmount_test.cpp delete mode 100644 src/test/basics/base58_test.cpp delete mode 100644 src/test/basics/base_uint_test.cpp delete mode 100644 src/test/basics/join_test.cpp create mode 100644 src/tests/libxrpl/basics/Buffer.cpp create mode 100644 src/tests/libxrpl/basics/FileUtilities.cpp create mode 100644 src/tests/libxrpl/basics/IOUAmount.cpp create mode 100644 src/tests/libxrpl/basics/IntrusiveShared.cpp create mode 100644 src/tests/libxrpl/basics/KeyCache.cpp rename src/{test/basics/Number_test.cpp => tests/libxrpl/basics/Number.cpp} (73%) create mode 100644 src/tests/libxrpl/basics/StringUtilities.cpp create mode 100644 src/tests/libxrpl/basics/TaggedCache.cpp create mode 100644 src/tests/libxrpl/basics/Units.cpp create mode 100644 src/tests/libxrpl/basics/XRPAmount.cpp create mode 100644 src/tests/libxrpl/basics/base58.cpp create mode 100644 src/tests/libxrpl/basics/base_uint_test.cpp rename src/{test/basics/hardened_hash_test.cpp => tests/libxrpl/basics/hardened_hash.cpp} (84%) create mode 100644 src/tests/libxrpl/basics/join.cpp diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index 9c9adeefe2..aee6f4c579 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -72,7 +72,6 @@ test.app > xrpl.server test.app > xrpl.shamap test.app > xrpl.tx test.basics > test.jtx -test.basics > test.unit_test test.basics > xrpl.basics test.basics > xrpl.core test.basics > xrpld.rpc diff --git a/src/test/basics/Buffer_test.cpp b/src/test/basics/Buffer_test.cpp deleted file mode 100644 index c748a3f9dd..0000000000 --- a/src/test/basics/Buffer_test.cpp +++ /dev/null @@ -1,268 +0,0 @@ -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace xrpl::test { - -struct Buffer_test : beast::unit_test::Suite -{ - static bool - sane(Buffer const& b) - { - if (b.empty()) - return b.data() == nullptr; - - return b.data() != nullptr; - } - - void - run() override - { - std::uint8_t const data[] = {0xa8, 0xa1, 0x38, 0x45, 0x23, 0xec, 0xe4, 0x23, - 0x71, 0x6d, 0x2a, 0x18, 0xb4, 0x70, 0xcb, 0xf5, - 0xac, 0x2d, 0x89, 0x4d, 0x19, 0x9c, 0xf0, 0x2c, - 0x15, 0xd1, 0xf9, 0x9b, 0x66, 0xd2, 0x30, 0xd3}; - - Buffer const b0; - BEAST_EXPECT(sane(b0)); - BEAST_EXPECT(b0.empty()); - - Buffer b1{0}; - BEAST_EXPECT(sane(b1)); - BEAST_EXPECT(b1.empty()); - std::memcpy(b1.alloc(16), data, 16); - BEAST_EXPECT(sane(b1)); - BEAST_EXPECT(!b1.empty()); - BEAST_EXPECT(b1.size() == 16); - - Buffer b2{b1.size()}; - BEAST_EXPECT(sane(b2)); - BEAST_EXPECT(!b2.empty()); - BEAST_EXPECT(b2.size() == b1.size()); - std::memcpy(b2.data(), data + 16, 16); - - Buffer b3{data, sizeof(data)}; - BEAST_EXPECT(sane(b3)); - BEAST_EXPECT(!b3.empty()); - BEAST_EXPECT(b3.size() == sizeof(data)); - BEAST_EXPECT(std::memcmp(b3.data(), data, b3.size()) == 0); - - // Check equality and inequality comparisons - BEAST_EXPECT(b0 == b0); - BEAST_EXPECT(b0 != b1); - BEAST_EXPECT(b1 == b1); - BEAST_EXPECT(b1 != b2); - BEAST_EXPECT(b2 != b3); - - // Check copy constructors and copy assignments: - { - testcase("Copy Construction / Assignment"); - - Buffer x{b0}; - BEAST_EXPECT(x == b0); - BEAST_EXPECT(sane(x)); - Buffer y{b1}; - BEAST_EXPECT(y == b1); - BEAST_EXPECT(sane(y)); - x = b2; - BEAST_EXPECT(x == b2); - BEAST_EXPECT(sane(x)); - x = y; - BEAST_EXPECT(x == y); - BEAST_EXPECT(sane(x)); - y = b3; - BEAST_EXPECT(y == b3); - BEAST_EXPECT(sane(y)); - x = b0; - BEAST_EXPECT(x == b0); - BEAST_EXPECT(sane(x)); -#if defined(__clang__) -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wself-assign-overloaded" -#endif - - x = x; - BEAST_EXPECT(x == b0); - BEAST_EXPECT(sane(x)); - y = y; - BEAST_EXPECT(y == b3); - BEAST_EXPECT(sane(y)); - -#if defined(__clang__) -#pragma clang diagnostic pop -#endif - } - - // Check move constructor & move assignments: - { - testcase("Move Construction / Assignment"); - - static_assert(std::is_nothrow_move_constructible_v); - static_assert(std::is_nothrow_move_assignable_v); - - { // Move-construct from empty buf - Buffer x; - Buffer const y{std::move(x)}; - BEAST_EXPECT(sane(x)); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(x.empty()); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(sane(y)); - BEAST_EXPECT(y.empty()); - BEAST_EXPECT(x == y); // NOLINT(bugprone-use-after-move) - } - - { // Move-construct from non-empty buf - Buffer x{b1}; - Buffer const y{std::move(x)}; - BEAST_EXPECT(sane(x)); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(x.empty()); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(sane(y)); - BEAST_EXPECT(y == b1); - } - - { // Move assign empty buf to empty buf - Buffer x; - Buffer y; - - x = std::move(y); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x.empty()); - BEAST_EXPECT(sane(y)); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(y.empty()); // NOLINT(bugprone-use-after-move) - } - - { // Move assign non-empty buf to empty buf - Buffer x; - Buffer y{b1}; - - x = std::move(y); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x == b1); - BEAST_EXPECT(sane(y)); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(y.empty()); // NOLINT(bugprone-use-after-move) - } - - { // Move assign empty buf to non-empty buf - Buffer x{b1}; - Buffer y; - - x = std::move(y); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x.empty()); - BEAST_EXPECT(sane(y)); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(y.empty()); // NOLINT(bugprone-use-after-move) - } - - { // Move assign non-empty buf to non-empty buf - Buffer x{b1}; - Buffer y{b2}; - Buffer z{b3}; - - x = std::move(y); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(!x.empty()); - BEAST_EXPECT(sane(y)); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(y.empty()); // NOLINT(bugprone-use-after-move) - - x = std::move(z); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(!x.empty()); - BEAST_EXPECT(sane(z)); // NOLINT(bugprone-use-after-move) - BEAST_EXPECT(z.empty()); // NOLINT(bugprone-use-after-move) - } - } - - { - testcase("Slice Conversion / Construction / Assignment"); - - Buffer w{static_cast(b0)}; - BEAST_EXPECT(sane(w)); - BEAST_EXPECT(w == b0); - - Buffer x{static_cast(b1)}; - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x == b1); - - Buffer y{static_cast(b2)}; - BEAST_EXPECT(sane(y)); - BEAST_EXPECT(y == b2); - - Buffer z{static_cast(b3)}; - BEAST_EXPECT(sane(z)); - BEAST_EXPECT(z == b3); - - // Assign empty slice to empty buffer - w = static_cast(b0); - BEAST_EXPECT(sane(w)); - BEAST_EXPECT(w == b0); - - // Assign non-empty slice to empty buffer - w = static_cast(b1); - BEAST_EXPECT(sane(w)); - BEAST_EXPECT(w == b1); - - // Assign non-empty slice to non-empty buffer - x = static_cast(b2); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x == b2); - - // Assign non-empty slice to non-empty buffer - y = static_cast(z); - BEAST_EXPECT(sane(y)); - BEAST_EXPECT(y == z); - - // Assign empty slice to non-empty buffer: - z = static_cast(b0); - BEAST_EXPECT(sane(z)); - BEAST_EXPECT(z == b0); - } - - { - testcase("Allocation, Deallocation and Clearing"); - - auto test = [this](Buffer const& b, std::size_t i) { - Buffer x{b}; - - // Try to allocate some number of bytes, possibly - // zero (which means clear) and sanity check - x(i); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x.size() == i); - BEAST_EXPECT((x.data() == nullptr) == (i == 0)); - - // Try to allocate some more data (always non-zero) - x(i + 1); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x.size() == i + 1); - BEAST_EXPECT(x.data() != nullptr); - - // Try to clear: - x.clear(); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x.empty()); - BEAST_EXPECT(x.data() == nullptr); - - // Try to clear again: - x.clear(); - BEAST_EXPECT(sane(x)); - BEAST_EXPECT(x.empty()); - BEAST_EXPECT(x.data() == nullptr); - }; - - for (std::size_t i = 0; i < 16; ++i) - { - test(b0, i); - test(b1, i); - } - } - } -}; - -BEAST_DEFINE_TESTSUITE(Buffer, basics, xrpl); - -} // namespace xrpl::test diff --git a/src/test/basics/FileUtilities_test.cpp b/src/test/basics/FileUtilities_test.cpp deleted file mode 100644 index 0e050b5168..0000000000 --- a/src/test/basics/FileUtilities_test.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include - -#include -#include -#include - -#include -#include - -namespace xrpl { - -class FileUtilities_test : public beast::unit_test::Suite -{ -public: - void - testGetFileContents() - { - using namespace xrpl::detail; - using namespace boost::system; - - static constexpr char const* kExpectedContents = - "This file is very short. That's all we need."; - - FileDirGuard const file( - *this, "test_file", "test.txt", "This is temporary text that should get overwritten"); - - error_code ec; - auto const path = file.file(); - - writeFileContents(ec, path, kExpectedContents); - BEAST_EXPECT(!ec); - - { - // Test with no max - auto const good = getFileContents(ec, path); - BEAST_EXPECT(!ec); - BEAST_EXPECT(good == kExpectedContents); - } - - { - // Test with large max - auto const good = getFileContents(ec, path, kilobytes(1)); - BEAST_EXPECT(!ec); - BEAST_EXPECT(good == kExpectedContents); - } - - { - // Test with small max - auto const bad = getFileContents(ec, path, 16); - BEAST_EXPECT(ec && ec.value() == boost::system::errc::file_too_large); - BEAST_EXPECT(bad.empty()); - } - } - - void - run() override - { - testGetFileContents(); - } -}; - -BEAST_DEFINE_TESTSUITE(FileUtilities, basics, xrpl); - -} // namespace xrpl diff --git a/src/test/basics/IOUAmount_test.cpp b/src/test/basics/IOUAmount_test.cpp deleted file mode 100644 index b652d27625..0000000000 --- a/src/test/basics/IOUAmount_test.cpp +++ /dev/null @@ -1,276 +0,0 @@ -#include -#include -#include -#include - -#include -#include -#include - -namespace xrpl { - -class IOUAmount_test : public beast::unit_test::Suite -{ -public: - void - testZero() - { - testcase("zero"); - - IOUAmount const z(0, 0); - - BEAST_EXPECT(z.mantissa() == 0); - BEAST_EXPECT(z.exponent() == -100); - BEAST_EXPECT(!z); - BEAST_EXPECT(z.signum() == 0); - BEAST_EXPECT(z == beast::kZero); - - BEAST_EXPECT((z + z) == z); - BEAST_EXPECT((z - z) == z); - BEAST_EXPECT(z == -z); - - IOUAmount const zz(beast::kZero); - BEAST_EXPECT(z == zz); - - // https://github.com/XRPLF/rippled/issues/5170 - IOUAmount const zzz{}; - BEAST_EXPECT(zzz == beast::kZero); - // BEAST_EXPECT(zzz == zz); - } - - void - testSigNum() - { - testcase("signum"); - - IOUAmount const neg(-1, 0); - BEAST_EXPECT(neg.signum() < 0); - - IOUAmount const zer(0, 0); - BEAST_EXPECT(zer.signum() == 0); - - IOUAmount const pos(1, 0); - BEAST_EXPECT(pos.signum() > 0); - } - - void - testBeastZero() - { - testcase("beast::Zero Comparisons"); - - using beast::kZero; - - { - IOUAmount const z(kZero); - BEAST_EXPECT(z == kZero); - BEAST_EXPECT(z >= kZero); - BEAST_EXPECT(z <= kZero); - unexpected(z != kZero); - unexpected(z > kZero); - unexpected(z < kZero); - } - - { - IOUAmount const neg(-2, 0); - BEAST_EXPECT(neg < kZero); - BEAST_EXPECT(neg <= kZero); - BEAST_EXPECT(neg != kZero); - unexpected(neg == kZero); - } - - { - IOUAmount const pos(2, 0); - BEAST_EXPECT(pos > kZero); - BEAST_EXPECT(pos >= kZero); - BEAST_EXPECT(pos != kZero); - unexpected(pos == kZero); - } - } - - void - testComparisons() - { - testcase("IOU Comparisons"); - - IOUAmount const n(-2, 0); - IOUAmount const z(0, 0); - IOUAmount const p(2, 0); - - BEAST_EXPECT(z == z); - BEAST_EXPECT(z >= z); - BEAST_EXPECT(z <= z); - BEAST_EXPECT(z == -z); - // NOLINTBEGIN(misc-redundant-expression) - unexpected(z > z); - unexpected(z < z); - unexpected(z != z); - // NOLINTEND(misc-redundant-expression) - unexpected(z != -z); - - BEAST_EXPECT(n < z); - BEAST_EXPECT(n <= z); - BEAST_EXPECT(n != z); - unexpected(n > z); - unexpected(n >= z); - unexpected(n == z); - - BEAST_EXPECT(p > z); - BEAST_EXPECT(p >= z); - BEAST_EXPECT(p != z); - unexpected(p < z); - unexpected(p <= z); - unexpected(p == z); - - BEAST_EXPECT(n < p); - BEAST_EXPECT(n <= p); - BEAST_EXPECT(n != p); - unexpected(n > p); - unexpected(n >= p); - unexpected(n == p); - - BEAST_EXPECT(p > n); - BEAST_EXPECT(p >= n); - BEAST_EXPECT(p != n); - unexpected(p < n); - unexpected(p <= n); - unexpected(p == n); - - BEAST_EXPECT(p > -p); - BEAST_EXPECT(p >= -p); - BEAST_EXPECT(p != -p); - - BEAST_EXPECT(n < -n); - BEAST_EXPECT(n <= -n); - BEAST_EXPECT(n != -n); - } - - void - testToString() - { - testcase("IOU strings"); - - auto test = [this](IOUAmount const& n, std::string const& expected) { - auto const result = to_string(n); - std::stringstream ss; - ss << "to_string(" << result << "). Expected: " << expected; - BEAST_EXPECTS(result == expected, ss.str()); - }; - - for (auto const mantissaSize : MantissaRange::getAllScales()) - { - NumberMantissaScaleGuard const mg(mantissaSize); - - test(IOUAmount(-2, 0), "-2"); - test(IOUAmount(0, 0), "0"); - test(IOUAmount(2, 0), "2"); - test(IOUAmount(25, -3), "0.025"); - test(IOUAmount(-25, -3), "-0.025"); - test(IOUAmount(25, 1), "250"); - test(IOUAmount(-25, 1), "-250"); - test(IOUAmount(2, 20), "2e20"); - test(IOUAmount(-2, -20), "-2e-20"); - } - } - - void - testMulRatio() - { - testcase("mulRatio"); - - /* The range for the mantissa when normalized */ - static constexpr std::int64_t kMinMantissa = 1000000000000000ull; - static constexpr std::int64_t kMaxMantissa = 9999999999999999ull; - // log(2,maxMantissa) ~ 53.15 - /* The range for the exponent when normalized */ - static constexpr int kMinExponent = -96; - static constexpr int kMaxExponent = 80; - constexpr auto kMaxUInt = std::numeric_limits::max(); - - { - // multiply by a number that would overflow the mantissa, then - // divide by the same number, and check we didn't lose any value - IOUAmount const bigMan(kMaxMantissa, 0); - BEAST_EXPECT(bigMan == mulRatio(bigMan, kMaxUInt, kMaxUInt, true)); - // rounding mode shouldn't matter as the result is exact - BEAST_EXPECT(bigMan == mulRatio(bigMan, kMaxUInt, kMaxUInt, false)); - } - { - // Similar test as above, but for negative values - IOUAmount const bigMan(-kMaxMantissa, 0); - BEAST_EXPECT(bigMan == mulRatio(bigMan, kMaxUInt, kMaxUInt, true)); - // rounding mode shouldn't matter as the result is exact - BEAST_EXPECT(bigMan == mulRatio(bigMan, kMaxUInt, kMaxUInt, false)); - } - - { - // small amounts - IOUAmount const tiny(kMinMantissa, kMinExponent); - // Round up should give the smallest allowable number - BEAST_EXPECT(tiny == mulRatio(tiny, 1, kMaxUInt, true)); - BEAST_EXPECT(tiny == mulRatio(tiny, kMaxUInt - 1, kMaxUInt, true)); - // rounding down should be zero - BEAST_EXPECT(beast::kZero == mulRatio(tiny, 1, kMaxUInt, false)); - BEAST_EXPECT(beast::kZero == mulRatio(tiny, kMaxUInt - 1, kMaxUInt, false)); - - // tiny negative numbers - IOUAmount const tinyNeg(-kMinMantissa, kMinExponent); - // Round up should give zero - BEAST_EXPECT(beast::kZero == mulRatio(tinyNeg, 1, kMaxUInt, true)); - BEAST_EXPECT(beast::kZero == mulRatio(tinyNeg, kMaxUInt - 1, kMaxUInt, true)); - // rounding down should be tiny - BEAST_EXPECT(tinyNeg == mulRatio(tinyNeg, 1, kMaxUInt, false)); - BEAST_EXPECT(tinyNeg == mulRatio(tinyNeg, kMaxUInt - 1, kMaxUInt, false)); - } - - { // rounding - { - IOUAmount const one(1, 0); - auto const rup = mulRatio(one, kMaxUInt - 1, kMaxUInt, true); - auto const rdown = mulRatio(one, kMaxUInt - 1, kMaxUInt, false); - BEAST_EXPECT(rup.mantissa() - rdown.mantissa() == 1); - } - { - IOUAmount const big(kMaxMantissa, kMaxExponent); - auto const rup = mulRatio(big, kMaxUInt - 1, kMaxUInt, true); - auto const rdown = mulRatio(big, kMaxUInt - 1, kMaxUInt, false); - BEAST_EXPECT(rup.mantissa() - rdown.mantissa() == 1); - } - - { - IOUAmount const negOne(-1, 0); - auto const rup = mulRatio(negOne, kMaxUInt - 1, kMaxUInt, true); - auto const rdown = mulRatio(negOne, kMaxUInt - 1, kMaxUInt, false); - BEAST_EXPECT(rup.mantissa() - rdown.mantissa() == 1); - } - } - - { - // division by zero - IOUAmount one(1, 0); - except([&] { mulRatio(one, 1, 0, true); }); - } - - { - // overflow - IOUAmount big(kMaxMantissa, kMaxExponent); - except([&] { mulRatio(big, 2, 0, true); }); - } - } // namespace xrpl - - //-------------------------------------------------------------------------- - - void - run() override - { - testZero(); - testSigNum(); - testBeastZero(); - testComparisons(); - testToString(); - testMulRatio(); - } -}; - -BEAST_DEFINE_TESTSUITE(IOUAmount, basics, xrpl); - -} // namespace xrpl diff --git a/src/test/basics/IntrusiveShared_test.cpp b/src/test/basics/IntrusiveShared_test.cpp deleted file mode 100644 index 185b877f23..0000000000 --- a/src/test/basics/IntrusiveShared_test.cpp +++ /dev/null @@ -1,879 +0,0 @@ - -#include // IWYU pragma: keep -#include // IWYU pragma: keep -#include -#include - -#include -#include -#include -#include -#include // IWYU pragma: keep -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl::tests { - -/** -Experimentally, we discovered that using std::barrier performs extremely -poorly (~1 hour vs ~1 minute to run the test suite) in certain macOS -environments. To unblock our macOS CI pipeline, we replaced std::barrier with a -custom mutex-based barrier (Barrier) that significantly improves performance -without compromising correctness. For future reference, if we ever consider -reintroducing std::barrier, the following configuration is known to exhibit the -problem: - - Model Name: Mac mini - Model Identifier: Mac14,3 - Model Number: Z16K000R4LL/A - Chip: Apple M2 - Total Number of Cores: 8 (4 performance and 4 efficiency) - Memory: 24 GB - System Firmware Version: 11881.41.5 - OS Loader Version: 11881.1.1 - Apple clang version 16.0.0 (clang-1600.0.26.3) - Target: arm64-apple-darwin24.0.0 - Thread model: posix - - */ -struct Barrier -{ - std::mutex mtx; - std::condition_variable cv; - int count; - int const initial; - - Barrier(int n) : count(n), initial(n) - { - } - - void - arriveAndWait() - { - std::unique_lock lock(mtx); - if (--count == 0) - { - count = initial; - cv.notify_all(); - } - else - { - cv.wait(lock, [&] { return count == initial; }); - } - } -}; - -namespace { -enum class TrackedState : std::uint8_t { - Uninitialized, - Alive, - PartiallyDeletedStarted, - PartiallyDeleted, - DeletedStarted, - Deleted -}; - -class TIBase : public IntrusiveRefCounts -{ -public: - static constexpr std::size_t kMaxStates = 128; - static std::array, kMaxStates> state; - static std::atomic nextId; - static TrackedState - getState(int id) - { - assert(id < state.size()); - return state[id].load(std::memory_order_acquire); - } - static void - resetStates(bool resetCallback) - { - for (int i = 0; i < kMaxStates; ++i) - { - state[i].store(TrackedState::Uninitialized, std::memory_order_release); - } - nextId.store(0, std::memory_order_release); - if (resetCallback) - TIBase::tracingCallback = [](TrackedState, std::optional) {}; - } - - struct ResetStatesGuard - { - bool resetCallback{false}; - - ResetStatesGuard(bool resetCallback) : resetCallback{resetCallback} - { - TIBase::resetStates(resetCallback); - } - ~ResetStatesGuard() - { - TIBase::resetStates(resetCallback); - } - }; - - TIBase() : id{checkoutID()} - { - assert(state.size() > id); - state[id].store(TrackedState::Alive, std::memory_order_relaxed); - } - ~TIBase() override - { - using enum TrackedState; - - assert(state.size() > id); - tracingCallback(state[id].load(std::memory_order_relaxed), DeletedStarted); - - assert(state.size() > id); - // Use relaxed memory order to try to avoid atomic operations from - // adding additional memory synchronizations that may hide threading - // errors in the underlying shared pointer class. - state[id].store(DeletedStarted, std::memory_order_relaxed); - - tracingCallback(DeletedStarted, Deleted); - - assert(state.size() > id); - state[id].store(TrackedState::Deleted, std::memory_order_relaxed); - - tracingCallback(TrackedState::Deleted, std::nullopt); - } - - void - partialDestructor() const - { - using enum TrackedState; - - assert(state.size() > id); - tracingCallback(state[id].load(std::memory_order_relaxed), PartiallyDeletedStarted); - - assert(state.size() > id); - state[id].store(PartiallyDeletedStarted, std::memory_order_relaxed); - - tracingCallback(PartiallyDeletedStarted, PartiallyDeleted); - - assert(state.size() > id); - state[id].store(PartiallyDeleted, std::memory_order_relaxed); - - tracingCallback(PartiallyDeleted, std::nullopt); - } - - static std::function)> tracingCallback; - - int id; - -private: - static int - checkoutID() - { - return nextId.fetch_add(1, std::memory_order_acq_rel); - } -}; - -std::array, TIBase::kMaxStates> TIBase::state; -std::atomic TIBase::nextId{0}; - -std::function)> TIBase::tracingCallback = - [](TrackedState, std::optional) {}; - -} // namespace - -class IntrusiveShared_test : public beast::unit_test::Suite -{ -public: - void - testBasics() - { - testcase("Basics"); - - { - TIBase::ResetStatesGuard const rsg{true}; - - TIBase const b; - BEAST_EXPECT(b.useCount() == 1); - b.addWeakRef(); - BEAST_EXPECT(b.useCount() == 1); - auto s = b.releaseStrongRef(); - BEAST_EXPECT(s == ReleaseStrongRefAction::PartialDestroy); - BEAST_EXPECT(b.useCount() == 0); - TIBase const* pb = &b; - partialDestructorFinished(&pb); - BEAST_EXPECT(!pb); - auto w = b.releaseWeakRef(); - BEAST_EXPECT(w == ReleaseWeakRefAction::Destroy); - } - - std::vector> strong; - std::vector> weak; - { - TIBase::ResetStatesGuard const rsg{true}; - - using enum TrackedState; - auto b = makeSharedIntrusive(); - auto id = b->id; - BEAST_EXPECT(TIBase::getState(id) == Alive); - BEAST_EXPECT(b->useCount() == 1); - for (int i = 0; i < 10; ++i) - { - strong.push_back(b); - } - b.reset(); - BEAST_EXPECT(TIBase::getState(id) == Alive); - strong.resize(strong.size() - 1); - BEAST_EXPECT(TIBase::getState(id) == Alive); - strong.clear(); - BEAST_EXPECT(TIBase::getState(id) == Deleted); - - b = makeSharedIntrusive(); - id = b->id; - BEAST_EXPECT(TIBase::getState(id) == Alive); - BEAST_EXPECT(b->useCount() == 1); - for (int i = 0; i < 10; ++i) - { - weak.emplace_back(b); - BEAST_EXPECT(b->useCount() == 1); - } - BEAST_EXPECT(TIBase::getState(id) == Alive); - weak.resize(weak.size() - 1); - BEAST_EXPECT(TIBase::getState(id) == Alive); - b.reset(); - BEAST_EXPECT(TIBase::getState(id) == PartiallyDeleted); - while (!weak.empty()) - { - weak.resize(weak.size() - 1); - if (!weak.empty()) - BEAST_EXPECT(TIBase::getState(id) == PartiallyDeleted); - } - BEAST_EXPECT(TIBase::getState(id) == Deleted); - } - { - TIBase::ResetStatesGuard const rsg{true}; - - using enum TrackedState; - auto b = makeSharedIntrusive(); - auto id = b->id; - BEAST_EXPECT(TIBase::getState(id) == Alive); - WeakIntrusive w{b}; - BEAST_EXPECT(TIBase::getState(id) == Alive); - auto s = w.lock(); - BEAST_EXPECT(s && s->useCount() == 2); - b.reset(); - BEAST_EXPECT(TIBase::getState(id) == Alive); - BEAST_EXPECT(s && s->useCount() == 1); - s.reset(); - BEAST_EXPECT(TIBase::getState(id) == PartiallyDeleted); - BEAST_EXPECT(w.expired()); - s = w.lock(); - // Cannot convert a weak pointer to a strong pointer if object is - // already partially deleted - BEAST_EXPECT(!s); - w.reset(); - BEAST_EXPECT(TIBase::getState(id) == Deleted); - } - { - TIBase::ResetStatesGuard const rsg{true}; - - using enum TrackedState; - using swu = SharedWeakUnion; - swu b = makeSharedIntrusive(); - BEAST_EXPECT(b.isStrong() && b.useCount() == 1); - auto id = b.get()->id; - BEAST_EXPECT(TIBase::getState(id) == Alive); - swu w = b; - BEAST_EXPECT(TIBase::getState(id) == Alive); - BEAST_EXPECT(w.isStrong() && b.useCount() == 2); - w.convertToWeak(); - BEAST_EXPECT(w.isWeak() && b.useCount() == 1); - swu s = w; - BEAST_EXPECT(s.isWeak() && b.useCount() == 1); - s.convertToStrong(); - BEAST_EXPECT(s.isStrong() && b.useCount() == 2); - b.reset(); - BEAST_EXPECT(TIBase::getState(id) == Alive); - BEAST_EXPECT(s.useCount() == 1); - BEAST_EXPECT(!w.expired()); - s.reset(); - BEAST_EXPECT(TIBase::getState(id) == PartiallyDeleted); - BEAST_EXPECT(w.expired()); - w.convertToStrong(); - // Cannot convert a weak pointer to a strong pointer if object is - // already partially deleted - BEAST_EXPECT(w.isWeak()); - w.reset(); - BEAST_EXPECT(TIBase::getState(id) == Deleted); - } - { - // Testing SharedWeakUnion assignment operator - - TIBase::ResetStatesGuard const rsg{true}; - - auto strong1 = makeSharedIntrusive(); - auto strong2 = makeSharedIntrusive(); - - auto id1 = strong1->id; - auto id2 = strong2->id; - - BEAST_EXPECT(id1 != id2); - - SharedWeakUnion union1 = strong1; - SharedWeakUnion union2 = strong2; - - BEAST_EXPECT(union1.isStrong()); - BEAST_EXPECT(union2.isStrong()); - BEAST_EXPECT(union1.get() == strong1.get()); - BEAST_EXPECT(union2.get() == strong2.get()); - - // 1) Normal assignment: explicitly calls SharedWeakUnion assignment - union1 = union2; - BEAST_EXPECT(union1.isStrong()); - BEAST_EXPECT(union2.isStrong()); - BEAST_EXPECT(union1.get() == union2.get()); - BEAST_EXPECT(TIBase::getState(id1) == TrackedState::Alive); - BEAST_EXPECT(TIBase::getState(id2) == TrackedState::Alive); - - // 2) Test self-assignment - BEAST_EXPECT(union1.isStrong()); - BEAST_EXPECT(TIBase::getState(id1) == TrackedState::Alive); - int const initialRefCount = strong1->useCount(); -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wself-assign-overloaded" - union1 = union1; // Self-assignment -#pragma clang diagnostic pop - BEAST_EXPECT(union1.isStrong()); - BEAST_EXPECT(TIBase::getState(id1) == TrackedState::Alive); - BEAST_EXPECT(strong1->useCount() == initialRefCount); - - // 3) Test assignment from null union pointer - union1 = SharedWeakUnion(); - BEAST_EXPECT(union1.get() == nullptr); - - // 4) Test assignment to expired union pointer - strong2.reset(); - union2.reset(); - union1 = union2; - BEAST_EXPECT(union1.get() == nullptr); - BEAST_EXPECT(TIBase::getState(id2) == TrackedState::Deleted); - } - } - - void - testPartialDelete() - { - testcase("Partial Delete"); - - // This test creates two threads. One with a strong pointer and one - // with a weak pointer. The strong pointer is reset while the weak - // pointer still holds a reference, triggering a partial delete. - // While the partial delete function runs (a sleep is inserted) the - // weak pointer is reset. The destructor should wait to run until - // after the partial delete function has completed running. - - using enum TrackedState; - - TIBase::ResetStatesGuard const rsg{true}; - - auto strong = makeSharedIntrusive(); - WeakIntrusive weak{strong}; - bool destructorRan = false; - bool partialDeleteRan = false; - std::latch partialDeleteStartedSyncPoint{2}; - strong->tracingCallback = [&](TrackedState cur, std::optional next) { - using enum TrackedState; - if (next == DeletedStarted) - { - // strong goes out of scope while weak is still in scope - // This checks that partialDelete has run to completion - // before the destructor is called. A sleep is inserted - // inside the partial delete to make sure the destructor is - // given an opportunity to run during partial delete. - BEAST_EXPECT(cur == PartiallyDeleted); - } - if (next == PartiallyDeletedStarted) - { - partialDeleteStartedSyncPoint.arrive_and_wait(); - using namespace std::chrono_literals; - // Sleep and let the weak pointer go out of scope, - // potentially triggering a destructor while partial delete - // is running. The test is to make sure that doesn't happen. - std::this_thread::sleep_for(800ms); - } - if (next == PartiallyDeleted) - { - BEAST_EXPECT(!partialDeleteRan && !destructorRan); - partialDeleteRan = true; - } - if (next == Deleted) - { - BEAST_EXPECT(!destructorRan); - destructorRan = true; - } - }; - std::thread t1{[&] { - partialDeleteStartedSyncPoint.arrive_and_wait(); - weak.reset(); // Trigger a full delete as soon as the partial - // delete starts - }}; - std::thread t2{[&] { - strong.reset(); // Trigger a partial delete - }}; - t1.join(); - t2.join(); - - BEAST_EXPECT(destructorRan && partialDeleteRan); - } - - void - testDestructor() - { - testcase("Destructor"); - - // This test creates two threads. One with a strong pointer and one - // with a weak pointer. The weak pointer is reset while the strong - // pointer still holds a reference. Then the strong pointer is - // reset. Only the destructor should run. The partial destructor - // should not be called. Since the weak reset runs to completion - // before the strong pointer is reset, threading doesn't add much to - // this test, but there is no harm in keeping it. - - using enum TrackedState; - - TIBase::ResetStatesGuard const rsg{true}; - - auto strong = makeSharedIntrusive(); - WeakIntrusive weak{strong}; - bool destructorRan = false; - bool partialDeleteRan = false; - std::latch weakResetSyncPoint{2}; - strong->tracingCallback = [&](TrackedState cur, std::optional next) { - using enum TrackedState; - if (next == PartiallyDeleted) - { - BEAST_EXPECT(!partialDeleteRan && !destructorRan); - partialDeleteRan = true; - } - if (next == Deleted) - { - BEAST_EXPECT(!destructorRan); - destructorRan = true; - } - }; - std::thread t1{[&] { - weak.reset(); - weakResetSyncPoint.arrive_and_wait(); - }}; - std::thread t2{[&] { - weakResetSyncPoint.arrive_and_wait(); - strong.reset(); // Trigger a partial delete - }}; - t1.join(); - t2.join(); - - BEAST_EXPECT(destructorRan && !partialDeleteRan); - } - - void - testMultithreadedClearMixedVariant() - { - testcase("Multithreaded Clear Mixed Variant"); - - // This test creates and destroys many strong and weak pointers in a - // loop. There is a random mix of strong and weak pointers stored in - // a vector (held as a variant). Both threads clear all the pointers - // and check that the invariants hold. - - using enum TrackedState; - TIBase::ResetStatesGuard const rsg{true}; - - std::atomic destructionState{0}; - // returns destructorRan and partialDestructorRan (in that order) - auto getDestructorState = [&]() -> std::pair { - int const s = destructionState.load(std::memory_order_relaxed); - return {(s & 1) != 0, (s & 2) != 0}; - }; - auto setDestructorRan = [&]() -> void { - destructionState.fetch_or(1, std::memory_order_acq_rel); - }; - auto setPartialDeleteRan = [&]() -> void { - destructionState.fetch_or(2, std::memory_order_acq_rel); - }; - auto tracingCallback = [&](TrackedState cur, std::optional next) { - using enum TrackedState; - auto [destructorRan, partialDeleteRan] = getDestructorState(); - if (next == PartiallyDeleted) - { - BEAST_EXPECT(!partialDeleteRan && !destructorRan); - setPartialDeleteRan(); - } - if (next == Deleted) - { - BEAST_EXPECT(!destructorRan); - setDestructorRan(); - } - }; - auto createVecOfPointers = [&](auto const& toClone, std::default_random_engine& eng) - -> std::vector, WeakIntrusive>> { - std::vector, WeakIntrusive>> result; - std::uniform_int_distribution<> toCreateDist(4, 64); - std::uniform_int_distribution<> isStrongDist(0, 1); - auto numToCreate = toCreateDist(eng); - result.reserve(numToCreate); - for (int i = 0; i < numToCreate; ++i) - { - if (isStrongDist(eng)) - { - result.emplace_back(SharedIntrusive(toClone)); - } - else - { - result.emplace_back(WeakIntrusive(toClone)); - } - } - return result; - }; - static constexpr int kLoopIters = 2 * 1024; - static constexpr int kNumThreads = 16; - std::vector> toClone; - Barrier loopStartSyncPoint{kNumThreads}; - Barrier postCreateToCloneSyncPoint{kNumThreads}; - Barrier postCreateVecOfPointersSyncPoint{kNumThreads}; - auto engines = [&]() -> std::vector { - std::random_device rd; - std::vector result; - result.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) - result.emplace_back(rd()); - return result; - }(); - - // cloneAndDestroy clones the strong pointer into a vector of mixed - // strong and weak pointers and destroys them all at once. - // threadId==0 is special. - auto cloneAndDestroy = [&](int threadId) { - for (int i = 0; i < kLoopIters; ++i) - { - // ------ Sync Point ------ - loopStartSyncPoint.arriveAndWait(); - - // only thread 0 should reset the state - std::optional rsg; - if (threadId == 0) - { - // Thread 0 is the genesis thread. It creates the strong - // pointers to be cloned by the other threads. This - // thread will also check that the destructor ran and - // clear the temporary variables. - - rsg.emplace(false); - auto [destructorRan, partialDeleteRan] = getDestructorState(); - BEAST_EXPECT(!i || destructorRan); - destructionState.store(0, std::memory_order_release); - - toClone.clear(); - toClone.resize(kNumThreads); - auto strong = makeSharedIntrusive(); - strong->tracingCallback = tracingCallback; - std::ranges::fill(toClone, strong); - } - - // ------ Sync Point ------ - postCreateToCloneSyncPoint.arriveAndWait(); - - auto v = createVecOfPointers(toClone[threadId], engines[threadId]); - toClone[threadId].reset(); - - // ------ Sync Point ------ - postCreateVecOfPointersSyncPoint.arriveAndWait(); - - v.clear(); - } - }; - std::vector threads; - threads.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) - { - threads.emplace_back(cloneAndDestroy, i); - } - for (int i = 0; i < kNumThreads; ++i) - { - threads[i].join(); - } - } - - void - testMultithreadedClearMixedUnion() - { - testcase("Multithreaded Clear Mixed Union"); - - // This test creates and destroys many SharedWeak pointers in a - // loop. All the pointers start as strong and a loop randomly - // convert them between strong and weak pointers. Both threads clear - // all the pointers and check that the invariants hold. - // - // Note: This test also differs from the test above in that the pointers - // randomly change from strong to weak and from weak to strong in a - // loop. This can't be done in the variant test above because variant is - // not thread safe while the SharedWeakUnion is thread safe. - - using enum TrackedState; - - TIBase::ResetStatesGuard const rsg{true}; - - std::atomic destructionState{0}; - // returns destructorRan and partialDestructorRan (in that order) - auto getDestructorState = [&]() -> std::pair { - int const s = destructionState.load(std::memory_order_relaxed); - return {(s & 1) != 0, (s & 2) != 0}; - }; - auto setDestructorRan = [&]() -> void { - destructionState.fetch_or(1, std::memory_order_acq_rel); - }; - auto setPartialDeleteRan = [&]() -> void { - destructionState.fetch_or(2, std::memory_order_acq_rel); - }; - auto tracingCallback = [&](TrackedState cur, std::optional next) { - using enum TrackedState; - auto [destructorRan, partialDeleteRan] = getDestructorState(); - if (next == PartiallyDeleted) - { - BEAST_EXPECT(!partialDeleteRan && !destructorRan); - setPartialDeleteRan(); - } - if (next == Deleted) - { - BEAST_EXPECT(!destructorRan); - setDestructorRan(); - } - }; - auto createVecOfPointers = - [&](auto const& toClone, - std::default_random_engine& eng) -> std::vector> { - std::vector> result; - std::uniform_int_distribution<> toCreateDist(4, 64); - auto numToCreate = toCreateDist(eng); - result.reserve(numToCreate); - for (int i = 0; i < numToCreate; ++i) - result.emplace_back(SharedIntrusive(toClone)); - return result; - }; - static constexpr int kLoopIters = 2 * 1024; - static constexpr int kFlipPointersLoopIters = 256; - static constexpr int kNumThreads = 16; - std::vector> toClone; - Barrier loopStartSyncPoint{kNumThreads}; - Barrier postCreateToCloneSyncPoint{kNumThreads}; - Barrier postCreateVecOfPointersSyncPoint{kNumThreads}; - Barrier postFlipPointersLoopSyncPoint{kNumThreads}; - auto engines = [&]() -> std::vector { - std::random_device rd; - std::vector result; - result.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) - result.emplace_back(rd()); - return result; - }(); - - // cloneAndDestroy clones the strong pointer into a vector of - // mixed strong and weak pointers, runs a loop that randomly - // changes strong pointers to weak pointers, and destroys them - // all at once. - auto cloneAndDestroy = [&](int threadId) { - for (int i = 0; i < kLoopIters; ++i) - { - // ------ Sync Point ------ - loopStartSyncPoint.arriveAndWait(); - - // only thread 0 should reset the state - std::optional rsg; - if (threadId == 0) - { - // threadId 0 is the genesis thread. It creates the - // strong point to be cloned by the other threads. This - // thread will also check that the destructor ran and - // clear the temporary variables. - rsg.emplace(false); - auto [destructorRan, partialDeleteRan] = getDestructorState(); - BEAST_EXPECT(!i || destructorRan); - destructionState.store(0, std::memory_order_release); - - toClone.clear(); - toClone.resize(kNumThreads); - auto strong = makeSharedIntrusive(); - strong->tracingCallback = tracingCallback; - std::ranges::fill(toClone, strong); - } - - // ------ Sync Point ------ - postCreateToCloneSyncPoint.arriveAndWait(); - - auto v = createVecOfPointers(toClone[threadId], engines[threadId]); - toClone[threadId].reset(); - - // ------ Sync Point ------ - postCreateVecOfPointersSyncPoint.arriveAndWait(); - - std::uniform_int_distribution<> isStrongDist(0, 1); - for (int f = 0; f < kFlipPointersLoopIters; ++f) - { - for (auto& p : v) - { - if (isStrongDist(engines[threadId])) - { - p.convertToStrong(); - } - else - { - p.convertToWeak(); - } - } - } - - // ------ Sync Point ------ - postFlipPointersLoopSyncPoint.arriveAndWait(); - - v.clear(); - } - }; - std::vector threads; - threads.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) - { - threads.emplace_back(cloneAndDestroy, i); - } - for (int i = 0; i < kNumThreads; ++i) - { - threads[i].join(); - } - } - - void - testMultithreadedLockingWeak() - { - testcase("Multithreaded Locking Weak"); - - // This test creates a single shared atomic pointer that multiple thread - // create weak pointers from. The threads then lock the weak pointers. - // Both threads clear all the pointers and check that the invariants - // hold. - - using enum TrackedState; - - TIBase::ResetStatesGuard const rsg{true}; - - std::atomic destructionState{0}; - // returns destructorRan and partialDestructorRan (in that order) - auto getDestructorState = [&]() -> std::pair { - int const s = destructionState.load(std::memory_order_relaxed); - return {(s & 1) != 0, (s & 2) != 0}; - }; - auto setDestructorRan = [&]() -> void { - destructionState.fetch_or(1, std::memory_order_acq_rel); - }; - auto setPartialDeleteRan = [&]() -> void { - destructionState.fetch_or(2, std::memory_order_acq_rel); - }; - auto tracingCallback = [&](TrackedState cur, std::optional next) { - using enum TrackedState; - auto [destructorRan, partialDeleteRan] = getDestructorState(); - if (next == PartiallyDeleted) - { - BEAST_EXPECT(!partialDeleteRan && !destructorRan); - setPartialDeleteRan(); - } - if (next == Deleted) - { - BEAST_EXPECT(!destructorRan); - setDestructorRan(); - } - }; - - static constexpr int kLoopIters = 2 * 1024; - static constexpr int kLockWeakLoopIters = 256; - static constexpr int kNumThreads = 16; - std::vector> toLock; - Barrier loopStartSyncPoint{kNumThreads}; - Barrier postCreateToLockSyncPoint{kNumThreads}; - Barrier postLockWeakLoopSyncPoint{kNumThreads}; - - // lockAndDestroy creates weak pointers from the strong pointer - // and runs a loop that locks the weak pointer. At the end of the loop - // all the pointers are destroyed all at once. - auto lockAndDestroy = [&](int threadId) { - for (int i = 0; i < kLoopIters; ++i) - { - // ------ Sync Point ------ - loopStartSyncPoint.arriveAndWait(); - - // only thread 0 should reset the state - std::optional rsg; - if (threadId == 0) - { - // threadId 0 is the genesis thread. It creates the - // strong point to be locked by the other threads. This - // thread will also check that the destructor ran and - // clear the temporary variables. - rsg.emplace(false); - auto [destructorRan, partialDeleteRan] = getDestructorState(); - BEAST_EXPECT(!i || destructorRan); - destructionState.store(0, std::memory_order_release); - - toLock.clear(); - toLock.resize(kNumThreads); - auto strong = makeSharedIntrusive(); - strong->tracingCallback = tracingCallback; - std::ranges::fill(toLock, strong); - } - - // ------ Sync Point ------ - postCreateToLockSyncPoint.arriveAndWait(); - - // Multiple threads all create a weak pointer from the same - // strong pointer - WeakIntrusive const weak{toLock[threadId]}; - for (int wi = 0; wi < kLockWeakLoopIters; ++wi) - { - BEAST_EXPECT(!weak.expired()); - auto strong = weak.lock(); - BEAST_EXPECT(strong); - } - - // ------ Sync Point ------ - postLockWeakLoopSyncPoint.arriveAndWait(); - - toLock[threadId].reset(); - } - }; - std::vector threads; - threads.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) - { - threads.emplace_back(lockAndDestroy, i); - } - for (int i = 0; i < kNumThreads; ++i) - { - threads[i].join(); - } - } - - void - run() override - { - testBasics(); - testPartialDelete(); - testDestructor(); - testMultithreadedClearMixedVariant(); - testMultithreadedClearMixedUnion(); - testMultithreadedLockingWeak(); - } -}; // namespace tests - -BEAST_DEFINE_TESTSUITE(IntrusiveShared, basics, xrpl); -} // namespace xrpl::tests diff --git a/src/test/basics/KeyCache_test.cpp b/src/test/basics/KeyCache_test.cpp deleted file mode 100644 index e83ae4c277..0000000000 --- a/src/test/basics/KeyCache_test.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#include - -#include -#include // IWYU pragma: keep -#include -#include -#include - -#include - -namespace xrpl { - -class KeyCache_test : public beast::unit_test::Suite -{ -public: - void - run() override - { - using namespace std::chrono_literals; - TestStopwatch clock; - clock.set(0); - - using Key = std::string; - using Cache = TaggedCache; - - test::SuiteJournal j("KeyCacheTest", *this); - - // Insert an item, retrieve it, and age it so it gets purged. - { - Cache c("test", LedgerIndex(1), 2s, clock, j); - - BEAST_EXPECT(c.size() == 0); - BEAST_EXPECT(c.insert("one")); - BEAST_EXPECT(!c.insert("one")); - BEAST_EXPECT(c.size() == 1); - BEAST_EXPECT(c.touchIfExists("one")); - ++clock; - c.sweep(); - BEAST_EXPECT(c.size() == 1); - ++clock; - c.sweep(); - BEAST_EXPECT(c.size() == 0); - BEAST_EXPECT(!c.touchIfExists("one")); - } - - // Insert two items, have one expire - { - Cache c("test", LedgerIndex(2), 2s, clock, j); - - BEAST_EXPECT(c.insert("one")); - BEAST_EXPECT(c.size() == 1); - BEAST_EXPECT(c.insert("two")); - BEAST_EXPECT(c.size() == 2); - ++clock; - c.sweep(); - BEAST_EXPECT(c.size() == 2); - BEAST_EXPECT(c.touchIfExists("two")); - ++clock; - c.sweep(); - BEAST_EXPECT(c.size() == 1); - } - - // Insert three items (1 over limit), sweep - { - Cache c("test", LedgerIndex(2), 3s, clock, j); - - BEAST_EXPECT(c.insert("one")); - ++clock; - BEAST_EXPECT(c.insert("two")); - ++clock; - BEAST_EXPECT(c.insert("three")); - ++clock; - BEAST_EXPECT(c.size() == 3); - c.sweep(); - BEAST_EXPECT(c.size() < 3); - } - } -}; - -BEAST_DEFINE_TESTSUITE(KeyCache, basics, xrpl); - -} // namespace xrpl diff --git a/src/test/basics/StringUtilities_test.cpp b/src/test/basics/StringUtilities_test.cpp deleted file mode 100644 index e12bc53857..0000000000 --- a/src/test/basics/StringUtilities_test.cpp +++ /dev/null @@ -1,309 +0,0 @@ -#include -#include -#include -#include - -#include - -namespace xrpl { - -class StringUtilities_test : public beast::unit_test::Suite -{ -public: - void - testUnHexSuccess(std::string const& strIn, std::string const& strExpected) - { - auto rv = strUnHex(strIn); - BEAST_EXPECT(rv); - - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - BEAST_EXPECT(makeSlice(*rv) == makeSlice(strExpected)); - } - - void - testUnHexFailure(std::string const& strIn) - { - auto rv = strUnHex(strIn); - BEAST_EXPECT(!rv); - } - - void - testUnHex() - { - testcase("strUnHex"); - - testUnHexSuccess("526970706c6544", "RippleD"); - testUnHexSuccess("A", "\n"); - testUnHexSuccess("0A", "\n"); - testUnHexSuccess("D0A", "\r\n"); - testUnHexSuccess("0D0A", "\r\n"); - testUnHexSuccess("200D0A", " \r\n"); - testUnHexSuccess("282A2B2C2D2E2F29", "(*+,-./)"); - - // Check for things which contain some or only invalid characters - testUnHexFailure("123X"); - testUnHexFailure("V"); - testUnHexFailure("XRP"); - } - - void - testParseUrl() - { - testcase("parseUrl"); - - // Expected passes. - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain.empty()); - BEAST_EXPECT(!pUrl.port); - // RFC 3986: - // > In general, a URI that uses the generic syntax for authority - // with an empty path should be normalized to a path of "/". - // Do we want to normalize paths? - BEAST_EXPECT(pUrl.path.empty()); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme:///")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain.empty()); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "lower://domain")); - BEAST_EXPECT(pUrl.scheme == "lower"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path.empty()); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "UPPER://domain:234/")); - BEAST_EXPECT(pUrl.scheme == "upper"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(*pUrl.port == 234); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT(pUrl.path == "/"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "Mixed://domain/path")); - BEAST_EXPECT(pUrl.scheme == "mixed"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/path"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://[::1]:123/path")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "::1"); - BEAST_EXPECT(*pUrl.port == 123); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT(pUrl.path == "/path"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://user:pass@domain:123/abc:321")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username == "user"); - BEAST_EXPECT(pUrl.password == "pass"); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(*pUrl.port == 123); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT(pUrl.path == "/abc:321"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://user@domain:123/abc:321")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username == "user"); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(*pUrl.port == 123); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT(pUrl.path == "/abc:321"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://:pass@domain:123/abc:321")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password == "pass"); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(*pUrl.port == 123); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT(pUrl.path == "/abc:321"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://domain:123/abc:321")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(*pUrl.port == 123); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT(pUrl.path == "/abc:321"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://user:pass@domain/abc:321")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username == "user"); - BEAST_EXPECT(pUrl.password == "pass"); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/abc:321"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://user@domain/abc:321")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username == "user"); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/abc:321"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://:pass@domain/abc:321")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password == "pass"); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/abc:321"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://domain/abc:321")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/abc:321"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme:///path/to/file")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain.empty()); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/path/to/file"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://user:pass@domain/path/with/an@sign")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username == "user"); - BEAST_EXPECT(pUrl.password == "pass"); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/path/with/an@sign"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://domain/path/with/an@sign")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "domain"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/path/with/an@sign"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "scheme://:999/")); - BEAST_EXPECT(pUrl.scheme == "scheme"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == ":999"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/"); - } - - { - ParsedUrl pUrl; - BEAST_EXPECT(parseUrl(pUrl, "http://::1:1234/validators")); - BEAST_EXPECT(pUrl.scheme == "http"); - BEAST_EXPECT(pUrl.username.empty()); - BEAST_EXPECT(pUrl.password.empty()); - BEAST_EXPECT(pUrl.domain == "::0.1.18.52"); - BEAST_EXPECT(!pUrl.port); - BEAST_EXPECT(pUrl.path == "/validators"); - } - - // Expected fails. - { - ParsedUrl pUrl; - BEAST_EXPECT(!parseUrl(pUrl, "")); - BEAST_EXPECT(!parseUrl(pUrl, "nonsense")); - BEAST_EXPECT(!parseUrl(pUrl, "://")); - BEAST_EXPECT(!parseUrl(pUrl, ":///")); - BEAST_EXPECT(!parseUrl(pUrl, "scheme://user:pass@domain:65536/abc:321")); - BEAST_EXPECT(!parseUrl(pUrl, "UPPER://domain:23498765/")); - BEAST_EXPECT(!parseUrl(pUrl, "UPPER://domain:0/")); - BEAST_EXPECT(!parseUrl(pUrl, "UPPER://domain:+7/")); - BEAST_EXPECT(!parseUrl(pUrl, "UPPER://domain:-7234/")); - BEAST_EXPECT(!parseUrl(pUrl, "UPPER://domain:@#$56!/")); - } - - { - std::string const strUrl("s://" + std::string(8192, ':')); - ParsedUrl pUrl; - BEAST_EXPECT(!parseUrl(pUrl, strUrl)); - } - } - - void - testToString() - { - testcase("toString"); - auto result = to_string("hello"); - BEAST_EXPECT(result == "hello"); - } - - void - run() override - { - testParseUrl(); - testUnHex(); - testToString(); - } -}; - -BEAST_DEFINE_TESTSUITE(StringUtilities, basics, xrpl); - -} // namespace xrpl diff --git a/src/test/basics/TaggedCache_test.cpp b/src/test/basics/TaggedCache_test.cpp deleted file mode 100644 index 26564a4de8..0000000000 --- a/src/test/basics/TaggedCache_test.cpp +++ /dev/null @@ -1,251 +0,0 @@ -#include - -#include -#include -#include -#include // IWYU pragma: keep -#include -#include -#include -#include - -#include -#include - -namespace xrpl { - -/* -I guess you can put some items in, make sure they're still there. Let some -time pass, make sure they're gone. Keep a strong pointer to one of them, make -sure you can still find it even after time passes. Create two objects with -the same key, canonicalize them both and make sure you get the same object. -Put an object in but keep a strong pointer to it, advance the clock a lot, -then canonicalize a new object with the same key, make sure you get the -original object. -*/ - -class TaggedCache_test : public beast::unit_test::Suite -{ -public: - void - run() override - { - using namespace std::chrono_literals; - using beast::Severity; - test::SuiteJournal journal("TaggedCache_test", *this); - - TestStopwatch clock; - clock.set(0); - - using Key = LedgerIndex; - using Value = std::string; - using Cache = TaggedCache; - - Cache c("test", 1, 1s, clock, journal); - - // Insert an item, retrieve it, and age it so it gets purged. - { - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.getTrackSize() == 0); - BEAST_EXPECT(!c.insert(1, "one")); - BEAST_EXPECT(c.getCacheSize() == 1); - BEAST_EXPECT(c.getTrackSize() == 1); - - { - std::string s; - BEAST_EXPECT(c.retrieve(1, s)); - BEAST_EXPECT(s == "one"); - } - - ++clock; - c.sweep(); - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.getTrackSize() == 0); - } - - // Insert an item, maintain a strong pointer, age it, and - // verify that the entry still exists. - { - BEAST_EXPECT(!c.insert(2, "two")); - BEAST_EXPECT(c.getCacheSize() == 1); - BEAST_EXPECT(c.getTrackSize() == 1); - - { - auto p = c.fetch(2); - BEAST_EXPECT(p != nullptr); - ++clock; - c.sweep(); - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.getTrackSize() == 1); - } - - // Make sure its gone now that our reference is gone - ++clock; - c.sweep(); - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.getTrackSize() == 0); - } - - // Insert the same key/value pair and make sure we get the same result - { - BEAST_EXPECT(!c.insert(3, "three")); - - { - auto const p1 = c.fetch(3); - auto p2 = std::make_shared("three"); - c.canonicalizeReplaceClient(3, p2); - BEAST_EXPECT(p1.get() == p2.get()); - } - ++clock; - c.sweep(); - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.getTrackSize() == 0); - } - - // Put an object in but keep a strong pointer to it, advance the clock a - // lot, then canonicalize a new object with the same key, make sure you - // get the original object. - { - // Put an object in - BEAST_EXPECT(!c.insert(4, "four")); - BEAST_EXPECT(c.getCacheSize() == 1); - BEAST_EXPECT(c.getTrackSize() == 1); - - { - // Keep a strong pointer to it - auto const p1 = c.fetch(4); - BEAST_EXPECT(p1 != nullptr); - BEAST_EXPECT(c.getCacheSize() == 1); - BEAST_EXPECT(c.getTrackSize() == 1); - // Advance the clock a lot - ++clock; - c.sweep(); - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.getTrackSize() == 1); - // Canonicalize a new object with the same key - auto p2 = std::make_shared("four"); - BEAST_EXPECT(c.canonicalizeReplaceClient(4, p2)); - BEAST_EXPECT(c.getCacheSize() == 1); - BEAST_EXPECT(c.getTrackSize() == 1); - // Make sure we get the original object - BEAST_EXPECT(p1.get() == p2.get()); - } - - ++clock; - c.sweep(); - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.getTrackSize() == 0); - } - { - BEAST_EXPECT(!c.insert(5, "five")); - BEAST_EXPECT(c.getCacheSize() == 1); - BEAST_EXPECT(c.size() == 1); - - { - auto const p1 = c.fetch(5); - BEAST_EXPECT(p1 != nullptr); - BEAST_EXPECT(c.getCacheSize() == 1); - BEAST_EXPECT(c.size() == 1); - - // Advance the clock a lot - ++clock; - c.sweep(); - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.size() == 1); - - auto p2 = std::make_shared("five_2"); - BEAST_EXPECT(c.canonicalizeReplaceCache(5, p2)); - BEAST_EXPECT(c.getCacheSize() == 1); - BEAST_EXPECT(c.size() == 1); - // Make sure the caller's original pointer is unchanged - BEAST_EXPECT(p1.get() != p2.get()); - BEAST_EXPECT(*p2 == "five_2"); - - auto const p3 = c.fetch(5); - BEAST_EXPECT(p3 != nullptr); - BEAST_EXPECT(p3.get() == p2.get()); - BEAST_EXPECT(p3.get() != p1.get()); - } - - ++clock; - c.sweep(); - BEAST_EXPECT(c.getCacheSize() == 0); - BEAST_EXPECT(c.size() == 0); - } - - { - testcase("intrptr"); - - struct MyRefCountObject : IntrusiveRefCounts - { - std::string data; - - // Needed to support weak intrusive pointers - virtual void - partialDestructor() {}; - - MyRefCountObject() = default; - explicit MyRefCountObject(std::string data) : data(std::move(data)) - { - } - - bool - operator==(std::string const& other) const - { - return data == other; - } - }; - - using IntrPtrCache = TaggedCache< - Key, - MyRefCountObject, - /*IsKeyCache*/ false, - intr_ptr::SharedWeakUnionPtr, - intr_ptr::SharedPtr>; - - IntrPtrCache intrPtrCache("IntrPtrTest", 1, 1s, clock, journal); - - intrPtrCache.canonicalizeReplaceCache(1, intr_ptr::makeShared("one")); - BEAST_EXPECT(intrPtrCache.getCacheSize() == 1); - BEAST_EXPECT(intrPtrCache.size() == 1); - - { - { - intrPtrCache.canonicalizeReplaceCache( - 1, intr_ptr::makeShared("one_replaced")); - - auto p = intrPtrCache.fetch(1); - BEAST_EXPECT(*p == "one_replaced"); - - // Advance the clock a lot - ++clock; - intrPtrCache.sweep(); - BEAST_EXPECT(intrPtrCache.getCacheSize() == 0); - BEAST_EXPECT(intrPtrCache.size() == 1); - - intrPtrCache.canonicalizeReplaceCache( - 1, intr_ptr::makeShared("one_replaced_2")); - - auto p2 = intrPtrCache.fetch(1); - BEAST_EXPECT(*p2 == "one_replaced_2"); - - intrPtrCache.del(1, true); - } - - intrPtrCache.canonicalizeReplaceCache( - 1, intr_ptr::makeShared("one_replaced_3")); - auto p3 = intrPtrCache.fetch(1); - BEAST_EXPECT(*p3 == "one_replaced_3"); - } - - ++clock; - intrPtrCache.sweep(); - BEAST_EXPECT(intrPtrCache.getCacheSize() == 0); - BEAST_EXPECT(intrPtrCache.size() == 0); - } - } -}; - -BEAST_DEFINE_TESTSUITE(TaggedCache, basics, xrpl); - -} // namespace xrpl diff --git a/src/test/basics/Units_test.cpp b/src/test/basics/Units_test.cpp deleted file mode 100644 index dc780166c6..0000000000 --- a/src/test/basics/Units_test.cpp +++ /dev/null @@ -1,344 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace xrpl::test { - -class units_test : public beast::unit_test::Suite -{ -private: - void - testTypes() - { - using FeeLevel32 = FeeLevel; - - { - XRPAmount const x{100}; - BEAST_EXPECT(x.drops() == 100); - BEAST_EXPECT((std::is_same_v)); - auto y = 4u * x; - BEAST_EXPECT(y.value() == 400); - BEAST_EXPECT((std::is_same_v)); - - auto z = 4 * y; - BEAST_EXPECT(z.value() == 1600); - BEAST_EXPECT((std::is_same_v)); - - FeeLevel32 const f{10}; - FeeLevel32 const baseFee{100}; - - auto drops = mulDiv(baseFee, x, f); - - BEAST_EXPECT(drops); - BEAST_EXPECT(drops.value() == 1000); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT((std::is_same_v< - std::remove_reference_t::unit_type, - unit::dropTag>)); - - BEAST_EXPECT((std::is_same_v, XRPAmount>)); - } - { - XRPAmount const x{100}; - BEAST_EXPECT(x.value() == 100); - BEAST_EXPECT((std::is_same_v)); - auto y = 4u * x; - BEAST_EXPECT(y.value() == 400); - BEAST_EXPECT((std::is_same_v)); - - FeeLevel64 const f{10}; - FeeLevel64 const baseFee{100}; - - auto drops = mulDiv(baseFee, x, f); - - BEAST_EXPECT(drops); - BEAST_EXPECT(drops.value() == 1000); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT((std::is_same_v< - std::remove_reference_t::unit_type, - unit::dropTag>)); - BEAST_EXPECT((std::is_same_v, XRPAmount>)); - } - { - FeeLevel64 const x{1024}; - BEAST_EXPECT(x.value() == 1024); - BEAST_EXPECT((std::is_same_v)); - std::uint64_t const m = 4; - auto y = m * x; - BEAST_EXPECT(y.value() == 4096); - BEAST_EXPECT((std::is_same_v)); - - XRPAmount const basefee{10}; - FeeLevel64 const referencefee{256}; - - auto drops = mulDiv(x, basefee, referencefee); - - BEAST_EXPECT(drops); - BEAST_EXPECT(drops.value() == 40); // NOLINT(bugprone-unchecked-optional-access) - BEAST_EXPECT((std::is_same_v< - std::remove_reference_t::unit_type, - unit::dropTag>)); - BEAST_EXPECT((std::is_same_v, XRPAmount>)); - } - } - - void - testJson() - { - // Json value functionality - using FeeLevel32 = FeeLevel; - - { - FeeLevel32 const x{std::numeric_limits::max()}; - auto y = x.jsonClipped(); - BEAST_EXPECT(y.type() == json::ValueType::UInt); - BEAST_EXPECT(y == json::Value{x.fee()}); - } - - { - FeeLevel32 const x{std::numeric_limits::min()}; - auto y = x.jsonClipped(); - BEAST_EXPECT(y.type() == json::ValueType::UInt); - BEAST_EXPECT(y == json::Value{x.fee()}); - } - - { - FeeLevel64 const x{std::numeric_limits::max()}; - auto y = x.jsonClipped(); - BEAST_EXPECT(y.type() == json::ValueType::UInt); - BEAST_EXPECT(y == json::Value{std::numeric_limits::max()}); - } - - { - FeeLevel64 const x{std::numeric_limits::min()}; - auto y = x.jsonClipped(); - BEAST_EXPECT(y.type() == json::ValueType::UInt); - BEAST_EXPECT(y == json::Value{0}); - } - - { - FeeLevelDouble const x{std::numeric_limits::max()}; - auto y = x.jsonClipped(); - BEAST_EXPECT(y.type() == json::ValueType::Real); - BEAST_EXPECT(y == json::Value{std::numeric_limits::max()}); - } - - { - FeeLevelDouble const x{std::numeric_limits::min()}; - auto y = x.jsonClipped(); - BEAST_EXPECT(y.type() == json::ValueType::Real); - BEAST_EXPECT(y == json::Value{std::numeric_limits::min()}); - } - - { - XRPAmount const x{std::numeric_limits::max()}; - auto y = x.jsonClipped(); - BEAST_EXPECT(y.type() == json::ValueType::Int); - BEAST_EXPECT(y == json::Value{std::numeric_limits::max()}); - } - - { - XRPAmount const x{std::numeric_limits::min()}; - auto y = x.jsonClipped(); - BEAST_EXPECT(y.type() == json::ValueType::Int); - BEAST_EXPECT(y == json::Value{std::numeric_limits::min()}); - } - } - - void - testFunctions() - { - // Explicitly test every defined function for the ValueUnit class - // since some of them are templated, but not used anywhere else. - using FeeLevel32 = FeeLevel; - - { - auto make = [&](auto x) -> FeeLevel64 { return x; }; - auto explicitmake = [&](auto x) -> FeeLevel64 { return FeeLevel64{x}; }; - - [[maybe_unused]] - FeeLevel64 const defaulted{}; - FeeLevel64 test{0}; - BEAST_EXPECT(test.fee() == 0); - - test = explicitmake(beast::kZero); - BEAST_EXPECT(test.fee() == 0); - - test = beast::kZero; - BEAST_EXPECT(test.fee() == 0); - - test = explicitmake(100u); - BEAST_EXPECT(test.fee() == 100); - - FeeLevel64 const targetSame{200u}; - FeeLevel32 const targetOther{300u}; - test = make(targetSame); - BEAST_EXPECT(test.fee() == 200); - BEAST_EXPECT(test == targetSame); - BEAST_EXPECT(test < FeeLevel64{1000}); - BEAST_EXPECT(test > FeeLevel64{100}); - test = make(targetOther); - BEAST_EXPECT(test.fee() == 300); - BEAST_EXPECT(test == targetOther); - - test = std::uint64_t(200); - BEAST_EXPECT(test.fee() == 200); - test = std::uint32_t(300); - BEAST_EXPECT(test.fee() == 300); - - test = targetSame; - BEAST_EXPECT(test.fee() == 200); - test = targetOther.fee(); - BEAST_EXPECT(test.fee() == 300); - BEAST_EXPECT(test == targetOther); - - test = targetSame * 2; - BEAST_EXPECT(test.fee() == 400); - test = 3 * targetSame; - BEAST_EXPECT(test.fee() == 600); - test = targetSame / 10; - BEAST_EXPECT(test.fee() == 20); - - test += targetSame; - BEAST_EXPECT(test.fee() == 220); - - test -= targetSame; - BEAST_EXPECT(test.fee() == 20); - - test++; - BEAST_EXPECT(test.fee() == 21); - ++test; - BEAST_EXPECT(test.fee() == 22); - test--; - BEAST_EXPECT(test.fee() == 21); - --test; - BEAST_EXPECT(test.fee() == 20); - - test *= 5; - BEAST_EXPECT(test.fee() == 100); - test /= 2; - BEAST_EXPECT(test.fee() == 50); - test %= 13; - BEAST_EXPECT(test.fee() == 11); - - /* - // illegal with unsigned - test = -test; - BEAST_EXPECT(test.fee() == -11); - BEAST_EXPECT(test.signum() == -1); - BEAST_EXPECT(to_string(test) == "-11"); - */ - - BEAST_EXPECT(test); - test = 0; - BEAST_EXPECT(!test); - BEAST_EXPECT(test.signum() == 0); - test = targetSame; - BEAST_EXPECT(test.signum() == 1); - BEAST_EXPECT(to_string(test) == "200"); - } - { - auto make = [&](auto x) -> FeeLevelDouble { return x; }; - auto explicitmake = [&](auto x) -> FeeLevelDouble { return FeeLevelDouble{x}; }; - - [[maybe_unused]] - FeeLevelDouble const defaulted{}; - FeeLevelDouble test{0}; - BEAST_EXPECT(test.fee() == 0); - - test = explicitmake(beast::kZero); - BEAST_EXPECT(test.fee() == 0); - - test = beast::kZero; - BEAST_EXPECT(test.fee() == 0); - - test = explicitmake(100.0); - BEAST_EXPECT(test.fee() == 100); - - FeeLevelDouble const targetSame{200.0}; - FeeLevel64 const targetOther{300}; - test = make(targetSame); - BEAST_EXPECT(test.fee() == 200); - BEAST_EXPECT(test == targetSame); - BEAST_EXPECT(test < FeeLevelDouble{1000.0}); - BEAST_EXPECT(test > FeeLevelDouble{100.0}); - test = targetOther.fee(); - BEAST_EXPECT(test.fee() == 300); - BEAST_EXPECT(test == targetOther); - - test = 200.0; - BEAST_EXPECT(test.fee() == 200); - test = std::uint64_t(300); - BEAST_EXPECT(test.fee() == 300); - - test = targetSame; - BEAST_EXPECT(test.fee() == 200); - - test = targetSame * 2; - BEAST_EXPECT(test.fee() == 400); - test = 3 * targetSame; - BEAST_EXPECT(test.fee() == 600); - test = targetSame / 10; - BEAST_EXPECT(test.fee() == 20); - - test += targetSame; - BEAST_EXPECT(test.fee() == 220); - - test -= targetSame; - BEAST_EXPECT(test.fee() == 20); - - test++; - BEAST_EXPECT(test.fee() == 21); - ++test; - BEAST_EXPECT(test.fee() == 22); - test--; - BEAST_EXPECT(test.fee() == 21); - --test; - BEAST_EXPECT(test.fee() == 20); - - test *= 5; - BEAST_EXPECT(test.fee() == 100); - test /= 2; - BEAST_EXPECT(test.fee() == 50); - /* illegal with floating - test %= 13; - BEAST_EXPECT(test.fee() == 11); - */ - - // legal with signed - test = -test; - BEAST_EXPECT(test.fee() == -50); - BEAST_EXPECT(test.signum() == -1); - BEAST_EXPECT(to_string(test) == "-50.000000"); - - BEAST_EXPECT(test); - test = 0; - BEAST_EXPECT(!test); - BEAST_EXPECT(test.signum() == 0); - test = targetSame; - BEAST_EXPECT(test.signum() == 1); - BEAST_EXPECT(to_string(test) == "200.000000"); - } - } - -public: - void - run() override - { - BEAST_EXPECT(kInitialXrp.drops() == 100'000'000'000'000'000); - BEAST_EXPECT(kInitialXrp == XRPAmount{100'000'000'000'000'000}); - - testTypes(); - testJson(); - testFunctions(); - } -}; - -BEAST_DEFINE_TESTSUITE(units, basics, xrpl); - -} // namespace xrpl::test diff --git a/src/test/basics/XRPAmount_test.cpp b/src/test/basics/XRPAmount_test.cpp deleted file mode 100644 index f393003365..0000000000 --- a/src/test/basics/XRPAmount_test.cpp +++ /dev/null @@ -1,326 +0,0 @@ -#include -#include -#include - -#include -#include - -namespace xrpl { - -class XRPAmount_test : public beast::unit_test::Suite -{ -public: - void - testSigNum() - { - testcase("signum"); - - for (auto i : {-1, 0, 1}) - { - XRPAmount const x(i); - - if (i < 0) - { - BEAST_EXPECT(x.signum() < 0); - } - else if (i > 0) - { - BEAST_EXPECT(x.signum() > 0); - } - else - { - BEAST_EXPECT(x.signum() == 0); - } - } - } - - void - testBeastZero() - { - testcase("beast::Zero Comparisons"); - - using beast::kZero; - - for (auto i : {-1, 0, 1}) - { - XRPAmount const x(i); - - BEAST_EXPECT((i == 0) == (x == kZero)); - BEAST_EXPECT((i != 0) == (x != kZero)); - BEAST_EXPECT((i < 0) == (x < kZero)); - BEAST_EXPECT((i > 0) == (x > kZero)); - BEAST_EXPECT((i <= 0) == (x <= kZero)); - BEAST_EXPECT((i >= 0) == (x >= kZero)); - - BEAST_EXPECT((0 == i) == (kZero == x)); - BEAST_EXPECT((0 != i) == (kZero != x)); - BEAST_EXPECT((0 < i) == (kZero < x)); - BEAST_EXPECT((0 > i) == (kZero > x)); - BEAST_EXPECT((0 <= i) == (kZero <= x)); - BEAST_EXPECT((0 >= i) == (kZero >= x)); - } - } - - void - testComparisons() - { - testcase("XRP Comparisons"); - - for (auto i : {-1, 0, 1}) - { - XRPAmount const x(i); - - for (auto j : {-1, 0, 1}) - { - XRPAmount const y(j); - - BEAST_EXPECT((i == j) == (x == y)); - BEAST_EXPECT((i != j) == (x != y)); - BEAST_EXPECT((i < j) == (x < y)); - BEAST_EXPECT((i > j) == (x > y)); - BEAST_EXPECT((i <= j) == (x <= y)); - BEAST_EXPECT((i >= j) == (x >= y)); - } - } - } - - void - testAddSub() - { - testcase("Addition & Subtraction"); - - for (auto i : {-1, 0, 1}) - { - XRPAmount const x(i); - - for (auto j : {-1, 0, 1}) - { - XRPAmount const y(j); - - BEAST_EXPECT(XRPAmount(i + j) == (x + y)); - BEAST_EXPECT(XRPAmount(i - j) == (x - y)); - - BEAST_EXPECT((x + y) == (y + x)); // addition is commutative - } - } - } - - void - testDecimal() - { - // Tautology - BEAST_EXPECT(kDropsPerXrp.decimalXRP() == 1); - - XRPAmount test{1}; - BEAST_EXPECT(test.decimalXRP() == 0.000001); - - test = -test; - BEAST_EXPECT(test.decimalXRP() == -0.000001); - - test = 100'000'000; - BEAST_EXPECT(test.decimalXRP() == 100); - - test = -test; - BEAST_EXPECT(test.decimalXRP() == -100); - } - - void - testFunctions() - { - // Explicitly test every defined function for the XRPAmount class - // since some of them are templated, but not used anywhere else. - auto make = [&](auto x) -> XRPAmount { return XRPAmount{x}; }; - - XRPAmount const defaulted{}; - (void)defaulted; - XRPAmount test{0}; - BEAST_EXPECT(test.drops() == 0); - - test = make(beast::kZero); - BEAST_EXPECT(test.drops() == 0); - - test = beast::kZero; - BEAST_EXPECT(test.drops() == 0); - - test = make(100); - BEAST_EXPECT(test.drops() == 100); - - test = make(100u); - BEAST_EXPECT(test.drops() == 100); - - XRPAmount const targetSame{200u}; - test = make(targetSame); - BEAST_EXPECT(test.drops() == 200); - BEAST_EXPECT(test == targetSame); - BEAST_EXPECT(test < XRPAmount{1000}); - BEAST_EXPECT(test > XRPAmount{100}); - - test = std::int64_t(200); - BEAST_EXPECT(test.drops() == 200); - test = std::uint32_t(300); - BEAST_EXPECT(test.drops() == 300); - - test = targetSame; - BEAST_EXPECT(test.drops() == 200); - auto testOther = test.dropsAs(); - BEAST_EXPECT(testOther); - BEAST_EXPECT(*testOther == 200); // NOLINT(bugprone-unchecked-optional-access) - test = std::numeric_limits::max(); - testOther = test.dropsAs(); - BEAST_EXPECT(!testOther); - test = -1; - testOther = test.dropsAs(); - BEAST_EXPECT(!testOther); - - test = targetSame * 2; - BEAST_EXPECT(test.drops() == 400); - test = 3 * targetSame; - BEAST_EXPECT(test.drops() == 600); - test = 20; - BEAST_EXPECT(test.drops() == 20); - - test += targetSame; - BEAST_EXPECT(test.drops() == 220); - - test -= targetSame; - BEAST_EXPECT(test.drops() == 20); - - test *= 5; - BEAST_EXPECT(test.drops() == 100); - test = 50; - BEAST_EXPECT(test.drops() == 50); - test -= 39; - BEAST_EXPECT(test.drops() == 11); - - // legal with signed - test = -test; - BEAST_EXPECT(test.drops() == -11); - BEAST_EXPECT(test.signum() == -1); - BEAST_EXPECT(to_string(test) == "-11"); - - BEAST_EXPECT(test); - test = 0; - BEAST_EXPECT(!test); - BEAST_EXPECT(test.signum() == 0); - test = targetSame; - BEAST_EXPECT(test.signum() == 1); - BEAST_EXPECT(to_string(test) == "200"); - } - - void - testMulRatio() - { - testcase("mulRatio"); - - constexpr auto kMaxUInt32 = std::numeric_limits::max(); - constexpr auto kMaxXrp = std::numeric_limits::max(); - constexpr auto kMinXrp = std::numeric_limits::min(); - - { - // multiply by a number that would overflow then divide by the same - // number, and check we didn't lose any value - XRPAmount big(kMaxXrp); - BEAST_EXPECT(big == mulRatio(big, kMaxUInt32, kMaxUInt32, true)); - // rounding mode shouldn't matter as the result is exact - BEAST_EXPECT(big == mulRatio(big, kMaxUInt32, kMaxUInt32, false)); - - // multiply and divide by values that would overflow if done - // naively, and check that it gives the correct answer - big -= 0xf; // Subtract a little so it's divisible by 4 - BEAST_EXPECT(mulRatio(big, 3, 4, false).value() == (big.value() / 4) * 3); - BEAST_EXPECT(mulRatio(big, 3, 4, true).value() == (big.value() / 4) * 3); - BEAST_EXPECT((big.value() * 3) / 4 != (big.value() / 4) * 3); - } - - { - // Similar test as above, but for negative values - XRPAmount big(kMinXrp); // NOLINT TODO - BEAST_EXPECT(big == mulRatio(big, kMaxUInt32, kMaxUInt32, true)); - // rounding mode shouldn't matter as the result is exact - BEAST_EXPECT(big == mulRatio(big, kMaxUInt32, kMaxUInt32, false)); - - // multiply and divide by values that would overflow if done - // naively, and check that it gives the correct answer - BEAST_EXPECT(mulRatio(big, 3, 4, false).value() == (big.value() / 4) * 3); - BEAST_EXPECT(mulRatio(big, 3, 4, true).value() == (big.value() / 4) * 3); - BEAST_EXPECT((big.value() * 3) / 4 != (big.value() / 4) * 3); - } - - { - // small amounts - XRPAmount const tiny(1); - // Round up should give the smallest allowable number - BEAST_EXPECT(tiny == mulRatio(tiny, 1, kMaxUInt32, true)); - // rounding down should be zero - BEAST_EXPECT(beast::kZero == mulRatio(tiny, 1, kMaxUInt32, false)); - BEAST_EXPECT(beast::kZero == mulRatio(tiny, kMaxUInt32 - 1, kMaxUInt32, false)); - - // tiny negative numbers - XRPAmount const tinyNeg(-1); - // Round up should give zero - BEAST_EXPECT(beast::kZero == mulRatio(tinyNeg, 1, kMaxUInt32, true)); - BEAST_EXPECT(beast::kZero == mulRatio(tinyNeg, kMaxUInt32 - 1, kMaxUInt32, true)); - // rounding down should be tiny - BEAST_EXPECT(tinyNeg == mulRatio(tinyNeg, kMaxUInt32 - 1, kMaxUInt32, false)); - } - - { // rounding - { - XRPAmount const one(1); - auto const rup = mulRatio(one, kMaxUInt32 - 1, kMaxUInt32, true); - auto const rdown = mulRatio(one, kMaxUInt32 - 1, kMaxUInt32, false); - BEAST_EXPECT(rup.drops() - rdown.drops() == 1); - } - - { - XRPAmount const big(kMaxXrp); - auto const rup = mulRatio(big, kMaxUInt32 - 1, kMaxUInt32, true); - auto const rdown = mulRatio(big, kMaxUInt32 - 1, kMaxUInt32, false); - BEAST_EXPECT(rup.drops() - rdown.drops() == 1); - } - - { - XRPAmount const negOne(-1); - auto const rup = mulRatio(negOne, kMaxUInt32 - 1, kMaxUInt32, true); - auto const rdown = mulRatio(negOne, kMaxUInt32 - 1, kMaxUInt32, false); - BEAST_EXPECT(rup.drops() - rdown.drops() == 1); - } - } - - { - // division by zero - XRPAmount one(1); - except([&] { mulRatio(one, 1, 0, true); }); - } - - { - // overflow - XRPAmount big(kMaxXrp); - except([&] { mulRatio(big, 2, 1, true); }); - } - - { - // underflow - XRPAmount const bigNegative(kMinXrp + 10); - BEAST_EXPECT(mulRatio(bigNegative, 2, 1, true) == kMinXrp); - } - } // namespace xrpl - - //-------------------------------------------------------------------------- - - void - run() override - { - testSigNum(); - testBeastZero(); - testComparisons(); - testAddSub(); - testDecimal(); - testFunctions(); - testMulRatio(); - } -}; - -BEAST_DEFINE_TESTSUITE(XRPAmount, basics, xrpl); - -} // namespace xrpl diff --git a/src/test/basics/base58_test.cpp b/src/test/basics/base58_test.cpp deleted file mode 100644 index d2c8d9eabc..0000000000 --- a/src/test/basics/base58_test.cpp +++ /dev/null @@ -1,440 +0,0 @@ -#include -#include - -#include // IWYU pragma: keep - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifndef _MSC_VER - -#include -#include - -#include -#include -#include -#include -#include - -namespace xrpl::test { -namespace { - -[[nodiscard]] inline auto -randEngine() -> std::mt19937& -{ - static std::mt19937 kR = [] { - std::random_device rd; - return std::mt19937{rd()}; - }(); - return kR; -} - -constexpr int kNumTokenTypeIndexes = 9; - -[[nodiscard]] inline auto -tokenTypeAndSize(int i) -> std::tuple -{ - assert(i < kNumTokenTypeIndexes); - - switch (i) - { - using enum xrpl::TokenType; - case 0: - return {None, 20}; - case 1: - return {NodePublic, 32}; - case 2: - return {NodePublic, 33}; - case 3: - return {NodePrivate, 32}; - case 4: - return {AccountID, 20}; - case 5: - return {AccountPublic, 32}; - case 6: - return {AccountPublic, 33}; - case 7: - return {AccountSecret, 32}; - case 8: - return {FamilySeed, 16}; - default: - throw std::invalid_argument( - "Invalid token selection passed to tokenTypeAndSize() " - "in " __FILE__); - } -} - -[[nodiscard]] inline auto -randomTokenTypeAndSize() -> std::tuple -{ - using namespace xrpl; - auto& rng = randEngine(); - std::uniform_int_distribution<> d(0, 8); - return tokenTypeAndSize(d(rng)); -} - -// Return the token type and subspan of `d` to use as test data. -[[nodiscard]] inline auto -randomB256TestData(std::span d) - -> std::tuple> -{ - auto& rng = randEngine(); - std::uniform_int_distribution dist(0, 255); - auto [tokType, tokSize] = randomTokenTypeAndSize(); - std::generate(d.begin(), d.begin() + tokSize, [&] { return dist(rng); }); - return {tokType, d.subspan(0, tokSize)}; -} - -inline void -printAsChar(std::span a, std::span b) -{ - auto asString = [](std::span s) { - std::string r; - r.resize(s.size()); - std::ranges::copy(s, r.begin()); - return r; - }; - auto sa = asString(a); - auto sb = asString(b); - std::cerr << "\n\n" << sa << "\n" << sb << "\n"; -} - -inline void -printAsInt(std::span a, std::span b) -{ - auto asString = [](std::span s) -> std::string { - std::stringstream sstr; - for (auto i : s) - { - sstr << std::setw(3) << int(i) << ','; - } - return sstr.str(); - }; - auto sa = asString(a); - auto sb = asString(b); - std::cerr << "\n\n" << sa << "\n" << sb << "\n"; -} - -} // namespace - -namespace multiprecision_utils { - -boost::multiprecision::checked_uint512_t -toBoostMP(std::span in) -{ - boost::multiprecision::checked_uint512_t mbp = 0; - for (auto& word : std::views::reverse(in)) - { - mbp <<= 64; - mbp += word; - } - return mbp; -} - -std::vector -randomBigInt(std::uint8_t minSize = 1, std::uint8_t maxSize = 5) -{ - auto eng = randEngine(); - std::uniform_int_distribution numCoeffDist(minSize, maxSize); - std::uniform_int_distribution dist; - auto const numCoeff = numCoeffDist(eng); - std::vector coeffs; - coeffs.reserve(numCoeff); - for (int i = 0; i < numCoeff; ++i) - { - coeffs.push_back(dist(eng)); - } - return coeffs; -} -} // namespace multiprecision_utils - -class base58_test : public beast::unit_test::Suite -{ - void - testMultiprecision() - { - testcase("b58_multiprecision"); - - using namespace boost::multiprecision; - - static constexpr std::size_t kIters = 100000; - auto eng = randEngine(); - std::uniform_int_distribution dist; - std::uniform_int_distribution dist1(1); - for (int i = 0; i < kIters; ++i) - { - std::uint64_t const d = dist(eng); - if (d == 0u) - continue; - auto bigInt = multiprecision_utils::randomBigInt(); - auto const boostBigInt = multiprecision_utils::toBoostMP( - std::span(bigInt.data(), bigInt.size())); - - auto const refDiv = boostBigInt / d; - auto const refMod = boostBigInt % d; - - auto const mod = b58_fast::detail::inplaceBigintDivRem( - std::span(bigInt.data(), bigInt.size()), d); - auto const foundDiv = multiprecision_utils::toBoostMP(bigInt); - BEAST_EXPECT(refMod.convert_to() == mod); - BEAST_EXPECT(foundDiv == refDiv); - } - for (int i = 0; i < kIters; ++i) - { - std::uint64_t const d = dist(eng); - auto bigInt = multiprecision_utils::randomBigInt(/*minSize*/ 2); - if (bigInt[bigInt.size() - 1] == std::numeric_limits::max()) - { - bigInt[bigInt.size() - 1] -= 1; // Prevent overflow - } - auto const boostBigInt = multiprecision_utils::toBoostMP( - std::span(bigInt.data(), bigInt.size())); - - auto const refAdd = boostBigInt + d; - - auto const result = b58_fast::detail::inplaceBigintAdd( - std::span(bigInt.data(), bigInt.size()), d); - BEAST_EXPECT(result == TokenCodecErrc::Success); - auto const foundAdd = multiprecision_utils::toBoostMP(bigInt); - BEAST_EXPECT(refAdd == foundAdd); - } - for (int i = 0; i < kIters; ++i) - { - std::uint64_t const d = dist1(eng); - // Force overflow - std::vector bigInt(5, std::numeric_limits::max()); - - auto const boostBigInt = multiprecision_utils::toBoostMP( - std::span(bigInt.data(), bigInt.size())); - - auto const refAdd = boostBigInt + d; - - auto const result = b58_fast::detail::inplaceBigintAdd( - std::span(bigInt.data(), bigInt.size()), d); - BEAST_EXPECT(result == TokenCodecErrc::OverflowAdd); - auto const foundAdd = multiprecision_utils::toBoostMP(bigInt); - BEAST_EXPECT(refAdd != foundAdd); - } - for (int i = 0; i < kIters; ++i) - { - std::uint64_t const d = dist(eng); - auto bigInt = multiprecision_utils::randomBigInt(/* minSize */ 2); - // inplace mul requires the most significant coeff to be zero to - // hold the result. - bigInt[bigInt.size() - 1] = 0; - auto const boostBigInt = multiprecision_utils::toBoostMP( - std::span(bigInt.data(), bigInt.size())); - - auto const refMul = boostBigInt * d; - - auto const result = b58_fast::detail::inplaceBigintMul( - std::span(bigInt.data(), bigInt.size()), d); - BEAST_EXPECT(result == TokenCodecErrc::Success); - auto const foundMul = multiprecision_utils::toBoostMP(bigInt); - BEAST_EXPECT(refMul == foundMul); - } - for (int i = 0; i < kIters; ++i) - { - std::uint64_t const d = dist1(eng); - // Force overflow - std::vector bigInt(5, std::numeric_limits::max()); - auto const boostBigInt = multiprecision_utils::toBoostMP( - std::span(bigInt.data(), bigInt.size())); - - auto const refMul = boostBigInt * d; - - auto const result = b58_fast::detail::inplaceBigintMul( - std::span(bigInt.data(), bigInt.size()), d); - BEAST_EXPECT(result == TokenCodecErrc::InputTooLarge); - auto const foundMul = multiprecision_utils::toBoostMP(bigInt); - BEAST_EXPECT(refMul != foundMul); - } - } - - void - testFastMatchesRef() - { - testcase("fast_matches_ref"); - auto testRawEncode = [&](std::span const& b256Data) { - std::array b58ResultBuf[2]; - std::array, 2> b58Result; - - std::array b256ResultBuf[2]; - std::array, 2> b256Result; - for (int i = 0; i < 2; ++i) - { - std::span const outBuf{b58ResultBuf[i]}; - if (i == 0) - { - auto const r = xrpl::b58_fast::detail::b256ToB58Be(b256Data, outBuf); - BEAST_EXPECT(r); - b58Result[i] = r.value(); - } - else - { - std::array tmpBuf{}; - std::string const s = xrpl::b58_ref::detail::encodeBase58( - b256Data.data(), b256Data.size(), tmpBuf.data(), tmpBuf.size()); - BEAST_EXPECT(s.size()); - b58Result[i] = outBuf.subspan(0, s.size()); - std::ranges::copy(s, b58Result[i].begin()); - } - } - if (BEAST_EXPECT(b58Result[0].size() == b58Result[1].size())) - { - if (!BEAST_EXPECT( - memcmp(b58Result[0].data(), b58Result[1].data(), b58Result[0].size()) == 0)) - { - printAsChar(b58Result[0], b58Result[1]); - } - } - - for (int i = 0; i < 2; ++i) - { - std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()}; - if (i == 0) - { - std::string const in( - b58Result[i].data(), b58Result[i].data() + b58Result[i].size()); - auto const r = xrpl::b58_fast::detail::b58ToB256Be(in, outBuf); - BEAST_EXPECT(r); - b256Result[i] = r.value(); - } - else - { - std::string const st(b58Result[i].begin(), b58Result[i].end()); - std::string const s = xrpl::b58_ref::detail::decodeBase58(st); - BEAST_EXPECT(s.size()); - b256Result[i] = outBuf.subspan(0, s.size()); - std::ranges::copy(s, b256Result[i].begin()); - } - } - - if (BEAST_EXPECT(b256Result[0].size() == b256Result[1].size())) - { - if (!BEAST_EXPECT( - memcmp(b256Result[0].data(), b256Result[1].data(), b256Result[0].size()) == - 0)) - { - printAsInt(b256Result[0], b256Result[1]); - } - } - }; - - auto testTokenEncode = [&](xrpl::TokenType const tokType, - std::span const& b256Data) { - std::array b58ResultBuf[2]; - std::array, 2> b58Result; - - std::array b256ResultBuf[2]; - std::array, 2> b256Result; - for (int i = 0; i < 2; ++i) - { - std::span const outBuf{b58ResultBuf[i].data(), b58ResultBuf[i].size()}; - if (i == 0) - { - auto const r = xrpl::b58_fast::encodeBase58Token(tokType, b256Data, outBuf); - BEAST_EXPECT(r); - b58Result[i] = r.value(); - } - else - { - std::string const s = - xrpl::b58_ref::encodeBase58Token(tokType, b256Data.data(), b256Data.size()); - BEAST_EXPECT(s.size()); - b58Result[i] = outBuf.subspan(0, s.size()); - std::ranges::copy(s, b58Result[i].begin()); - } - } - if (BEAST_EXPECT(b58Result[0].size() == b58Result[1].size())) - { - if (!BEAST_EXPECT( - memcmp(b58Result[0].data(), b58Result[1].data(), b58Result[0].size()) == 0)) - { - printAsChar(b58Result[0], b58Result[1]); - } - } - - for (int i = 0; i < 2; ++i) - { - std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()}; - if (i == 0) - { - std::string const in( - b58Result[i].data(), b58Result[i].data() + b58Result[i].size()); - auto const r = xrpl::b58_fast::decodeBase58Token(tokType, in, outBuf); - BEAST_EXPECT(r); - b256Result[i] = r.value(); - } - else - { - std::string const st(b58Result[i].begin(), b58Result[i].end()); - std::string const s = xrpl::b58_ref::decodeBase58Token(st, tokType); - BEAST_EXPECT(s.size()); - b256Result[i] = outBuf.subspan(0, s.size()); - std::ranges::copy(s, b256Result[i].begin()); - } - } - - if (BEAST_EXPECT(b256Result[0].size() == b256Result[1].size())) - { - if (!BEAST_EXPECT( - memcmp(b256Result[0].data(), b256Result[1].data(), b256Result[0].size()) == - 0)) - { - printAsInt(b256Result[0], b256Result[1]); - } - } - }; - - auto testIt = [&](xrpl::TokenType const tokType, std::span const& b256Data) { - testRawEncode(b256Data); - testTokenEncode(tokType, b256Data); - }; - - // test every token type with data where every byte is the same and the - // bytes range from 0-255 - for (int i = 0; i < kNumTokenTypeIndexes; ++i) - { - std::array b256DataBuf{}; - auto const [tokType, tokSize] = tokenTypeAndSize(i); - for (int d = 0; d <= 255; ++d) - { - memset(b256DataBuf.data(), d, tokSize); - testIt(tokType, std::span(b256DataBuf.data(), tokSize)); - } - } - - // test with random data - static constexpr std::size_t kIters = 100000; - for (int i = 0; i < kIters; ++i) - { - std::array b256DataBuf{}; - auto const [tokType, b256Data] = randomB256TestData(b256DataBuf); - testIt(tokType, b256Data); - } - } - - void - run() override - { - testMultiprecision(); - testFastMatchesRef(); - } -}; - -BEAST_DEFINE_TESTSUITE(base58, basics, xrpl); - -} // namespace xrpl::test - -#endif // _MSC_VER diff --git a/src/test/basics/base_uint_test.cpp b/src/test/basics/base_uint_test.cpp deleted file mode 100644 index c6572a0028..0000000000 --- a/src/test/basics/base_uint_test.cpp +++ /dev/null @@ -1,376 +0,0 @@ -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl::test { - -// a non-hashing Hasher that just copies the bytes. -// Used to test hash_append in base_uint -template -struct Nonhash -{ - static constexpr auto kEndian = boost::endian::order::big; - static constexpr std::size_t kWidth = Bits / 8; - - std::array data; - - Nonhash() = default; - - void - operator()(void const* key, std::size_t len) noexcept - { - assert(len == kWidth); - memcpy(data.data(), key, len); - } - - explicit - operator std::size_t() noexcept - { - return kWidth; - } -}; - -struct base_uint_test : beast::unit_test::Suite -{ - using test96 = BaseUInt<96>; - static_assert(std::is_copy_constructible_v); - static_assert(std::is_copy_assignable_v); - - void - testComparisons() - { - { - static constexpr std::array, 6> kTestArgs{ - {{"0000000000000000", "0000000000000001"}, - {"0000000000000000", "ffffffffffffffff"}, - {"1234567812345678", "2345678923456789"}, - {"8000000000000000", "8000000000000001"}, - {"aaaaaaaaaaaaaaa9", "aaaaaaaaaaaaaaaa"}, - {"fffffffffffffffe", "ffffffffffffffff"}}}; - - for (auto const& arg : kTestArgs) - { - xrpl::BaseUInt<64> const u{arg.first}, v{arg.second}; - BEAST_EXPECT(u < v); - BEAST_EXPECT(u <= v); - BEAST_EXPECT(u != v); - BEAST_EXPECT(!(u == v)); - BEAST_EXPECT(!(u > v)); - BEAST_EXPECT(!(u >= v)); - BEAST_EXPECT(!(v < u)); - BEAST_EXPECT(!(v <= u)); - BEAST_EXPECT(v != u); - BEAST_EXPECT(!(v == u)); - BEAST_EXPECT(v > u); - BEAST_EXPECT(v >= u); - BEAST_EXPECT(u == u); - BEAST_EXPECT(v == v); - } - } - - { - static constexpr std::array, 6> kTestArgs{ - { - {"000000000000000000000000", "000000000000000000000001"}, - {"000000000000000000000000", "ffffffffffffffffffffffff"}, - {"0123456789ab0123456789ab", "123456789abc123456789abc"}, - {"555555555555555555555555", "55555555555a555555555555"}, - {"aaaaaaaaaaaaaaa9aaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaa"}, - {"fffffffffffffffffffffffe", "ffffffffffffffffffffffff"}, - }}; - - for (auto const& arg : kTestArgs) - { - xrpl::BaseUInt<96> const u{arg.first}, v{arg.second}; - BEAST_EXPECT(u < v); - BEAST_EXPECT(u <= v); - BEAST_EXPECT(u != v); - BEAST_EXPECT(!(u == v)); - BEAST_EXPECT(!(u > v)); - BEAST_EXPECT(!(u >= v)); - BEAST_EXPECT(!(v < u)); - BEAST_EXPECT(!(v <= u)); - BEAST_EXPECT(v != u); - BEAST_EXPECT(!(v == u)); - BEAST_EXPECT(v > u); - BEAST_EXPECT(v >= u); - BEAST_EXPECT(u == u); - BEAST_EXPECT(v == v); - } - } - } - - void - testFromRawSizeMismatch() - { - testcase("base_uint: fromRaw size mismatch"); - - // Container larger than the base_uint (16 bytes vs 12 bytes for test96). - // Only the first 12 bytes are copied; the extra bytes are ignored. - { - Blob const tooBig{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; - test96 const result = test96::fromRaw(tooBig); - BEAST_EXPECT(to_string(result) == "0102030405060708090A0B0C"); - } - } - - void - run() override - { - testcase("base_uint: general purpose tests"); - -#ifdef NDEBUG - testFromRawSizeMismatch(); -#endif - - static_assert(!std::is_constructible_v>); - static_assert(!std::is_assignable_v>); - - testComparisons(); - - // used to verify set insertion (hashing required) - std::unordered_set> uset; - - Blob const raw{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; - BEAST_EXPECT(test96::kBytes == raw.size()); - - test96 u = test96::fromRaw(raw); - uset.insert(u); - BEAST_EXPECT(raw.size() == u.size()); - BEAST_EXPECT(to_string(u) == "0102030405060708090A0B0C"); - BEAST_EXPECT(toShortString(u) == "01020304..."); - BEAST_EXPECT(*u.data() == 1); - BEAST_EXPECT(u.signum() == 1); - BEAST_EXPECT(!!u); - BEAST_EXPECT(!u.isZero()); - BEAST_EXPECT(u.isNonZero()); - unsigned char t = 0; - for (auto& d : u) - { - BEAST_EXPECT(d == ++t); - } - - // Test hash_append by "hashing" with a no-op hasher (h) - // and then extracting the bytes that were written during hashing - // back into another base_uint (w) for comparison with the original - Nonhash<96> h{}; - hash_append(h, u); - test96 const w = test96::fromRaw(std::vector(h.data.begin(), h.data.end())); - BEAST_EXPECT(w == u); - - test96 v{~u}; - uset.insert(v); - BEAST_EXPECT(to_string(v) == "FEFDFCFBFAF9F8F7F6F5F4F3"); - BEAST_EXPECT(toShortString(v) == "FEFDFCFB..."); - BEAST_EXPECT(*v.data() == 0xfe); - BEAST_EXPECT(v.signum() == 1); - BEAST_EXPECT(!!v); - BEAST_EXPECT(!v.isZero()); - BEAST_EXPECT(v.isNonZero()); - t = 0xff; - for (auto& d : v) - { - BEAST_EXPECT(d == --t); - } - - BEAST_EXPECT(u < v); - BEAST_EXPECT(v > u); - - v = u; - BEAST_EXPECT(v == u); - - test96 z{beast::kZero}; - uset.insert(z); - BEAST_EXPECT(to_string(z) == "000000000000000000000000"); - BEAST_EXPECT(toShortString(z) == "00000000..."); - BEAST_EXPECT(*z.data() == 0); - BEAST_EXPECT(*z.begin() == 0); - BEAST_EXPECT(*std::prev(z.end(), 1) == 0); - BEAST_EXPECT(z.signum() == 0); - BEAST_EXPECT(!z); - BEAST_EXPECT(z.isZero()); - BEAST_EXPECT(!z.isNonZero()); - for (auto& d : z) - { - BEAST_EXPECT(d == 0); - } - - test96 n{z}; - n++; - BEAST_EXPECT(n == test96(1)); - n--; - BEAST_EXPECT(n == beast::kZero); - BEAST_EXPECT(n == z); - n--; - BEAST_EXPECT(to_string(n) == "FFFFFFFFFFFFFFFFFFFFFFFF"); - BEAST_EXPECT(toShortString(n) == "FFFFFFFF..."); - n = beast::kZero; - BEAST_EXPECT(n == z); - - test96 zp1{z}; - zp1++; - test96 zm1{z}; - zm1--; - test96 const x{zm1 ^ zp1}; - uset.insert(x); - BEAST_EXPECTS(to_string(x) == "FFFFFFFFFFFFFFFFFFFFFFFE", to_string(x)); - BEAST_EXPECTS(toShortString(x) == "FFFFFFFF...", toShortString(x)); - - BEAST_EXPECT(uset.size() == 4); - - test96 tmp; - BEAST_EXPECT(tmp.parseHex(to_string(u))); - BEAST_EXPECT(tmp == u); - tmp = z; - - // fails with extra char - BEAST_EXPECT(!tmp.parseHex("A" + to_string(u))); - tmp = z; - - // fails with extra char at end - BEAST_EXPECT(!tmp.parseHex(to_string(u) + "A")); - - // fails with a non-hex character at some point in the string: - tmp = z; - - for (std::size_t i = 0; i != 24; ++i) - { - std::string x = to_string(z); - x[i] = ('G' + (i % 10)); - BEAST_EXPECT(!tmp.parseHex(x)); - } - - // Walking 1s: - for (std::size_t i = 0; i != 24; ++i) - { - std::string s1 = "000000000000000000000000"; - s1[i] = '1'; - - BEAST_EXPECT(tmp.parseHex(s1)); - BEAST_EXPECT(to_string(tmp) == s1); - } - - // Walking 0s: - for (std::size_t i = 0; i != 24; ++i) - { - std::string s1 = "111111111111111111111111"; - s1[i] = '0'; - - BEAST_EXPECT(tmp.parseHex(s1)); - BEAST_EXPECT(to_string(tmp) == s1); - } - - // Constexpr constructors - { - static_assert(test96{}.signum() == 0); - static_assert(test96("0").signum() == 0); - static_assert(test96("000000000000000000000000").signum() == 0); - static_assert(test96("000000000000000000000001").signum() == 1); - static_assert(test96("800000000000000000000000").signum() == 1); - -// Everything within the #if should fail during compilation. -#if 0 - // Too few characters - static_assert(test96("00000000000000000000000").signum() == 0); - - // Too many characters - static_assert(test96("0000000000000000000000000").signum() == 0); - - // Non-hex characters - static_assert(test96("00000000000000000000000 ").signum() == 1); - static_assert(test96("00000000000000000000000/").signum() == 1); - static_assert(test96("00000000000000000000000:").signum() == 1); - static_assert(test96("00000000000000000000000@").signum() == 1); - static_assert(test96("00000000000000000000000G").signum() == 1); - static_assert(test96("00000000000000000000000`").signum() == 1); - static_assert(test96("00000000000000000000000g").signum() == 1); - static_assert(test96("00000000000000000000000~").signum() == 1); -#endif // 0 - - // Using the constexpr constructor in a non-constexpr context - // with an error in the parsing throws an exception. - { - // Invalid length for string. - bool caught = false; - try - { - // Try to prevent constant evaluation. - std::vector str(23, '7'); - std::string_view const sView(str.data(), str.size()); - [[maybe_unused]] test96 const t96(sView); - } - catch (std::invalid_argument const& e) - { - BEAST_EXPECT(e.what() == std::string("invalid length for hex string")); - caught = true; - } - BEAST_EXPECT(caught); - } - { - // Invalid character in string. - bool caught = false; - try - { - // Try to prevent constant evaluation. - std::vector str(23, '7'); - str.push_back('G'); - std::string_view const sView(str.data(), str.size()); - [[maybe_unused]] test96 const t96(sView); - } - catch (std::range_error const& e) - { - BEAST_EXPECT(e.what() == std::string("invalid hex character")); - caught = true; - } - BEAST_EXPECT(caught); - } - - // Verify that constexpr base_uints interpret a string the same - // way parseHex() does. - struct StrBaseUInt - { - char const* const str; - test96 tst; - - constexpr StrBaseUInt(char const* s) : str(s), tst(s) - { - } - }; - static constexpr StrBaseUInt kTestCases[] = { - "000000000000000000000000", - "000000000000000000000001", - "fedcba9876543210ABCDEF91", - "19FEDCBA0123456789abcdef", - "800000000000000000000000", - "fFfFfFfFfFfFfFfFfFfFfFfF"}; - - for (StrBaseUInt const& t : kTestCases) - { - test96 t96; - BEAST_EXPECT(t96.parseHex(t.str)); - BEAST_EXPECT(t96 == t.tst); - } - } - } -}; - -BEAST_DEFINE_TESTSUITE(base_uint, basics, xrpl); - -} // namespace xrpl::test diff --git a/src/test/basics/join_test.cpp b/src/test/basics/join_test.cpp deleted file mode 100644 index 1ee81f786a..0000000000 --- a/src/test/basics/join_test.cpp +++ /dev/null @@ -1,83 +0,0 @@ -#include - -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace xrpl::test { - -struct join_test : beast::unit_test::Suite -{ - void - run() override - { - auto test = [this](auto collectionanddelimiter, std::string expected) { - std::stringstream ss; - // Put something else in the buffer before and after to ensure that - // the << operator returns the stream correctly. - ss << "(" << collectionanddelimiter << ")"; - auto const str = ss.str(); - BEAST_EXPECT(str.substr(1, str.length() - 2) == expected); - BEAST_EXPECT(str.front() == '('); - BEAST_EXPECT(str.back() == ')'); - }; - - // C++ array - test(CollectionAndDelimiter(std::array{2, -1, 5, 10}, "/"), "2/-1/5/10"); - // One item C++ array edge case - test(CollectionAndDelimiter(std::array{"test"}, " & "), "test"); - // Empty C++ array edge case - test(CollectionAndDelimiter(std::array{}, ","), ""); - { - // C-style array - char letters[4]{'w', 'a', 's', 'd'}; - test(CollectionAndDelimiter(letters, std::to_string(0)), "w0a0s0d"); - } - { - // Auto sized C-style array - std::string words[]{"one", "two", "three", "four"}; - test(CollectionAndDelimiter(words, "\n"), "one\ntwo\nthree\nfour"); - } - { - // One item C-style array edge case - std::string words[]{"thing"}; - test(CollectionAndDelimiter(words, "\n"), "thing"); - } - // Initializer list - test(CollectionAndDelimiter(std::initializer_list{19, 25}, "+"), "19+25"); - // vector - test(CollectionAndDelimiter(std::vector{0, 42}, std::to_string(99)), "09942"); - { - // vector with one item edge case - using namespace jtx; - test( - CollectionAndDelimiter(std::vector{Account::kMaster}, "xxx"), - Account::kMaster.human()); - } - // empty vector edge case - test(CollectionAndDelimiter(std::vector{}, ","), ""); - // C-style string - test(CollectionAndDelimiter("string", " "), "s t r i n g"); - // Empty C-style string edge case - test(CollectionAndDelimiter("", "*"), ""); - // Single char C-style string edge case - test(CollectionAndDelimiter("x", "*"), "x"); - // std::string - test(CollectionAndDelimiter(std::string{"string"}, "-"), "s-t-r-i-n-g"); - // Empty std::string edge case - test(CollectionAndDelimiter(std::string{""}, "*"), ""); - // Single char std::string edge case - test(CollectionAndDelimiter(std::string{"y"}, "*"), "y"); - } -}; // namespace test - -BEAST_DEFINE_TESTSUITE(join, basics, xrpl); - -} // namespace xrpl::test diff --git a/src/tests/libxrpl/basics/Buffer.cpp b/src/tests/libxrpl/basics/Buffer.cpp new file mode 100644 index 0000000000..5b34585fcf --- /dev/null +++ b/src/tests/libxrpl/basics/Buffer.cpp @@ -0,0 +1,260 @@ +#include + +#include + +#include + +#include +#include +#include +#include +#include + +namespace xrpl::test { + +struct BufferTest : public ::testing::Test +{ + static bool + sane(Buffer const& b) + { + if (b.empty()) + return b.data() == nullptr; + + return b.data() != nullptr; + } +}; + +TEST_F(BufferTest, buffer) +{ + std::uint8_t const data[] = {0xa8, 0xa1, 0x38, 0x45, 0x23, 0xec, 0xe4, 0x23, 0x71, 0x6d, 0x2a, + 0x18, 0xb4, 0x70, 0xcb, 0xf5, 0xac, 0x2d, 0x89, 0x4d, 0x19, 0x9c, + 0xf0, 0x2c, 0x15, 0xd1, 0xf9, 0x9b, 0x66, 0xd2, 0x30, 0xd3}; + + Buffer const b0; + EXPECT_TRUE(sane(b0)); + EXPECT_TRUE(b0.empty()); + + Buffer b1{0}; + EXPECT_TRUE(sane(b1)); + EXPECT_TRUE(b1.empty()); + std::memcpy(b1.alloc(16), data, 16); + EXPECT_TRUE(sane(b1)); + EXPECT_FALSE(b1.empty()); + EXPECT_EQ(b1.size(), 16); + + Buffer b2{b1.size()}; + EXPECT_TRUE(sane(b2)); + EXPECT_FALSE(b2.empty()); + EXPECT_EQ(b2.size(), b1.size()); + std::memcpy(b2.data(), data + 16, 16); + + Buffer b3{data, sizeof(data)}; + EXPECT_TRUE(sane(b3)); + EXPECT_FALSE(b3.empty()); + EXPECT_EQ(b3.size(), sizeof(data)); + EXPECT_EQ(std::memcmp(b3.data(), data, b3.size()), 0); + + // Check equality and inequality comparisons. + // For code readability, we want to use general + // EXPECT_TRUE instead of specific EXPECT_EQ etc. + EXPECT_TRUE(b0 == b0); + EXPECT_TRUE(b0 != b1); + EXPECT_TRUE(b1 == b1); + EXPECT_TRUE(b1 != b2); + EXPECT_TRUE(b2 != b3); + + // Check copy constructors and copy assignments: + { + Buffer x{b0}; + EXPECT_EQ(x, b0); + EXPECT_TRUE(sane(x)); + Buffer y{b1}; + EXPECT_EQ(y, b1); + EXPECT_TRUE(sane(y)); + x = b2; + EXPECT_EQ(x, b2); + EXPECT_TRUE(sane(x)); + x = y; + EXPECT_EQ(x, y); + EXPECT_TRUE(sane(x)); + y = b3; + EXPECT_EQ(y, b3); + EXPECT_TRUE(sane(y)); + x = b0; + EXPECT_EQ(x, b0); + EXPECT_TRUE(sane(x)); +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wself-assign-overloaded" +#endif + + x = x; + EXPECT_EQ(x, b0); + EXPECT_TRUE(sane(x)); + y = y; + EXPECT_EQ(y, b3); + EXPECT_TRUE(sane(y)); + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + } + + // Check move constructor & move assignments: + { + static_assert(std::is_nothrow_move_constructible_v); + static_assert(std::is_nothrow_move_assignable_v); + + { // Move-construct from empty buf + Buffer x; + Buffer const y{std::move(x)}; + EXPECT_TRUE(sane(x)); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(x.empty()); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(sane(y)); + EXPECT_TRUE(y.empty()); + EXPECT_EQ(x, y); // NOLINT(bugprone-use-after-move) + } + + { // Move-construct from non-empty buf + Buffer x{b1}; + Buffer const y{std::move(x)}; + EXPECT_TRUE(sane(x)); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(x.empty()); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(sane(y)); + EXPECT_EQ(y, b1); + } + + { // Move assign empty buf to empty buf + Buffer x; + Buffer y; + + x = std::move(y); + EXPECT_TRUE(sane(x)); + EXPECT_TRUE(x.empty()); + EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move) + } + + { // Move assign non-empty buf to empty buf + Buffer x; + Buffer y{b1}; + + x = std::move(y); + EXPECT_TRUE(sane(x)); + EXPECT_EQ(x, b1); + EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move) + } + + { // Move assign empty buf to non-empty buf + Buffer x{b1}; + Buffer y; + + x = std::move(y); + EXPECT_TRUE(sane(x)); + EXPECT_TRUE(x.empty()); + EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move) + } + + { // Move assign non-empty buf to non-empty buf + Buffer x{b1}; + Buffer y{b2}; + Buffer z{b3}; + + x = std::move(y); + EXPECT_TRUE(sane(x)); + EXPECT_FALSE(x.empty()); + EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move) + + x = std::move(z); + EXPECT_TRUE(sane(x)); + EXPECT_FALSE(x.empty()); + EXPECT_TRUE(sane(z)); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(z.empty()); // NOLINT(bugprone-use-after-move) + } + } + + { + Buffer w{static_cast(b0)}; + EXPECT_TRUE(sane(w)); + EXPECT_EQ(w, b0); + + Buffer x{static_cast(b1)}; + EXPECT_TRUE(sane(x)); + EXPECT_EQ(x, b1); + + Buffer y{static_cast(b2)}; + EXPECT_TRUE(sane(y)); + EXPECT_EQ(y, b2); + + Buffer z{static_cast(b3)}; + EXPECT_TRUE(sane(z)); + EXPECT_EQ(z, b3); + + // Assign empty slice to empty buffer + w = static_cast(b0); + EXPECT_TRUE(sane(w)); + EXPECT_EQ(w, b0); + + // Assign non-empty slice to empty buffer + w = static_cast(b1); + EXPECT_TRUE(sane(w)); + EXPECT_EQ(w, b1); + + // Assign non-empty slice to non-empty buffer + x = static_cast(b2); + EXPECT_TRUE(sane(x)); + EXPECT_EQ(x, b2); + + // Assign non-empty slice to non-empty buffer + y = static_cast(z); + EXPECT_TRUE(sane(y)); + EXPECT_EQ(y, z); + + // Assign empty slice to non-empty buffer: + z = static_cast(b0); + EXPECT_TRUE(sane(z)); + EXPECT_EQ(z, b0); + } + + { + auto test = [](Buffer const& b, std::size_t i) { + Buffer x{b}; + + // Try to allocate some number of bytes, possibly + // zero (which means clear) and sanity check + x(i); + EXPECT_TRUE(sane(x)); + EXPECT_EQ(x.size(), i); + EXPECT_EQ((x.data() == nullptr), (i == 0)); + + // Try to allocate some more data (always non-zero) + x(i + 1); + EXPECT_TRUE(sane(x)); + EXPECT_EQ(x.size(), i + 1); + EXPECT_NE(x.data(), nullptr); + + // Try to clear: + x.clear(); + EXPECT_TRUE(sane(x)); + EXPECT_TRUE(x.empty()); + EXPECT_EQ(x.data(), nullptr); + + // Try to clear again: + x.clear(); + EXPECT_TRUE(sane(x)); + EXPECT_TRUE(x.empty()); + EXPECT_EQ(x.data(), nullptr); + }; + + for (std::size_t i = 0; i < 16; ++i) + { + test(b0, i); + test(b1, i); + } + } +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/basics/FileUtilities.cpp b/src/tests/libxrpl/basics/FileUtilities.cpp new file mode 100644 index 0000000000..cd24abd696 --- /dev/null +++ b/src/tests/libxrpl/basics/FileUtilities.cpp @@ -0,0 +1,94 @@ +#include + +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace xrpl { + +namespace { + +class TempFile +{ +public: + explicit TempFile(boost::filesystem::path file, std::string const& contents) + : dir_( + boost::filesystem::temp_directory_path() / + boost::filesystem::unique_path("xrpl-file-utilities-%%%%-%%%%-%%%%")) + , file_(dir_ / file) + { + boost::filesystem::create_directory(dir_); + + std::ofstream output(file_.string()); + if (!output) + throw std::runtime_error("Unable to create temporary test file"); + + output << contents; + } + + ~TempFile() + { + boost::system::error_code ec; + boost::filesystem::remove(file_, ec); + boost::filesystem::remove(dir_, ec); + } + + [[nodiscard]] boost::filesystem::path const& + file() const + { + return file_; + } + +private: + boost::filesystem::path dir_; + boost::filesystem::path file_; +}; + +} // namespace + +TEST(FileUtilitiesTest, get_file_contents) +{ + using namespace boost::system; + + constexpr char const* kExpectedContents = "This file is very short. That's all we need."; + + TempFile const file("test_file", "This is temporary text that should get overwritten"); + + error_code ec; + auto const path = file.file(); + + writeFileContents(ec, path, kExpectedContents); + EXPECT_FALSE(ec); + + { + // Test with no max + auto const good = getFileContents(ec, path); + EXPECT_FALSE(ec); + EXPECT_EQ(good, kExpectedContents); + } + + { + // Test with large max + auto const good = getFileContents(ec, path, kilobytes(1)); + EXPECT_FALSE(ec); + EXPECT_EQ(good, kExpectedContents); + } + + { + // Test with small max + auto const bad = getFileContents(ec, path, 16); + EXPECT_TRUE(ec && ec.value() == boost::system::errc::file_too_large); + EXPECT_TRUE(bad.empty()); + } +} + +} // namespace xrpl diff --git a/src/tests/libxrpl/basics/IOUAmount.cpp b/src/tests/libxrpl/basics/IOUAmount.cpp new file mode 100644 index 0000000000..46d6d7c5e1 --- /dev/null +++ b/src/tests/libxrpl/basics/IOUAmount.cpp @@ -0,0 +1,243 @@ +#include + +#include +#include + +#include + +#include +#include +#include +#include + +namespace xrpl { + +TEST(IOUAmountTest, zero) +{ + IOUAmount const z(0, 0); + + EXPECT_EQ(z.mantissa(), 0); + EXPECT_EQ(z.exponent(), -100); + EXPECT_FALSE(z); + EXPECT_EQ(z.signum(), 0); + EXPECT_EQ(z, beast::kZero); + + EXPECT_EQ((z + z), z); + EXPECT_EQ((z - z), z); + EXPECT_EQ(z, -z); + + IOUAmount const zz(beast::kZero); + EXPECT_EQ(z, zz); + + // https://github.com/XRPLF/rippled/issues/5170 + IOUAmount const zzz{}; + EXPECT_EQ(zzz, beast::kZero); + // EXPECT_EQ(zzz, zz); +} + +TEST(IOUAmountTest, sig_num) +{ + IOUAmount const neg(-1, 0); + EXPECT_LT(neg.signum(), 0); + + IOUAmount const zer(0, 0); + EXPECT_EQ(zer.signum(), 0); + + IOUAmount const pos(1, 0); + EXPECT_GT(pos.signum(), 0); +} + +TEST(IOUAmountTest, beast_zero) +{ + using beast::kZero; + + { + IOUAmount const z(kZero); + EXPECT_TRUE(z == kZero); + EXPECT_TRUE(z >= kZero); + EXPECT_TRUE(z <= kZero); + EXPECT_FALSE(z != kZero); + EXPECT_FALSE(z > kZero); + EXPECT_FALSE(z < kZero); + } + + { + IOUAmount const neg(-2, 0); + EXPECT_TRUE(neg < kZero); + EXPECT_TRUE(neg <= kZero); + EXPECT_TRUE(neg != kZero); + EXPECT_FALSE(neg == kZero); + } + + { + IOUAmount const pos(2, 0); + EXPECT_TRUE(pos > kZero); + EXPECT_TRUE(pos >= kZero); + EXPECT_TRUE(pos != kZero); + EXPECT_FALSE(pos == kZero); + } +} + +TEST(IOUAmountTest, comparisons) +{ + IOUAmount const n(-2, 0); + IOUAmount const z(0, 0); + IOUAmount const p(2, 0); + // For code readability, we want to use general + // EXPECT_TRUE instead of specific EXPECT_EQ etc. + EXPECT_TRUE(z == z); + EXPECT_TRUE(z >= z); + EXPECT_TRUE(z <= z); + EXPECT_TRUE(z == -z); + // NOLINTBEGIN(misc-redundant-expression) + EXPECT_FALSE(z > z); + EXPECT_FALSE(z < z); + EXPECT_FALSE(z != z); + // NOLINTEND(misc-redundant-expression) + EXPECT_FALSE(z != -z); + + EXPECT_TRUE(n < z); + EXPECT_TRUE(n <= z); + EXPECT_TRUE(n != z); + EXPECT_FALSE(n > z); + EXPECT_FALSE(n >= z); + EXPECT_FALSE(n == z); + + EXPECT_TRUE(p > z); + EXPECT_TRUE(p >= z); + EXPECT_TRUE(p != z); + EXPECT_FALSE(p < z); + EXPECT_FALSE(p <= z); + EXPECT_FALSE(p == z); + + EXPECT_TRUE(n < p); + EXPECT_TRUE(n <= p); + EXPECT_TRUE(n != p); + EXPECT_FALSE(n > p); + EXPECT_FALSE(n >= p); + EXPECT_FALSE(n == p); + + EXPECT_TRUE(p > n); + EXPECT_TRUE(p >= n); + EXPECT_TRUE(p != n); + EXPECT_FALSE(p < n); + EXPECT_FALSE(p <= n); + EXPECT_FALSE(p == n); + + EXPECT_TRUE(p > -p); + EXPECT_TRUE(p >= -p); + EXPECT_TRUE(p != -p); + + EXPECT_TRUE(n < -n); + EXPECT_TRUE(n <= -n); + EXPECT_TRUE(n != -n); +} + +TEST(IOUAmountTest, to_string) +{ + auto test = [](IOUAmount const& n, std::string const& expected) { + auto const result = to_string(n); + std::stringstream ss; + ss << "to_string(" << result << "). Expected: " << expected; + EXPECT_EQ(result, expected) << ss.str(); + }; + + for (auto const mantissaSize : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const mg(mantissaSize); + + test(IOUAmount(-2, 0), "-2"); + test(IOUAmount(0, 0), "0"); + test(IOUAmount(2, 0), "2"); + test(IOUAmount(25, -3), "0.025"); + test(IOUAmount(-25, -3), "-0.025"); + test(IOUAmount(25, 1), "250"); + test(IOUAmount(-25, 1), "-250"); + test(IOUAmount(2, 20), "2e20"); + test(IOUAmount(-2, -20), "-2e-20"); + } +} + +TEST(IOUAmountTest, mul_ratio) +{ + /* The range for the mantissa when normalized */ + constexpr std::int64_t kMinMantissa = 1000000000000000ull; + constexpr std::int64_t kMaxMantissa = 9999999999999999ull; + // log(2,maxMantissa) ~ 53.15 + /* The range for the exponent when normalized */ + constexpr int kMinExponent = -96; + constexpr int kMaxExponent = 80; + constexpr auto kMaxUInt = std::numeric_limits::max(); + + { + // multiply by a number that would overflow the mantissa, then + // divide by the same number, and check we didn't lose any value + IOUAmount const bigMan(kMaxMantissa, 0); + EXPECT_EQ(bigMan, mulRatio(bigMan, kMaxUInt, kMaxUInt, true)); + // rounding mode shouldn't matter as the result is exact + EXPECT_EQ(bigMan, mulRatio(bigMan, kMaxUInt, kMaxUInt, false)); + } + { + // Similar test as above, but for negative values + IOUAmount const bigMan(-kMaxMantissa, 0); + EXPECT_EQ(bigMan, mulRatio(bigMan, kMaxUInt, kMaxUInt, true)); + // rounding mode shouldn't matter as the result is exact + EXPECT_EQ(bigMan, mulRatio(bigMan, kMaxUInt, kMaxUInt, false)); + } + + { + // small amounts + IOUAmount const tiny(kMinMantissa, kMinExponent); + // Round up should give the smallest allowable number + EXPECT_EQ(tiny, mulRatio(tiny, 1, kMaxUInt, true)); + EXPECT_EQ(tiny, mulRatio(tiny, kMaxUInt - 1, kMaxUInt, true)); + // rounding down should be zero + EXPECT_EQ(beast::kZero, mulRatio(tiny, 1, kMaxUInt, false)); + EXPECT_EQ(beast::kZero, mulRatio(tiny, kMaxUInt - 1, kMaxUInt, false)); + + // tiny negative numbers + IOUAmount const tinyNeg(-kMinMantissa, kMinExponent); + // Round up should give zero + EXPECT_EQ(beast::kZero, mulRatio(tinyNeg, 1, kMaxUInt, true)); + EXPECT_EQ(beast::kZero, mulRatio(tinyNeg, kMaxUInt - 1, kMaxUInt, true)); + // rounding down should be tiny + EXPECT_EQ(tinyNeg, mulRatio(tinyNeg, 1, kMaxUInt, false)); + EXPECT_EQ(tinyNeg, mulRatio(tinyNeg, kMaxUInt - 1, kMaxUInt, false)); + } + + { // rounding + { + IOUAmount const one(1, 0); + auto const rup = mulRatio(one, kMaxUInt - 1, kMaxUInt, true); + auto const rdown = mulRatio(one, kMaxUInt - 1, kMaxUInt, false); + EXPECT_EQ(rup.mantissa() - rdown.mantissa(), 1); + } + { + IOUAmount const big(kMaxMantissa, kMaxExponent); + auto const rup = mulRatio(big, kMaxUInt - 1, kMaxUInt, true); + auto const rdown = mulRatio(big, kMaxUInt - 1, kMaxUInt, false); + EXPECT_EQ(rup.mantissa() - rdown.mantissa(), 1); + } + + { + IOUAmount const negOne(-1, 0); + auto const rup = mulRatio(negOne, kMaxUInt - 1, kMaxUInt, true); + auto const rdown = mulRatio(negOne, kMaxUInt - 1, kMaxUInt, false); + EXPECT_EQ(rup.mantissa() - rdown.mantissa(), 1); + } + } + + { + // division by zero + IOUAmount const one(1, 0); + EXPECT_ANY_THROW({ mulRatio(one, 1, 0, true); }); + } + + { + // overflow + IOUAmount const big(kMaxMantissa, kMaxExponent); + EXPECT_ANY_THROW({ mulRatio(big, 2, 0, true); }); + } +} + +} // namespace xrpl diff --git a/src/tests/libxrpl/basics/IntrusiveShared.cpp b/src/tests/libxrpl/basics/IntrusiveShared.cpp new file mode 100644 index 0000000000..e798cd1ccc --- /dev/null +++ b/src/tests/libxrpl/basics/IntrusiveShared.cpp @@ -0,0 +1,844 @@ +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include + +#include + +#include +#include +#include +#include // IWYU pragma: keep +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::tests { + +/* + * Experimentally, we discovered that using std::barrier performs extremely + * poorly (~1 hour vs ~1 minute to run the test suite) in certain macOS + * environments. To unblock our macOS CI pipeline, we replaced std::barrier with a + * custom mutex-based barrier (Barrier) that significantly improves performance + * without compromising correctness. For future reference, if we ever consider + * reintroducing std::barrier, the following configuration is known to exhibit the + * problem: + * + * Model Name: Mac mini + * Model Identifier: Mac14,3 + * Model Number: Z16K000R4LL/A + * Chip: Apple M2 + * Total Number of Cores: 8 (4 performance and 4 efficiency) + * Memory: 24 GB + * System Firmware Version: 11881.41.5 + * OS Loader Version: 11881.1.1 + * Apple clang version 16.0.0 (clang-1600.0.26.3) + * Target: arm64-apple-darwin24.0.0 + * Thread model: posix + * + */ +struct Barrier +{ + std::mutex mtx; + std::condition_variable cv; + int count; + int const initial; + std::size_t generation{0}; + + explicit Barrier(int n) : count(n), initial(n) + { + } + + void + arriveAndWait() + { + std::unique_lock lock(mtx); + auto const currentGeneration = generation; + if (--count == 0) + { + ++generation; + count = initial; + cv.notify_all(); + } + else + { + cv.wait(lock, [&] { return generation != currentGeneration; }); + } + } +}; + +namespace { +enum class TrackedState : std::uint8_t { + Uninitialized, + Alive, + PartiallyDeletedStarted, + PartiallyDeleted, + DeletedStarted, + Deleted +}; + +class TIBase : public IntrusiveRefCounts +{ +public: + static constexpr std::size_t kMaxStates = 128; + static std::array, kMaxStates> state; + static std::atomic nextId; + static TrackedState + getState(std::size_t id) + { + if (id >= state.size()) + throw std::out_of_range("TIBase state id out of range"); + + return state[id].load(std::memory_order_acquire); + } + static void + resetStates(bool resetCallback) + { + for (std::size_t i = 0; i < kMaxStates; ++i) + { + state[i].store(TrackedState::Uninitialized, std::memory_order_release); + } + nextId.store(0, std::memory_order_release); + if (resetCallback) + TIBase::tracingCallback = [](TrackedState, std::optional) {}; + } + + struct ResetStatesGuard + { + bool resetCallback{false}; + + ResetStatesGuard(bool resetCallback) : resetCallback{resetCallback} + { + TIBase::resetStates(resetCallback); + } + ~ResetStatesGuard() + { + TIBase::resetStates(resetCallback); + } + }; + + TIBase() : id{checkoutID()} + { + state[id].store(TrackedState::Alive, std::memory_order_relaxed); + } + ~TIBase() override + { + using enum TrackedState; + + tracingCallback(state[id].load(std::memory_order_relaxed), DeletedStarted); + + // Use relaxed memory order to try to avoid atomic operations from + // adding additional memory synchronizations that may hide threading + // errors in the underlying shared pointer class. + state[id].store(DeletedStarted, std::memory_order_relaxed); + + tracingCallback(DeletedStarted, Deleted); + + state[id].store(TrackedState::Deleted, std::memory_order_relaxed); + + tracingCallback(TrackedState::Deleted, std::nullopt); + } + + void + partialDestructor() const + { + using enum TrackedState; + + tracingCallback(state[id].load(std::memory_order_relaxed), PartiallyDeletedStarted); + + state[id].store(PartiallyDeletedStarted, std::memory_order_relaxed); + + tracingCallback(PartiallyDeletedStarted, PartiallyDeleted); + + state[id].store(PartiallyDeleted, std::memory_order_relaxed); + + tracingCallback(PartiallyDeleted, std::nullopt); + } + + static std::function)> tracingCallback; + + std::size_t const id; + +private: + static std::size_t + checkoutID() + { + auto const id = nextId.fetch_add(1, std::memory_order_acq_rel); + if (id >= state.size()) + throw std::out_of_range("TIBase state capacity exceeded"); + + return id; + } +}; + +std::array, TIBase::kMaxStates> TIBase::state; +std::atomic TIBase::nextId{0}; + +std::function)> TIBase::tracingCallback = + [](TrackedState, std::optional) {}; + +} // namespace + +TEST(IntrusiveSharedTest, basics) +{ + { + TIBase::ResetStatesGuard const rsg{true}; + + TIBase const b; + EXPECT_EQ(b.useCount(), 1); + b.addWeakRef(); + EXPECT_EQ(b.useCount(), 1); + auto s = b.releaseStrongRef(); + EXPECT_EQ(s, ReleaseStrongRefAction::PartialDestroy); + EXPECT_EQ(b.useCount(), 0); + TIBase const* pb = &b; + partialDestructorFinished(&pb); + EXPECT_FALSE(pb); + auto w = b.releaseWeakRef(); + EXPECT_EQ(w, ReleaseWeakRefAction::Destroy); + } + + std::vector> strong; + std::vector> weak; + { + TIBase::ResetStatesGuard const rsg{true}; + + using enum TrackedState; + auto b = makeSharedIntrusive(); + auto id = b->id; + EXPECT_EQ(TIBase::getState(id), Alive); + EXPECT_EQ(b->useCount(), 1); + for (int i = 0; i < 10; ++i) + { + strong.push_back(b); + } + b.reset(); + EXPECT_EQ(TIBase::getState(id), Alive); + strong.resize(strong.size() - 1); + EXPECT_EQ(TIBase::getState(id), Alive); + strong.clear(); + EXPECT_EQ(TIBase::getState(id), Deleted); + + b = makeSharedIntrusive(); + id = b->id; + EXPECT_EQ(TIBase::getState(id), Alive); + EXPECT_EQ(b->useCount(), 1); + for (int i = 0; i < 10; ++i) + { + weak.emplace_back(b); + EXPECT_EQ(b->useCount(), 1); + } + EXPECT_EQ(TIBase::getState(id), Alive); + weak.resize(weak.size() - 1); + EXPECT_EQ(TIBase::getState(id), Alive); + b.reset(); + EXPECT_EQ(TIBase::getState(id), PartiallyDeleted); + while (!weak.empty()) + { + weak.resize(weak.size() - 1); + if (!weak.empty()) + { + EXPECT_EQ(TIBase::getState(id), PartiallyDeleted); + } + } + EXPECT_EQ(TIBase::getState(id), Deleted); + } + { + TIBase::ResetStatesGuard const rsg{true}; + + using enum TrackedState; + auto b = makeSharedIntrusive(); + auto id = b->id; + EXPECT_EQ(TIBase::getState(id), Alive); + WeakIntrusive w{b}; + EXPECT_EQ(TIBase::getState(id), Alive); + auto s = w.lock(); + EXPECT_TRUE(s && s->useCount() == 2); + b.reset(); + EXPECT_TRUE(TIBase::getState(id) == Alive); + EXPECT_TRUE(s && s->useCount() == 1); + s.reset(); + EXPECT_EQ(TIBase::getState(id), PartiallyDeleted); + EXPECT_TRUE(w.expired()); + s = w.lock(); + // Cannot convert a weak pointer to a strong pointer if object is + // already partially deleted + EXPECT_FALSE(s); + w.reset(); + EXPECT_EQ(TIBase::getState(id), Deleted); + } + { + TIBase::ResetStatesGuard const rsg{true}; + + using enum TrackedState; + using swu = SharedWeakUnion; + swu b = makeSharedIntrusive(); + EXPECT_TRUE(b.isStrong() && b.useCount() == 1); + auto id = b.get()->id; + EXPECT_EQ(TIBase::getState(id), Alive); + swu w = b; + EXPECT_TRUE(TIBase::getState(id) == Alive); + EXPECT_TRUE(w.isStrong() && b.useCount() == 2); + w.convertToWeak(); + EXPECT_TRUE(w.isWeak() && b.useCount() == 1); + swu s = w; + EXPECT_TRUE(s.isWeak() && b.useCount() == 1); + s.convertToStrong(); + EXPECT_TRUE(s.isStrong() && b.useCount() == 2); + b.reset(); + EXPECT_EQ(TIBase::getState(id), Alive); + EXPECT_EQ(s.useCount(), 1); + EXPECT_FALSE(w.expired()); + s.reset(); + EXPECT_EQ(TIBase::getState(id), PartiallyDeleted); + EXPECT_TRUE(w.expired()); + w.convertToStrong(); + // Cannot convert a weak pointer to a strong pointer if object is + // already partially deleted + EXPECT_TRUE(w.isWeak()); + w.reset(); + EXPECT_EQ(TIBase::getState(id), Deleted); + } + { + // Testing SharedWeakUnion assignment operator + + TIBase::ResetStatesGuard const rsg{true}; + + auto strong1 = makeSharedIntrusive(); + auto strong2 = makeSharedIntrusive(); + + auto id1 = strong1->id; + auto id2 = strong2->id; + + EXPECT_NE(id1, id2); + + SharedWeakUnion union1 = strong1; + SharedWeakUnion union2 = strong2; + + EXPECT_TRUE(union1.isStrong()); + EXPECT_TRUE(union2.isStrong()); + EXPECT_EQ(union1.get(), strong1.get()); + EXPECT_EQ(union2.get(), strong2.get()); + + // 1) Normal assignment: explicitly calls SharedWeakUnion assignment + union1 = union2; + EXPECT_TRUE(union1.isStrong()); + EXPECT_TRUE(union2.isStrong()); + EXPECT_EQ(union1.get(), union2.get()); + EXPECT_EQ(TIBase::getState(id1), TrackedState::Alive); + EXPECT_EQ(TIBase::getState(id2), TrackedState::Alive); + + // 2) Test self-assignment + EXPECT_TRUE(union1.isStrong()); + EXPECT_EQ(TIBase::getState(id1), TrackedState::Alive); + int const initialRefCount = strong1->useCount(); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wself-assign-overloaded" + union1 = union1; // Self-assignment +#pragma clang diagnostic pop + EXPECT_TRUE(union1.isStrong()); + EXPECT_EQ(TIBase::getState(id1), TrackedState::Alive); + EXPECT_EQ(strong1->useCount(), initialRefCount); + + // 3) Test assignment from null union pointer + union1 = SharedWeakUnion(); + EXPECT_EQ(union1.get(), nullptr); + + // 4) Test assignment to expired union pointer + strong2.reset(); + union2.reset(); + union1 = union2; + EXPECT_EQ(union1.get(), nullptr); + EXPECT_EQ(TIBase::getState(id2), TrackedState::Deleted); + } +} + +TEST(IntrusiveSharedTest, partial_delete) +{ + // This test creates two threads. One with a strong pointer and one + // with a weak pointer. The strong pointer is reset while the weak + // pointer still holds a reference, triggering a partial delete. + // While the partial delete function runs (a sleep is inserted) the + // weak pointer is reset. The destructor should wait to run until + // after the partial delete function has completed running. + + using enum TrackedState; + + TIBase::ResetStatesGuard const rsg{true}; + + auto strong = makeSharedIntrusive(); + WeakIntrusive weak{strong}; + std::atomic destructorRan{false}; + std::atomic partialDeleteRan{false}; + std::latch partialDeleteStartedSyncPoint{2}; + strong->tracingCallback = [&](TrackedState cur, std::optional next) { + using enum TrackedState; + if (next == DeletedStarted) + { + // strong goes out of scope while weak is still in scope + // This checks that partialDelete has run to completion + // before the destructor is called. A sleep is inserted + // inside the partial delete to make sure the destructor is + // given an opportunity to run during partial delete. + EXPECT_EQ(cur, PartiallyDeleted); + } + if (next == PartiallyDeletedStarted) + { + partialDeleteStartedSyncPoint.arrive_and_wait(); + using namespace std::chrono_literals; + // Sleep and let the weak pointer go out of scope, + // potentially triggering a destructor while partial delete + // is running. The test is to make sure that doesn't happen. + std::this_thread::sleep_for(800ms); + } + if (next == PartiallyDeleted) + { + EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load()); + } + if (next == Deleted) + { + EXPECT_FALSE(destructorRan.exchange(true)); + } + }; + std::thread t1{[&] { + partialDeleteStartedSyncPoint.arrive_and_wait(); + weak.reset(); // Trigger a full delete as soon as the partial + // delete starts + }}; + std::thread t2{[&] { + strong.reset(); // Trigger a partial delete + }}; + t1.join(); + t2.join(); + + EXPECT_TRUE(destructorRan.load() && partialDeleteRan.load()); +} + +TEST(IntrusiveSharedTest, destructor) +{ + // This test creates two threads. One with a strong pointer and one + // with a weak pointer. The weak pointer is reset while the strong + // pointer still holds a reference. Then the strong pointer is + // reset. Only the destructor should run. The partial destructor + // should not be called. Since the weak reset runs to completion + // before the strong pointer is reset, threading doesn't add much to + // this test, but there is no harm in keeping it. + + using enum TrackedState; + + TIBase::ResetStatesGuard const rsg{true}; + + auto strong = makeSharedIntrusive(); + WeakIntrusive weak{strong}; + std::atomic destructorRan{false}; + std::atomic partialDeleteRan{false}; + std::latch weakResetSyncPoint{2}; + strong->tracingCallback = [&](TrackedState cur, std::optional next) { + using enum TrackedState; + if (next == PartiallyDeleted) + { + EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load()); + } + if (next == Deleted) + { + EXPECT_FALSE(destructorRan.exchange(true)); + } + }; + std::thread t1{[&] { + weak.reset(); + weakResetSyncPoint.arrive_and_wait(); + }}; + std::thread t2{[&] { + weakResetSyncPoint.arrive_and_wait(); + strong.reset(); // Trigger a partial delete + }}; + t1.join(); + t2.join(); + + EXPECT_TRUE(destructorRan.load() && !partialDeleteRan.load()); +} + +TEST(IntrusiveSharedTest, multithreaded_clear_mixed_variant) +{ + // This test creates and destroys many strong and weak pointers in a + // loop. There is a random mix of strong and weak pointers stored in + // a vector (held as a variant). Both threads clear all the pointers + // and check that the invariants hold. + + using enum TrackedState; + TIBase::ResetStatesGuard const rsg{true}; + + std::atomic destructionState{0}; + // returns destructorRan and partialDestructorRan (in that order) + auto getDestructorState = [&]() -> std::pair { + int const s = destructionState.load(std::memory_order_relaxed); + return {(s & 1) != 0, (s & 2) != 0}; + }; + auto setDestructorRan = [&]() -> void { + destructionState.fetch_or(1, std::memory_order_acq_rel); + }; + auto setPartialDeleteRan = [&]() -> void { + destructionState.fetch_or(2, std::memory_order_acq_rel); + }; + auto tracingCallback = [&](TrackedState cur, std::optional next) { + using enum TrackedState; + auto [destructorRan, partialDeleteRan] = getDestructorState(); + if (next == PartiallyDeleted) + { + EXPECT_FALSE(partialDeleteRan || destructorRan); + setPartialDeleteRan(); + } + if (next == Deleted) + { + EXPECT_FALSE(destructorRan); + setDestructorRan(); + } + }; + auto createVecOfPointers = [&](auto const& toClone, std::default_random_engine& eng) + -> std::vector, WeakIntrusive>> { + std::vector, WeakIntrusive>> result; + std::uniform_int_distribution<> toCreateDist(4, 64); + std::uniform_int_distribution<> isStrongDist(0, 1); + auto numToCreate = toCreateDist(eng); + result.reserve(numToCreate); + for (int i = 0; i < numToCreate; ++i) + { + if (isStrongDist(eng)) + { + result.emplace_back(SharedIntrusive(toClone)); + } + else + { + result.emplace_back(WeakIntrusive(toClone)); + } + } + return result; + }; + constexpr int kLoopIters = 2 * 1024; + constexpr int kNumThreads = 16; + std::vector> toClone; + Barrier loopStartSyncPoint{kNumThreads}; + Barrier postCreateToCloneSyncPoint{kNumThreads}; + Barrier postCreateVecOfPointersSyncPoint{kNumThreads}; + auto engines = [&]() -> std::vector { + std::random_device rd; + std::vector result; + result.reserve(kNumThreads); + for (int i = 0; i < kNumThreads; ++i) + result.emplace_back(rd()); + return result; + }(); + + // cloneAndDestroy clones the strong pointer into a vector of mixed + // strong and weak pointers and destroys them all at once. + // threadId==0 is special. + auto cloneAndDestroy = [&](int threadId) { + for (int i = 0; i < kLoopIters; ++i) + { + // ------ Sync Point ------ + loopStartSyncPoint.arriveAndWait(); + + // only thread 0 should reset the state + std::optional rsg; + if (threadId == 0) + { + // Thread 0 is the genesis thread. It creates the strong + // pointers to be cloned by the other threads. This + // thread will also check that the destructor ran and + // clear the temporary variables. + + rsg.emplace(false); + auto [destructorRan, partialDeleteRan] = getDestructorState(); + EXPECT_TRUE(i == 0 || destructorRan); + destructionState.store(0, std::memory_order_release); + + toClone.clear(); + toClone.resize(kNumThreads); + auto strong = makeSharedIntrusive(); + strong->tracingCallback = tracingCallback; + std::ranges::fill(toClone, strong); + } + + // ------ Sync Point ------ + postCreateToCloneSyncPoint.arriveAndWait(); + + auto v = createVecOfPointers(toClone[threadId], engines[threadId]); + toClone[threadId].reset(); + + // ------ Sync Point ------ + postCreateVecOfPointersSyncPoint.arriveAndWait(); + + v.clear(); + } + }; + std::vector threads; + threads.reserve(kNumThreads); + for (int i = 0; i < kNumThreads; ++i) + { + threads.emplace_back(cloneAndDestroy, i); + } + for (int i = 0; i < kNumThreads; ++i) + { + threads[i].join(); + } +} + +TEST(IntrusiveSharedTest, multithreaded_clear_mixed_union) +{ + // This test creates and destroys many SharedWeak pointers in a + // loop. All the pointers start as strong and a loop randomly + // convert them between strong and weak pointers. Both threads clear + // all the pointers and check that the invariants hold. + // + // Note: This test also differs from the test above in that the pointers + // randomly change from strong to weak and from weak to strong in a + // loop. This can't be done in the variant test above because variant is + // not thread safe while the SharedWeakUnion is thread safe. + + using enum TrackedState; + + TIBase::ResetStatesGuard const rsg{true}; + + std::atomic destructionState{0}; + // returns destructorRan and partialDestructorRan (in that order) + auto getDestructorState = [&]() -> std::pair { + int const s = destructionState.load(std::memory_order_relaxed); + return {(s & 1) != 0, (s & 2) != 0}; + }; + auto setDestructorRan = [&]() -> void { + destructionState.fetch_or(1, std::memory_order_acq_rel); + }; + auto setPartialDeleteRan = [&]() -> void { + destructionState.fetch_or(2, std::memory_order_acq_rel); + }; + auto tracingCallback = [&](TrackedState cur, std::optional next) { + using enum TrackedState; + auto [destructorRan, partialDeleteRan] = getDestructorState(); + if (next == PartiallyDeleted) + { + EXPECT_FALSE(partialDeleteRan || destructorRan); + setPartialDeleteRan(); + } + if (next == Deleted) + { + EXPECT_FALSE(destructorRan); + setDestructorRan(); + } + }; + auto createVecOfPointers = + [&](auto const& toClone, + std::default_random_engine& eng) -> std::vector> { + std::vector> result; + std::uniform_int_distribution<> toCreateDist(4, 64); + auto numToCreate = toCreateDist(eng); + result.reserve(numToCreate); + for (int i = 0; i < numToCreate; ++i) + result.emplace_back(SharedIntrusive(toClone)); + return result; + }; + constexpr int kLoopIters = 2 * 1024; + constexpr int kFlipPointersLoopIters = 256; + constexpr int kNumThreads = 16; + std::vector> toClone; + Barrier loopStartSyncPoint{kNumThreads}; + Barrier postCreateToCloneSyncPoint{kNumThreads}; + Barrier postCreateVecOfPointersSyncPoint{kNumThreads}; + Barrier postFlipPointersLoopSyncPoint{kNumThreads}; + auto engines = [&]() -> std::vector { + std::random_device rd; + std::vector result; + result.reserve(kNumThreads); + for (int i = 0; i < kNumThreads; ++i) + result.emplace_back(rd()); + return result; + }(); + + // cloneAndDestroy clones the strong pointer into a vector of + // mixed strong and weak pointers, runs a loop that randomly + // changes strong pointers to weak pointers, and destroys them + // all at once. + auto cloneAndDestroy = [&](int threadId) { + for (int i = 0; i < kLoopIters; ++i) + { + // ------ Sync Point ------ + loopStartSyncPoint.arriveAndWait(); + + // only thread 0 should reset the state + std::optional rsg; + if (threadId == 0) + { + // threadId 0 is the genesis thread. It creates the + // strong point to be cloned by the other threads. This + // thread will also check that the destructor ran and + // clear the temporary variables. + rsg.emplace(false); + auto [destructorRan, partialDeleteRan] = getDestructorState(); + EXPECT_TRUE(i == 0 || destructorRan); + destructionState.store(0, std::memory_order_release); + + toClone.clear(); + toClone.resize(kNumThreads); + auto strong = makeSharedIntrusive(); + strong->tracingCallback = tracingCallback; + std::ranges::fill(toClone, strong); + } + + // ------ Sync Point ------ + postCreateToCloneSyncPoint.arriveAndWait(); + + auto v = createVecOfPointers(toClone[threadId], engines[threadId]); + toClone[threadId].reset(); + + // ------ Sync Point ------ + postCreateVecOfPointersSyncPoint.arriveAndWait(); + + std::uniform_int_distribution<> isStrongDist(0, 1); + for (int f = 0; f < kFlipPointersLoopIters; ++f) + { + for (auto& p : v) + { + if (isStrongDist(engines[threadId])) + { + p.convertToStrong(); + } + else + { + p.convertToWeak(); + } + } + } + + // ------ Sync Point ------ + postFlipPointersLoopSyncPoint.arriveAndWait(); + + v.clear(); + } + }; + std::vector threads; + threads.reserve(kNumThreads); + for (int i = 0; i < kNumThreads; ++i) + { + threads.emplace_back(cloneAndDestroy, i); + } + for (int i = 0; i < kNumThreads; ++i) + { + threads[i].join(); + } +} + +TEST(IntrusiveSharedTest, multithreaded_locking_weak) +{ + // This test creates a single shared atomic pointer that multiple thread + // create weak pointers from. The threads then lock the weak pointers. + // Both threads clear all the pointers and check that the invariants + // hold. + + using enum TrackedState; + + TIBase::ResetStatesGuard const rsg{true}; + + std::atomic destructionState{0}; + // returns destructorRan and partialDestructorRan (in that order) + auto getDestructorState = [&]() -> std::pair { + int const s = destructionState.load(std::memory_order_relaxed); + return {(s & 1) != 0, (s & 2) != 0}; + }; + auto setDestructorRan = [&]() -> void { + destructionState.fetch_or(1, std::memory_order_acq_rel); + }; + auto setPartialDeleteRan = [&]() -> void { + destructionState.fetch_or(2, std::memory_order_acq_rel); + }; + auto tracingCallback = [&](TrackedState cur, std::optional next) { + using enum TrackedState; + auto [destructorRan, partialDeleteRan] = getDestructorState(); + if (next == PartiallyDeleted) + { + EXPECT_FALSE(partialDeleteRan || destructorRan); + setPartialDeleteRan(); + } + if (next == Deleted) + { + EXPECT_FALSE(destructorRan); + setDestructorRan(); + } + }; + + constexpr int kLoopIters = 2 * 1024; + constexpr int kLockWeakLoopIters = 256; + constexpr int kNumThreads = 16; + std::vector> toLock; + Barrier loopStartSyncPoint{kNumThreads}; + Barrier postCreateToLockSyncPoint{kNumThreads}; + Barrier postLockWeakLoopSyncPoint{kNumThreads}; + + // lockAndDestroy creates weak pointers from the strong pointer + // and runs a loop that locks the weak pointer. At the end of the loop + // all the pointers are destroyed all at once. + auto lockAndDestroy = [&](int threadId) { + for (int i = 0; i < kLoopIters; ++i) + { + // ------ Sync Point ------ + loopStartSyncPoint.arriveAndWait(); + + // only thread 0 should reset the state + std::optional rsg; + if (threadId == 0) + { + // threadId 0 is the genesis thread. It creates the + // strong point to be locked by the other threads. This + // thread will also check that the destructor ran and + // clear the temporary variables. + rsg.emplace(false); + auto [destructorRan, partialDeleteRan] = getDestructorState(); + EXPECT_TRUE(i == 0 || destructorRan); + destructionState.store(0, std::memory_order_release); + + toLock.clear(); + toLock.resize(kNumThreads); + auto strong = makeSharedIntrusive(); + strong->tracingCallback = tracingCallback; + std::ranges::fill(toLock, strong); + } + + // ------ Sync Point ------ + postCreateToLockSyncPoint.arriveAndWait(); + + // Multiple threads all create a weak pointer from the same + // strong pointer + WeakIntrusive const weak{toLock[threadId]}; + for (int wi = 0; wi < kLockWeakLoopIters; ++wi) + { + EXPECT_FALSE(weak.expired()); + auto strong = weak.lock(); + EXPECT_TRUE(strong); + } + + // ------ Sync Point ------ + postLockWeakLoopSyncPoint.arriveAndWait(); + + toLock[threadId].reset(); + } + }; + std::vector threads; + threads.reserve(kNumThreads); + for (int i = 0; i < kNumThreads; ++i) + { + threads.emplace_back(lockAndDestroy, i); + } + for (int i = 0; i < kNumThreads; ++i) + { + threads[i].join(); + } +} + +} // namespace xrpl::tests diff --git a/src/tests/libxrpl/basics/KeyCache.cpp b/src/tests/libxrpl/basics/KeyCache.cpp new file mode 100644 index 0000000000..061264b11d --- /dev/null +++ b/src/tests/libxrpl/basics/KeyCache.cpp @@ -0,0 +1,81 @@ +#include +#include // IWYU pragma: keep +#include +#include +#include + +#include +#include + +#include + +namespace xrpl { + +class KeyCacheTest : public ::testing::Test +{ +public: +}; + +TEST_F(KeyCacheTest, key_cache) +{ + using namespace std::chrono_literals; + TestStopwatch clock; + clock.set(0); + + using Key = std::string; + using Cache = TaggedCache; + + beast::Journal const j{TestSink::instance()}; + + // Insert an item, retrieve it, and age it so it gets purged. + { + Cache c("test", LedgerIndex(1), 2s, clock, j); + + EXPECT_EQ(c.size(), 0); + EXPECT_TRUE(c.insert("one")); + EXPECT_FALSE(c.insert("one")); + EXPECT_EQ(c.size(), 1); + EXPECT_TRUE(c.touchIfExists("one")); + ++clock; + c.sweep(); + EXPECT_EQ(c.size(), 1); + ++clock; + c.sweep(); + EXPECT_EQ(c.size(), 0); + EXPECT_FALSE(c.touchIfExists("one")); + } + + // Insert two items, have one expire + { + Cache c("test", LedgerIndex(2), 2s, clock, j); + + EXPECT_TRUE(c.insert("one")); + EXPECT_EQ(c.size(), 1); + EXPECT_TRUE(c.insert("two")); + EXPECT_EQ(c.size(), 2); + ++clock; + c.sweep(); + EXPECT_EQ(c.size(), 2); + EXPECT_TRUE(c.touchIfExists("two")); + ++clock; + c.sweep(); + EXPECT_EQ(c.size(), 1); + } + + // Insert three items (1 over limit), sweep + { + Cache c("test", LedgerIndex(2), 3s, clock, j); + + EXPECT_TRUE(c.insert("one")); + ++clock; + EXPECT_TRUE(c.insert("two")); + ++clock; + EXPECT_TRUE(c.insert("three")); + ++clock; + EXPECT_EQ(c.size(), 3); + c.sweep(); + EXPECT_LT(c.size(), 3); + } +} + +} // namespace xrpl diff --git a/src/test/basics/Number_test.cpp b/src/tests/libxrpl/basics/Number.cpp similarity index 73% rename from src/test/basics/Number_test.cpp rename to src/tests/libxrpl/basics/Number.cpp index 9383fd6808..e8c4e4209f 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/tests/libxrpl/basics/Number.cpp @@ -1,5 +1,5 @@ #include -#include + #include #include #include @@ -10,6 +10,8 @@ #include #include +#include + #include #include #include @@ -27,82 +29,81 @@ namespace xrpl { -class Number_test : public beast::unit_test::Suite +using BigInt = boost::multiprecision::cpp_int; +using Dec = boost::multiprecision::cpp_dec_float_50; + +static std::string +fmt(BigInt const& value) { - using BigInt = boost::multiprecision::cpp_int; - - static std::string - fmt(BigInt const& value) + auto s = to_string(value); + std::string out; + int count = 0; + for (char const& ch : std::views::reverse(s)) { - auto s = to_string(value); - std::string out; - int count = 0; - for (char const& ch : std::views::reverse(s)) - { - if (count != 0 && count % 3 == 0 && (isdigit(ch) != 0)) - out.insert(out.begin(), '_'); - out.insert(out.begin(), ch); - ++count; - } - return out; + if (count != 0 && count % 3 == 0 && (isdigit(ch) != 0)) + out.insert(out.begin(), '_'); + out.insert(out.begin(), ch); + ++count; + } + return out; +} + +template +static T +pow10(int n) +{ + if (n == 0) + return 1; + if (n == 1) + return 10; + + if (n > 1) + { + auto r = pow10(n / 2); + r *= r; + if (n % 2 != 0) + r *= 10; + return r; } - using dec = boost::multiprecision::cpp_dec_float_50; + T p = 1; + p /= pow10(-n); + return p; +} - template - static T - pow10(int n) +static std::string +fmt(Dec const& value) +{ + std::ostringstream os; + os << std::setprecision(40) << value; + return os.str(); +} + +TEST(NumberTest, zero) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { - if (n == 0) - return 1; - if (n == 1) - return 10; - - if (n > 1) - { - auto r = pow10(n / 2); - r *= r; - if (n % 2 != 0) - r *= 10; - return r; - } - - // n < 0 - T p = 1; - p /= pow10(-n); - return p; - } - - static std::string - fmt(dec const& v) - { - std::ostringstream os; - os << std::setprecision(40) << v; - return os.str(); - } - -public: - void - testZero() - { - testcase << "zero " << to_string(Number::getMantissaScale()); + NumberMantissaScaleGuard const sg(mantissaScale); for (Number const& z : {Number{0, 0}, Number{0}}) { - BEAST_EXPECT(z.mantissa() == 0); - BEAST_EXPECT(z.exponent() == Number{}.exponent()); + EXPECT_EQ(z.mantissa(), 0); + EXPECT_EQ(z.exponent(), Number{}.exponent()); - BEAST_EXPECT((z + z) == z); - BEAST_EXPECT((z - z) == z); - BEAST_EXPECT(z == -z); + EXPECT_EQ((z + z), z); + EXPECT_EQ((z - z), z); + EXPECT_EQ(z, -z); } } +} - void - testLimits() +TEST(NumberTest, limits) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { + NumberMantissaScaleGuard const sg(mantissaScale); + auto const scale = Number::getMantissaScale(); - testcase << "test_limits " << to_string(scale); bool caught = false; auto const minMantissa = Number::minMantissa(); try @@ -114,13 +115,13 @@ public: { caught = true; } - BEAST_EXPECT(caught); + EXPECT_TRUE(caught); - auto test = [this](auto const& x, auto const& y, int line) { + auto test = [](auto const& x, auto const& y, int line) { auto const result = x == y; std::stringstream ss; ss << x << " == " << y << " -> " << (result ? "true" : "false"); - expect(result, ss.str(), __FILE__, line); + EXPECT_TRUE(result) << ss.str() << " (" << __FILE__ << ":" << line << ")"; }; test( @@ -164,14 +165,17 @@ public: { caught = true; } - BEAST_EXPECT(caught); + EXPECT_TRUE(caught); } +} - void - testAdd() +TEST(NumberTest, add) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { + NumberMantissaScaleGuard const sg(mantissaScale); + auto const scale = Number::getMantissaScale(); - testcase << "test_add " << to_string(scale); using Case = std::tuple; auto const cSmall = std::to_array( @@ -250,11 +254,11 @@ public: Number{2, 19}, }, { - // Does not round. Mantissas are going to be > kMaxRep, so if + // Does not round. Mantissas are going to be > maxRep, so if // added together as uint64_t's, the result will overflow. // With addition using uint128_t, there's no problem. After // normalizing, the resulting mantissa ends up less than - // kMaxRep. + // maxRep. Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}}, Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}}, Number{false, 1'999'999'999'999'999'998ULL, 1, Number::Normalized{}}, @@ -266,13 +270,13 @@ public: auto const cLargeCorrected = std::to_array({ {Number{Number::kMaxRep}, Number{6, -1}, Number{(Number::kMaxRep / 10) + 1, 1}}, }); - auto test = [this](auto const& c) { + auto test = [](auto const& c) { for (auto const& [x, y, z] : c) { auto const result = x + y; std::stringstream ss; ss << x << " + " << y << " = " << result << ". Expected: " << z; - BEAST_EXPECTS(result == z, ss.str()); + EXPECT_EQ(result, z) << ss.str(); } }; if (scale == MantissaRange::MantissaScale::Small) @@ -302,15 +306,18 @@ public: { caught = true; } - BEAST_EXPECT(caught); + EXPECT_TRUE(caught); } } +} - void - testSub() +TEST(NumberTest, sub) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { + NumberMantissaScaleGuard const sg(mantissaScale); + auto const scale = Number::getMantissaScale(); - testcase << "test_sub " << to_string(scale); using Case = std::tuple; auto const cSmall = std::to_array( @@ -370,13 +377,13 @@ public: Number{Number::kMaxRep}}, {power(2, 63), Number{3, 0}, Number{Number::kMaxRep}}, }); - auto test = [this](auto const& c) { + auto test = [](auto const& c) { for (auto const& [x, y, z] : c) { auto const result = x - y; std::stringstream ss; ss << x << " - " << y << " = " << result << ". Expected: " << z; - BEAST_EXPECTS(result == z, ss.str()); + EXPECT_EQ(result, z) << ss.str(); } }; if (scale == MantissaRange::MantissaScale::Small) @@ -388,21 +395,24 @@ public: test(cLarge); } } +} - void - testMul() +TEST(NumberTest, mul) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { + NumberMantissaScaleGuard const sg(mantissaScale); + auto const scale = Number::getMantissaScale(); - testcase << "test_mul " << to_string(scale); using Case = std::tuple; - auto test = [this](auto const& c) { + auto test = [](auto const& c) { for (auto const& [x, y, z] : c) { auto const result = x * y; std::stringstream ss; ss << x << " * " << y << " = " << result << ". Expected: " << z; - BEAST_EXPECTS(result == z, ss.str()); + EXPECT_EQ(result, z) << ss.str(); } }; auto tests = [&](auto const& cSmall, auto const& cLarge) { @@ -484,7 +494,6 @@ public: tests(cSmall, cLarge); } Number::setround(Number::RoundingMode::TowardsZero); - testcase << "test_mul " << to_string(Number::getMantissaScale()) << " towards_zero"; { auto const cSmall = std::to_array( {{Number{7}, Number{8}, Number{56}}, @@ -551,7 +560,6 @@ public: tests(cSmall, cLarge); } Number::setround(Number::RoundingMode::Downward); - testcase << "test_mul " << to_string(Number::getMantissaScale()) << " downward"; { auto const cSmall = std::to_array( {{Number{7}, Number{8}, Number{56}}, @@ -618,7 +626,6 @@ public: tests(cSmall, cLarge); } Number::setround(Number::RoundingMode::Upward); - testcase << "test_mul " << to_string(Number::getMantissaScale()) << " upward"; { auto const cSmall = std::to_array( {{Number{7}, Number{8}, Number{56}}, @@ -684,7 +691,6 @@ public: }); tests(cSmall, cLarge); } - testcase << "test_mul " << to_string(Number::getMantissaScale()) << " overflow"; { bool caught = false; try @@ -696,24 +702,27 @@ public: { caught = true; } - BEAST_EXPECT(caught); + EXPECT_TRUE(caught); } } +} - void - testDiv() +TEST(NumberTest, div) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { + NumberMantissaScaleGuard const sg(mantissaScale); + auto const scale = Number::getMantissaScale(); - testcase << "test_div " << to_string(scale); using Case = std::tuple; - auto test = [this](auto const& c) { + auto test = [](auto const& c) { for (auto const& [x, y, z] : c) { auto const result = x / y; std::stringstream ss; ss << x << " / " << y << " = " << result << ". Expected: " << z; - BEAST_EXPECTS(result == z, ss.str()); + EXPECT_EQ(result, z) << ss.str(); } }; auto const maxMantissa = Number::maxMantissa(); @@ -764,7 +773,6 @@ public: Number{false, maxMantissa, -18, Number::Normalized{}}}}); tests(cSmall, cLarge); } - testcase << "test_div " << to_string(Number::getMantissaScale()) << " towards_zero"; Number::setround(Number::RoundingMode::TowardsZero); { auto const cSmall = std::to_array( @@ -802,7 +810,6 @@ public: Number{false, maxMantissa, -18, Number::Normalized{}}}}); tests(cSmall, cLarge); } - testcase << "test_div " << to_string(Number::getMantissaScale()) << " downward"; Number::setround(Number::RoundingMode::Downward); { auto const cSmall = std::to_array( @@ -840,7 +847,6 @@ public: Number{false, maxMantissa, -18, Number::Normalized{}}}}); tests(cSmall, cLarge); } - testcase << "test_div " << to_string(Number::getMantissaScale()) << " upward"; Number::setround(Number::RoundingMode::Upward); { auto const cSmall = std::to_array( @@ -878,7 +884,6 @@ public: Number{false, maxMantissa, -18, Number::Normalized{}}}}); tests(cSmall, cLarge); } - testcase << "test_div " << to_string(Number::getMantissaScale()) << " overflow"; bool caught = false; try { @@ -888,29 +893,30 @@ public: { caught = true; } - BEAST_EXPECT(caught); + EXPECT_TRUE(caught); } +} - void - testRoot() +TEST(NumberTest, root) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { - auto const scale = Number::getMantissaScale(); - testcase << "test_root " << to_string(scale); + NumberMantissaScaleGuard const sg(mantissaScale); using Case = std::tuple; - auto test = [this](auto const& c) { + auto test = [](auto const& c) { for (auto const& [x, y, z] : c) { auto const result = root(x, y); std::stringstream ss; ss << "root(" << x << ", " << y << ") = " << result << ". Expected: " << z; - BEAST_EXPECTS(result == z, ss.str()); + EXPECT_EQ(result, z) << ss.str(); } }; /* auto tests = [&](auto const& cSmall, auto const& cLarge) { test(cSmall); - if (scale != MantissaRange::MantissaScale::Small) + if (scale != MantissaRange::mantissa_scale::small) test(cLarge); }; */ @@ -954,7 +960,7 @@ public: { caught = true; } - BEAST_EXPECT(caught); + EXPECT_TRUE(caught); caught = false; try { @@ -964,23 +970,24 @@ public: { caught = true; } - BEAST_EXPECT(caught); + EXPECT_TRUE(caught); } +} - void - testRoot2() +TEST(NumberTest, root2) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { - auto const scale = Number::getMantissaScale(); - testcase << "test_root2 " << to_string(scale); + NumberMantissaScaleGuard const sg(mantissaScale); - auto test = [this](auto const& c) { + auto test = [](auto const& c) { for (auto const& x : c) { auto const expected = root(x, 2); auto const result = root2(x); std::stringstream ss; ss << "root2(" << x << ") = " << result << ". Expected: " << expected; - BEAST_EXPECTS(result == expected, ss.str()); + EXPECT_EQ(result, expected) << ss.str(); } }; @@ -1005,13 +1012,16 @@ public: { caught = true; } - BEAST_EXPECT(caught); + EXPECT_TRUE(caught); } +} - void - testPower1() +TEST(NumberTest, power1) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { - testcase << "test_power1 " << to_string(Number::getMantissaScale()); + NumberMantissaScaleGuard const sg(mantissaScale); + using Case = std::tuple; Case const c[]{ {Number{64}, 0, Number{1}}, @@ -1023,13 +1033,16 @@ public: {Number{64}, 11, Number{false, 7378697629483820646ULL, 1, Number::Normalized{}}}, {Number{-64}, 11, Number{true, 7378697629483820646ULL, 1, Number::Normalized{}}}}; for (auto const& [x, y, z] : c) - BEAST_EXPECT((power(x, y) == z)); + EXPECT_EQ(power(x, y), z); } +} - void - testPower2() +TEST(NumberTest, power2) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { - testcase << "test_power2 " << to_string(Number::getMantissaScale()); + NumberMantissaScaleGuard const sg(mantissaScale); + using Case = std::tuple; Case const c[]{ {Number{1}, 3, 7, Number{1}}, @@ -1039,7 +1052,7 @@ public: {Number{34}, 3, 3, Number{34}}, {Number{4}, 3, 2, Number{8}}}; for (auto const& [x, n, d, z] : c) - BEAST_EXPECT((power(x, n, d) == z)); + EXPECT_EQ(power(x, n, d), z); bool caught = false; try { @@ -1049,7 +1062,7 @@ public: { caught = true; } - BEAST_EXPECT(caught); + EXPECT_TRUE(caught); caught = false; try { @@ -1059,7 +1072,7 @@ public: { caught = true; } - BEAST_EXPECT(caught); + EXPECT_TRUE(caught); caught = false; try { @@ -1069,40 +1082,44 @@ public: { caught = true; } - BEAST_EXPECT(caught); + EXPECT_TRUE(caught); } +} - void - testConversions() +TEST(NumberTest, conversions) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { - testcase << "testConversions " << to_string(Number::getMantissaScale()); + NumberMantissaScaleGuard const sg(mantissaScale); IOUAmount const x{5, 6}; Number const y = x; - BEAST_EXPECT((y == Number{5, 6})); + EXPECT_EQ(y, (Number{5, 6})); IOUAmount const z{y}; - BEAST_EXPECT(x == z); + EXPECT_EQ(x, z); XRPAmount const xrp{500}; STAmount const st = xrp; Number const n = st; - BEAST_EXPECT(XRPAmount{n} == xrp); + EXPECT_EQ(XRPAmount{n}, xrp); IOUAmount const x0{0, 0}; Number const y0 = x0; - BEAST_EXPECT((y0 == Number{0})); + EXPECT_EQ(y0, Number{0}); IOUAmount const z0{y0}; - BEAST_EXPECT(x0 == z0); + EXPECT_EQ(x0, z0); XRPAmount const xrp0{0}; Number const n0 = xrp0; - BEAST_EXPECT(n0 == Number{0}); - // NOLINTNEXTLINE(misc-confusable-identifiers) - XRPAmount const xrp1{n0}; - BEAST_EXPECT(xrp1 == xrp0); + EXPECT_EQ(n0, Number{0}); + XRPAmount const xrp1{n0}; // NOLINT misc-confusable-identifiers + EXPECT_EQ(xrp1, xrp0); } +} - void - testToInteger() +TEST(NumberTest, to_integer) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { - testcase << "test_to_integer " << to_string(Number::getMantissaScale()); + NumberMantissaScaleGuard const sg(mantissaScale); + using Case = std::tuple; SaveNumberRoundMode const save{Number::setround(Number::RoundingMode::ToNearest)}; { @@ -1138,11 +1155,11 @@ public: for (auto const& [x, y] : c) { auto j = static_cast(x); - BEAST_EXPECT(j == y); + EXPECT_EQ(j, y); } } auto prevMode = Number::setround(Number::RoundingMode::TowardsZero); - BEAST_EXPECT(prevMode == Number::RoundingMode::ToNearest); + EXPECT_EQ(prevMode, Number::RoundingMode::ToNearest); { Case const c[]{ {Number{0}, 0}, @@ -1176,11 +1193,11 @@ public: for (auto const& [x, y] : c) { auto j = static_cast(x); - BEAST_EXPECT(j == y); + EXPECT_EQ(j, y); } } prevMode = Number::setround(Number::RoundingMode::Downward); - BEAST_EXPECT(prevMode == Number::RoundingMode::TowardsZero); + EXPECT_EQ(prevMode, Number::RoundingMode::TowardsZero); { Case const c[]{ {Number{0}, 0}, @@ -1214,11 +1231,11 @@ public: for (auto const& [x, y] : c) { auto j = static_cast(x); - BEAST_EXPECT(j == y); + EXPECT_EQ(j, y); } } prevMode = Number::setround(Number::RoundingMode::Upward); - BEAST_EXPECT(prevMode == Number::RoundingMode::Downward); + EXPECT_EQ(prevMode, Number::RoundingMode::Downward); { Case const c[]{ {Number{0}, 0}, @@ -1252,7 +1269,7 @@ public: for (auto const& [x, y] : c) { auto j = static_cast(x); - BEAST_EXPECT(j == y); + EXPECT_EQ(j, y); } } bool caught = false; @@ -1264,33 +1281,39 @@ public: { caught = true; } - BEAST_EXPECT(caught); + EXPECT_TRUE(caught); } +} - void - testSquelch() +TEST(NumberTest, squelch) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { - testcase << "test_squelch " << to_string(Number::getMantissaScale()); + NumberMantissaScaleGuard const sg(mantissaScale); + Number const limit{1, -6}; - BEAST_EXPECT((squelch(Number{2, -6}, limit) == Number{2, -6})); - BEAST_EXPECT((squelch(Number{1, -6}, limit) == Number{1, -6})); - BEAST_EXPECT((squelch(Number{9, -7}, limit) == Number{0})); - BEAST_EXPECT((squelch(Number{-2, -6}, limit) == Number{-2, -6})); - BEAST_EXPECT((squelch(Number{-1, -6}, limit) == Number{-1, -6})); - BEAST_EXPECT((squelch(Number{-9, -7}, limit) == Number{0})); + EXPECT_EQ(squelch(Number{2, -6}, limit), (Number{2, -6})); + EXPECT_EQ(squelch(Number{1, -6}, limit), (Number{1, -6})); + EXPECT_EQ(squelch(Number{9, -7}, limit), Number{0}); + EXPECT_EQ(squelch(Number{-2, -6}, limit), (Number{-2, -6})); + EXPECT_EQ(squelch(Number{-1, -6}, limit), (Number{-1, -6})); + EXPECT_EQ(squelch(Number{-9, -7}, limit), Number{0}); } +} - void - testToString() +TEST(NumberTest, to_string) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { - auto const scale = Number::getMantissaScale(); - testcase << "testToString " << to_string(scale); + NumberMantissaScaleGuard const sg(mantissaScale); - auto test = [this](Number const& n, std::string const& expected) { + auto const scale = Number::getMantissaScale(); + + auto test = [](Number const& n, std::string const& expected) { auto const result = to_string(n); std::stringstream ss; ss << "to_string(" << result << "). Expected: " << expected; - BEAST_EXPECTS(result == expected, ss.str()); + EXPECT_EQ(result, expected) << ss.str(); }; test(Number(-2, 0), "-2"); @@ -1322,7 +1345,7 @@ public: NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); auto const maxMantissa = Number::maxMantissa(); - BEAST_EXPECT(maxMantissa == 9'999'999'999'999'999); + EXPECT_EQ(maxMantissa, (9'999'999'999'999'999)); test( Number{false, (maxMantissa * 1000) + 999, -3, Number::Normalized()}, "9999999999999999"); @@ -1353,7 +1376,7 @@ public: NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); auto const maxMantissa = Number::maxMantissa(); - BEAST_EXPECT(maxMantissa == 9'999'999'999'999'999'999ULL); + EXPECT_EQ((maxMantissa), (9'999'999'999'999'999'999ULL)); test( Number{false, maxMantissa, 0, Number::Normalized{}}, "9999999999999999990"); test( @@ -1383,18 +1406,20 @@ public: "-9223372036854775810"); break; default: - BEAST_EXPECT(false); + EXPECT_TRUE(false); } } +} - void - testRelationals() +TEST(NumberTest, relationals) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { - testcase << "test_relationals " << to_string(Number::getMantissaScale()); + NumberMantissaScaleGuard const sg(mantissaScale); { - auto test = [this](auto const& nums) { - BEAST_EXPECT(std::ranges::is_sorted(nums)); + auto test = [](auto const& nums) { + EXPECT_TRUE(std::ranges::is_sorted(nums)); for (auto iter1 = nums.begin(); iter1 != nums.end(); ++iter1) { @@ -1410,32 +1435,32 @@ public: // The ==/!= operators use a completely different code path than <, etc. // This helps detect a breakage in one but not the other. It also helps // verify that the values are being ordered correctly. - BEAST_EXPECTS(smaller != larger, str + " (!=)"); - BEAST_EXPECTS(!(smaller == larger), str + " (==)"); + EXPECT_TRUE(smaller != larger) << str << " (!=)"; + EXPECT_FALSE(smaller == larger) << str << " (==)"; // true results using operator< and derived operators - BEAST_EXPECTS(smaller < larger, str + " (<)"); - BEAST_EXPECTS(larger > smaller, str + " (>)"); - BEAST_EXPECTS(larger >= smaller, str + " (>=)"); - BEAST_EXPECTS(smaller <= larger, str + " (<=)"); + EXPECT_TRUE(smaller < larger) << str << " (<)"; + EXPECT_TRUE(larger > smaller) << str << " (>)"; + EXPECT_TRUE(larger >= smaller) << str << " (>=)"; + EXPECT_TRUE(smaller <= larger) << str << " (<=)"; // false results using operator< and derived operators - BEAST_EXPECTS(!(larger < smaller), str + " (! <)"); - BEAST_EXPECTS(!(smaller > larger), str + " (! >)"); - BEAST_EXPECTS(!(smaller >= larger), str + " (! >=)"); - BEAST_EXPECTS(!(larger <= smaller), str + " (! <=)"); + EXPECT_FALSE(larger < smaller) << str << " (! <)"; + EXPECT_FALSE(smaller > larger) << str << " (! >)"; + EXPECT_FALSE(smaller >= larger) << str << " (! >=)"; + EXPECT_FALSE(larger <= smaller) << str << " (! <=)"; } } }; - auto const intNums = [this]() { + auto const intNums = []() { // Inequality test cases are built from a list of sorted integers auto const values = std::to_array({-100, -50, -20, -10, -1, 0, 1, 10, 20, 50, 100}); // Check this list is sorted before converting it to Numbers. // That way if any of the other tests fail, we know it's because of code and not the // source data. - BEAST_EXPECT(std::ranges::is_sorted(values)); + EXPECT_TRUE(std::ranges::is_sorted(values)); std::vector result; result.reserve(values.size()); @@ -1475,95 +1500,114 @@ public: for (auto const& [n, line] : c) { auto const str = to_string(n); + auto const location = + std::string{" ("} + __FILE__ + ":" + std::to_string(line) + ")"; // NOLINTBEGIN(misc-redundant-expression) Explicitly testing operators with // equivalent values - expect(n == n, str + " ==", __FILE__, line); - expect(!(n != n), str + " !=", __FILE__, line); + EXPECT_TRUE(n == n) << str << " ==" << location; + EXPECT_FALSE(n != n) << str << " !=" << location; - expect(!(n < n), str + " < ", __FILE__, line); - expect(!(n > n), str + " >", __FILE__, line); - expect(n >= n, str + " >=", __FILE__, line); - expect(n <= n, str + " <=", __FILE__, line); + EXPECT_FALSE(n < n) << str << " <" << location; + EXPECT_FALSE(n > n) << str << " >" << location; + EXPECT_TRUE(n >= n) << str << " >=" << location; + EXPECT_TRUE(n <= n) << str << " <=" << location; // NOLINTEND(misc-redundant-expression) } } } +} - void - testStream() +TEST(NumberTest, stream) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { - testcase << "test_stream " << to_string(Number::getMantissaScale()); + NumberMantissaScaleGuard const sg(mantissaScale); + Number const x{100}; std::ostringstream os; os << x; - BEAST_EXPECT(os.str() == to_string(x)); + EXPECT_EQ((os.str()), (to_string(x))); } +} - void - testIncDec() +TEST(NumberTest, inc_dec) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { - testcase << "test_inc_dec " << to_string(Number::getMantissaScale()); + NumberMantissaScaleGuard const sg(mantissaScale); + Number x{100}; Number const y = +x; - BEAST_EXPECT(x == y); - BEAST_EXPECT(x++ == y); - BEAST_EXPECT(x == Number{101}); - BEAST_EXPECT(x-- == Number{101}); - BEAST_EXPECT(x == y); + EXPECT_EQ((x), (y)); + EXPECT_EQ((x++), (y)); + EXPECT_EQ((x), (Number{101})); + EXPECT_EQ((x--), (Number{101})); + EXPECT_EQ((x), (y)); } +} - void - testToStAmount() +TEST(NumberTest, to_st_amount) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { + NumberMantissaScaleGuard const sg(mantissaScale); + Issue const issue; Number const n{7'518'783'80596, -5}; SaveNumberRoundMode const save{Number::setround(Number::RoundingMode::ToNearest)}; auto res2 = STAmount{issue, n}; - BEAST_EXPECT(res2 == STAmount{7518784}); + EXPECT_EQ((res2), (STAmount{7518784})); Number::setround(Number::RoundingMode::TowardsZero); res2 = STAmount{issue, n}; - BEAST_EXPECT(res2 == STAmount{7518783}); + EXPECT_EQ((res2), (STAmount{7518783})); Number::setround(Number::RoundingMode::Downward); res2 = STAmount{issue, n}; - BEAST_EXPECT(res2 == STAmount{7518783}); + EXPECT_EQ((res2), (STAmount{7518783})); Number::setround(Number::RoundingMode::Upward); res2 = STAmount{issue, n}; - BEAST_EXPECT(res2 == STAmount{7518784}); + EXPECT_EQ((res2), (STAmount{7518784})); } +} - void - testTruncate() +TEST(NumberTest, truncate) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { - BEAST_EXPECT(Number(25, +1).truncate() == Number(250, 0)); - BEAST_EXPECT(Number(25, 0).truncate() == Number(25, 0)); - BEAST_EXPECT(Number(25, -1).truncate() == Number(2, 0)); - BEAST_EXPECT(Number(25, -2).truncate() == Number(0, 0)); - BEAST_EXPECT(Number(99, -2).truncate() == Number(0, 0)); + NumberMantissaScaleGuard const sg(mantissaScale); - BEAST_EXPECT(Number(-25, +1).truncate() == Number(-250, 0)); - BEAST_EXPECT(Number(-25, 0).truncate() == Number(-25, 0)); - BEAST_EXPECT(Number(-25, -1).truncate() == Number(-2, 0)); - BEAST_EXPECT(Number(-25, -2).truncate() == Number(0, 0)); - BEAST_EXPECT(Number(-99, -2).truncate() == Number(0, 0)); + EXPECT_EQ((Number(25, +1).truncate()), (Number(250, 0))); + EXPECT_EQ((Number(25, 0).truncate()), (Number(25, 0))); + EXPECT_EQ((Number(25, -1).truncate()), (Number(2, 0))); + EXPECT_EQ((Number(25, -2).truncate()), (Number(0, 0))); + EXPECT_EQ((Number(99, -2).truncate()), (Number(0, 0))); - BEAST_EXPECT(Number(0, 0).truncate() == Number(0, 0)); - BEAST_EXPECT(Number(0, 30000).truncate() == Number(0, 0)); - BEAST_EXPECT(Number(0, -30000).truncate() == Number(0, 0)); - BEAST_EXPECT(Number(100, -30000).truncate() == Number(0, 0)); - BEAST_EXPECT(Number(100, -30000).truncate() == Number(0, 0)); - BEAST_EXPECT(Number(-100, -30000).truncate() == Number(0, 0)); - BEAST_EXPECT(Number(-100, -30000).truncate() == Number(0, 0)); + EXPECT_EQ((Number(-25, +1).truncate()), (Number(-250, 0))); + EXPECT_EQ((Number(-25, 0).truncate()), (Number(-25, 0))); + EXPECT_EQ((Number(-25, -1).truncate()), (Number(-2, 0))); + EXPECT_EQ((Number(-25, -2).truncate()), (Number(0, 0))); + EXPECT_EQ((Number(-99, -2).truncate()), (Number(0, 0))); + + EXPECT_EQ((Number(0, 0).truncate()), (Number(0, 0))); + EXPECT_EQ((Number(0, 30000).truncate()), (Number(0, 0))); + EXPECT_EQ((Number(0, -30000).truncate()), (Number(0, 0))); + EXPECT_EQ((Number(100, -30000).truncate()), (Number(0, 0))); + EXPECT_EQ((Number(100, -30000).truncate()), (Number(0, 0))); + EXPECT_EQ((Number(-100, -30000).truncate()), (Number(0, 0))); + EXPECT_EQ((Number(-100, -30000).truncate()), (Number(0, 0))); } +} - void - testRounding() +TEST(NumberTest, rounding) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { + NumberMantissaScaleGuard const sg(mantissaScale); + // Test that rounding works as expected. - testcase("Rounding"); using NumberRoundings = std::map; @@ -1659,348 +1703,340 @@ public: { NumberRoundModeGuard const g{mode}; auto const res = static_cast(num); - BEAST_EXPECTS( - res == val, - to_string(num) + " with mode " + std::to_string(static_cast(mode)) + - " expected " + std::to_string(val) + " got " + std::to_string(res)); + EXPECT_EQ((res), (val)) << to_string(num) + " with mode " + + std::to_string(static_cast(mode)) + " expected " + + std::to_string(val) + " got " + std::to_string(res); } } } +} - void - testInt64() +TEST(NumberTest, int64) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { + NumberMantissaScaleGuard const sg(mantissaScale); + auto const scale = Number::getMantissaScale(); - testcase << "std::int64_t " << to_string(scale); // Control case - BEAST_EXPECT(Number::maxMantissa() > 10); + EXPECT_GT((Number::maxMantissa()), (10)); Number const ten{10}; - BEAST_EXPECT(ten.exponent() <= 0); + EXPECT_LE((ten.exponent()), (0)); if (scale == MantissaRange::MantissaScale::Small) { - BEAST_EXPECT(std::numeric_limits::max() > kInitialXrp.drops()); - BEAST_EXPECT(Number::maxMantissa() < kInitialXrp.drops()); + EXPECT_GT((std::numeric_limits::max()), (kInitialXrp.drops())); + EXPECT_LT((Number::maxMantissa()), (kInitialXrp.drops())); Number const initalXrp{kInitialXrp}; - BEAST_EXPECT(initalXrp.exponent() > 0); + EXPECT_GT((initalXrp.exponent()), (0)); Number const maxInt64{Number::kMaxRep}; - BEAST_EXPECT(maxInt64.exponent() > 0); + EXPECT_GT((maxInt64.exponent()), (0)); // 85'070'591'730'234'615'865'843'651'857'942'052'864 - 38 digits - BEAST_EXPECT((power(maxInt64, 2) == Number{85'070'591'730'234'62, 22})); + EXPECT_EQ((power(maxInt64, 2)), (Number{85'070'591'730'234'62, 22})); Number const max = Number{false, Number::maxMantissa(), 0, Number::Normalized{}}; - BEAST_EXPECT(max.exponent() <= 0); + EXPECT_LE(max.exponent(), 0); // 99'999'999'999'999'980'000'000'000'000'001 - 32 digits - BEAST_EXPECT((power(max, 2) == Number{99'999'999'999'999'98, 16})); + EXPECT_EQ(power(max, 2), (Number{99'999'999'999'999'98, 16})); } else { - BEAST_EXPECT(std::numeric_limits::max() > kInitialXrp.drops()); - BEAST_EXPECT(Number::maxMantissa() > kInitialXrp.drops()); + EXPECT_GT((std::numeric_limits::max()), (kInitialXrp.drops())); + EXPECT_GT((Number::maxMantissa()), (kInitialXrp.drops())); Number const initalXrp{kInitialXrp}; - BEAST_EXPECT(initalXrp.exponent() <= 0); + EXPECT_LE((initalXrp.exponent()), (0)); Number const maxInt64{Number::kMaxRep}; - BEAST_EXPECT(maxInt64.exponent() <= 0); + EXPECT_LE((maxInt64.exponent()), (0)); // 85'070'591'730'234'615'847'396'907'784'232'501'249 - 38 digits - BEAST_EXPECT((power(maxInt64, 2) == Number{85'070'591'730'234'615'85, 19})); + EXPECT_EQ((power(maxInt64, 2)), (Number{85'070'591'730'234'615'85, 19})); NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); auto const maxMantissa = Number::maxMantissa(); Number const max = Number{false, maxMantissa, 0, Number::Normalized{}}; - BEAST_EXPECT(max.mantissa() == maxMantissa / 10); - BEAST_EXPECT(max.exponent() == 1); + EXPECT_EQ((max.mantissa()), (maxMantissa / 10)); + EXPECT_EQ((max.exponent()), (1)); // 99'999'999'999'999'999'800'000'000'000'000'000'100 - also 38 // digits - BEAST_EXPECT( - (power(max, 2) == Number{false, (maxMantissa / 10) - 1, 20, Number::Normalized{}})); + EXPECT_EQ( + (power(max, 2)), (Number{false, (maxMantissa / 10) - 1, 20, Number::Normalized{}})); } } +} - void - testUpwardRoundsDown() +TEST(NumberTest, upward_rounding_produces_value_not_below_exact_at_k_max_rep_cusp) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { + NumberMantissaScaleGuard const mg{mantissaScale}; + NumberRoundModeGuard const rg{Number::RoundingMode::Upward}; + auto const scale = Number::getMantissaScale(); + + constexpr std::int64_t kAValue = 1'000'000'000'000'049'863LL; + constexpr std::int64_t kBValue = 9'223'372'036'854'315'903LL; + + Number const a = kAValue; + Number const b = kBValue; + Number const product = a * b; + + // Exact reference in BigInt. + BigInt const exactProduct = BigInt(kAValue) * BigInt(kBValue); + + // What Number actually stored. + BigInt storedValue = BigInt(product.mantissa()); + for (int i = 0; i < product.exponent(); ++i) + storedValue *= 10; + + BigInt const signedDifference = storedValue - exactProduct; + + auto const message = [&] { + std::ostringstream os; + os << "\n" + << " a = " << fmt(BigInt(kAValue)) << "\n" + << " b = " << fmt(BigInt(kBValue)) << "\n" + << " exact a*b = " << fmt(exactProduct) << "\n" + << " stored = " << fmt(storedValue) << "\n" + << " stored - exact = " << fmt(signedDifference) << "\n" + << " upward = " << (signedDifference >= 0 ? "held" : "VIOLATED") << "\n" + << " stored.mantissa = " << product.mantissa() << "\n" + << " stored.exponent = " << product.exponent() << "\n"; + return os.str(); + }; + + switch (scale) { - testcase << "upward rounding produces a value below exact at kMaxRep cusp " - << to_string(scale); + case MantissaRange::MantissaScale::Large: + EXPECT_TRUE(signedDifference >= 0) << message(); + EXPECT_TRUE(signedDifference < pow10(product.exponent())) << message(); + EXPECT_EQ(product.mantissa(), (std::numeric_limits::max() / 10) + 1); + EXPECT_EQ(product.exponent(), 19); + break; - NumberRoundModeGuard const rg{Number::RoundingMode::Upward}; + case MantissaRange::MantissaScale::LargeLegacy: + EXPECT_TRUE(signedDifference < 0) << message(); + EXPECT_EQ( + product.mantissa(), (std::numeric_limits::max() / 100) * 100); + EXPECT_EQ(product.exponent(), 18); + break; - constexpr std::int64_t kAValue = 1'000'000'000'000'049'863LL; - constexpr std::int64_t kBValue = 9'223'372'036'854'315'903LL; - - Number const a = kAValue; - Number const b = kBValue; - Number const product = a * b; - - // Exact reference in BigInt. - BigInt const exactProduct = BigInt(kAValue) * BigInt(kBValue); - - // What Number actually stored. - BigInt storedValue = BigInt(product.mantissa()); - for (int i = 0; i < product.exponent(); ++i) - storedValue *= 10; - - BigInt const signedDifference = storedValue - exactProduct; - - log << "\n" - << " a = " << fmt(BigInt(kAValue)) << "\n" - << " b = " << fmt(BigInt(kBValue)) << "\n" - << " exact a*b = " << fmt(exactProduct) << "\n" - << " stored = " << fmt(storedValue) << "\n" - << " stored - exact = " << fmt(signedDifference) << "\n" - << " upward = " << (signedDifference >= 0 ? "held" : "VIOLATED") << "\n" - << " stored.mantissa = " << product.mantissa() << "\n" - << " stored.exponent = " << product.exponent() << "\n"; - log.flush(); - - switch (scale) - { - case MantissaRange::MantissaScale::Large: - BEAST_EXPECT(signedDifference >= 0); - BEAST_EXPECT(signedDifference < pow10(product.exponent())); - BEAST_EXPECT( - product.mantissa() == (std::numeric_limits::max() / 10) + 1); - BEAST_EXPECT(product.exponent() == 19); - break; - - case MantissaRange::MantissaScale::LargeLegacy: - BEAST_EXPECT(signedDifference < 0); - BEAST_EXPECT( - product.mantissa() == - (std::numeric_limits::max() / 100) * 100); - BEAST_EXPECT(product.exponent() == 18); - break; - - case MantissaRange::MantissaScale::Small: - // The seemingly weird rounding here is because - // a & b are both normalized, and both round up when - // being converted to Number, so you're really - // getting 1_000_000_000_000_050 * 9_223_372_036_854_316 - BEAST_EXPECT(signedDifference >= 0); - BEAST_EXPECT( - product.mantissa() == - (std::numeric_limits::max() / 1000) + 3); - BEAST_EXPECT(product.exponent() == 21); - break; - } - } - - { - /* Companion regression for the kMaxRep cusp behavior, but for - * `operator/=` on the cusp-fix-ENABLED `Large` scale. - * - * Before the dropped-remainder fix, `operator/=` with Upward - * rounding could return a value STRICTLY LESS than the exact quotient, - * violating Upward's directional invariant. - * - * Mechanism (fix-enabled path): - * 1. `operator/=` computes `numerator = nm * 10^17` and - * `zm = numerator / dm` (integer division, truncates remainder). - * 2. If `remainder != 0`, the correction block runs: - * zm *= 100000 - * correction = (remainder * 100000) / dm // also truncates - * zm += correction - * ze -= 5 - * The truncation in `correction` discards a sub-1/100000 residual. - * 3. `normalize`'s shift loop reduces zm to fit, but the discarded - * residual is BELOW the Guard's visibility, so the Guard sees fraction = 0. - * 4. Under Upward + positive, `round()` returns -1 (no round-up), and - * the algorithm returns the truncated zm - */ - testcase << "operator/= Upward on Large returns value < truth " << to_string(scale); - - NumberRoundModeGuard const roundGuard{Number::RoundingMode::Upward}; - - constexpr std::int64_t aValue = 2LL; - constexpr std::int64_t bValue = 1'000'000'000'000'000'007LL; - // bValue = 10^18 + 7 (prime, in [minMantissa, kMaxRep]). - - Number const a{aValue, 0}; - Number const b{bValue, 0}; - Number const quotient = a / b; - - dec const exact = dec(aValue) / dec(bValue); - dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent()); - dec const diff = stored - exact; - - log << "\n" - << " a = " << aValue << "\n" - << " b = " << bValue << "\n" - << " exact a/b = " << fmt(exact) << "\n" - << " stored a/b = " << fmt(stored) << "\n" - << " stored - exact = " << fmt(diff) - << " (negative => Upward gave value BELOW truth)\n" - << " quotient.mantissa = " << quotient.mantissa() << "\n" - << " quotient.exponent = " << quotient.exponent() << "\n"; - log.flush(); - - // Upward invariant: stored >= exact. Bug: stored < exact. - switch (scale) - { - case MantissaRange::MantissaScale::Large: - BEAST_EXPECT(stored >= exact); - BEAST_EXPECT(diff < pow10(quotient.exponent())); - break; - - case MantissaRange::MantissaScale::LargeLegacy: - BEAST_EXPECT(stored < exact); - BEAST_EXPECT(diff >= -pow10(quotient.exponent())); - break; - - case MantissaRange::MantissaScale::Small: - // Small mantissa doesn't have the correction for - // dropped remainders - BEAST_EXPECT(stored < exact); - break; - } - } - { - /* Companion test case for Upward positive operator/=: Downward negative - */ - testcase << "operator/= Downward on Large returns value < truth " << to_string(scale); - - NumberRoundModeGuard const roundGuard{Number::RoundingMode::Downward}; - - constexpr std::int64_t aValue = -2LL; - constexpr std::int64_t bValue = 1'000'000'000'000'000'007LL; - // bValue = 10^18 + 7 (prime, in [minMantissa, kMaxRep]). - - Number const a{aValue, 0}; - Number const b{bValue, 0}; - Number const quotient = a / b; - - dec const exact = dec(aValue) / dec(bValue); - dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent()); - dec const diff = stored - exact; - - log << "\n" - << " a = " << aValue << "\n" - << " b = " << bValue << "\n" - << " exact a/b = " << fmt(exact) << "\n" - << " stored a/b = " << fmt(stored) << "\n" - << " stored - exact = " << fmt(diff) - << " (positive => Downward gave value ABOVE truth)\n" - << " quotient.mantissa = " << quotient.mantissa() << "\n" - << " quotient.exponent = " << quotient.exponent() << "\n"; - log.flush(); - - // invariant: stored <= exact. Bug: stored > exact. - switch (scale) - { - case MantissaRange::MantissaScale::Large: - BEAST_EXPECT(stored <= exact); - BEAST_EXPECT(diff > -pow10(quotient.exponent())); - break; - - case MantissaRange::MantissaScale::LargeLegacy: - BEAST_EXPECT(stored > exact); - BEAST_EXPECT(diff <= pow10(quotient.exponent())); - break; - - case MantissaRange::MantissaScale::Small: - // Small mantissa doesn't have the correction for - // dropped remainders - BEAST_EXPECT(stored < exact); - break; - } - } - { - /* Companion test case for Upward positive operator/=: ToNearest - * - * With ToNearest, if the dropped digits are exactly "5", then the mantissa will be - * rounded to even. The numbers below result in a value where the unrounded mantissa - * ends in an even digit, and "infinite precision" would drop - * "500000000000000000145...", but doNormalize only sees "5". Without the rounding fix, - * doNormalize rounds down to the even value. With the rounding fix, doNormalize knows - * there are more digits beyond "5", and so rounds _up_ to the odd value. - */ - testcase << "operator/= ToNearest on Large returns value < truth " << to_string(scale); - - NumberRoundModeGuard const roundGuard{Number::RoundingMode::ToNearest}; - - constexpr std::int64_t aValue = 1'269'917'268'816'087'809LL; - constexpr std::int64_t bValue = 3'458'525'013'821'685'511LL; - // bValue = 10^18 + 7 (prime, in [minMantissa, kMaxRep]). - - Number const a{aValue, 0}; - Number const b{bValue, 0}; - Number const quotient = a / b; - - dec const exact = dec(aValue) / dec(bValue); - dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent()); - dec const diff = stored - exact; - - log << "\n" - << " a = " << aValue << "\n" - << " b = " << bValue << "\n" - << " exact a/b = " << fmt(exact) << "\n" - << " stored a/b = " << fmt(stored) << "\n" - << " stored - exact = " << fmt(diff) - << " (negative => ToNearest gave value BELOW truth)\n" - << " quotient.mantissa = " << quotient.mantissa() << "\n" - << " quotient.exponent = " << quotient.exponent() << "\n"; - log.flush(); - - // invariant: stored >= exact. Bug: stored < exact. - switch (scale) - { - case MantissaRange::MantissaScale::Large: - BEAST_EXPECT(stored >= exact); - BEAST_EXPECT(diff < pow10(quotient.exponent())); - break; - - case MantissaRange::MantissaScale::LargeLegacy: - BEAST_EXPECT(stored < exact); - BEAST_EXPECT(diff >= -pow10(quotient.exponent())); - break; - - case MantissaRange::MantissaScale::Small: - // Small mantissa doesn't have the correction for - // dropped remainders - BEAST_EXPECT(stored < exact); - break; - } + case MantissaRange::MantissaScale::Small: + // The seemingly weird rounding here is because a & b are both + // normalized, and both round up when being converted to Number, + // so you're really getting + // 1_000_000_000_000_050 * 9_223_372_036_854_316. + EXPECT_TRUE(signedDifference >= 0) << message(); + EXPECT_EQ( + product.mantissa(), (std::numeric_limits::max() / 1000) + 3); + EXPECT_EQ(product.exponent(), 21); + break; } } +} - void - run() override +/* + * Companion regression for the kMaxRep cusp behavior, but for `operator/=` on + * the cusp-fix-ENABLED `Large` scale. + * + * Before the dropped-remainder fix, `operator/=` with Upward rounding could + * return a value STRICTLY LESS than the exact quotient, violating Upward's + * directional invariant. + * + * Mechanism (fix-enabled path): + * 1. `operator/=` computes `numerator = nm * 10^17` and + * `zm = numerator / dm` (integer division, truncates remainder). + * 2. If `remainder != 0`, the correction block runs: + * zm *= 100000 + * correction = (remainder * 100000) / dm // also truncates + * zm += correction + * ze -= 5 + * The truncation in `correction` discards a sub-1/100000 residual. + * 3. `normalize`'s shift loop reduces zm to fit, but the discarded residual + * is BELOW the Guard's visibility, so the Guard sees fraction = 0. + * 4. Under Upward + positive, `round()` returns -1 (no round-up), and the + * algorithm returns the truncated zm. + */ +TEST(NumberTest, upward_division_returns_value_not_below_exact_on_large_scale) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) { - for (auto const scale : MantissaRange::getAllScales()) - { - NumberMantissaScaleGuard const sg(scale); - testZero(); - testLimits(); - testToString(); - testAdd(); - testSub(); - testMul(); - testDiv(); - testRoot(); - testRoot2(); - testPower1(); - testPower2(); - testConversions(); - testToInteger(); - testSquelch(); - testRelationals(); - testStream(); - testIncDec(); - testToStAmount(); - testTruncate(); - testRounding(); - testInt64(); + NumberMantissaScaleGuard const mg{mantissaScale}; + NumberRoundModeGuard const rg{Number::RoundingMode::Upward}; - testUpwardRoundsDown(); + auto const scale = Number::getMantissaScale(); + + constexpr std::int64_t kAValue = 2LL; + constexpr std::int64_t kBValue = 1'000'000'000'000'000'007LL; + // kBValue = 10^18 + 7 (prime, in [minMantissa, kMaxRep]). + + Number const a{kAValue, 0}; + Number const b{kBValue, 0}; + Number const quotient = a / b; + + Dec const exact = Dec(kAValue) / Dec(kBValue); + Dec const stored = Dec(quotient.mantissa()) * pow10(quotient.exponent()); + Dec const diff = stored - exact; + + auto const message = [&] { + std::ostringstream os; + os << "\n" + << " a = " << kAValue << "\n" + << " b = " << kBValue << "\n" + << " exact a/b = " << fmt(exact) << "\n" + << " stored a/b = " << fmt(stored) << "\n" + << " stored - exact = " << fmt(diff) + << " (negative => Upward gave value BELOW truth)\n" + << " quotient.mantissa = " << quotient.mantissa() << "\n" + << " quotient.exponent = " << quotient.exponent() << "\n"; + return os.str(); + }; + + // Upward invariant: stored >= exact. Bug: stored < exact. + switch (scale) + { + case MantissaRange::MantissaScale::Large: + EXPECT_TRUE(stored >= exact) << message(); + EXPECT_TRUE(diff < pow10(quotient.exponent())) << message(); + break; + + case MantissaRange::MantissaScale::LargeLegacy: + EXPECT_TRUE(stored < exact) << message(); + EXPECT_TRUE(diff >= -pow10(quotient.exponent())) << message(); + break; + + case MantissaRange::MantissaScale::Small: + // Small mantissa doesn't have the correction for dropped remainders. + EXPECT_TRUE(stored < exact) << message(); + break; } } -}; +} -BEAST_DEFINE_TESTSUITE(Number, basics, xrpl); +// Companion test case for Upward positive operator/=: Downward negative. +TEST(NumberTest, downward_division_returns_value_not_above_exact_on_large_scale) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const mg{mantissaScale}; + NumberRoundModeGuard const rg{Number::RoundingMode::Downward}; + + auto const scale = Number::getMantissaScale(); + + constexpr std::int64_t kAValue = -2LL; + constexpr std::int64_t kBValue = 1'000'000'000'000'000'007LL; + // kBValue = 10^18 + 7 (prime, in [minMantissa, kMaxRep]). + + Number const a{kAValue, 0}; + Number const b{kBValue, 0}; + Number const quotient = a / b; + + Dec const exact = Dec(kAValue) / Dec(kBValue); + Dec const stored = Dec(quotient.mantissa()) * pow10(quotient.exponent()); + Dec const diff = stored - exact; + + auto const message = [&] { + std::ostringstream os; + os << "\n" + << " a = " << kAValue << "\n" + << " b = " << kBValue << "\n" + << " exact a/b = " << fmt(exact) << "\n" + << " stored a/b = " << fmt(stored) << "\n" + << " stored - exact = " << fmt(diff) + << " (positive => Downward gave value ABOVE truth)\n" + << " quotient.mantissa = " << quotient.mantissa() << "\n" + << " quotient.exponent = " << quotient.exponent() << "\n"; + return os.str(); + }; + + // Downward invariant: stored <= exact. Bug: stored > exact. + switch (scale) + { + case MantissaRange::MantissaScale::Large: + EXPECT_TRUE(stored <= exact) << message(); + EXPECT_TRUE(diff > -pow10(quotient.exponent())) << message(); + break; + + case MantissaRange::MantissaScale::LargeLegacy: + EXPECT_TRUE(stored > exact) << message(); + EXPECT_TRUE(diff <= pow10(quotient.exponent())) << message(); + break; + + case MantissaRange::MantissaScale::Small: + // Small mantissa doesn't have the correction for dropped remainders. + EXPECT_TRUE(stored < exact) << message(); + break; + } + } +} + +/* + * Companion test case for Upward positive operator/=: ToNearest. + * + * With ToNearest, if the dropped digits are exactly "5", then the mantissa will + * be rounded to even. The numbers below result in a value where the unrounded + * mantissa ends in an even digit, and "infinite precision" would drop + * "500000000000000000145...", but doNormalize only sees "5". Without the + * rounding fix, doNormalize rounds down to the even value. With the rounding + * fix, doNormalize knows there are more digits beyond "5", and so rounds _up_ + * to the odd value. + */ +TEST(NumberTest, to_nearest_division_uses_dropped_digits_on_large_scale) +{ + for (auto const mantissaScale : MantissaRange::getAllScales()) + { + NumberMantissaScaleGuard const mg{mantissaScale}; + NumberRoundModeGuard const rg{Number::RoundingMode::ToNearest}; + + auto const scale = Number::getMantissaScale(); + + constexpr std::int64_t kAValue = 1'269'917'268'816'087'809LL; + constexpr std::int64_t kBValue = 3'458'525'013'821'685'511LL; + // kBValue is prime and in [minMantissa, kMaxRep]. + + Number const a{kAValue, 0}; + Number const b{kBValue, 0}; + Number const quotient = a / b; + + Dec const exact = Dec(kAValue) / Dec(kBValue); + Dec const stored = Dec(quotient.mantissa()) * pow10(quotient.exponent()); + Dec const diff = stored - exact; + + auto const message = [&] { + std::ostringstream os; + os << "\n" + << " a = " << kAValue << "\n" + << " b = " << kBValue << "\n" + << " exact a/b = " << fmt(exact) << "\n" + << " stored a/b = " << fmt(stored) << "\n" + << " stored - exact = " << fmt(diff) + << " (negative => ToNearest gave value BELOW truth)\n" + << " quotient.mantissa = " << quotient.mantissa() << "\n" + << " quotient.exponent = " << quotient.exponent() << "\n"; + return os.str(); + }; + + // ToNearest should account for dropped digits beyond the visible "5". + switch (scale) + { + case MantissaRange::MantissaScale::Large: + EXPECT_TRUE(stored >= exact) << message(); + EXPECT_TRUE(diff < pow10(quotient.exponent())) << message(); + break; + + case MantissaRange::MantissaScale::LargeLegacy: + EXPECT_TRUE(stored < exact) << message(); + EXPECT_TRUE(diff >= -pow10(quotient.exponent())) << message(); + break; + + case MantissaRange::MantissaScale::Small: + // Small mantissa doesn't have the correction for dropped remainders. + EXPECT_TRUE(stored < exact) << message(); + break; + } + } +} } // namespace xrpl diff --git a/src/tests/libxrpl/basics/StringUtilities.cpp b/src/tests/libxrpl/basics/StringUtilities.cpp new file mode 100644 index 0000000000..a10711abdb --- /dev/null +++ b/src/tests/libxrpl/basics/StringUtilities.cpp @@ -0,0 +1,293 @@ +#include + +#include +#include + +#include + +#include + +namespace xrpl { + +class StringUtilitiesTest : public ::testing::Test +{ +public: + static void + testUnHexSuccess(std::string const& strIn, std::string const& strExpected) + { + auto rv = strUnHex(strIn); + EXPECT_TRUE(rv); + + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + EXPECT_EQ(makeSlice(*rv), makeSlice(strExpected)); + } + + static void + testUnHexFailure(std::string const& strIn) + { + auto rv = strUnHex(strIn); + EXPECT_FALSE(rv); + } +}; + +TEST_F(StringUtilitiesTest, un_hex) +{ + testUnHexSuccess("526970706c6544", "RippleD"); + testUnHexSuccess("A", "\n"); + testUnHexSuccess("0A", "\n"); + testUnHexSuccess("D0A", "\r\n"); + testUnHexSuccess("0D0A", "\r\n"); + testUnHexSuccess("200D0A", " \r\n"); + testUnHexSuccess("282A2B2C2D2E2F29", "(*+,-./)"); + + // Check for things which contain some or only invalid characters + testUnHexFailure("123X"); + testUnHexFailure("V"); + testUnHexFailure("XRP"); +} + +TEST_F(StringUtilitiesTest, parse_url) +{ + // Expected passes. + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_TRUE(pUrl.domain.empty()); + EXPECT_FALSE(pUrl.port); + // RFC 3986: + // > In general, a URI that uses the generic syntax for authority + // with an empty path should be normalized to a path of "/". + // Do we want to normalize paths? + EXPECT_TRUE(pUrl.path.empty()); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme:///")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_TRUE(pUrl.domain.empty()); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "lower://domain")); + EXPECT_EQ(pUrl.scheme, "lower"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_FALSE(pUrl.port); + EXPECT_TRUE(pUrl.path.empty()); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "UPPER://domain:234/")); + EXPECT_EQ(pUrl.scheme, "upper"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_EQ(*pUrl.port, 234); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_EQ(pUrl.path, "/"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "Mixed://domain/path")); + EXPECT_EQ(pUrl.scheme, "mixed"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/path"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://[::1]:123/path")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "::1"); + EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_EQ(pUrl.path, "/path"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://user:pass@domain:123/abc:321")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_EQ(pUrl.username, "user"); + EXPECT_EQ(pUrl.password, "pass"); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_EQ(pUrl.path, "/abc:321"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://user@domain:123/abc:321")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_EQ(pUrl.username, "user"); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_EQ(pUrl.path, "/abc:321"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://:pass@domain:123/abc:321")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_EQ(pUrl.password, "pass"); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_EQ(pUrl.path, "/abc:321"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://domain:123/abc:321")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_EQ(*pUrl.port, 123); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_EQ(pUrl.path, "/abc:321"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://user:pass@domain/abc:321")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_EQ(pUrl.username, "user"); + EXPECT_EQ(pUrl.password, "pass"); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/abc:321"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://user@domain/abc:321")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_EQ(pUrl.username, "user"); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/abc:321"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://:pass@domain/abc:321")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_EQ(pUrl.password, "pass"); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/abc:321"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://domain/abc:321")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/abc:321"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme:///path/to/file")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_TRUE(pUrl.domain.empty()); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/path/to/file"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://user:pass@domain/path/with/an@sign")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_EQ(pUrl.username, "user"); + EXPECT_EQ(pUrl.password, "pass"); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/path/with/an@sign"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://domain/path/with/an@sign")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "domain"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/path/with/an@sign"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "scheme://:999/")); + EXPECT_EQ(pUrl.scheme, "scheme"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, ":999"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/"); + } + + { + ParsedUrl pUrl; + EXPECT_TRUE(parseUrl(pUrl, "http://::1:1234/validators")); + EXPECT_EQ(pUrl.scheme, "http"); + EXPECT_TRUE(pUrl.username.empty()); + EXPECT_TRUE(pUrl.password.empty()); + EXPECT_EQ(pUrl.domain, "::0.1.18.52"); + EXPECT_FALSE(pUrl.port); + EXPECT_EQ(pUrl.path, "/validators"); + } + + // Expected fails. + { + ParsedUrl pUrl; + EXPECT_FALSE(parseUrl(pUrl, "")); + EXPECT_FALSE(parseUrl(pUrl, "nonsense")); + EXPECT_FALSE(parseUrl(pUrl, "://")); + EXPECT_FALSE(parseUrl(pUrl, ":///")); + EXPECT_FALSE(parseUrl(pUrl, "scheme://user:pass@domain:65536/abc:321")); + EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:23498765/")); + EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:0/")); + EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:+7/")); + EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:-7234/")); + EXPECT_FALSE(parseUrl(pUrl, "UPPER://domain:@#$56!/")); + } + + { + std::string const strUrl("s://" + std::string(8192, ':')); + ParsedUrl pUrl; + EXPECT_FALSE(parseUrl(pUrl, strUrl)); + } +} + +TEST_F(StringUtilitiesTest, to_string) +{ + auto result = to_string("hello"); + EXPECT_EQ(result, "hello"); +} + +} // namespace xrpl diff --git a/src/tests/libxrpl/basics/TaggedCache.cpp b/src/tests/libxrpl/basics/TaggedCache.cpp new file mode 100644 index 0000000000..c8ccc415ad --- /dev/null +++ b/src/tests/libxrpl/basics/TaggedCache.cpp @@ -0,0 +1,246 @@ +#include + +#include +#include +#include // IWYU pragma: keep +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace xrpl { + +/* +I guess you can put some items in, make sure they're still there. Let some +time pass, make sure they're gone. Keep a strong pointer to one of them, make +sure you can still find it even after time passes. Create two objects with +the same key, canonicalize them both and make sure you get the same object. +Put an object in but keep a strong pointer to it, advance the clock a lot, +then canonicalize a new object with the same key, make sure you get the +original object. +*/ + +TEST(TaggedCacheTest, tagged_cache) +{ + using namespace std::chrono_literals; + beast::Journal const journal{TestSink::instance()}; + + TestStopwatch clock; + clock.set(0); + + using Key = LedgerIndex; + using Value = std::string; + using Cache = TaggedCache; + + Cache c("test", 1, 1s, clock, journal); + + // Insert an item, retrieve it, and age it so it gets purged. + { + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.getTrackSize(), 0); + EXPECT_FALSE(c.insert(1, "one")); + EXPECT_EQ(c.getCacheSize(), 1); + EXPECT_EQ(c.getTrackSize(), 1); + + { + std::string s; + EXPECT_TRUE(c.retrieve(1, s)); + EXPECT_EQ(s, "one"); + } + + ++clock; + c.sweep(); + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.getTrackSize(), 0); + } + + // Insert an item, maintain a strong pointer, age it, and + // verify that the entry still exists. + { + EXPECT_FALSE(c.insert(2, "two")); + EXPECT_EQ(c.getCacheSize(), 1); + EXPECT_EQ(c.getTrackSize(), 1); + + { + auto p = c.fetch(2); + EXPECT_NE(p, nullptr); + ++clock; + c.sweep(); + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.getTrackSize(), 1); + } + + // Make sure its gone now that our reference is gone + ++clock; + c.sweep(); + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.getTrackSize(), 0); + } + + // Insert the same key/value pair and make sure we get the same result + { + EXPECT_FALSE(c.insert(3, "three")); + + { + auto const p1 = c.fetch(3); + auto p2 = std::make_shared("three"); + c.canonicalizeReplaceClient(3, p2); + EXPECT_EQ(p1.get(), p2.get()); + } + ++clock; + c.sweep(); + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.getTrackSize(), 0); + } + + // Put an object in but keep a strong pointer to it, advance the clock a + // lot, then canonicalize a new object with the same key, make sure you + // get the original object. + { + // Put an object in + EXPECT_FALSE(c.insert(4, "four")); + EXPECT_EQ(c.getCacheSize(), 1); + EXPECT_EQ(c.getTrackSize(), 1); + + { + // Keep a strong pointer to it + auto const p1 = c.fetch(4); + EXPECT_NE(p1, nullptr); + EXPECT_EQ(c.getCacheSize(), 1); + EXPECT_EQ(c.getTrackSize(), 1); + // Advance the clock a lot + ++clock; + c.sweep(); + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.getTrackSize(), 1); + // Canonicalize a new object with the same key + auto p2 = std::make_shared("four"); + EXPECT_TRUE(c.canonicalizeReplaceClient(4, p2)); + EXPECT_EQ(c.getCacheSize(), 1); + EXPECT_EQ(c.getTrackSize(), 1); + // Make sure we get the original object + EXPECT_EQ(p1.get(), p2.get()); + } + + ++clock; + c.sweep(); + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.getTrackSize(), 0); + } + + { + EXPECT_FALSE(c.insert(5, "five")); + EXPECT_EQ(c.getCacheSize(), 1); + EXPECT_EQ(c.size(), 1); + + { + auto const p1 = c.fetch(5); + EXPECT_NE(p1, nullptr); + EXPECT_EQ(c.getCacheSize(), 1); + EXPECT_EQ(c.size(), 1); + + // Advance the clock a lot + ++clock; + c.sweep(); + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.size(), 1); + + auto p2 = std::make_shared("five_2"); + EXPECT_TRUE(c.canonicalizeReplaceCache(5, p2)); + EXPECT_EQ(c.getCacheSize(), 1); + EXPECT_EQ(c.size(), 1); + // Make sure the caller's original pointer is unchanged + EXPECT_NE(p1.get(), p2.get()); + EXPECT_EQ(*p2, "five_2"); + + auto const p3 = c.fetch(5); + EXPECT_NE(p3, nullptr); + EXPECT_EQ(p3.get(), p2.get()); + EXPECT_NE(p3.get(), p1.get()); + } + + ++clock; + c.sweep(); + EXPECT_EQ(c.getCacheSize(), 0); + EXPECT_EQ(c.size(), 0); + } + + { + struct MyRefCountObject : IntrusiveRefCounts + { + std::string data; + + // Needed to support weak intrusive pointers + virtual void + partialDestructor() + { + } + + MyRefCountObject() = default; + explicit MyRefCountObject(std::string data) : data(std::move(data)) + { + } + + bool + operator==(std::string const& other) const + { + return data == other; + } + }; + + using IntrPtrCache = TaggedCache< + Key, + MyRefCountObject, + /*IsKeyCache*/ false, + intr_ptr::SharedWeakUnionPtr, + intr_ptr::SharedPtr>; + + IntrPtrCache intrPtrCache("IntrPtrTest", 1, 1s, clock, journal); + + intrPtrCache.canonicalizeReplaceCache(1, intr_ptr::makeShared("one")); + EXPECT_EQ(intrPtrCache.getCacheSize(), 1); + EXPECT_EQ(intrPtrCache.size(), 1); + + { + { + intrPtrCache.canonicalizeReplaceCache( + 1, intr_ptr::makeShared("one_replaced")); + + auto p = intrPtrCache.fetch(1); + EXPECT_EQ(*p, "one_replaced"); + + // Advance the clock a lot + ++clock; + intrPtrCache.sweep(); + EXPECT_EQ(intrPtrCache.getCacheSize(), 0); + EXPECT_EQ(intrPtrCache.size(), 1); + + intrPtrCache.canonicalizeReplaceCache( + 1, intr_ptr::makeShared("one_replaced_2")); + + auto p2 = intrPtrCache.fetch(1); + EXPECT_EQ(*p2, "one_replaced_2"); + + intrPtrCache.del(1, true); + } + + intrPtrCache.canonicalizeReplaceCache( + 1, intr_ptr::makeShared("one_replaced_3")); + auto p3 = intrPtrCache.fetch(1); + EXPECT_EQ(*p3, "one_replaced_3"); + } + + ++clock; + intrPtrCache.sweep(); + EXPECT_EQ(intrPtrCache.getCacheSize(), 0); + EXPECT_EQ(intrPtrCache.size(), 0); + } +} + +} // namespace xrpl diff --git a/src/tests/libxrpl/basics/Units.cpp b/src/tests/libxrpl/basics/Units.cpp new file mode 100644 index 0000000000..1cf7024fe0 --- /dev/null +++ b/src/tests/libxrpl/basics/Units.cpp @@ -0,0 +1,328 @@ +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace xrpl::test { + +TEST(UnitsTest, types) +{ + using FeeLevel32 = FeeLevel; + + { + XRPAmount const x{100}; + EXPECT_EQ(x.drops(), 100); + EXPECT_TRUE((std::is_same_v)); + auto y = 4u * x; + EXPECT_EQ(y.value(), 400); + EXPECT_TRUE((std::is_same_v)); + + auto z = 4 * y; + EXPECT_EQ(z.value(), 1600); + EXPECT_TRUE((std::is_same_v)); + + FeeLevel32 const f{10}; + FeeLevel32 const baseFee{100}; + + auto drops = mulDiv(baseFee, x, f); + + EXPECT_TRUE(drops); + EXPECT_EQ(drops.value(), 1000); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_TRUE( + (std::is_same_v::unit_type, unit::dropTag>)); + + EXPECT_TRUE((std::is_same_v, XRPAmount>)); + } + { + XRPAmount const x{100}; + EXPECT_EQ(x.value(), 100); + EXPECT_TRUE((std::is_same_v)); + auto y = 4u * x; + EXPECT_EQ(y.value(), 400); + EXPECT_TRUE((std::is_same_v)); + + FeeLevel64 const f{10}; + FeeLevel64 const baseFee{100}; + + auto drops = mulDiv(baseFee, x, f); + + EXPECT_TRUE(drops); + EXPECT_EQ(drops.value(), 1000); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_TRUE( + (std::is_same_v::unit_type, unit::dropTag>)); + EXPECT_TRUE((std::is_same_v, XRPAmount>)); + } + { + FeeLevel64 const x{1024}; + EXPECT_EQ(x.value(), 1024); + EXPECT_TRUE((std::is_same_v)); + std::uint64_t const m = 4; + auto y = m * x; + EXPECT_EQ(y.value(), 4096); + EXPECT_TRUE((std::is_same_v)); + + XRPAmount const basefee{10}; + FeeLevel64 const referencefee{256}; + + auto drops = mulDiv(x, basefee, referencefee); + + EXPECT_TRUE(drops); + EXPECT_EQ(drops.value(), 40); // NOLINT(bugprone-unchecked-optional-access) + EXPECT_TRUE( + (std::is_same_v::unit_type, unit::dropTag>)); + EXPECT_TRUE((std::is_same_v, XRPAmount>)); + } +} + +TEST(UnitsTest, json) +{ + // Json value functionality + using FeeLevel32 = FeeLevel; + + { + FeeLevel32 const x{std::numeric_limits::max()}; + auto y = x.jsonClipped(); + EXPECT_EQ(y.type(), json::ValueType::UInt); + EXPECT_EQ(y, json::Value{x.fee()}); + } + + { + FeeLevel32 const x{std::numeric_limits::min()}; + auto y = x.jsonClipped(); + EXPECT_EQ(y.type(), json::ValueType::UInt); + EXPECT_EQ(y, json::Value{x.fee()}); + } + + { + FeeLevel64 const x{std::numeric_limits::max()}; + auto y = x.jsonClipped(); + EXPECT_EQ(y.type(), json::ValueType::UInt); + EXPECT_EQ(y, json::Value{std::numeric_limits::max()}); + } + + { + FeeLevel64 const x{std::numeric_limits::min()}; + auto y = x.jsonClipped(); + EXPECT_EQ(y.type(), json::ValueType::UInt); + EXPECT_EQ(y, json::Value{0}); + } + + { + FeeLevelDouble const x{std::numeric_limits::max()}; + auto y = x.jsonClipped(); + EXPECT_EQ(y.type(), json::ValueType::Real); + EXPECT_EQ(y, json::Value{std::numeric_limits::max()}); + } + + { + FeeLevelDouble const x{std::numeric_limits::min()}; + auto y = x.jsonClipped(); + EXPECT_EQ(y.type(), json::ValueType::Real); + EXPECT_EQ(y, json::Value{std::numeric_limits::min()}); + } + + { + XRPAmount const x{std::numeric_limits::max()}; + auto y = x.jsonClipped(); + EXPECT_EQ(y.type(), json::ValueType::Int); + EXPECT_EQ(y, json::Value{std::numeric_limits::max()}); + } + + { + XRPAmount const x{std::numeric_limits::min()}; + auto y = x.jsonClipped(); + EXPECT_EQ(y.type(), json::ValueType::Int); + EXPECT_EQ(y, json::Value{std::numeric_limits::min()}); + } +} + +TEST(UnitsTest, functions) +{ + // Explicitly test every defined function for the ValueUnit class + // since some of them are templated, but not used anywhere else. + using FeeLevel32 = FeeLevel; + + { + auto make = [&](auto x) -> FeeLevel64 { return x; }; + auto explicitmake = [&](auto x) -> FeeLevel64 { return FeeLevel64{x}; }; + + [[maybe_unused]] + FeeLevel64 const defaulted{}; + FeeLevel64 test{0}; + EXPECT_EQ(test.fee(), 0); + + test = explicitmake(beast::kZero); + EXPECT_EQ(test.fee(), 0); + + test = beast::kZero; + EXPECT_EQ(test.fee(), 0); + + test = explicitmake(100u); + EXPECT_EQ(test.fee(), 100); + + FeeLevel64 const targetSame{200u}; + FeeLevel32 const targetOther{300u}; + test = make(targetSame); + EXPECT_EQ(test.fee(), 200); + EXPECT_EQ(test, targetSame); + EXPECT_TRUE(test < FeeLevel64{1000}); + EXPECT_TRUE(test > FeeLevel64{100}); + test = make(targetOther); + EXPECT_EQ(test.fee(), 300); + EXPECT_EQ(test, targetOther); + + test = std::uint64_t(200); + EXPECT_EQ(test.fee(), 200); + test = std::uint32_t(300); + EXPECT_EQ(test.fee(), 300); + + test = targetSame; + EXPECT_EQ(test.fee(), 200); + test = targetOther.fee(); + EXPECT_EQ(test.fee(), 300); + EXPECT_EQ(test, targetOther); + + test = targetSame * 2; + EXPECT_EQ(test.fee(), 400); + test = 3 * targetSame; + EXPECT_EQ(test.fee(), 600); + test = targetSame / 10; + EXPECT_EQ(test.fee(), 20); + + test += targetSame; + EXPECT_EQ(test.fee(), 220); + + test -= targetSame; + EXPECT_EQ(test.fee(), 20); + + test++; + EXPECT_EQ(test.fee(), 21); + ++test; + EXPECT_EQ(test.fee(), 22); + test--; + EXPECT_EQ(test.fee(), 21); + --test; + EXPECT_EQ(test.fee(), 20); + + test *= 5; + EXPECT_EQ(test.fee(), 100); + test /= 2; + EXPECT_EQ(test.fee(), 50); + test %= 13; + EXPECT_EQ(test.fee(), 11); + + /* + // illegal with unsigned + test = -test; + EXPECT_EQ(test.fee(), -11); + EXPECT_EQ(test.signum(), -1); + EXPECT_EQ(to_string(test), "-11"); + */ + + EXPECT_TRUE(test); + test = 0; + EXPECT_FALSE(test); + EXPECT_EQ(test.signum(), 0); + test = targetSame; + EXPECT_EQ(test.signum(), 1); + EXPECT_EQ(to_string(test), "200"); + } + { + auto make = [&](auto x) -> FeeLevelDouble { return x; }; + auto explicitmake = [&](auto x) -> FeeLevelDouble { return FeeLevelDouble{x}; }; + + [[maybe_unused]] + FeeLevelDouble const defaulted{}; + FeeLevelDouble test{0}; + EXPECT_EQ(test.fee(), 0); + + test = explicitmake(beast::kZero); + EXPECT_EQ(test.fee(), 0); + + test = beast::kZero; + EXPECT_EQ(test.fee(), 0); + + test = explicitmake(100.0); + EXPECT_EQ(test.fee(), 100); + + FeeLevelDouble const targetSame{200.0}; + FeeLevel64 const targetOther{300}; + test = make(targetSame); + EXPECT_EQ(test.fee(), 200); + EXPECT_EQ(test, targetSame); + EXPECT_TRUE(test < FeeLevelDouble{1000.0}); + EXPECT_TRUE(test > FeeLevelDouble{100.0}); + test = targetOther.fee(); + EXPECT_EQ(test.fee(), 300); + EXPECT_EQ(test, targetOther); + + test = 200.0; + EXPECT_EQ(test.fee(), 200); + test = std::uint64_t(300); + EXPECT_EQ(test.fee(), 300); + + test = targetSame; + EXPECT_EQ(test.fee(), 200); + + test = targetSame * 2; + EXPECT_EQ(test.fee(), 400); + test = 3 * targetSame; + EXPECT_EQ(test.fee(), 600); + test = targetSame / 10; + EXPECT_EQ(test.fee(), 20); + + test += targetSame; + EXPECT_EQ(test.fee(), 220); + + test -= targetSame; + EXPECT_EQ(test.fee(), 20); + + test++; + EXPECT_EQ(test.fee(), 21); + ++test; + EXPECT_EQ(test.fee(), 22); + test--; + EXPECT_EQ(test.fee(), 21); + --test; + EXPECT_EQ(test.fee(), 20); + + test *= 5; + EXPECT_EQ(test.fee(), 100); + test /= 2; + EXPECT_EQ(test.fee(), 50); + /* illegal with floating + test %= 13; + EXPECT_EQ(test.fee(), 11); + */ + + // legal with signed + test = -test; + EXPECT_EQ(test.fee(), -50); + EXPECT_EQ(test.signum(), -1); + EXPECT_EQ(to_string(test), "-50.000000"); + + EXPECT_TRUE(test); + test = 0; + EXPECT_FALSE(test); + EXPECT_EQ(test.signum(), 0); + test = targetSame; + EXPECT_EQ(test.signum(), 1); + EXPECT_EQ(to_string(test), "200.000000"); + } +} + +TEST(UnitsTest, initial_xrp) +{ + EXPECT_EQ(kInitialXrp.drops(), 100'000'000'000'000'000); + EXPECT_EQ(kInitialXrp, XRPAmount{100'000'000'000'000'000}); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/basics/XRPAmount.cpp b/src/tests/libxrpl/basics/XRPAmount.cpp new file mode 100644 index 0000000000..243afbd1a0 --- /dev/null +++ b/src/tests/libxrpl/basics/XRPAmount.cpp @@ -0,0 +1,295 @@ +#include + +#include + +#include + +#include +#include + +namespace xrpl { + +TEST(XRPAmountTest, sig_num) +{ + for (auto i : {-1, 0, 1}) + { + XRPAmount const x(i); + + if (i < 0) + { + EXPECT_TRUE(x.signum() < 0); + } + else if (i > 0) + { + EXPECT_TRUE(x.signum() > 0); + } + else + { + EXPECT_EQ(x.signum(), 0); + } + } +} + +TEST(XRPAmountTest, beast_zero) +{ + using beast::kZero; + + for (auto i : {-1, 0, 1}) + { + XRPAmount const x(i); + + EXPECT_TRUE((i == 0) == (x == kZero)); + EXPECT_TRUE((i != 0) == (x != kZero)); + EXPECT_TRUE((i < 0) == (x < kZero)); + EXPECT_TRUE((i > 0) == (x > kZero)); + EXPECT_TRUE((i <= 0) == (x <= kZero)); + EXPECT_TRUE((i >= 0) == (x >= kZero)); + + EXPECT_TRUE((0 == i) == (kZero == x)); + EXPECT_TRUE((0 != i) == (kZero != x)); + EXPECT_TRUE((0 < i) == (kZero < x)); + EXPECT_TRUE((0 > i) == (kZero > x)); + EXPECT_TRUE((0 <= i) == (kZero <= x)); + EXPECT_TRUE((0 >= i) == (kZero >= x)); + } +} + +TEST(XRPAmountTest, comparisons) +{ + for (auto i : {-1, 0, 1}) + { + XRPAmount const x(i); + + for (auto j : {-1, 0, 1}) + { + XRPAmount const y(j); + + EXPECT_EQ((i == j), (x == y)); + EXPECT_EQ((i != j), (x != y)); + EXPECT_EQ((i < j), (x < y)); + EXPECT_EQ((i > j), (x > y)); + EXPECT_EQ((i <= j), (x <= y)); + EXPECT_EQ((i >= j), (x >= y)); + } + } +} + +TEST(XRPAmountTest, add_sub) +{ + for (auto i : {-1, 0, 1}) + { + XRPAmount const x(i); + + for (auto j : {-1, 0, 1}) + { + XRPAmount const y(j); + + EXPECT_EQ(XRPAmount(i + j), (x + y)); + EXPECT_EQ(XRPAmount(i - j), (x - y)); + + EXPECT_EQ((x + y), (y + x)); // addition is commutative + } + } +} + +TEST(XRPAmountTest, decimal) +{ + // Tautology + EXPECT_EQ(kDropsPerXrp.decimalXRP(), 1); + + XRPAmount test{1}; + EXPECT_EQ(test.decimalXRP(), 0.000001); + + test = -test; + EXPECT_EQ(test.decimalXRP(), -0.000001); + + test = 100'000'000; + EXPECT_EQ(test.decimalXRP(), 100); + + test = -test; + EXPECT_EQ(test.decimalXRP(), -100); +} + +TEST(XRPAmountTest, functions) +{ + // Explicitly test every defined function for the XRPAmount class + // since some of them are templated, but not used anywhere else. + auto make = [&](auto x) -> XRPAmount { return XRPAmount{x}; }; + + XRPAmount const defaulted{}; + (void)defaulted; + XRPAmount test{0}; + EXPECT_EQ(test.drops(), 0); + + test = make(beast::kZero); + EXPECT_EQ(test.drops(), 0); + + test = beast::kZero; + EXPECT_EQ(test.drops(), 0); + + test = make(100); + EXPECT_EQ(test.drops(), 100); + + test = make(100u); + EXPECT_EQ(test.drops(), 100); + + XRPAmount const targetSame{200u}; + test = make(targetSame); + EXPECT_EQ(test.drops(), 200); + EXPECT_EQ(test, targetSame); + EXPECT_TRUE(test < XRPAmount{1000}); + EXPECT_TRUE(test > XRPAmount{100}); + + test = std::int64_t(200); + EXPECT_EQ(test.drops(), 200); + test = std::uint32_t(300); + EXPECT_EQ(test.drops(), 300); + + test = targetSame; + EXPECT_EQ(test.drops(), 200); + auto testOther = test.dropsAs(); + EXPECT_TRUE(testOther); + EXPECT_EQ(*testOther, 200); // NOLINT(bugprone-unchecked-optional-access) + test = std::numeric_limits::max(); + testOther = test.dropsAs(); + EXPECT_FALSE(testOther); + test = -1; + testOther = test.dropsAs(); + EXPECT_FALSE(testOther); + + test = targetSame * 2; + EXPECT_EQ(test.drops(), 400); + test = 3 * targetSame; + EXPECT_EQ(test.drops(), 600); + test = 20; + EXPECT_EQ(test.drops(), 20); + + test += targetSame; + EXPECT_EQ(test.drops(), 220); + + test -= targetSame; + EXPECT_EQ(test.drops(), 20); + + test *= 5; + EXPECT_EQ(test.drops(), 100); + test = 50; + EXPECT_EQ(test.drops(), 50); + test -= 39; + EXPECT_EQ(test.drops(), 11); + + // legal with signed + test = -test; + EXPECT_EQ(test.drops(), -11); + EXPECT_EQ(test.signum(), -1); + EXPECT_EQ(to_string(test), "-11"); + + EXPECT_TRUE(test); + test = 0; + EXPECT_FALSE(test); + EXPECT_EQ(test.signum(), 0); + test = targetSame; + EXPECT_EQ(test.signum(), 1); + EXPECT_EQ(to_string(test), "200"); +} + +TEST(XRPAmountTest, mul_ratio) +{ + constexpr auto kMaxUInt32 = std::numeric_limits::max(); + constexpr auto kMaxXrp = std::numeric_limits::max(); + constexpr auto kMinXrp = std::numeric_limits::min(); + + { + // multiply by a number that would overflow then divide by the same + // number, and check we didn't lose any value + XRPAmount big(kMaxXrp); + EXPECT_EQ(big, mulRatio(big, kMaxUInt32, kMaxUInt32, true)); + // rounding mode shouldn't matter as the result is exact + EXPECT_EQ(big, mulRatio(big, kMaxUInt32, kMaxUInt32, false)); + + // multiply and divide by values that would overflow if done + // naively, and check that it gives the correct answer + big -= 0xf; // Subtract a little so it's divisible by 4 + EXPECT_EQ(mulRatio(big, 3, 4, false).value(), (big.value() / 4) * 3); + EXPECT_EQ(mulRatio(big, 3, 4, true).value(), (big.value() / 4) * 3); + EXPECT_EQ(big.value() % 4, 0); + EXPECT_GT(big.value(), kMaxXrp / 3); + EXPECT_LE(big.value() / 4, kMaxXrp / 3); + } + + { + // Similar test as above, but for negative values + XRPAmount big(kMinXrp); // NOLINT TODO + EXPECT_EQ(big, mulRatio(big, kMaxUInt32, kMaxUInt32, true)); + // rounding mode shouldn't matter as the result is exact + EXPECT_EQ(big, mulRatio(big, kMaxUInt32, kMaxUInt32, false)); + + // multiply and divide by values that would overflow if done + // naively, and check that it gives the correct answer + EXPECT_EQ(mulRatio(big, 3, 4, false).value(), (big.value() / 4) * 3); + EXPECT_EQ(mulRatio(big, 3, 4, true).value(), (big.value() / 4) * 3); + EXPECT_EQ(big.value() % 4, 0); + EXPECT_LT(big.value(), kMinXrp / 3); + EXPECT_GE(big.value() / 4, kMinXrp / 3); + } + + { + // small amounts + XRPAmount const tiny(1); + // Round up should give the smallest allowable number + EXPECT_EQ(tiny, mulRatio(tiny, 1, kMaxUInt32, true)); + // rounding down should be zero + EXPECT_EQ(beast::kZero, mulRatio(tiny, 1, kMaxUInt32, false)); + EXPECT_EQ(beast::kZero, mulRatio(tiny, kMaxUInt32 - 1, kMaxUInt32, false)); + + // tiny negative numbers + XRPAmount const tinyNeg(-1); + // Round up should give zero + EXPECT_EQ(beast::kZero, mulRatio(tinyNeg, 1, kMaxUInt32, true)); + EXPECT_EQ(beast::kZero, mulRatio(tinyNeg, kMaxUInt32 - 1, kMaxUInt32, true)); + // rounding down should be tiny + EXPECT_EQ(tinyNeg, mulRatio(tinyNeg, kMaxUInt32 - 1, kMaxUInt32, false)); + } + + { // rounding + { + XRPAmount const one(1); + auto const rup = mulRatio(one, kMaxUInt32 - 1, kMaxUInt32, true); + auto const rdown = mulRatio(one, kMaxUInt32 - 1, kMaxUInt32, false); + EXPECT_EQ(rup.drops() - rdown.drops(), 1); + } + + { + XRPAmount const big(kMaxXrp); + auto const rup = mulRatio(big, kMaxUInt32 - 1, kMaxUInt32, true); + auto const rdown = mulRatio(big, kMaxUInt32 - 1, kMaxUInt32, false); + EXPECT_EQ(rup.drops() - rdown.drops(), 1); + } + + { + XRPAmount const negOne(-1); + auto const rup = mulRatio(negOne, kMaxUInt32 - 1, kMaxUInt32, true); + auto const rdown = mulRatio(negOne, kMaxUInt32 - 1, kMaxUInt32, false); + EXPECT_EQ(rup.drops() - rdown.drops(), 1); + } + } + + { + // division by zero + XRPAmount const one(1); + EXPECT_ANY_THROW({ mulRatio(one, 1, 0, true); }); + } + + { + // overflow + XRPAmount const big(kMaxXrp); + EXPECT_ANY_THROW({ mulRatio(big, 2, 1, true); }); + } + + { + // underflow + XRPAmount const bigNegative(kMinXrp + 10); + EXPECT_EQ(mulRatio(bigNegative, 2, 1, true), kMinXrp); + } +} + +} // namespace xrpl diff --git a/src/tests/libxrpl/basics/base58.cpp b/src/tests/libxrpl/basics/base58.cpp new file mode 100644 index 0000000000..d452453f76 --- /dev/null +++ b/src/tests/libxrpl/basics/base58.cpp @@ -0,0 +1,438 @@ +#include + +#include // IWYU pragma: keep + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef _MSC_VER + +#include +#include + +#include +#include +#include +#include +#include + +namespace xrpl::test { +namespace { + +[[nodiscard]] inline auto +randEngine() -> std::mt19937& +{ + static std::mt19937 kR = [] { + std::random_device rd; + return std::mt19937{rd()}; + }(); + return kR; +} + +constexpr int kNumTokenTypeIndexes = 9; + +[[nodiscard]] inline auto +tokenTypeAndSize(int i) -> std::tuple +{ + assert(i < kNumTokenTypeIndexes); + + switch (i) + { + using enum xrpl::TokenType; + case 0: + return {None, 20}; + case 1: + return {NodePublic, 32}; + case 2: + return {NodePublic, 33}; + case 3: + return {NodePrivate, 32}; + case 4: + return {AccountID, 20}; + case 5: + return {AccountPublic, 32}; + case 6: + return {AccountPublic, 33}; + case 7: + return {AccountSecret, 32}; + case 8: + return {FamilySeed, 16}; + default: + throw std::invalid_argument( + "Invalid token selection passed to tokenTypeAndSize() " + "in " __FILE__); + } +} + +[[nodiscard]] inline auto +randomTokenTypeAndSize() -> std::tuple +{ + using namespace xrpl; + auto& rng = randEngine(); + std::uniform_int_distribution<> d(0, 8); + return tokenTypeAndSize(d(rng)); +} + +// Return the token type and subspan of `d` to use as test data. +[[nodiscard]] inline auto +randomB256TestData(std::span d) + -> std::tuple> +{ + auto& rng = randEngine(); + std::uniform_int_distribution dist(0, 255); + auto [tokType, tokSize] = randomTokenTypeAndSize(); + std::generate(d.begin(), d.begin() + tokSize, [&] { return dist(rng); }); + return {tokType, d.subspan(0, tokSize)}; +} + +inline void +printAsChar(std::span a, std::span b) +{ + auto asString = [](std::span s) { + std::string r; + r.resize(s.size()); + std::ranges::copy(s, r.begin()); + return r; + }; + auto sa = asString(a); + auto sb = asString(b); + std::cerr << "\n\n" << sa << "\n" << sb << "\n"; +} + +inline void +printAsInt(std::span a, std::span b) +{ + auto asString = [](std::span s) -> std::string { + std::stringstream sstr; + for (auto i : s) + { + sstr << std::setw(3) << int(i) << ','; + } + return sstr.str(); + }; + auto sa = asString(a); + auto sb = asString(b); + std::cerr << "\n\n" << sa << "\n" << sb << "\n"; +} + +} // namespace + +namespace multiprecision_utils { + +boost::multiprecision::checked_uint512_t +toBoostMP(std::span in) +{ + boost::multiprecision::checked_uint512_t mbp = 0; + for (auto const& i : std::views::reverse(in)) + { + mbp <<= 64; + mbp += i; + } + return mbp; +} + +std::vector +randomBigInt(std::uint8_t minSize = 1, std::uint8_t maxSize = 5) +{ + auto eng = randEngine(); + std::uniform_int_distribution numCoeffDist(minSize, maxSize); + std::uniform_int_distribution dist; + auto const numCoeff = numCoeffDist(eng); + std::vector coeffs; + coeffs.reserve(numCoeff); + for (int i = 0; i < numCoeff; ++i) + { + coeffs.push_back(dist(eng)); + } + return coeffs; +} +} // namespace multiprecision_utils + +TEST(Base58Test, multiprecision) +{ + using namespace boost::multiprecision; + + constexpr std::size_t kIters = 100000; + auto eng = randEngine(); + std::uniform_int_distribution dist; + std::uniform_int_distribution dist1(1); + for (int i = 0; i < kIters; ++i) + { + std::uint64_t const d = dist(eng); + if (d == 0u) + continue; + auto bigInt = multiprecision_utils::randomBigInt(); + auto const boostBigInt = + multiprecision_utils::toBoostMP(std::span(bigInt.data(), bigInt.size())); + + auto const refDiv = boostBigInt / d; + auto const refMod = boostBigInt % d; + + auto const mod = b58_fast::detail::inplaceBigintDivRem( + std::span(bigInt.data(), bigInt.size()), d); + auto const foundDiv = multiprecision_utils::toBoostMP(bigInt); + EXPECT_EQ(refMod.convert_to(), mod); + EXPECT_EQ(foundDiv, refDiv); + } + for (int i = 0; i < kIters; ++i) + { + std::uint64_t const d = dist(eng); + auto bigInt = multiprecision_utils::randomBigInt(/*minSize*/ 2); + if (bigInt[bigInt.size() - 1] == std::numeric_limits::max()) + { + bigInt[bigInt.size() - 1] -= 1; // Prevent overflow + } + auto const boostBigInt = + multiprecision_utils::toBoostMP(std::span(bigInt.data(), bigInt.size())); + + auto const refAdd = boostBigInt + d; + + auto const result = b58_fast::detail::inplaceBigintAdd( + std::span(bigInt.data(), bigInt.size()), d); + EXPECT_EQ(result, TokenCodecErrc::Success); + auto const foundAdd = multiprecision_utils::toBoostMP(bigInt); + EXPECT_EQ(refAdd, foundAdd); + } + for (int i = 0; i < kIters; ++i) + { + std::uint64_t const d = dist1(eng); + // Force overflow + std::vector bigInt(5, std::numeric_limits::max()); + + auto const boostBigInt = + multiprecision_utils::toBoostMP(std::span(bigInt.data(), bigInt.size())); + + auto const refAdd = boostBigInt + d; + + auto const result = b58_fast::detail::inplaceBigintAdd( + std::span(bigInt.data(), bigInt.size()), d); + EXPECT_EQ(result, TokenCodecErrc::OverflowAdd); + auto const foundAdd = multiprecision_utils::toBoostMP(bigInt); + EXPECT_NE(refAdd, foundAdd); + } + for (int i = 0; i < kIters; ++i) + { + std::uint64_t const d = dist(eng); + auto bigInt = multiprecision_utils::randomBigInt(/* minSize */ 2); + // inplace mul requires the most significant coeff to be zero to + // hold the result. + bigInt[bigInt.size() - 1] = 0; + auto const boostBigInt = + multiprecision_utils::toBoostMP(std::span(bigInt.data(), bigInt.size())); + + auto const refMul = boostBigInt * d; + + auto const result = b58_fast::detail::inplaceBigintMul( + std::span(bigInt.data(), bigInt.size()), d); + EXPECT_EQ(result, TokenCodecErrc::Success); + auto const foundMul = multiprecision_utils::toBoostMP(bigInt); + EXPECT_EQ(refMul, foundMul); + } + for (int i = 0; i < kIters; ++i) + { + std::uint64_t const d = dist1(eng); + // Force overflow + std::vector bigInt(5, std::numeric_limits::max()); + auto const boostBigInt = + multiprecision_utils::toBoostMP(std::span(bigInt.data(), bigInt.size())); + + auto const refMul = boostBigInt * d; + + auto const result = b58_fast::detail::inplaceBigintMul( + std::span(bigInt.data(), bigInt.size()), d); + EXPECT_EQ(result, TokenCodecErrc::InputTooLarge); + auto const foundMul = multiprecision_utils::toBoostMP(bigInt); + EXPECT_NE(refMul, foundMul); + } +} + +TEST(Base58Test, fast_matches_ref) +{ + auto testRawEncode = [&](std::span const& b256Data) { + std::array b58ResultBuf[2]; + std::array, 2> b58Result; + + std::array b256ResultBuf[2]; + std::array, 2> b256Result; + for (int i = 0; i < 2; ++i) + { + std::span const outBuf{b58ResultBuf[i]}; + if (i == 0) + { + auto const r = xrpl::b58_fast::detail::b256ToB58Be(b256Data, outBuf); + EXPECT_TRUE(r); + b58Result[i] = r.value(); + } + else + { + std::array tmpBuf{}; + std::string const s = xrpl::b58_ref::detail::encodeBase58( + b256Data.data(), b256Data.size(), tmpBuf.data(), tmpBuf.size()); + EXPECT_TRUE(s.size()); + b58Result[i] = outBuf.subspan(0, s.size()); + std::ranges::copy(s, b58Result[i].begin()); + } + } + auto const rawB58SameSize = b58Result[0].size() == b58Result[1].size(); + EXPECT_TRUE(rawB58SameSize); + if (rawB58SameSize) + { + auto const rawB58SameData = + memcmp(b58Result[0].data(), b58Result[1].data(), b58Result[0].size()) == 0; + EXPECT_TRUE(rawB58SameData); + if (!rawB58SameData) + { + printAsChar(b58Result[0], b58Result[1]); + } + } + + for (int i = 0; i < 2; ++i) + { + std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()}; + if (i == 0) + { + std::string const in( + b58Result[i].data(), b58Result[i].data() + b58Result[i].size()); + auto const r = xrpl::b58_fast::detail::b58ToB256Be(in, outBuf); + EXPECT_TRUE(r); + b256Result[i] = r.value(); + } + else + { + std::string const st(b58Result[i].begin(), b58Result[i].end()); + std::string const s = xrpl::b58_ref::detail::decodeBase58(st); + EXPECT_TRUE(s.size()); + b256Result[i] = outBuf.subspan(0, s.size()); + std::ranges::copy(s, b256Result[i].begin()); + } + } + + auto const rawB256SameSize = b256Result[0].size() == b256Result[1].size(); + EXPECT_TRUE(rawB256SameSize); + if (rawB256SameSize) + { + auto const rawB256SameData = + memcmp(b256Result[0].data(), b256Result[1].data(), b256Result[0].size()) == 0; + EXPECT_TRUE(rawB256SameData); + if (!rawB256SameData) + { + printAsInt(b256Result[0], b256Result[1]); + } + } + }; + + auto testTokenEncode = [&](xrpl::TokenType const tokType, + std::span const& b256Data) { + std::array b58ResultBuf[2]; + std::array, 2> b58Result; + + std::array b256ResultBuf[2]; + std::array, 2> b256Result; + for (int i = 0; i < 2; ++i) + { + std::span const outBuf{b58ResultBuf[i].data(), b58ResultBuf[i].size()}; + if (i == 0) + { + auto const r = xrpl::b58_fast::encodeBase58Token(tokType, b256Data, outBuf); + EXPECT_TRUE(r); + b58Result[i] = r.value(); + } + else + { + std::string const s = + xrpl::b58_ref::encodeBase58Token(tokType, b256Data.data(), b256Data.size()); + EXPECT_TRUE(s.size()); + b58Result[i] = outBuf.subspan(0, s.size()); + std::ranges::copy(s, b58Result[i].begin()); + } + } + auto const tokenB58SameSize = b58Result[0].size() == b58Result[1].size(); + EXPECT_TRUE(tokenB58SameSize); + if (tokenB58SameSize) + { + auto const tokenB58SameData = + memcmp(b58Result[0].data(), b58Result[1].data(), b58Result[0].size()) == 0; + EXPECT_TRUE(tokenB58SameData); + if (!tokenB58SameData) + { + printAsChar(b58Result[0], b58Result[1]); + } + } + + for (int i = 0; i < 2; ++i) + { + std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()}; + if (i == 0) + { + std::string const in( + b58Result[i].data(), b58Result[i].data() + b58Result[i].size()); + auto const r = xrpl::b58_fast::decodeBase58Token(tokType, in, outBuf); + EXPECT_TRUE(r); + b256Result[i] = r.value(); + } + else + { + std::string const st(b58Result[i].begin(), b58Result[i].end()); + std::string const s = xrpl::b58_ref::decodeBase58Token(st, tokType); + EXPECT_TRUE(s.size()); + b256Result[i] = outBuf.subspan(0, s.size()); + std::ranges::copy(s, b256Result[i].begin()); + } + } + + auto const tokenB256SameSize = b256Result[0].size() == b256Result[1].size(); + EXPECT_TRUE(tokenB256SameSize); + if (tokenB256SameSize) + { + auto const tokenB256SameData = + memcmp(b256Result[0].data(), b256Result[1].data(), b256Result[0].size()) == 0; + EXPECT_TRUE(tokenB256SameData); + if (!tokenB256SameData) + { + printAsInt(b256Result[0], b256Result[1]); + } + } + }; + + auto testIt = [&](xrpl::TokenType const tokType, std::span const& b256Data) { + testRawEncode(b256Data); + testTokenEncode(tokType, b256Data); + }; + + // test every token type with data where every byte is the same and the + // bytes range from 0-255 + for (int i = 0; i < kNumTokenTypeIndexes; ++i) + { + std::array b256DataBuf{}; + auto const [tokType, tokSize] = tokenTypeAndSize(i); + for (int d = 0; d <= 255; ++d) + { + memset(b256DataBuf.data(), d, tokSize); + testIt(tokType, std::span(b256DataBuf.data(), tokSize)); + } + } + + // test with random data + constexpr std::size_t kIters = 100000; + for (int i = 0; i < kIters; ++i) + { + std::array b256DataBuf{}; + auto const [tokType, b256Data] = randomB256TestData(b256DataBuf); + testIt(tokType, b256Data); + } +} + +} // namespace xrpl::test + +#endif // _MSC_VER diff --git a/src/tests/libxrpl/basics/base_uint_test.cpp b/src/tests/libxrpl/basics/base_uint_test.cpp new file mode 100644 index 0000000000..365b43930c --- /dev/null +++ b/src/tests/libxrpl/basics/base_uint_test.cpp @@ -0,0 +1,360 @@ +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +// a non-hashing Hasher that just copies the bytes. +// Used to test hash_append in base_uint +template +struct Nonhash +{ + static constexpr auto const kEndian = boost::endian::order::big; + static constexpr std::size_t kWidth = Bits / 8; + + std::array data; + + Nonhash() = default; + + void + operator()(void const* key, std::size_t len) noexcept + { + assert(len == kWidth); + memcpy(data.data(), key, len); + } + + explicit + operator std::size_t() noexcept + { + return kWidth; + } +}; + +struct BaseUintTest : public ::testing::Test +{ + using BaseUInt96 = BaseUInt<96>; + static_assert(std::is_copy_constructible_v); + static_assert(std::is_copy_assignable_v); + + static void + testComparisons() + { + { + static constexpr std::array, 6> kTestArgs{ + {{"0000000000000000", "0000000000000001"}, + {"0000000000000000", "ffffffffffffffff"}, + {"1234567812345678", "2345678923456789"}, + {"8000000000000000", "8000000000000001"}, + {"aaaaaaaaaaaaaaa9", "aaaaaaaaaaaaaaaa"}, + {"fffffffffffffffe", "ffffffffffffffff"}}}; + + for (auto const& arg : kTestArgs) + { + xrpl::BaseUInt<64> const u{arg.first}, v{arg.second}; + // For code readability, we want to use general boolean + // expectations instead of specific EXPECT_LT etc. + EXPECT_TRUE(u < v); + EXPECT_TRUE(u <= v); + EXPECT_TRUE(u != v); + EXPECT_FALSE(u == v); + EXPECT_FALSE(u > v); + EXPECT_FALSE(u >= v); + EXPECT_FALSE(v < u); + EXPECT_FALSE(v <= u); + EXPECT_TRUE(v != u); + EXPECT_FALSE(v == u); + EXPECT_TRUE(v > u); + EXPECT_TRUE(v >= u); + EXPECT_TRUE(u == u); + EXPECT_TRUE(v == v); + } + } + + { + static constexpr std::array, 6> kTestArgs{ + { + {"000000000000000000000000", "000000000000000000000001"}, + {"000000000000000000000000", "ffffffffffffffffffffffff"}, + {"0123456789ab0123456789ab", "123456789abc123456789abc"}, + {"555555555555555555555555", "55555555555a555555555555"}, + {"aaaaaaaaaaaaaaa9aaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaa"}, + {"fffffffffffffffffffffffe", "ffffffffffffffffffffffff"}, + }}; + + for (auto const& arg : kTestArgs) + { + xrpl::BaseUInt<96> const u{arg.first}, v{arg.second}; + EXPECT_TRUE(u < v); + EXPECT_TRUE(u <= v); + EXPECT_TRUE(u != v); + EXPECT_FALSE(u == v); + EXPECT_FALSE(u > v); + EXPECT_FALSE(u >= v); + EXPECT_FALSE(v < u); + EXPECT_FALSE(v <= u); + EXPECT_TRUE(v != u); + EXPECT_FALSE(v == u); + EXPECT_TRUE(v > u); + EXPECT_TRUE(v >= u); + EXPECT_TRUE(u == u); + EXPECT_TRUE(v == v); + } + } + } +}; + +TEST_F(BaseUintTest, base_uint) +{ + static_assert(!std::is_constructible_v>); + static_assert(!std::is_assignable_v>); + + testComparisons(); + + // used to verify set insertion (hashing required) + std::unordered_set> uset; + + Blob const raw{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; + EXPECT_EQ(BaseUInt96::kBytes, raw.size()); + + BaseUInt96 u = BaseUInt96::fromRaw(raw); + uset.insert(u); + EXPECT_EQ(raw.size(), u.size()); + EXPECT_EQ(to_string(u), "0102030405060708090A0B0C"); + EXPECT_EQ(toShortString(u), "01020304..."); + EXPECT_EQ(*u.data(), 1); + EXPECT_EQ(u.signum(), 1); + EXPECT_FALSE(!u); + EXPECT_FALSE(u.isZero()); + EXPECT_TRUE(u.isNonZero()); + unsigned char t = 0; + for (auto& d : u) + { + EXPECT_EQ(d, ++t); + } + + // Test hash_append by "hashing" with a no-op hasher (h) + // and then extracting the bytes that were written during hashing + // back into another base_uint (w) for comparison with the original + Nonhash<96> h{}; + hash_append(h, u); + BaseUInt96 const w = + BaseUInt96::fromRaw(std::vector(h.data.begin(), h.data.end())); + EXPECT_EQ(w, u); + + BaseUInt96 v{~u}; + uset.insert(v); + EXPECT_EQ(to_string(v), "FEFDFCFBFAF9F8F7F6F5F4F3"); + EXPECT_EQ(toShortString(v), "FEFDFCFB..."); + EXPECT_EQ(*v.data(), 0xfe); + EXPECT_EQ(v.signum(), 1); + EXPECT_FALSE(!v); + EXPECT_FALSE(v.isZero()); + EXPECT_TRUE(v.isNonZero()); + + t = 0xff; + for (auto& d : v) + { + EXPECT_EQ(d, --t); + } + + EXPECT_LT(u, v); + EXPECT_GT(v, u); + + v = u; + EXPECT_EQ(v, u); + + BaseUInt96 z{beast::kZero}; + uset.insert(z); + EXPECT_EQ(to_string(z), "000000000000000000000000"); + EXPECT_EQ(toShortString(z), "00000000..."); + EXPECT_EQ(*z.data(), 0); + EXPECT_EQ(*z.begin(), 0); + EXPECT_EQ(*std::prev(z.end(), 1), 0); + EXPECT_EQ(z.signum(), 0); + EXPECT_TRUE(!z); + EXPECT_TRUE(z.isZero()); + EXPECT_FALSE(z.isNonZero()); + for (auto& d : z) + { + EXPECT_EQ(d, 0); + } + + BaseUInt96 n{z}; + n++; + EXPECT_EQ(n, BaseUInt96(1)); + n--; + EXPECT_EQ(n, beast::kZero); + EXPECT_EQ(n, z); + n--; + EXPECT_EQ(to_string(n), "FFFFFFFFFFFFFFFFFFFFFFFF"); + EXPECT_EQ(toShortString(n), "FFFFFFFF..."); + n = beast::kZero; + EXPECT_EQ(n, z); + + BaseUInt96 zp1{z}; + zp1++; + BaseUInt96 zm1{z}; + zm1--; + BaseUInt96 const x{zm1 ^ zp1}; + uset.insert(x); + EXPECT_EQ(to_string(x), "FFFFFFFFFFFFFFFFFFFFFFFE") << to_string(x); + EXPECT_EQ(toShortString(x), "FFFFFFFF...") << toShortString(x); + + EXPECT_EQ(uset.size(), 4); + + BaseUInt96 tmp; + EXPECT_TRUE(tmp.parseHex(to_string(u))); + EXPECT_EQ(tmp, u); + tmp = z; + + // fails with extra char + EXPECT_FALSE(tmp.parseHex("A" + to_string(u))); + tmp = z; + + // fails with extra char at end + EXPECT_FALSE(tmp.parseHex(to_string(u) + "A")); + + // fails with a non-hex character at some point in the string: + tmp = z; + + for (std::size_t i = 0; i != 24; ++i) + { + std::string x = to_string(z); + x[i] = ('G' + (i % 10)); + EXPECT_FALSE(tmp.parseHex(x)); + } + + // Walking 1s: + for (std::size_t i = 0; i != 24; ++i) + { + std::string s1 = "000000000000000000000000"; + s1[i] = '1'; + + EXPECT_TRUE(tmp.parseHex(s1)); + EXPECT_EQ(to_string(tmp), s1); + } + + // Walking 0s: + for (std::size_t i = 0; i != 24; ++i) + { + std::string s1 = "111111111111111111111111"; + s1[i] = '0'; + + EXPECT_TRUE(tmp.parseHex(s1)); + EXPECT_EQ(to_string(tmp), s1); + } + + // Constexpr constructors + { + static_assert(BaseUInt96{}.signum() == 0); + static_assert(BaseUInt96("0").signum() == 0); + static_assert(BaseUInt96("000000000000000000000000").signum() == 0); + static_assert(BaseUInt96("000000000000000000000001").signum() == 1); + static_assert(BaseUInt96("800000000000000000000000").signum() == 1); + +// Everything within the #if should fail during compilation. +#if 0 + // Too few characters + static_assert(BaseUInt96("00000000000000000000000").signum() == 0); + + // Too many characters + static_assert(BaseUInt96("0000000000000000000000000").signum() == 0); + + // Non-hex characters + static_assert(BaseUInt96("00000000000000000000000 ").signum() == 1); + static_assert(BaseUInt96("00000000000000000000000/").signum() == 1); + static_assert(BaseUInt96("00000000000000000000000:").signum() == 1); + static_assert(BaseUInt96("00000000000000000000000@").signum() == 1); + static_assert(BaseUInt96("00000000000000000000000G").signum() == 1); + static_assert(BaseUInt96("00000000000000000000000`").signum() == 1); + static_assert(BaseUInt96("00000000000000000000000g").signum() == 1); + static_assert(BaseUInt96("00000000000000000000000~").signum() == 1); +#endif // 0 + + // Using the constexpr constructor in a non-constexpr context + // with an error in the parsing throws an exception. + { + // Invalid length for string. + bool caught = false; + try + { + // Try to prevent constant evaluation. + std::vector str(23, '7'); + std::string_view const sView(str.data(), str.size()); + [[maybe_unused]] BaseUInt96 const t96(sView); + } + catch (std::invalid_argument const& e) + { + EXPECT_EQ(e.what(), std::string("invalid length for hex string")); + caught = true; + } + EXPECT_TRUE(caught); + } + { + // Invalid character in string. + bool caught = false; + try + { + // Try to prevent constant evaluation. + std::vector str(23, '7'); + str.push_back('G'); + std::string_view const sView(str.data(), str.size()); + [[maybe_unused]] BaseUInt96 const t96(sView); + } + catch (std::range_error const& e) + { + EXPECT_EQ(e.what(), std::string("invalid hex character")); + caught = true; + } + EXPECT_TRUE(caught); + } + + // Verify that constexpr base_uints interpret a string the same + // way parseHex() does. + struct StrBaseUInt + { + char const* const str; + BaseUInt96 tst; + + constexpr StrBaseUInt(char const* s) : str(s), tst(s) + { + } + }; + constexpr StrBaseUInt kTestCases[] = { + "000000000000000000000000", + "000000000000000000000001", + "fedcba9876543210ABCDEF91", + "19FEDCBA0123456789abcdef", + "800000000000000000000000", + "fFfFfFfFfFfFfFfFfFfFfFfF", + }; + + for (StrBaseUInt const& t : kTestCases) + { + BaseUInt96 t96; + EXPECT_TRUE(t96.parseHex(t.str)); + EXPECT_EQ(t96, t.tst); + } + } +} + +} // namespace xrpl::test diff --git a/src/test/basics/hardened_hash_test.cpp b/src/tests/libxrpl/basics/hardened_hash.cpp similarity index 84% rename from src/test/basics/hardened_hash_test.cpp rename to src/tests/libxrpl/basics/hardened_hash.cpp index 8b10967932..4f51ce4305 100644 --- a/src/test/basics/hardened_hash_test.cpp +++ b/src/tests/libxrpl/basics/hardened_hash.cpp @@ -1,6 +1,8 @@ #include + #include -#include + +#include #include #include @@ -153,20 +155,20 @@ static_assert(sha256_t::kBits == 256, "sha256_t must have 256 bits"); namespace xrpl { -class hardened_hash_test : public beast::unit_test::Suite +class HardenedHashTest : public ::testing::Test { public: template - void + static void check() { T t{}; HardenedHash<>()(t); - pass(); + SUCCEED(); } template