test: Migrate basics Beast tests to GTest (#7136)

Co-authored-by: Alex Kremer <akremer@ripple.com>
This commit is contained in:
Marek Foss
2026-07-06 21:48:36 +02:00
committed by GitHub
parent 7a153a2bce
commit f5e63f8a91
27 changed files with 4145 additions and 4257 deletions

View File

@@ -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

View File

@@ -1,268 +0,0 @@
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/beast/unit_test/suite.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <type_traits>
#include <utility>
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<Buffer>);
static_assert(std::is_nothrow_move_assignable_v<Buffer>);
{ // 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<Slice>(b0)};
BEAST_EXPECT(sane(w));
BEAST_EXPECT(w == b0);
Buffer x{static_cast<Slice>(b1)};
BEAST_EXPECT(sane(x));
BEAST_EXPECT(x == b1);
Buffer y{static_cast<Slice>(b2)};
BEAST_EXPECT(sane(y));
BEAST_EXPECT(y == b2);
Buffer z{static_cast<Slice>(b3)};
BEAST_EXPECT(sane(z));
BEAST_EXPECT(z == b3);
// Assign empty slice to empty buffer
w = static_cast<Slice>(b0);
BEAST_EXPECT(sane(w));
BEAST_EXPECT(w == b0);
// Assign non-empty slice to empty buffer
w = static_cast<Slice>(b1);
BEAST_EXPECT(sane(w));
BEAST_EXPECT(w == b1);
// Assign non-empty slice to non-empty buffer
x = static_cast<Slice>(b2);
BEAST_EXPECT(sane(x));
BEAST_EXPECT(x == b2);
// Assign non-empty slice to non-empty buffer
y = static_cast<Slice>(z);
BEAST_EXPECT(sane(y));
BEAST_EXPECT(y == z);
// Assign empty slice to non-empty buffer:
z = static_cast<Slice>(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

View File

@@ -1,64 +0,0 @@
#include <test/unit_test/FileDirGuard.h>
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/basics/FileUtilities.h>
#include <xrpl/beast/unit_test/suite.h>
#include <boost/system/detail/errc.hpp>
#include <boost/system/detail/error_code.hpp>
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

View File

@@ -1,276 +0,0 @@
#include <xrpl/basics/Number.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/protocol/IOUAmount.h>
#include <cstdint>
#include <limits>
#include <sstream>
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<std::uint32_t>::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

View File

@@ -1,879 +0,0 @@
#include <xrpl/basics/IntrusivePointer.h> // IWYU pragma: keep
#include <xrpl/basics/IntrusivePointer.ipp> // IWYU pragma: keep
#include <xrpl/basics/IntrusiveRefCounts.h>
#include <xrpl/beast/unit_test/suite.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <cassert>
#include <chrono> // IWYU pragma: keep
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <latch>
#include <mutex>
#include <optional>
#include <random>
#include <string>
#include <thread>
#include <utility>
#include <variant>
#include <vector>
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<std::atomic<TrackedState>, kMaxStates> state;
static std::atomic<int> 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<TrackedState>) {};
}
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<void(TrackedState, std::optional<TrackedState>)> tracingCallback;
int id;
private:
static int
checkoutID()
{
return nextId.fetch_add(1, std::memory_order_acq_rel);
}
};
std::array<std::atomic<TrackedState>, TIBase::kMaxStates> TIBase::state;
std::atomic<int> TIBase::nextId{0};
std::function<void(TrackedState, std::optional<TrackedState>)> TIBase::tracingCallback =
[](TrackedState, std::optional<TrackedState>) {};
} // 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<SharedIntrusive<TIBase>> strong;
std::vector<WeakIntrusive<TIBase>> weak;
{
TIBase::ResetStatesGuard const rsg{true};
using enum TrackedState;
auto b = makeSharedIntrusive<TIBase>();
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<TIBase>();
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<TIBase>();
auto id = b->id;
BEAST_EXPECT(TIBase::getState(id) == Alive);
WeakIntrusive<TIBase> 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<TIBase>;
swu b = makeSharedIntrusive<TIBase>();
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<TIBase>();
auto strong2 = makeSharedIntrusive<TIBase>();
auto id1 = strong1->id;
auto id2 = strong2->id;
BEAST_EXPECT(id1 != id2);
SharedWeakUnion<TIBase> union1 = strong1;
SharedWeakUnion<TIBase> 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<TIBase>();
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<TIBase>();
WeakIntrusive<TIBase> weak{strong};
bool destructorRan = false;
bool partialDeleteRan = false;
std::latch partialDeleteStartedSyncPoint{2};
strong->tracingCallback = [&](TrackedState cur, std::optional<TrackedState> 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<TIBase>();
WeakIntrusive<TIBase> weak{strong};
bool destructorRan = false;
bool partialDeleteRan = false;
std::latch weakResetSyncPoint{2};
strong->tracingCallback = [&](TrackedState cur, std::optional<TrackedState> 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<int> destructionState{0};
// returns destructorRan and partialDestructorRan (in that order)
auto getDestructorState = [&]() -> std::pair<bool, bool> {
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<TrackedState> 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::variant<SharedIntrusive<TIBase>, WeakIntrusive<TIBase>>> {
std::vector<std::variant<SharedIntrusive<TIBase>, WeakIntrusive<TIBase>>> 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<TIBase>(toClone));
}
else
{
result.emplace_back(WeakIntrusive<TIBase>(toClone));
}
}
return result;
};
static constexpr int kLoopIters = 2 * 1024;
static constexpr int kNumThreads = 16;
std::vector<SharedIntrusive<TIBase>> toClone;
Barrier loopStartSyncPoint{kNumThreads};
Barrier postCreateToCloneSyncPoint{kNumThreads};
Barrier postCreateVecOfPointersSyncPoint{kNumThreads};
auto engines = [&]() -> std::vector<std::default_random_engine> {
std::random_device rd;
std::vector<std::default_random_engine> 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<TIBase::ResetStatesGuard> 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<TIBase>();
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<std::thread> 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<int> destructionState{0};
// returns destructorRan and partialDestructorRan (in that order)
auto getDestructorState = [&]() -> std::pair<bool, bool> {
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<TrackedState> 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<SharedWeakUnion<TIBase>> {
std::vector<SharedWeakUnion<TIBase>> 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<TIBase>(toClone));
return result;
};
static constexpr int kLoopIters = 2 * 1024;
static constexpr int kFlipPointersLoopIters = 256;
static constexpr int kNumThreads = 16;
std::vector<SharedIntrusive<TIBase>> toClone;
Barrier loopStartSyncPoint{kNumThreads};
Barrier postCreateToCloneSyncPoint{kNumThreads};
Barrier postCreateVecOfPointersSyncPoint{kNumThreads};
Barrier postFlipPointersLoopSyncPoint{kNumThreads};
auto engines = [&]() -> std::vector<std::default_random_engine> {
std::random_device rd;
std::vector<std::default_random_engine> 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<TIBase::ResetStatesGuard> 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<TIBase>();
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<std::thread> 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<int> destructionState{0};
// returns destructorRan and partialDestructorRan (in that order)
auto getDestructorState = [&]() -> std::pair<bool, bool> {
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<TrackedState> 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<SharedIntrusive<TIBase>> 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<TIBase::ResetStatesGuard> 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<TIBase>();
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<std::thread> 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

View File

@@ -1,82 +0,0 @@
#include <test/unit_test/SuiteJournal.h>
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/TaggedCache.ipp> // IWYU pragma: keep
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/Protocol.h>
#include <string>
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<Key, int, true>;
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

View File

@@ -1,309 +0,0 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/ToString.h>
#include <xrpl/beast/unit_test/suite.h>
#include <string>
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

View File

@@ -1,251 +0,0 @@
#include <test/unit_test/SuiteJournal.h>
#include <xrpl/basics/IntrusivePointer.h>
#include <xrpl/basics/IntrusiveRefCounts.h>
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/TaggedCache.ipp> // IWYU pragma: keep
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Protocol.h>
#include <memory>
#include <utility>
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<Key, Value>;
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<Value>("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<std::string>("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<std::string>("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<MyRefCountObject>,
intr_ptr::SharedPtr<MyRefCountObject>>;
IntrPtrCache intrPtrCache("IntrPtrTest", 1, 1s, clock, journal);
intrPtrCache.canonicalizeReplaceCache(1, intr_ptr::makeShared<MyRefCountObject>("one"));
BEAST_EXPECT(intrPtrCache.getCacheSize() == 1);
BEAST_EXPECT(intrPtrCache.size() == 1);
{
{
intrPtrCache.canonicalizeReplaceCache(
1, intr_ptr::makeShared<MyRefCountObject>("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<MyRefCountObject>("one_replaced_2"));
auto p2 = intrPtrCache.fetch(1);
BEAST_EXPECT(*p2 == "one_replaced_2");
intrPtrCache.del(1, true);
}
intrPtrCache.canonicalizeReplaceCache(
1, intr_ptr::makeShared<MyRefCountObject>("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

View File

@@ -1,344 +0,0 @@
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/Units.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
#include <limits>
#include <type_traits>
namespace xrpl::test {
class units_test : public beast::unit_test::Suite
{
private:
void
testTypes()
{
using FeeLevel32 = FeeLevel<std::uint32_t>;
{
XRPAmount const x{100};
BEAST_EXPECT(x.drops() == 100);
BEAST_EXPECT((std::is_same_v<decltype(x)::unit_type, unit::dropTag>));
auto y = 4u * x;
BEAST_EXPECT(y.value() == 400);
BEAST_EXPECT((std::is_same_v<decltype(y)::unit_type, unit::dropTag>));
auto z = 4 * y;
BEAST_EXPECT(z.value() == 1600);
BEAST_EXPECT((std::is_same_v<decltype(z)::unit_type, unit::dropTag>));
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<decltype(*drops)>::unit_type,
unit::dropTag>));
BEAST_EXPECT((std::is_same_v<std::remove_reference_t<decltype(*drops)>, XRPAmount>));
}
{
XRPAmount const x{100};
BEAST_EXPECT(x.value() == 100);
BEAST_EXPECT((std::is_same_v<decltype(x)::unit_type, unit::dropTag>));
auto y = 4u * x;
BEAST_EXPECT(y.value() == 400);
BEAST_EXPECT((std::is_same_v<decltype(y)::unit_type, unit::dropTag>));
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<decltype(*drops)>::unit_type,
unit::dropTag>));
BEAST_EXPECT((std::is_same_v<std::remove_reference_t<decltype(*drops)>, XRPAmount>));
}
{
FeeLevel64 const x{1024};
BEAST_EXPECT(x.value() == 1024);
BEAST_EXPECT((std::is_same_v<decltype(x)::unit_type, unit::feelevelTag>));
std::uint64_t const m = 4;
auto y = m * x;
BEAST_EXPECT(y.value() == 4096);
BEAST_EXPECT((std::is_same_v<decltype(y)::unit_type, unit::feelevelTag>));
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<decltype(*drops)>::unit_type,
unit::dropTag>));
BEAST_EXPECT((std::is_same_v<std::remove_reference_t<decltype(*drops)>, XRPAmount>));
}
}
void
testJson()
{
// Json value functionality
using FeeLevel32 = FeeLevel<std::uint32_t>;
{
FeeLevel32 const x{std::numeric_limits<std::uint32_t>::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<std::uint32_t>::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<std::uint64_t>::max()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == json::ValueType::UInt);
BEAST_EXPECT(y == json::Value{std::numeric_limits<std::uint32_t>::max()});
}
{
FeeLevel64 const x{std::numeric_limits<std::uint64_t>::min()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == json::ValueType::UInt);
BEAST_EXPECT(y == json::Value{0});
}
{
FeeLevelDouble const x{std::numeric_limits<double>::max()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == json::ValueType::Real);
BEAST_EXPECT(y == json::Value{std::numeric_limits<double>::max()});
}
{
FeeLevelDouble const x{std::numeric_limits<double>::min()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == json::ValueType::Real);
BEAST_EXPECT(y == json::Value{std::numeric_limits<double>::min()});
}
{
XRPAmount const x{std::numeric_limits<std::int64_t>::max()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == json::ValueType::Int);
BEAST_EXPECT(y == json::Value{std::numeric_limits<std::int32_t>::max()});
}
{
XRPAmount const x{std::numeric_limits<std::int64_t>::min()};
auto y = x.jsonClipped();
BEAST_EXPECT(y.type() == json::ValueType::Int);
BEAST_EXPECT(y == json::Value{std::numeric_limits<std::int32_t>::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<std::uint32_t>;
{
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

View File

@@ -1,326 +0,0 @@
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
#include <limits>
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<std::uint32_t>();
BEAST_EXPECT(testOther);
BEAST_EXPECT(*testOther == 200); // NOLINT(bugprone-unchecked-optional-access)
test = std::numeric_limits<std::uint64_t>::max();
testOther = test.dropsAs<std::uint32_t>();
BEAST_EXPECT(!testOther);
test = -1;
testOther = test.dropsAs<std::uint32_t>();
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<std::uint32_t>::max();
constexpr auto kMaxXrp = std::numeric_limits<XRPAmount::value_type>::max();
constexpr auto kMinXrp = std::numeric_limits<XRPAmount::value_type>::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

View File

@@ -1,440 +0,0 @@
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/detail/token_errors.h>
#include <boost/multiprecision/cpp_int.hpp> // IWYU pragma: keep
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <limits>
#include <ranges>
#include <stdexcept>
#include <string>
#include <tuple>
#include <vector>
#ifndef _MSC_VER
#include <xrpl/protocol/detail/b58_utils.h>
#include <xrpl/protocol/tokens.h>
#include <array>
#include <cstddef>
#include <random>
#include <span>
#include <sstream>
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<xrpl::TokenType, std::size_t>
{
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<xrpl::TokenType, std::size_t>
{
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<std::uint8_t> d)
-> std::tuple<xrpl::TokenType, std::span<std::uint8_t>>
{
auto& rng = randEngine();
std::uniform_int_distribution<std::uint8_t> 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<std::uint8_t> a, std::span<std::uint8_t> b)
{
auto asString = [](std::span<std::uint8_t> 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<std::uint8_t> a, std::span<std::uint8_t> b)
{
auto asString = [](std::span<std::uint8_t> 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<std::uint64_t> in)
{
boost::multiprecision::checked_uint512_t mbp = 0;
for (auto& word : std::views::reverse(in))
{
mbp <<= 64;
mbp += word;
}
return mbp;
}
std::vector<std::uint64_t>
randomBigInt(std::uint8_t minSize = 1, std::uint8_t maxSize = 5)
{
auto eng = randEngine();
std::uniform_int_distribution<std::uint8_t> numCoeffDist(minSize, maxSize);
std::uniform_int_distribution<std::uint64_t> dist;
auto const numCoeff = numCoeffDist(eng);
std::vector<std::uint64_t> 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<std::uint64_t> dist;
std::uniform_int_distribution<std::uint64_t> 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<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refDiv = boostBigInt / d;
auto const refMod = boostBigInt % d;
auto const mod = b58_fast::detail::inplaceBigintDivRem(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
auto const foundDiv = multiprecision_utils::toBoostMP(bigInt);
BEAST_EXPECT(refMod.convert_to<std::uint64_t>() == 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<std::uint64_t>::max())
{
bigInt[bigInt.size() - 1] -= 1; // Prevent overflow
}
auto const boostBigInt = multiprecision_utils::toBoostMP(
std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refAdd = boostBigInt + d;
auto const result = b58_fast::detail::inplaceBigintAdd(
std::span<uint64_t>(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<std::uint64_t> bigInt(5, std::numeric_limits<std::uint64_t>::max());
auto const boostBigInt = multiprecision_utils::toBoostMP(
std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refAdd = boostBigInt + d;
auto const result = b58_fast::detail::inplaceBigintAdd(
std::span<uint64_t>(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<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refMul = boostBigInt * d;
auto const result = b58_fast::detail::inplaceBigintMul(
std::span<uint64_t>(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<std::uint64_t> bigInt(5, std::numeric_limits<std::uint64_t>::max());
auto const boostBigInt = multiprecision_utils::toBoostMP(
std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refMul = boostBigInt * d;
auto const result = b58_fast::detail::inplaceBigintMul(
std::span<uint64_t>(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<std::uint8_t> const& b256Data) {
std::array<std::uint8_t, 64> b58ResultBuf[2];
std::array<std::span<std::uint8_t>, 2> b58Result;
std::array<std::uint8_t, 64> b256ResultBuf[2];
std::array<std::span<std::uint8_t>, 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<std::uint8_t, 128> 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<std::uint8_t> const& b256Data) {
std::array<std::uint8_t, 64> b58ResultBuf[2];
std::array<std::span<std::uint8_t>, 2> b58Result;
std::array<std::uint8_t, 64> b256ResultBuf[2];
std::array<std::span<std::uint8_t>, 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<std::uint8_t> 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<std::uint8_t, 128> 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<std::uint8_t, 128> 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

View File

@@ -1,376 +0,0 @@
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/hardened_hash.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Zero.h>
#include <boost/endian/detail/order.hpp>
#include <array>
#include <cassert>
#include <complex>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <stdexcept>
#include <string_view>
#include <type_traits>
#include <unordered_set>
#include <utility>
#include <vector>
namespace xrpl::test {
// a non-hashing Hasher that just copies the bytes.
// Used to test hash_append in base_uint
template <std::size_t Bits>
struct Nonhash
{
static constexpr auto kEndian = boost::endian::order::big;
static constexpr std::size_t kWidth = Bits / 8;
std::array<std::uint8_t, kWidth> 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<test96>);
static_assert(std::is_copy_assignable_v<test96>);
void
testComparisons()
{
{
static constexpr std::array<std::pair<std::string_view, std::string_view>, 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<std::pair<std::string_view, std::string_view>, 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<test96, std::complex<double>>);
static_assert(!std::is_assignable_v<test96&, std::complex<double>>);
testComparisons();
// used to verify set insertion (hashing required)
std::unordered_set<test96, HardenedHash<>> 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<std::uint8_t>(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<char> 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<char> 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

View File

@@ -1,83 +0,0 @@
#include <test/jtx/Account.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/join.h>
#include <xrpl/beast/unit_test/suite.h>
#include <array>
#include <cstddef>
#include <initializer_list>
#include <sstream>
#include <string>
#include <vector>
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<int, 4>{2, -1, 5, 10}, "/"), "2/-1/5/10");
// One item C++ array edge case
test(CollectionAndDelimiter(std::array<std::string, 1>{"test"}, " & "), "test");
// Empty C++ array edge case
test(CollectionAndDelimiter(std::array<int, 0>{}, ","), "");
{
// 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<size_t>{19, 25}, "+"), "19+25");
// vector
test(CollectionAndDelimiter(std::vector<int>{0, 42}, std::to_string(99)), "09942");
{
// vector with one item edge case
using namespace jtx;
test(
CollectionAndDelimiter(std::vector<Account>{Account::kMaster}, "xxx"),
Account::kMaster.human());
}
// empty vector edge case
test(CollectionAndDelimiter(std::vector<uint256>{}, ","), "");
// 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

View File

@@ -0,0 +1,260 @@
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <gtest/gtest.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <type_traits>
#include <utility>
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<Buffer>);
static_assert(std::is_nothrow_move_assignable_v<Buffer>);
{ // 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<Slice>(b0)};
EXPECT_TRUE(sane(w));
EXPECT_EQ(w, b0);
Buffer x{static_cast<Slice>(b1)};
EXPECT_TRUE(sane(x));
EXPECT_EQ(x, b1);
Buffer y{static_cast<Slice>(b2)};
EXPECT_TRUE(sane(y));
EXPECT_EQ(y, b2);
Buffer z{static_cast<Slice>(b3)};
EXPECT_TRUE(sane(z));
EXPECT_EQ(z, b3);
// Assign empty slice to empty buffer
w = static_cast<Slice>(b0);
EXPECT_TRUE(sane(w));
EXPECT_EQ(w, b0);
// Assign non-empty slice to empty buffer
w = static_cast<Slice>(b1);
EXPECT_TRUE(sane(w));
EXPECT_EQ(w, b1);
// Assign non-empty slice to non-empty buffer
x = static_cast<Slice>(b2);
EXPECT_TRUE(sane(x));
EXPECT_EQ(x, b2);
// Assign non-empty slice to non-empty buffer
y = static_cast<Slice>(z);
EXPECT_TRUE(sane(y));
EXPECT_EQ(y, z);
// Assign empty slice to non-empty buffer:
z = static_cast<Slice>(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

View File

@@ -0,0 +1,94 @@
#include <xrpl/basics/FileUtilities.h>
#include <xrpl/basics/ByteUtilities.h>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/system/detail/errc.hpp>
#include <boost/system/detail/error_code.hpp>
#include <gtest/gtest.h>
#include <fstream>
#include <stdexcept>
#include <string>
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

View File

@@ -0,0 +1,243 @@
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Zero.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <limits>
#include <sstream>
#include <string>
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<std::uint32_t>::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

View File

@@ -0,0 +1,844 @@
#include <xrpl/basics/IntrusivePointer.h> // IWYU pragma: keep
#include <xrpl/basics/IntrusivePointer.ipp> // IWYU pragma: keep
#include <xrpl/basics/IntrusiveRefCounts.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono> // IWYU pragma: keep
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <latch>
#include <mutex>
#include <optional>
#include <random>
#include <stdexcept>
#include <thread>
#include <utility>
#include <variant>
#include <vector>
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<std::atomic<TrackedState>, kMaxStates> state;
static std::atomic<std::size_t> 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<TrackedState>) {};
}
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<void(TrackedState, std::optional<TrackedState>)> 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<std::atomic<TrackedState>, TIBase::kMaxStates> TIBase::state;
std::atomic<std::size_t> TIBase::nextId{0};
std::function<void(TrackedState, std::optional<TrackedState>)> TIBase::tracingCallback =
[](TrackedState, std::optional<TrackedState>) {};
} // 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<SharedIntrusive<TIBase>> strong;
std::vector<WeakIntrusive<TIBase>> weak;
{
TIBase::ResetStatesGuard const rsg{true};
using enum TrackedState;
auto b = makeSharedIntrusive<TIBase>();
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<TIBase>();
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<TIBase>();
auto id = b->id;
EXPECT_EQ(TIBase::getState(id), Alive);
WeakIntrusive<TIBase> 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<TIBase>;
swu b = makeSharedIntrusive<TIBase>();
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<TIBase>();
auto strong2 = makeSharedIntrusive<TIBase>();
auto id1 = strong1->id;
auto id2 = strong2->id;
EXPECT_NE(id1, id2);
SharedWeakUnion<TIBase> union1 = strong1;
SharedWeakUnion<TIBase> 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<TIBase>();
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<TIBase>();
WeakIntrusive<TIBase> weak{strong};
std::atomic<bool> destructorRan{false};
std::atomic<bool> partialDeleteRan{false};
std::latch partialDeleteStartedSyncPoint{2};
strong->tracingCallback = [&](TrackedState cur, std::optional<TrackedState> 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<TIBase>();
WeakIntrusive<TIBase> weak{strong};
std::atomic<bool> destructorRan{false};
std::atomic<bool> partialDeleteRan{false};
std::latch weakResetSyncPoint{2};
strong->tracingCallback = [&](TrackedState cur, std::optional<TrackedState> 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<int> destructionState{0};
// returns destructorRan and partialDestructorRan (in that order)
auto getDestructorState = [&]() -> std::pair<bool, bool> {
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<TrackedState> 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::variant<SharedIntrusive<TIBase>, WeakIntrusive<TIBase>>> {
std::vector<std::variant<SharedIntrusive<TIBase>, WeakIntrusive<TIBase>>> 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<TIBase>(toClone));
}
else
{
result.emplace_back(WeakIntrusive<TIBase>(toClone));
}
}
return result;
};
constexpr int kLoopIters = 2 * 1024;
constexpr int kNumThreads = 16;
std::vector<SharedIntrusive<TIBase>> toClone;
Barrier loopStartSyncPoint{kNumThreads};
Barrier postCreateToCloneSyncPoint{kNumThreads};
Barrier postCreateVecOfPointersSyncPoint{kNumThreads};
auto engines = [&]() -> std::vector<std::default_random_engine> {
std::random_device rd;
std::vector<std::default_random_engine> 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<TIBase::ResetStatesGuard> 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<TIBase>();
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<std::thread> 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<int> destructionState{0};
// returns destructorRan and partialDestructorRan (in that order)
auto getDestructorState = [&]() -> std::pair<bool, bool> {
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<TrackedState> 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<SharedWeakUnion<TIBase>> {
std::vector<SharedWeakUnion<TIBase>> 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<TIBase>(toClone));
return result;
};
constexpr int kLoopIters = 2 * 1024;
constexpr int kFlipPointersLoopIters = 256;
constexpr int kNumThreads = 16;
std::vector<SharedIntrusive<TIBase>> toClone;
Barrier loopStartSyncPoint{kNumThreads};
Barrier postCreateToCloneSyncPoint{kNumThreads};
Barrier postCreateVecOfPointersSyncPoint{kNumThreads};
Barrier postFlipPointersLoopSyncPoint{kNumThreads};
auto engines = [&]() -> std::vector<std::default_random_engine> {
std::random_device rd;
std::vector<std::default_random_engine> 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<TIBase::ResetStatesGuard> 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<TIBase>();
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<std::thread> 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<int> destructionState{0};
// returns destructorRan and partialDestructorRan (in that order)
auto getDestructorState = [&]() -> std::pair<bool, bool> {
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<TrackedState> 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<SharedIntrusive<TIBase>> 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<TIBase::ResetStatesGuard> 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<TIBase>();
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<std::thread> 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

View File

@@ -0,0 +1,81 @@
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/TaggedCache.ipp> // IWYU pragma: keep
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Protocol.h>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <string>
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<Key, int, true>;
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

View File

@@ -0,0 +1,293 @@
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/ToString.h>
#include <gtest/gtest.h>
#include <string>
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

View File

@@ -0,0 +1,246 @@
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/IntrusivePointer.h>
#include <xrpl/basics/IntrusiveRefCounts.h>
#include <xrpl/basics/TaggedCache.ipp> // IWYU pragma: keep
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Protocol.h>
#include <gtest/gtest.h>
#include <helpers/TestSink.h>
#include <memory>
#include <string>
#include <utility>
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<Key, Value>;
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<Value>("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<std::string>("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<std::string>("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<MyRefCountObject>,
intr_ptr::SharedPtr<MyRefCountObject>>;
IntrPtrCache intrPtrCache("IntrPtrTest", 1, 1s, clock, journal);
intrPtrCache.canonicalizeReplaceCache(1, intr_ptr::makeShared<MyRefCountObject>("one"));
EXPECT_EQ(intrPtrCache.getCacheSize(), 1);
EXPECT_EQ(intrPtrCache.size(), 1);
{
{
intrPtrCache.canonicalizeReplaceCache(
1, intr_ptr::makeShared<MyRefCountObject>("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<MyRefCountObject>("one_replaced_2"));
auto p2 = intrPtrCache.fetch(1);
EXPECT_EQ(*p2, "one_replaced_2");
intrPtrCache.del(1, true);
}
intrPtrCache.canonicalizeReplaceCache(
1, intr_ptr::makeShared<MyRefCountObject>("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

View File

@@ -0,0 +1,328 @@
#include <xrpl/protocol/Units.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/XRPAmount.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <limits>
#include <type_traits>
namespace xrpl::test {
TEST(UnitsTest, types)
{
using FeeLevel32 = FeeLevel<std::uint32_t>;
{
XRPAmount const x{100};
EXPECT_EQ(x.drops(), 100);
EXPECT_TRUE((std::is_same_v<decltype(x)::unit_type, unit::dropTag>));
auto y = 4u * x;
EXPECT_EQ(y.value(), 400);
EXPECT_TRUE((std::is_same_v<decltype(y)::unit_type, unit::dropTag>));
auto z = 4 * y;
EXPECT_EQ(z.value(), 1600);
EXPECT_TRUE((std::is_same_v<decltype(z)::unit_type, unit::dropTag>));
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<std::remove_reference_t<decltype(*drops)>::unit_type, unit::dropTag>));
EXPECT_TRUE((std::is_same_v<std::remove_reference_t<decltype(*drops)>, XRPAmount>));
}
{
XRPAmount const x{100};
EXPECT_EQ(x.value(), 100);
EXPECT_TRUE((std::is_same_v<decltype(x)::unit_type, unit::dropTag>));
auto y = 4u * x;
EXPECT_EQ(y.value(), 400);
EXPECT_TRUE((std::is_same_v<decltype(y)::unit_type, unit::dropTag>));
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<std::remove_reference_t<decltype(*drops)>::unit_type, unit::dropTag>));
EXPECT_TRUE((std::is_same_v<std::remove_reference_t<decltype(*drops)>, XRPAmount>));
}
{
FeeLevel64 const x{1024};
EXPECT_EQ(x.value(), 1024);
EXPECT_TRUE((std::is_same_v<decltype(x)::unit_type, unit::feelevelTag>));
std::uint64_t const m = 4;
auto y = m * x;
EXPECT_EQ(y.value(), 4096);
EXPECT_TRUE((std::is_same_v<decltype(y)::unit_type, unit::feelevelTag>));
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<std::remove_reference_t<decltype(*drops)>::unit_type, unit::dropTag>));
EXPECT_TRUE((std::is_same_v<std::remove_reference_t<decltype(*drops)>, XRPAmount>));
}
}
TEST(UnitsTest, json)
{
// Json value functionality
using FeeLevel32 = FeeLevel<std::uint32_t>;
{
FeeLevel32 const x{std::numeric_limits<std::uint32_t>::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<std::uint32_t>::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<std::uint64_t>::max()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::UInt);
EXPECT_EQ(y, json::Value{std::numeric_limits<std::uint32_t>::max()});
}
{
FeeLevel64 const x{std::numeric_limits<std::uint64_t>::min()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::UInt);
EXPECT_EQ(y, json::Value{0});
}
{
FeeLevelDouble const x{std::numeric_limits<double>::max()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::Real);
EXPECT_EQ(y, json::Value{std::numeric_limits<double>::max()});
}
{
FeeLevelDouble const x{std::numeric_limits<double>::min()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::Real);
EXPECT_EQ(y, json::Value{std::numeric_limits<double>::min()});
}
{
XRPAmount const x{std::numeric_limits<std::int64_t>::max()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::Int);
EXPECT_EQ(y, json::Value{std::numeric_limits<std::int32_t>::max()});
}
{
XRPAmount const x{std::numeric_limits<std::int64_t>::min()};
auto y = x.jsonClipped();
EXPECT_EQ(y.type(), json::ValueType::Int);
EXPECT_EQ(y, json::Value{std::numeric_limits<std::int32_t>::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<std::uint32_t>;
{
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

View File

@@ -0,0 +1,295 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/beast/utility/Zero.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <limits>
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<std::uint32_t>();
EXPECT_TRUE(testOther);
EXPECT_EQ(*testOther, 200); // NOLINT(bugprone-unchecked-optional-access)
test = std::numeric_limits<std::uint64_t>::max();
testOther = test.dropsAs<std::uint32_t>();
EXPECT_FALSE(testOther);
test = -1;
testOther = test.dropsAs<std::uint32_t>();
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<std::uint32_t>::max();
constexpr auto kMaxXrp = std::numeric_limits<XRPAmount::value_type>::max();
constexpr auto kMinXrp = std::numeric_limits<XRPAmount::value_type>::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

View File

@@ -0,0 +1,438 @@
#include <xrpl/protocol/detail/token_errors.h>
#include <boost/multiprecision/cpp_int.hpp> // IWYU pragma: keep
#include <gtest/gtest.h>
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <limits>
#include <ranges>
#include <stdexcept>
#include <string>
#include <tuple>
#include <vector>
#ifndef _MSC_VER
#include <xrpl/protocol/detail/b58_utils.h>
#include <xrpl/protocol/tokens.h>
#include <array>
#include <cstddef>
#include <random>
#include <span>
#include <sstream>
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<xrpl::TokenType, std::size_t>
{
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<xrpl::TokenType, std::size_t>
{
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<std::uint8_t> d)
-> std::tuple<xrpl::TokenType, std::span<std::uint8_t>>
{
auto& rng = randEngine();
std::uniform_int_distribution<std::uint8_t> 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<std::uint8_t> a, std::span<std::uint8_t> b)
{
auto asString = [](std::span<std::uint8_t> 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<std::uint8_t> a, std::span<std::uint8_t> b)
{
auto asString = [](std::span<std::uint8_t> 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<std::uint64_t> in)
{
boost::multiprecision::checked_uint512_t mbp = 0;
for (auto const& i : std::views::reverse(in))
{
mbp <<= 64;
mbp += i;
}
return mbp;
}
std::vector<std::uint64_t>
randomBigInt(std::uint8_t minSize = 1, std::uint8_t maxSize = 5)
{
auto eng = randEngine();
std::uniform_int_distribution<std::uint8_t> numCoeffDist(minSize, maxSize);
std::uniform_int_distribution<std::uint64_t> dist;
auto const numCoeff = numCoeffDist(eng);
std::vector<std::uint64_t> 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<std::uint64_t> dist;
std::uniform_int_distribution<std::uint64_t> 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<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refDiv = boostBigInt / d;
auto const refMod = boostBigInt % d;
auto const mod = b58_fast::detail::inplaceBigintDivRem(
std::span<uint64_t>(bigInt.data(), bigInt.size()), d);
auto const foundDiv = multiprecision_utils::toBoostMP(bigInt);
EXPECT_EQ(refMod.convert_to<std::uint64_t>(), 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<std::uint64_t>::max())
{
bigInt[bigInt.size() - 1] -= 1; // Prevent overflow
}
auto const boostBigInt =
multiprecision_utils::toBoostMP(std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refAdd = boostBigInt + d;
auto const result = b58_fast::detail::inplaceBigintAdd(
std::span<uint64_t>(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<std::uint64_t> bigInt(5, std::numeric_limits<std::uint64_t>::max());
auto const boostBigInt =
multiprecision_utils::toBoostMP(std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refAdd = boostBigInt + d;
auto const result = b58_fast::detail::inplaceBigintAdd(
std::span<uint64_t>(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<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refMul = boostBigInt * d;
auto const result = b58_fast::detail::inplaceBigintMul(
std::span<uint64_t>(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<std::uint64_t> bigInt(5, std::numeric_limits<std::uint64_t>::max());
auto const boostBigInt =
multiprecision_utils::toBoostMP(std::span<std::uint64_t>(bigInt.data(), bigInt.size()));
auto const refMul = boostBigInt * d;
auto const result = b58_fast::detail::inplaceBigintMul(
std::span<uint64_t>(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<std::uint8_t> const& b256Data) {
std::array<std::uint8_t, 64> b58ResultBuf[2];
std::array<std::span<std::uint8_t>, 2> b58Result;
std::array<std::uint8_t, 64> b256ResultBuf[2];
std::array<std::span<std::uint8_t>, 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<std::uint8_t, 128> 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<std::uint8_t> const& b256Data) {
std::array<std::uint8_t, 64> b58ResultBuf[2];
std::array<std::span<std::uint8_t>, 2> b58Result;
std::array<std::uint8_t, 64> b256ResultBuf[2];
std::array<std::span<std::uint8_t>, 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<std::uint8_t> 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<std::uint8_t, 128> 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<std::uint8_t, 128> b256DataBuf{};
auto const [tokType, b256Data] = randomB256TestData(b256DataBuf);
testIt(tokType, b256Data);
}
}
} // namespace xrpl::test
#endif // _MSC_VER

View File

@@ -0,0 +1,360 @@
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/hardened_hash.h>
#include <xrpl/beast/utility/Zero.h>
#include <boost/endian/detail/order.hpp>
#include <gtest/gtest.h>
#include <array>
#include <cassert>
#include <complex>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <unordered_set>
#include <utility>
#include <vector>
namespace xrpl::test {
// a non-hashing Hasher that just copies the bytes.
// Used to test hash_append in base_uint
template <std::size_t Bits>
struct Nonhash
{
static constexpr auto const kEndian = boost::endian::order::big;
static constexpr std::size_t kWidth = Bits / 8;
std::array<std::uint8_t, kWidth> 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<BaseUInt96>);
static_assert(std::is_copy_assignable_v<BaseUInt96>);
static void
testComparisons()
{
{
static constexpr std::array<std::pair<std::string_view, std::string_view>, 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<std::pair<std::string_view, std::string_view>, 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<BaseUInt96, std::complex<double>>);
static_assert(!std::is_assignable_v<BaseUInt96&, std::complex<double>>);
testComparisons();
// used to verify set insertion (hashing required)
std::unordered_set<BaseUInt96, HardenedHash<>> 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<std::uint8_t>(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<char> 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<char> 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

View File

@@ -1,6 +1,8 @@
#include <xrpl/basics/hardened_hash.h>
#include <xrpl/beast/hash/hash_append.h>
#include <xrpl/beast/unit_test/suite.h>
#include <gtest/gtest.h>
#include <array>
#include <cstddef>
@@ -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 <class T>
void
static void
check()
{
T t{};
HardenedHash<>()(t);
pass();
SUCCEED();
}
template <template <class T> class U>
void
static void
checkUserType()
{
check<U<bool>>();
@@ -191,48 +193,35 @@ public:
}
template <template <class T> class C>
void
static void
checkContainer()
{
{
C<detail::TestUserTypeMember<std::string>> const c;
}
pass();
SUCCEED();
{
C<detail::TestUserTypeFree<std::string>> const c;
}
pass();
}
void
testUserTypes()
{
testcase("user types");
checkUserType<detail::TestUserTypeMember>();
checkUserType<detail::TestUserTypeFree>();
}
void
testContainers()
{
testcase("containers");
checkContainer<detail::test_hardened_unordered_set>();
checkContainer<detail::test_hardened_unordered_map>();
checkContainer<detail::test_hardened_unordered_multiset>();
checkContainer<detail::test_hardened_unordered_multimap>();
}
void
run() override
{
testUserTypes();
testContainers();
SUCCEED();
}
};
BEAST_DEFINE_TESTSUITE(hardened_hash, basics, xrpl);
TEST_F(HardenedHashTest, user_types)
{
checkUserType<detail::TestUserTypeMember>();
checkUserType<detail::TestUserTypeFree>();
}
TEST_F(HardenedHashTest, containers)
{
checkContainer<detail::test_hardened_unordered_set>();
checkContainer<detail::test_hardened_unordered_map>();
checkContainer<detail::test_hardened_unordered_multiset>();
checkContainer<detail::test_hardened_unordered_multimap>();
}
} // namespace xrpl

View File

@@ -0,0 +1,80 @@
#include <xrpl/basics/join.h>
#include <xrpl/basics/base_uint.h>
#include <gtest/gtest.h>
#include <array>
#include <cstddef>
#include <initializer_list>
#include <sstream>
#include <string>
#include <vector>
namespace xrpl::test {
struct JoinTest : public ::testing::Test
{
};
TEST_F(JoinTest, join)
{
auto test = [](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();
EXPECT_EQ(str.substr(1, str.length() - 2), expected);
EXPECT_EQ(str.front(), '(');
EXPECT_EQ(str.back(), ')');
};
// C++ array
test(CollectionAndDelimiter(std::array<int, 4>{2, -1, 5, 10}, "/"), "2/-1/5/10");
// One item C++ array edge case
test(CollectionAndDelimiter(std::array<std::string, 1>{"test"}, " & "), "test");
// Empty C++ array edge case
test(CollectionAndDelimiter(std::array<int, 0>{}, ","), "");
{
// 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<size_t>{19, 25}, "+"), "19+25");
// vector
test(CollectionAndDelimiter(std::vector<int>{0, 42}, std::to_string(99)), "09942");
// vector with one item edge case
test(CollectionAndDelimiter(std::vector<std::string>{"master"}, "xxx"), "master");
// vector with one non-trivial streamable item edge case
test(
CollectionAndDelimiter(std::vector<uint256>{uint256{1}}, "xxx"),
"0000000000000000000000000000000000000000000000000000000000000001");
// empty vector edge case
test(CollectionAndDelimiter(std::vector<uint256>{}, ","), "");
// 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 xrpl::test