diff --git a/CMakeLists.txt b/CMakeLists.txt index ade9c2f995..3bb6fad3a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,4 +147,5 @@ include(XrplValidatorKeys) if(tests) include(CTest) add_subdirectory(src/tests/libxrpl) + add_subdirectory(src/doctest) endif() diff --git a/src/doctest/CMakeLists.txt b/src/doctest/CMakeLists.txt new file mode 100644 index 0000000000..81449b6e43 --- /dev/null +++ b/src/doctest/CMakeLists.txt @@ -0,0 +1,48 @@ +# CMake configuration for doctest-based tests +# These are converted from the beast unit_test framework + +find_package(doctest REQUIRED) + +# Custom target for all doctest tests defined in this file +add_custom_target(xrpl.doctests) + +# Common library dependencies +add_library(xrpl.imports.doctest INTERFACE) +target_link_libraries(xrpl.imports.doctest INTERFACE + doctest::doctest + xrpl.libxrpl +) + +# Include xrpld sources for tests that need app-level functionality +target_include_directories(xrpl.imports.doctest INTERFACE + $ +) + +# Link against xrpld libraries for tests that need app-level functionality +# Tests like LoadFeeTrack, PendingSaves need xrpld components +target_link_libraries(xrpl.imports.doctest INTERFACE + Xrpl::boost + Xrpl::opts + Xrpl::libs +) + +# Collect all doctest source files +file(GLOB_RECURSE doctest_sources CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp" +) + +# Create the main test executable +add_executable(xrpl.doctest ${doctest_sources}) +target_link_libraries(xrpl.doctest PRIVATE xrpl.imports.doctest) + +# Make sure the test isn't optimized away in unity builds +set_target_properties(xrpl.doctest PROPERTIES + UNITY_BUILD_MODE GROUP + UNITY_BUILD_BATCH_SIZE 0 +) + +# Add as a CTest test +add_test(NAME xrpl.doctest COMMAND xrpl.doctest) + +add_dependencies(xrpl.doctests xrpl.doctest) + diff --git a/src/doctest/basics/Buffer.cpp b/src/doctest/basics/Buffer.cpp new file mode 100644 index 0000000000..dc7cef1788 --- /dev/null +++ b/src/doctest/basics/Buffer.cpp @@ -0,0 +1,257 @@ +#include + +#include + +#include +#include + +using namespace xrpl; + +namespace { +bool +sane(Buffer const& b) +{ + if (b.size() == 0) + return b.data() == nullptr; + + return b.data() != nullptr; +} +} // namespace + +TEST_SUITE_BEGIN("Buffer"); + +TEST_CASE("basic operations") +{ + 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 b0; + CHECK(sane(b0)); + CHECK(b0.empty()); + + Buffer b1{0}; + CHECK(sane(b1)); + CHECK(b1.empty()); + std::memcpy(b1.alloc(16), data, 16); + CHECK(sane(b1)); + CHECK(!b1.empty()); + CHECK(b1.size() == 16); + + Buffer b2{b1.size()}; + CHECK(sane(b2)); + CHECK(!b2.empty()); + CHECK(b2.size() == b1.size()); + std::memcpy(b2.data(), data + 16, 16); + + Buffer b3{data, sizeof(data)}; + CHECK(sane(b3)); + CHECK(!b3.empty()); + CHECK(b3.size() == sizeof(data)); + CHECK(std::memcmp(b3.data(), data, b3.size()) == 0); + + // Check equality and inequality comparisons + CHECK(b0 == b0); + CHECK(b0 != b1); + CHECK(b1 == b1); + CHECK(b1 != b2); + CHECK(b2 != b3); + + SUBCASE("Copy Construction / Assignment") + { + Buffer x{b0}; + CHECK(x == b0); + CHECK(sane(x)); + Buffer y{b1}; + CHECK(y == b1); + CHECK(sane(y)); + x = b2; + CHECK(x == b2); + CHECK(sane(x)); + x = y; + CHECK(x == y); + CHECK(sane(x)); + y = b3; + CHECK(y == b3); + CHECK(sane(y)); + x = b0; + CHECK(x == b0); + CHECK(sane(x)); +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wself-assign-overloaded" +#endif + + x = x; + CHECK(x == b0); + CHECK(sane(x)); + y = y; + CHECK(y == b3); + CHECK(sane(y)); + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + } + + SUBCASE("Move Construction / Assignment") + { + static_assert(std::is_nothrow_move_constructible::value, ""); + static_assert(std::is_nothrow_move_assignable::value, ""); + + { // Move-construct from empty buf + Buffer x; + Buffer y{std::move(x)}; + CHECK(sane(x)); + CHECK(x.empty()); + CHECK(sane(y)); + CHECK(y.empty()); + CHECK(x == y); + } + + { // Move-construct from non-empty buf + Buffer x{b1}; + Buffer y{std::move(x)}; + CHECK(sane(x)); + CHECK(x.empty()); + CHECK(sane(y)); + CHECK(y == b1); + } + + { // Move assign empty buf to empty buf + Buffer x; + Buffer y; + + x = std::move(y); + CHECK(sane(x)); + CHECK(x.empty()); + CHECK(sane(y)); + CHECK(y.empty()); + } + + { // Move assign non-empty buf to empty buf + Buffer x; + Buffer y{b1}; + + x = std::move(y); + CHECK(sane(x)); + CHECK(x == b1); + CHECK(sane(y)); + CHECK(y.empty()); + } + + { // Move assign empty buf to non-empty buf + Buffer x{b1}; + Buffer y; + + x = std::move(y); + CHECK(sane(x)); + CHECK(x.empty()); + CHECK(sane(y)); + CHECK(y.empty()); + } + + { // Move assign non-empty buf to non-empty buf + Buffer x{b1}; + Buffer y{b2}; + Buffer z{b3}; + + x = std::move(y); + CHECK(sane(x)); + CHECK(!x.empty()); + CHECK(sane(y)); + CHECK(y.empty()); + + x = std::move(z); + CHECK(sane(x)); + CHECK(!x.empty()); + CHECK(sane(z)); + CHECK(z.empty()); + } + } + + SUBCASE("Slice Conversion / Construction / Assignment") + { + Buffer w{static_cast(b0)}; + CHECK(sane(w)); + CHECK(w == b0); + + Buffer x{static_cast(b1)}; + CHECK(sane(x)); + CHECK(x == b1); + + Buffer y{static_cast(b2)}; + CHECK(sane(y)); + CHECK(y == b2); + + Buffer z{static_cast(b3)}; + CHECK(sane(z)); + CHECK(z == b3); + + // Assign empty slice to empty buffer + w = static_cast(b0); + CHECK(sane(w)); + CHECK(w == b0); + + // Assign non-empty slice to empty buffer + w = static_cast(b1); + CHECK(sane(w)); + CHECK(w == b1); + + // Assign non-empty slice to non-empty buffer + x = static_cast(b2); + CHECK(sane(x)); + CHECK(x == b2); + + // Assign non-empty slice to non-empty buffer + y = static_cast(z); + CHECK(sane(y)); + CHECK(y == z); + + // Assign empty slice to non-empty buffer: + z = static_cast(b0); + CHECK(sane(z)); + CHECK(z == b0); + } + + SUBCASE("Allocation, Deallocation and Clearing") + { + 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); + CHECK(sane(x)); + CHECK(x.size() == i); + CHECK((x.data() == nullptr) == (i == 0)); + + // Try to allocate some more data (always non-zero) + x(i + 1); + CHECK(sane(x)); + CHECK(x.size() == i + 1); + CHECK(x.data() != nullptr); + + // Try to clear: + x.clear(); + CHECK(sane(x)); + CHECK(x.size() == 0); + CHECK(x.data() == nullptr); + + // Try to clear again: + x.clear(); + CHECK(sane(x)); + CHECK(x.size() == 0); + CHECK(x.data() == nullptr); + }; + + for (std::size_t i = 0; i < 16; ++i) + { + test(b0, i); + test(b1, i); + } + } +} + +TEST_SUITE_END(); diff --git a/src/doctest/basics/Expected.cpp b/src/doctest/basics/Expected.cpp new file mode 100644 index 0000000000..3edb30770b --- /dev/null +++ b/src/doctest/basics/Expected.cpp @@ -0,0 +1,228 @@ +#include +#include + +#include + +#if BOOST_VERSION >= 107500 +#include // Not part of boost before version 1.75 +#endif // BOOST_VERSION +#include +#include + +using namespace xrpl; + +TEST_SUITE_BEGIN("Expected"); + +TEST_CASE("non-error const construction") +{ + auto const expected = []() -> Expected { + return "Valid value"; + }(); + CHECK(expected); + CHECK(expected.has_value()); + CHECK(expected.value() == "Valid value"); + CHECK(*expected == "Valid value"); + CHECK(expected->at(0) == 'V'); + + bool throwOccurred = false; + try + { + // There's no error, so should throw. + [[maybe_unused]] TER const t = expected.error(); + } + catch (std::runtime_error const& e) + { + CHECK(e.what() == std::string("bad expected access")); + throwOccurred = true; + } + CHECK(throwOccurred); +} + +TEST_CASE("non-error non-const construction") +{ + auto expected = []() -> Expected { + return "Valid value"; + }(); + CHECK(expected); + CHECK(expected.has_value()); + CHECK(expected.value() == "Valid value"); + CHECK(*expected == "Valid value"); + CHECK(expected->at(0) == 'V'); + std::string mv = std::move(*expected); + CHECK(mv == "Valid value"); + + bool throwOccurred = false; + try + { + // There's no error, so should throw. + [[maybe_unused]] TER const t = expected.error(); + } + catch (std::runtime_error const& e) + { + CHECK(e.what() == std::string("bad expected access")); + throwOccurred = true; + } + CHECK(throwOccurred); +} + +TEST_CASE("non-error overlapping type construction") +{ + auto expected = []() -> Expected { + return 1; + }(); + CHECK(expected); + CHECK(expected.has_value()); + CHECK(expected.value() == 1); + CHECK(*expected == 1); + + bool throwOccurred = false; + try + { + // There's no error, so should throw. + [[maybe_unused]] std::uint16_t const t = expected.error(); + } + catch (std::runtime_error const& e) + { + CHECK(e.what() == std::string("bad expected access")); + throwOccurred = true; + } + CHECK(throwOccurred); +} + +TEST_CASE("error construction from rvalue") +{ + auto const expected = []() -> Expected { + return Unexpected(telLOCAL_ERROR); + }(); + CHECK(!expected); + CHECK(!expected.has_value()); + CHECK(expected.error() == telLOCAL_ERROR); + + bool throwOccurred = false; + try + { + // There's no result, so should throw. + [[maybe_unused]] std::string const s = *expected; + } + catch (std::runtime_error const& e) + { + CHECK(e.what() == std::string("bad expected access")); + throwOccurred = true; + } + CHECK(throwOccurred); +} + +TEST_CASE("error construction from lvalue") +{ + auto const err(telLOCAL_ERROR); + auto expected = [&err]() -> Expected { + return Unexpected(err); + }(); + CHECK(!expected); + CHECK(!expected.has_value()); + CHECK(expected.error() == telLOCAL_ERROR); + + bool throwOccurred = false; + try + { + // There's no result, so should throw. + [[maybe_unused]] std::size_t const s = expected->size(); + } + catch (std::runtime_error const& e) + { + CHECK(e.what() == std::string("bad expected access")); + throwOccurred = true; + } + CHECK(throwOccurred); +} + +TEST_CASE("error construction from const char*") +{ + auto const expected = []() -> Expected { + return Unexpected("Not what is expected!"); + }(); + CHECK(!expected); + CHECK(!expected.has_value()); + CHECK(expected.error() == std::string("Not what is expected!")); +} + +TEST_CASE("error construction of string from const char*") +{ + auto expected = []() -> Expected { + return Unexpected("Not what is expected!"); + }(); + CHECK(!expected); + CHECK(!expected.has_value()); + CHECK(expected.error() == "Not what is expected!"); + std::string const s(std::move(expected.error())); + CHECK(s == "Not what is expected!"); +} + +TEST_CASE("non-error const construction of Expected") +{ + auto const expected = []() -> Expected { return {}; }(); + CHECK(expected); + bool throwOccurred = false; + try + { + // There's no error, so should throw. + [[maybe_unused]] std::size_t const s = expected.error().size(); + } + catch (std::runtime_error const& e) + { + CHECK(e.what() == std::string("bad expected access")); + throwOccurred = true; + } + CHECK(throwOccurred); +} + +TEST_CASE("non-error non-const construction of Expected") +{ + auto expected = []() -> Expected { return {}; }(); + CHECK(expected); + bool throwOccurred = false; + try + { + // There's no error, so should throw. + [[maybe_unused]] std::size_t const s = expected.error().size(); + } + catch (std::runtime_error const& e) + { + CHECK(e.what() == std::string("bad expected access")); + throwOccurred = true; + } + CHECK(throwOccurred); +} + +TEST_CASE("error const construction of Expected") +{ + auto const expected = []() -> Expected { + return Unexpected("Not what is expected!"); + }(); + CHECK(!expected); + CHECK(expected.error() == "Not what is expected!"); +} + +TEST_CASE("error non-const construction of Expected") +{ + auto expected = []() -> Expected { + return Unexpected("Not what is expected!"); + }(); + CHECK(!expected); + CHECK(expected.error() == "Not what is expected!"); + std::string const s(std::move(expected.error())); + CHECK(s == "Not what is expected!"); +} + +#if BOOST_VERSION >= 107500 +TEST_CASE("boost::json::value construction") +{ + auto expected = []() -> Expected { + return boost::json::object{{"oops", "me array now"}}; + }(); + CHECK(expected); + CHECK(!expected.value().is_array()); +} +#endif // BOOST_VERSION + +TEST_SUITE_END(); diff --git a/src/doctest/basics/IOUAmount.cpp b/src/doctest/basics/IOUAmount.cpp new file mode 100644 index 0000000000..7b662a1afa --- /dev/null +++ b/src/doctest/basics/IOUAmount.cpp @@ -0,0 +1,220 @@ +#include + +#include + +using namespace xrpl; + +TEST_SUITE_BEGIN("IOUAmount"); + +TEST_CASE("zero") +{ + IOUAmount const z(0, 0); + + CHECK(z.mantissa() == 0); + CHECK(z.exponent() == -100); + CHECK(!z); + CHECK(z.signum() == 0); + CHECK(z == beast::zero); + + CHECK((z + z) == z); + CHECK((z - z) == z); + CHECK(z == -z); + + IOUAmount const zz(beast::zero); + CHECK(z == zz); + + // https://github.com/XRPLF/rippled/issues/5170 + IOUAmount const zzz{}; + CHECK(zzz == beast::zero); +} + +TEST_CASE("signum") +{ + IOUAmount const neg(-1, 0); + CHECK(neg.signum() < 0); + + IOUAmount const zer(0, 0); + CHECK(zer.signum() == 0); + + IOUAmount const pos(1, 0); + CHECK(pos.signum() > 0); +} + +TEST_CASE("beast::Zero Comparisons") +{ + using beast::zero; + + { + IOUAmount z(zero); + CHECK(z == zero); + CHECK(z >= zero); + CHECK(z <= zero); + CHECK(!(z != zero)); + CHECK(!(z > zero)); + CHECK(!(z < zero)); + } + + { + IOUAmount const neg(-2, 0); + CHECK(neg < zero); + CHECK(neg <= zero); + CHECK(neg != zero); + CHECK(!(neg == zero)); + } + + { + IOUAmount const pos(2, 0); + CHECK(pos > zero); + CHECK(pos >= zero); + CHECK(pos != zero); + CHECK(!(pos == zero)); + } +} + +TEST_CASE("IOU Comparisons") +{ + IOUAmount const n(-2, 0); + IOUAmount const z(0, 0); + IOUAmount const p(2, 0); + + CHECK(z == z); + CHECK(z >= z); + CHECK(z <= z); + CHECK(z == -z); + CHECK(!(z > z)); + CHECK(!(z < z)); + CHECK(!(z != z)); + CHECK(!(z != -z)); + + CHECK(n < z); + CHECK(n <= z); + CHECK(n != z); + CHECK(!(n > z)); + CHECK(!(n >= z)); + CHECK(!(n == z)); + + CHECK(p > z); + CHECK(p >= z); + CHECK(p != z); + CHECK(!(p < z)); + CHECK(!(p <= z)); + CHECK(!(p == z)); + + CHECK(n < p); + CHECK(n <= p); + CHECK(n != p); + CHECK(!(n > p)); + CHECK(!(n >= p)); + CHECK(!(n == p)); + + CHECK(p > n); + CHECK(p >= n); + CHECK(p != n); + CHECK(!(p < n)); + CHECK(!(p <= n)); + CHECK(!(p == n)); + + CHECK(p > -p); + CHECK(p >= -p); + CHECK(p != -p); + + CHECK(n < -n); + CHECK(n <= -n); + CHECK(n != -n); +} + +TEST_CASE("IOU strings") +{ + CHECK(to_string(IOUAmount(-2, 0)) == "-2"); + CHECK(to_string(IOUAmount(0, 0)) == "0"); + CHECK(to_string(IOUAmount(2, 0)) == "2"); + CHECK(to_string(IOUAmount(25, -3)) == "0.025"); + CHECK(to_string(IOUAmount(-25, -3)) == "-0.025"); + CHECK(to_string(IOUAmount(25, 1)) == "250"); + CHECK(to_string(IOUAmount(-25, 1)) == "-250"); + CHECK(to_string(IOUAmount(2, 20)) == "2000000000000000e5"); + CHECK(to_string(IOUAmount(-2, -20)) == "-2000000000000000e-35"); +} + +TEST_CASE("mulRatio") +{ + /* The range for the mantissa when normalized */ + constexpr std::int64_t minMantissa = 1000000000000000ull; + constexpr std::int64_t maxMantissa = 9999999999999999ull; + /* The range for the exponent when normalized */ + constexpr int minExponent = -96; + constexpr int maxExponent = 80; + constexpr auto maxUInt = std::numeric_limits::max(); + + { + // multiply by a number that would overflow the mantissa, then + // divide by the same number, and check we didn't lose any value + IOUAmount bigMan(maxMantissa, 0); + CHECK(bigMan == mulRatio(bigMan, maxUInt, maxUInt, true)); + // rounding mode shouldn't matter as the result is exact + CHECK(bigMan == mulRatio(bigMan, maxUInt, maxUInt, false)); + } + { + // Similar test as above, but for negative values + IOUAmount bigMan(-maxMantissa, 0); + CHECK(bigMan == mulRatio(bigMan, maxUInt, maxUInt, true)); + // rounding mode shouldn't matter as the result is exact + CHECK(bigMan == mulRatio(bigMan, maxUInt, maxUInt, false)); + } + + { + // small amounts + IOUAmount tiny(minMantissa, minExponent); + // Round up should give the smallest allowable number + CHECK(tiny == mulRatio(tiny, 1, maxUInt, true)); + CHECK(tiny == mulRatio(tiny, maxUInt - 1, maxUInt, true)); + // rounding down should be zero + CHECK(beast::zero == mulRatio(tiny, 1, maxUInt, false)); + CHECK(beast::zero == mulRatio(tiny, maxUInt - 1, maxUInt, false)); + + // tiny negative numbers + IOUAmount tinyNeg(-minMantissa, minExponent); + // Round up should give zero + CHECK(beast::zero == mulRatio(tinyNeg, 1, maxUInt, true)); + CHECK(beast::zero == mulRatio(tinyNeg, maxUInt - 1, maxUInt, true)); + // rounding down should be tiny + CHECK(tinyNeg == mulRatio(tinyNeg, 1, maxUInt, false)); + CHECK(tinyNeg == mulRatio(tinyNeg, maxUInt - 1, maxUInt, false)); + } + + { // rounding + { + IOUAmount one(1, 0); + auto const rup = mulRatio(one, maxUInt - 1, maxUInt, true); + auto const rdown = mulRatio(one, maxUInt - 1, maxUInt, false); + CHECK(rup.mantissa() - rdown.mantissa() == 1); + } + { + IOUAmount big(maxMantissa, maxExponent); + auto const rup = mulRatio(big, maxUInt - 1, maxUInt, true); + auto const rdown = mulRatio(big, maxUInt - 1, maxUInt, false); + CHECK(rup.mantissa() - rdown.mantissa() == 1); + } + + { + IOUAmount negOne(-1, 0); + auto const rup = mulRatio(negOne, maxUInt - 1, maxUInt, true); + auto const rdown = mulRatio(negOne, maxUInt - 1, maxUInt, false); + CHECK(rup.mantissa() - rdown.mantissa() == 1); + } + } + + { + // division by zero + IOUAmount one(1, 0); + CHECK_THROWS([&] { mulRatio(one, 1, 0, true); }()); + } + + { + // overflow + IOUAmount big(maxMantissa, maxExponent); + CHECK_THROWS([&] { mulRatio(big, 2, 0, true); }()); + } +} + +TEST_SUITE_END(); diff --git a/src/doctest/basics/KeyCache.cpp b/src/doctest/basics/KeyCache.cpp new file mode 100644 index 0000000000..286e24e7ef --- /dev/null +++ b/src/doctest/basics/KeyCache.cpp @@ -0,0 +1,74 @@ +#include +#include +#include +#include + +#include + +using namespace xrpl; + +TEST_SUITE_BEGIN("KeyCache"); + +TEST_CASE("KeyCache operations") +{ + using namespace std::chrono_literals; + TestStopwatch clock; + clock.set(0); + + using Key = std::string; + using Cache = TaggedCache; + + beast::Journal j{beast::Journal::getNullSink()}; + + SUBCASE("Insert, retrieve, and age item") + { + Cache c("test", LedgerIndex(1), 2s, clock, j); + + CHECK(c.size() == 0); + CHECK(c.insert("one")); + CHECK(!c.insert("one")); + CHECK(c.size() == 1); + CHECK(c.touch_if_exists("one")); + ++clock; + c.sweep(); + CHECK(c.size() == 1); + ++clock; + c.sweep(); + CHECK(c.size() == 0); + CHECK(!c.touch_if_exists("one")); + } + + SUBCASE("Insert two items, have one expire") + { + Cache c("test", LedgerIndex(2), 2s, clock, j); + + CHECK(c.insert("one")); + CHECK(c.size() == 1); + CHECK(c.insert("two")); + CHECK(c.size() == 2); + ++clock; + c.sweep(); + CHECK(c.size() == 2); + CHECK(c.touch_if_exists("two")); + ++clock; + c.sweep(); + CHECK(c.size() == 1); + } + + SUBCASE("Insert three items (1 over limit), sweep") + { + Cache c("test", LedgerIndex(2), 3s, clock, j); + + CHECK(c.insert("one")); + ++clock; + CHECK(c.insert("two")); + ++clock; + CHECK(c.insert("three")); + ++clock; + CHECK(c.size() == 3); + c.sweep(); + CHECK(c.size() < 3); + } +} + +TEST_SUITE_END(); diff --git a/src/doctest/basics/Number.cpp b/src/doctest/basics/Number.cpp new file mode 100644 index 0000000000..4855915599 --- /dev/null +++ b/src/doctest/basics/Number.cpp @@ -0,0 +1,416 @@ +#include +#include +#include + +#include + +#include +#include + +using namespace xrpl; + +TEST_SUITE_BEGIN("Number"); + +TEST_CASE("zero") +{ + Number const z{0, 0}; + + CHECK(z.mantissa() == 0); + CHECK(z.exponent() == Number{}.exponent()); + + CHECK((z + z) == z); + CHECK((z - z) == z); + CHECK(z == -z); +} + +TEST_CASE("limits") +{ + bool caught = false; + try + { + Number x{10'000'000'000'000'000, 32768}; + } + catch (std::overflow_error const&) + { + caught = true; + } + CHECK(caught); + Number x{10'000'000'000'000'000, 32767}; + CHECK((x == Number{1'000'000'000'000'000, 32768})); + Number z{1'000'000'000'000'000, -32769}; + CHECK(z == Number{}); + Number y{1'000'000'000'000'001'500, 32000}; + CHECK((y == Number{1'000'000'000'000'002, 32003})); + Number m{std::numeric_limits::min()}; + CHECK((m == Number{-9'223'372'036'854'776, 3})); + Number M{std::numeric_limits::max()}; + CHECK((M == Number{9'223'372'036'854'776, 3})); + caught = false; + try + { + Number q{99'999'999'999'999'999, 32767}; + } + catch (std::overflow_error const&) + { + caught = true; + } + CHECK(caught); +} + +TEST_CASE("add") +{ + using Case = std::tuple; + Case c[]{ + {Number{1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'066, -15}}, + {Number{-1'000'000'000'000'000, -15}, + Number{-6'555'555'555'555'555, -29}, + Number{-1'000'000'000'000'066, -15}}, + {Number{-1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{-9'999'999'999'999'344, -16}}, + {Number{-6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'000, -15}, + Number{9'999'999'999'999'344, -16}}, + {Number{}, Number{5}, Number{5}}, + {Number{5'555'555'555'555'555, -32768}, + Number{-5'555'555'555'555'554, -32768}, + Number{0}}, + {Number{-9'999'999'999'999'999, -31}, + Number{1'000'000'000'000'000, -15}, + Number{9'999'999'999'999'990, -16}}}; + for (auto const& [x, y, z] : c) + CHECK(x + y == z); + bool caught = false; + try + { + Number{9'999'999'999'999'999, 32768} + + Number{5'000'000'000'000'000, 32767}; + } + catch (std::overflow_error const&) + { + caught = true; + } + CHECK(caught); +} + +TEST_CASE("sub") +{ + using Case = std::tuple; + Case c[]{ + {Number{1'000'000'000'000'000, -15}, + Number{6'555'555'555'555'555, -29}, + Number{9'999'999'999'999'344, -16}}, + {Number{6'555'555'555'555'555, -29}, + Number{1'000'000'000'000'000, -15}, + Number{-9'999'999'999'999'344, -16}}, + {Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'000, -15}, + Number{0}}, + {Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'001, -15}, + Number{-1'000'000'000'000'000, -30}}, + {Number{1'000'000'000'000'001, -15}, + Number{1'000'000'000'000'000, -15}, + Number{1'000'000'000'000'000, -30}}}; + for (auto const& [x, y, z] : c) + CHECK(x - y == z); +} + +TEST_CASE("mul") +{ + using Case = std::tuple; + saveNumberRoundMode save{Number::setround(Number::to_nearest)}; + { + Case c[]{ + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{2000000000000000, -15}}, + {Number{-1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{-2000000000000000, -15}}, + {Number{-1414213562373095, -15}, + Number{-1414213562373095, -15}, + Number{2000000000000000, -15}}, + {Number{3214285714285706, -15}, + Number{3111111111111119, -15}, + Number{1000000000000000, -14}}, + {Number{1000000000000000, -32768}, + Number{1000000000000000, -32768}, + Number{0}}}; + for (auto const& [x, y, z] : c) + CHECK(x * y == z); + } + Number::setround(Number::towards_zero); + { + Case c[]{ + {Number{7}, Number{8}, Number{56}}, + {Number{1414213562373095, -15}, + Number{1414213562373095, -15}, + Number{1999999999999999, -15}}}; + for (auto const& [x, y, z] : c) + CHECK(x * y == z); + } + bool caught = false; + try + { + Number{9'999'999'999'999'999, 32768} * + Number{5'000'000'000'000'000, 32767}; + } + catch (std::overflow_error const&) + { + caught = true; + } + CHECK(caught); +} + +TEST_CASE("div") +{ + using Case = std::tuple; + saveNumberRoundMode save{Number::setround(Number::to_nearest)}; + { + Case c[]{ + {Number{1}, Number{2}, Number{5, -1}}, + {Number{1}, Number{10}, Number{1, -1}}, + {Number{1}, Number{-10}, Number{-1, -1}}, + {Number{0}, Number{100}, Number{0}}, + {Number{1414213562373095, -10}, + Number{1414213562373095, -10}, + Number{1}}, + {Number{9'999'999'999'999'999}, + Number{1'000'000'000'000'000}, + Number{9'999'999'999'999'999, -15}}, + {Number{2}, Number{3}, Number{6'666'666'666'666'667, -16}}, + {Number{-2}, Number{3}, Number{-6'666'666'666'666'667, -16}}}; + for (auto const& [x, y, z] : c) + CHECK(x / y == z); + } + bool caught = false; + try + { + Number{1000000000000000, -15} / Number{0}; + } + catch (std::overflow_error const&) + { + caught = true; + } + CHECK(caught); +} + +TEST_CASE("root") +{ + using Case = std::tuple; + Case c[]{ + {Number{2}, 2, Number{1414213562373095, -15}}, + {Number{2'000'000}, 2, Number{1414213562373095, -12}}, + {Number{2, -30}, 2, Number{1414213562373095, -30}}, + {Number{-27}, 3, Number{-3}}, + {Number{1}, 5, Number{1}}, + {Number{-1}, 0, Number{1}}, + {Number{5, -1}, 0, Number{0}}, + {Number{0}, 5, Number{0}}, + {Number{5625, -4}, 2, Number{75, -2}}}; + for (auto const& [x, y, z] : c) + CHECK((root(x, y) == z)); + bool caught = false; + try + { + (void)root(Number{-2}, 0); + } + catch (std::overflow_error const&) + { + caught = true; + } + CHECK(caught); + caught = false; + try + { + (void)root(Number{-2}, 4); + } + catch (std::overflow_error const&) + { + caught = true; + } + CHECK(caught); +} + +TEST_CASE("power1") +{ + using Case = std::tuple; + Case c[]{ + {Number{64}, 0, Number{1}}, + {Number{64}, 1, Number{64}}, + {Number{64}, 2, Number{4096}}, + {Number{-64}, 2, Number{4096}}, + {Number{64}, 3, Number{262144}}, + {Number{-64}, 3, Number{-262144}}}; + for (auto const& [x, y, z] : c) + CHECK((power(x, y) == z)); +} + +TEST_CASE("power2") +{ + using Case = std::tuple; + Case c[]{ + {Number{1}, 3, 7, Number{1}}, + {Number{-1}, 1, 0, Number{1}}, + {Number{-1, -1}, 1, 0, Number{0}}, + {Number{16}, 0, 5, Number{1}}, + {Number{34}, 3, 3, Number{34}}, + {Number{4}, 3, 2, Number{8}}}; + for (auto const& [x, n, d, z] : c) + CHECK((power(x, n, d) == z)); + bool caught = false; + try + { + (void)power(Number{7}, 0, 0); + } + catch (std::overflow_error const&) + { + caught = true; + } + CHECK(caught); + caught = false; + try + { + (void)power(Number{7}, 1, 0); + } + catch (std::overflow_error const&) + { + caught = true; + } + CHECK(caught); + caught = false; + try + { + (void)power(Number{-1, -1}, 3, 2); + } + catch (std::overflow_error const&) + { + caught = true; + } + CHECK(caught); +} + +TEST_CASE("conversions") +{ + IOUAmount x{5, 6}; + Number y = x; + CHECK((y == Number{5, 6})); + IOUAmount z{y}; + CHECK(x == z); + XRPAmount xrp{500}; + STAmount st = xrp; + Number n = st; + CHECK(XRPAmount{n} == xrp); + IOUAmount x0{0, 0}; + Number y0 = x0; + CHECK((y0 == Number{0})); + IOUAmount z0{y0}; + CHECK(x0 == z0); + XRPAmount xrp0{0}; + Number n0 = xrp0; + CHECK(n0 == Number{0}); + XRPAmount xrp1{n0}; + CHECK(xrp1 == xrp0); +} + +TEST_CASE("squelch") +{ + Number limit{1, -6}; + CHECK((squelch(Number{2, -6}, limit) == Number{2, -6})); + CHECK((squelch(Number{1, -6}, limit) == Number{1, -6})); + CHECK((squelch(Number{9, -7}, limit) == Number{0})); + CHECK((squelch(Number{-2, -6}, limit) == Number{-2, -6})); + CHECK((squelch(Number{-1, -6}, limit) == Number{-1, -6})); + CHECK((squelch(Number{-9, -7}, limit) == Number{0})); +} + +TEST_CASE("toString") +{ + CHECK(to_string(Number(-2, 0)) == "-2"); + CHECK(to_string(Number(0, 0)) == "0"); + CHECK(to_string(Number(2, 0)) == "2"); + CHECK(to_string(Number(25, -3)) == "0.025"); + CHECK(to_string(Number(-25, -3)) == "-0.025"); + CHECK(to_string(Number(25, 1)) == "250"); + CHECK(to_string(Number(-25, 1)) == "-250"); + CHECK(to_string(Number(2, 20)) == "2000000000000000e5"); + CHECK(to_string(Number(-2, -20)) == "-2000000000000000e-35"); +} + +TEST_CASE("relationals") +{ + CHECK(!(Number{100} < Number{10})); + CHECK(Number{100} > Number{10}); + CHECK(Number{100} >= Number{10}); + CHECK(!(Number{100} <= Number{10})); +} + +TEST_CASE("stream") +{ + Number x{100}; + std::ostringstream os; + os << x; + CHECK(os.str() == to_string(x)); +} + +TEST_CASE("inc_dec") +{ + Number x{100}; + Number y = +x; + CHECK(x == y); + CHECK(x++ == y); + CHECK(x == Number{101}); + CHECK(x-- == Number{101}); + CHECK(x == y); +} + +TEST_CASE("toSTAmount") +{ + NumberSO stNumberSO{true}; + Issue const issue; + Number const n{7'518'783'80596, -5}; + saveNumberRoundMode const save{Number::setround(Number::to_nearest)}; + auto res2 = STAmount{issue, n.mantissa(), n.exponent()}; + CHECK(res2 == STAmount{7518784}); + + Number::setround(Number::towards_zero); + res2 = STAmount{issue, n.mantissa(), n.exponent()}; + CHECK(res2 == STAmount{7518783}); + + Number::setround(Number::downward); + res2 = STAmount{issue, n.mantissa(), n.exponent()}; + CHECK(res2 == STAmount{7518783}); + + Number::setround(Number::upward); + res2 = STAmount{issue, n.mantissa(), n.exponent()}; + CHECK(res2 == STAmount{7518784}); +} + +TEST_CASE("truncate") +{ + CHECK(Number(25, +1).truncate() == Number(250, 0)); + CHECK(Number(25, 0).truncate() == Number(25, 0)); + CHECK(Number(25, -1).truncate() == Number(2, 0)); + CHECK(Number(25, -2).truncate() == Number(0, 0)); + CHECK(Number(99, -2).truncate() == Number(0, 0)); + + CHECK(Number(-25, +1).truncate() == Number(-250, 0)); + CHECK(Number(-25, 0).truncate() == Number(-25, 0)); + CHECK(Number(-25, -1).truncate() == Number(-2, 0)); + CHECK(Number(-25, -2).truncate() == Number(0, 0)); + CHECK(Number(-99, -2).truncate() == Number(0, 0)); + + CHECK(Number(0, 0).truncate() == Number(0, 0)); + CHECK(Number(0, 30000).truncate() == Number(0, 0)); + CHECK(Number(0, -30000).truncate() == Number(0, 0)); + CHECK(Number(100, -30000).truncate() == Number(0, 0)); + CHECK(Number(100, -30000).truncate() == Number(0, 0)); + CHECK(Number(-100, -30000).truncate() == Number(0, 0)); + CHECK(Number(-100, -30000).truncate() == Number(0, 0)); +} + +TEST_SUITE_END(); diff --git a/src/doctest/basics/StringUtilities.cpp b/src/doctest/basics/StringUtilities.cpp new file mode 100644 index 0000000000..b848afe191 --- /dev/null +++ b/src/doctest/basics/StringUtilities.cpp @@ -0,0 +1,284 @@ +#include +#include +#include + +#include + +using namespace xrpl; + +namespace { +void +testUnHexSuccess(std::string const& strIn, std::string const& strExpected) +{ + auto rv = strUnHex(strIn); + CHECK(rv); + CHECK(makeSlice(*rv) == makeSlice(strExpected)); +} + +void +testUnHexFailure(std::string const& strIn) +{ + auto rv = strUnHex(strIn); + CHECK(!rv); +} +} // namespace + +TEST_SUITE_BEGIN("StringUtilities"); + +TEST_CASE("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"); +} + +TEST_CASE("parseUrl") +{ + // Expected passes. + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "scheme://")); + CHECK(pUrl.scheme == "scheme"); + CHECK(pUrl.username.empty()); + CHECK(pUrl.password.empty()); + CHECK(pUrl.domain.empty()); + CHECK(!pUrl.port); + CHECK(pUrl.path.empty()); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "scheme:///")); + CHECK(pUrl.scheme == "scheme"); + CHECK(pUrl.username.empty()); + CHECK(pUrl.password.empty()); + CHECK(pUrl.domain.empty()); + CHECK(!pUrl.port); + CHECK(pUrl.path == "/"); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "lower://domain")); + CHECK(pUrl.scheme == "lower"); + CHECK(pUrl.username.empty()); + CHECK(pUrl.password.empty()); + CHECK(pUrl.domain == "domain"); + CHECK(!pUrl.port); + CHECK(pUrl.path.empty()); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "UPPER://domain:234/")); + CHECK(pUrl.scheme == "upper"); + CHECK(pUrl.username.empty()); + CHECK(pUrl.password.empty()); + CHECK(pUrl.domain == "domain"); + CHECK(*pUrl.port == 234); + CHECK(pUrl.path == "/"); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "Mixed://domain/path")); + CHECK(pUrl.scheme == "mixed"); + CHECK(pUrl.username.empty()); + CHECK(pUrl.password.empty()); + CHECK(pUrl.domain == "domain"); + CHECK(!pUrl.port); + CHECK(pUrl.path == "/path"); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "scheme://[::1]:123/path")); + CHECK(pUrl.scheme == "scheme"); + CHECK(pUrl.username.empty()); + CHECK(pUrl.password.empty()); + CHECK(pUrl.domain == "::1"); + CHECK(*pUrl.port == 123); + CHECK(pUrl.path == "/path"); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "scheme://user:pass@domain:123/abc:321")); + CHECK(pUrl.scheme == "scheme"); + CHECK(pUrl.username == "user"); + CHECK(pUrl.password == "pass"); + CHECK(pUrl.domain == "domain"); + CHECK(*pUrl.port == 123); + CHECK(pUrl.path == "/abc:321"); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "scheme://user@domain:123/abc:321")); + CHECK(pUrl.scheme == "scheme"); + CHECK(pUrl.username == "user"); + CHECK(pUrl.password.empty()); + CHECK(pUrl.domain == "domain"); + CHECK(*pUrl.port == 123); + CHECK(pUrl.path == "/abc:321"); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "scheme://:pass@domain:123/abc:321")); + CHECK(pUrl.scheme == "scheme"); + CHECK(pUrl.username.empty()); + CHECK(pUrl.password == "pass"); + CHECK(pUrl.domain == "domain"); + CHECK(*pUrl.port == 123); + CHECK(pUrl.path == "/abc:321"); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "scheme://domain:123/abc:321")); + CHECK(pUrl.scheme == "scheme"); + CHECK(pUrl.username.empty()); + CHECK(pUrl.password.empty()); + CHECK(pUrl.domain == "domain"); + CHECK(*pUrl.port == 123); + CHECK(pUrl.path == "/abc:321"); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "scheme://user:pass@domain/abc:321")); + CHECK(pUrl.scheme == "scheme"); + CHECK(pUrl.username == "user"); + CHECK(pUrl.password == "pass"); + CHECK(pUrl.domain == "domain"); + CHECK(!pUrl.port); + CHECK(pUrl.path == "/abc:321"); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "scheme://user@domain/abc:321")); + CHECK(pUrl.scheme == "scheme"); + CHECK(pUrl.username == "user"); + CHECK(pUrl.password.empty()); + CHECK(pUrl.domain == "domain"); + CHECK(!pUrl.port); + CHECK(pUrl.path == "/abc:321"); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "scheme://:pass@domain/abc:321")); + CHECK(pUrl.scheme == "scheme"); + CHECK(pUrl.username.empty()); + CHECK(pUrl.password == "pass"); + CHECK(pUrl.domain == "domain"); + CHECK(!pUrl.port); + CHECK(pUrl.path == "/abc:321"); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "scheme://domain/abc:321")); + CHECK(pUrl.scheme == "scheme"); + CHECK(pUrl.username.empty()); + CHECK(pUrl.password.empty()); + CHECK(pUrl.domain == "domain"); + CHECK(!pUrl.port); + CHECK(pUrl.path == "/abc:321"); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "scheme:///path/to/file")); + CHECK(pUrl.scheme == "scheme"); + CHECK(pUrl.username.empty()); + CHECK(pUrl.password.empty()); + CHECK(pUrl.domain.empty()); + CHECK(!pUrl.port); + CHECK(pUrl.path == "/path/to/file"); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "scheme://user:pass@domain/path/with/an@sign")); + CHECK(pUrl.scheme == "scheme"); + CHECK(pUrl.username == "user"); + CHECK(pUrl.password == "pass"); + CHECK(pUrl.domain == "domain"); + CHECK(!pUrl.port); + CHECK(pUrl.path == "/path/with/an@sign"); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "scheme://domain/path/with/an@sign")); + CHECK(pUrl.scheme == "scheme"); + CHECK(pUrl.username.empty()); + CHECK(pUrl.password.empty()); + CHECK(pUrl.domain == "domain"); + CHECK(!pUrl.port); + CHECK(pUrl.path == "/path/with/an@sign"); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "scheme://:999/")); + CHECK(pUrl.scheme == "scheme"); + CHECK(pUrl.username.empty()); + CHECK(pUrl.password.empty()); + CHECK(pUrl.domain == ":999"); + CHECK(!pUrl.port); + CHECK(pUrl.path == "/"); + } + + { + parsedURL pUrl; + CHECK(parseUrl(pUrl, "http://::1:1234/validators")); + CHECK(pUrl.scheme == "http"); + CHECK(pUrl.username.empty()); + CHECK(pUrl.password.empty()); + CHECK(pUrl.domain == "::0.1.18.52"); + CHECK(!pUrl.port); + CHECK(pUrl.path == "/validators"); + } + + // Expected fails. + { + parsedURL pUrl; + CHECK(!parseUrl(pUrl, "")); + CHECK(!parseUrl(pUrl, "nonsense")); + CHECK(!parseUrl(pUrl, "://")); + CHECK(!parseUrl(pUrl, ":///")); + CHECK(!parseUrl(pUrl, "scheme://user:pass@domain:65536/abc:321")); + CHECK(!parseUrl(pUrl, "UPPER://domain:23498765/")); + CHECK(!parseUrl(pUrl, "UPPER://domain:0/")); + CHECK(!parseUrl(pUrl, "UPPER://domain:+7/")); + CHECK(!parseUrl(pUrl, "UPPER://domain:-7234/")); + CHECK(!parseUrl(pUrl, "UPPER://domain:@#$56!/")); + } + + { + std::string strUrl("s://" + std::string(8192, ':')); + parsedURL pUrl; + CHECK(!parseUrl(pUrl, strUrl)); + } +} + +TEST_CASE("toString") +{ + auto result = to_string("hello"); + CHECK(result == "hello"); +} + +TEST_SUITE_END(); diff --git a/src/doctest/basics/TaggedCache.cpp b/src/doctest/basics/TaggedCache.cpp new file mode 100644 index 0000000000..dee8423a88 --- /dev/null +++ b/src/doctest/basics/TaggedCache.cpp @@ -0,0 +1,119 @@ +#include +#include +#include +#include + +#include + +using namespace xrpl; + +TEST_SUITE_BEGIN("TaggedCache"); + +TEST_CASE("TaggedCache operations") +{ + using namespace std::chrono_literals; + + TestStopwatch clock; + clock.set(0); + + using Key = LedgerIndex; + using Value = std::string; + using Cache = TaggedCache; + + beast::Journal j{beast::Journal::getNullSink()}; + + Cache c("test", 1, 1s, clock, j); + + SUBCASE("Insert, retrieve, and age item") + { + CHECK(c.getCacheSize() == 0); + CHECK(c.getTrackSize() == 0); + CHECK(!c.insert(1, "one")); + CHECK(c.getCacheSize() == 1); + CHECK(c.getTrackSize() == 1); + + { + std::string s; + CHECK(c.retrieve(1, s)); + CHECK(s == "one"); + } + + ++clock; + c.sweep(); + CHECK(c.getCacheSize() == 0); + CHECK(c.getTrackSize() == 0); + } + + SUBCASE("Insert item, maintain strong pointer, age it") + { + CHECK(!c.insert(2, "two")); + CHECK(c.getCacheSize() == 1); + CHECK(c.getTrackSize() == 1); + + { + auto p = c.fetch(2); + CHECK(p != nullptr); + ++clock; + c.sweep(); + CHECK(c.getCacheSize() == 0); + CHECK(c.getTrackSize() == 1); + } + + // Make sure its gone now that our reference is gone + ++clock; + c.sweep(); + CHECK(c.getCacheSize() == 0); + CHECK(c.getTrackSize() == 0); + } + + SUBCASE("Insert same key/value pair and canonicalize") + { + CHECK(!c.insert(3, "three")); + + { + auto const p1 = c.fetch(3); + auto p2 = std::make_shared("three"); + c.canonicalize_replace_client(3, p2); + CHECK(p1.get() == p2.get()); + } + ++clock; + c.sweep(); + CHECK(c.getCacheSize() == 0); + CHECK(c.getTrackSize() == 0); + } + + SUBCASE("Put object, keep strong pointer, advance clock, canonicalize") + { + // Put an object in + CHECK(!c.insert(4, "four")); + CHECK(c.getCacheSize() == 1); + CHECK(c.getTrackSize() == 1); + + { + // Keep a strong pointer to it + auto const p1 = c.fetch(4); + CHECK(p1 != nullptr); + CHECK(c.getCacheSize() == 1); + CHECK(c.getTrackSize() == 1); + // Advance the clock a lot + ++clock; + c.sweep(); + CHECK(c.getCacheSize() == 0); + CHECK(c.getTrackSize() == 1); + // Canonicalize a new object with the same key + auto p2 = std::make_shared("four"); + CHECK(c.canonicalize_replace_client(4, p2)); + CHECK(c.getCacheSize() == 1); + CHECK(c.getTrackSize() == 1); + // Make sure we get the original object + CHECK(p1.get() == p2.get()); + } + + ++clock; + c.sweep(); + CHECK(c.getCacheSize() == 0); + CHECK(c.getTrackSize() == 0); + } +} + +TEST_SUITE_END(); diff --git a/src/doctest/basics/Units.cpp b/src/doctest/basics/Units.cpp new file mode 100644 index 0000000000..ebfc56f964 --- /dev/null +++ b/src/doctest/basics/Units.cpp @@ -0,0 +1,332 @@ +#include +#include + +#include + +using namespace xrpl; + +TEST_SUITE_BEGIN("Units"); + +TEST_CASE("Initial XRP") +{ + CHECK(INITIAL_XRP.drops() == 100'000'000'000'000'000); + CHECK(INITIAL_XRP == XRPAmount{100'000'000'000'000'000}); +} + +TEST_CASE("Types") +{ + using FeeLevel32 = FeeLevel; + + SUBCASE("XRPAmount with uint32 FeeLevel") + { + XRPAmount x{100}; + CHECK(x.drops() == 100); + CHECK((std::is_same_v)); + auto y = 4u * x; + CHECK(y.value() == 400); + CHECK((std::is_same_v)); + + auto z = 4 * y; + CHECK(z.value() == 1600); + CHECK((std::is_same_v)); + + FeeLevel32 f{10}; + FeeLevel32 baseFee{100}; + + auto drops = mulDiv(baseFee, x, f); + + CHECK(drops); + CHECK(drops.value() == 1000); + CHECK((std::is_same_v< + std::remove_reference_t::unit_type, + unit::dropTag>)); + + CHECK((std::is_same_v< + std::remove_reference_t, + XRPAmount>)); + } + + SUBCASE("XRPAmount with uint64 FeeLevel") + { + XRPAmount x{100}; + CHECK(x.value() == 100); + CHECK((std::is_same_v)); + auto y = 4u * x; + CHECK(y.value() == 400); + CHECK((std::is_same_v)); + + FeeLevel64 f{10}; + FeeLevel64 baseFee{100}; + + auto drops = mulDiv(baseFee, x, f); + + CHECK(drops); + CHECK(drops.value() == 1000); + CHECK((std::is_same_v< + std::remove_reference_t::unit_type, + unit::dropTag>)); + CHECK((std::is_same_v< + std::remove_reference_t, + XRPAmount>)); + } + + SUBCASE("FeeLevel64 operations") + { + FeeLevel64 x{1024}; + CHECK(x.value() == 1024); + CHECK((std::is_same_v)); + std::uint64_t m = 4; + auto y = m * x; + CHECK(y.value() == 4096); + CHECK((std::is_same_v)); + + XRPAmount basefee{10}; + FeeLevel64 referencefee{256}; + + auto drops = mulDiv(x, basefee, referencefee); + + CHECK(drops); + CHECK(drops.value() == 40); + CHECK((std::is_same_v< + std::remove_reference_t::unit_type, + unit::dropTag>)); + CHECK((std::is_same_v< + std::remove_reference_t, + XRPAmount>)); + } +} + +TEST_CASE("Json") +{ + using FeeLevel32 = FeeLevel; + + SUBCASE("FeeLevel32 max") + { + FeeLevel32 x{std::numeric_limits::max()}; + auto y = x.jsonClipped(); + CHECK(y.type() == Json::uintValue); + CHECK(y == Json::Value{x.fee()}); + } + + SUBCASE("FeeLevel32 min") + { + FeeLevel32 x{std::numeric_limits::min()}; + auto y = x.jsonClipped(); + CHECK(y.type() == Json::uintValue); + CHECK(y == Json::Value{x.fee()}); + } + + SUBCASE("FeeLevel64 max") + { + FeeLevel64 x{std::numeric_limits::max()}; + auto y = x.jsonClipped(); + CHECK(y.type() == Json::uintValue); + CHECK(y == Json::Value{std::numeric_limits::max()}); + } + + SUBCASE("FeeLevel64 min") + { + FeeLevel64 x{std::numeric_limits::min()}; + auto y = x.jsonClipped(); + CHECK(y.type() == Json::uintValue); + CHECK(y == Json::Value{0}); + } + + SUBCASE("FeeLevelDouble max") + { + FeeLevelDouble x{std::numeric_limits::max()}; + auto y = x.jsonClipped(); + CHECK(y.type() == Json::realValue); + CHECK(y == Json::Value{std::numeric_limits::max()}); + } + + SUBCASE("FeeLevelDouble min") + { + FeeLevelDouble x{std::numeric_limits::min()}; + auto y = x.jsonClipped(); + CHECK(y.type() == Json::realValue); + CHECK(y == Json::Value{std::numeric_limits::min()}); + } + + SUBCASE("XRPAmount max") + { + XRPAmount x{std::numeric_limits::max()}; + auto y = x.jsonClipped(); + CHECK(y.type() == Json::intValue); + CHECK(y == Json::Value{std::numeric_limits::max()}); + } + + SUBCASE("XRPAmount min") + { + XRPAmount x{std::numeric_limits::min()}; + auto y = x.jsonClipped(); + CHECK(y.type() == Json::intValue); + CHECK(y == Json::Value{std::numeric_limits::min()}); + } +} + +TEST_CASE("Functions") +{ + using FeeLevel32 = FeeLevel; + + SUBCASE("FeeLevel64 functions") + { + auto make = [&](auto x) -> FeeLevel64 { return x; }; + auto explicitmake = [&](auto x) -> FeeLevel64 { return FeeLevel64{x}; }; + + [[maybe_unused]] FeeLevel64 defaulted; + FeeLevel64 test{0}; + CHECK(test.fee() == 0); + + test = explicitmake(beast::zero); + CHECK(test.fee() == 0); + + test = beast::zero; + CHECK(test.fee() == 0); + + test = explicitmake(100u); + CHECK(test.fee() == 100); + + FeeLevel64 const targetSame{200u}; + FeeLevel32 const targetOther{300u}; + test = make(targetSame); + CHECK(test.fee() == 200); + CHECK(test == targetSame); + CHECK(test < FeeLevel64{1000}); + CHECK(test > FeeLevel64{100}); + test = make(targetOther); + CHECK(test.fee() == 300); + CHECK(test == targetOther); + + test = std::uint64_t(200); + CHECK(test.fee() == 200); + test = std::uint32_t(300); + CHECK(test.fee() == 300); + + test = targetSame; + CHECK(test.fee() == 200); + test = targetOther.fee(); + CHECK(test.fee() == 300); + CHECK(test == targetOther); + + test = targetSame * 2; + CHECK(test.fee() == 400); + test = 3 * targetSame; + CHECK(test.fee() == 600); + test = targetSame / 10; + CHECK(test.fee() == 20); + + test += targetSame; + CHECK(test.fee() == 220); + + test -= targetSame; + CHECK(test.fee() == 20); + + test++; + CHECK(test.fee() == 21); + ++test; + CHECK(test.fee() == 22); + test--; + CHECK(test.fee() == 21); + --test; + CHECK(test.fee() == 20); + + test *= 5; + CHECK(test.fee() == 100); + test /= 2; + CHECK(test.fee() == 50); + test %= 13; + CHECK(test.fee() == 11); + + CHECK(test); + test = 0; + CHECK(!test); + CHECK(test.signum() == 0); + test = targetSame; + CHECK(test.signum() == 1); + CHECK(to_string(test) == "200"); + } + + SUBCASE("FeeLevelDouble functions") + { + auto make = [&](auto x) -> FeeLevelDouble { return x; }; + auto explicitmake = [&](auto x) -> FeeLevelDouble { + return FeeLevelDouble{x}; + }; + + [[maybe_unused]] FeeLevelDouble defaulted; + FeeLevelDouble test{0}; + CHECK(test.fee() == 0); + + test = explicitmake(beast::zero); + CHECK(test.fee() == 0); + + test = beast::zero; + CHECK(test.fee() == 0); + + test = explicitmake(100.0); + CHECK(test.fee() == 100); + + FeeLevelDouble const targetSame{200.0}; + FeeLevel64 const targetOther{300}; + test = make(targetSame); + CHECK(test.fee() == 200); + CHECK(test == targetSame); + CHECK(test < FeeLevelDouble{1000.0}); + CHECK(test > FeeLevelDouble{100.0}); + test = targetOther.fee(); + CHECK(test.fee() == 300); + CHECK(test == targetOther); + + test = 200.0; + CHECK(test.fee() == 200); + test = std::uint64_t(300); + CHECK(test.fee() == 300); + + test = targetSame; + CHECK(test.fee() == 200); + + test = targetSame * 2; + CHECK(test.fee() == 400); + test = 3 * targetSame; + CHECK(test.fee() == 600); + test = targetSame / 10; + CHECK(test.fee() == 20); + + test += targetSame; + CHECK(test.fee() == 220); + + test -= targetSame; + CHECK(test.fee() == 20); + + test++; + CHECK(test.fee() == 21); + ++test; + CHECK(test.fee() == 22); + test--; + CHECK(test.fee() == 21); + --test; + CHECK(test.fee() == 20); + + test *= 5; + CHECK(test.fee() == 100); + test /= 2; + CHECK(test.fee() == 50); + + // legal with signed + test = -test; + CHECK(test.fee() == -50); + CHECK(test.signum() == -1); + CHECK(to_string(test) == "-50.000000"); + + CHECK(test); + test = 0; + CHECK(!test); + CHECK(test.signum() == 0); + test = targetSame; + CHECK(test.signum() == 1); + CHECK(to_string(test) == "200.000000"); + } +} + +TEST_SUITE_END(); diff --git a/src/doctest/basics/XRPAmount.cpp b/src/doctest/basics/XRPAmount.cpp new file mode 100644 index 0000000000..bc5bb31290 --- /dev/null +++ b/src/doctest/basics/XRPAmount.cpp @@ -0,0 +1,283 @@ +#include + +#include + +using namespace xrpl; + +TEST_SUITE_BEGIN("XRPAmount"); + +TEST_CASE("signum") +{ + for (auto i : {-1, 0, 1}) + { + XRPAmount const x(i); + + if (i < 0) + CHECK(x.signum() < 0); + else if (i > 0) + CHECK(x.signum() > 0); + else + CHECK(x.signum() == 0); + } +} + +TEST_CASE("beast::Zero Comparisons") +{ + using beast::zero; + + for (auto i : {-1, 0, 1}) + { + XRPAmount const x(i); + + CHECK((i == 0) == (x == zero)); + CHECK((i != 0) == (x != zero)); + CHECK((i < 0) == (x < zero)); + CHECK((i > 0) == (x > zero)); + CHECK((i <= 0) == (x <= zero)); + CHECK((i >= 0) == (x >= zero)); + + CHECK((0 == i) == (zero == x)); + CHECK((0 != i) == (zero != x)); + CHECK((0 < i) == (zero < x)); + CHECK((0 > i) == (zero > x)); + CHECK((0 <= i) == (zero <= x)); + CHECK((0 >= i) == (zero >= x)); + } +} + +TEST_CASE("XRP Comparisons") +{ + for (auto i : {-1, 0, 1}) + { + XRPAmount const x(i); + + for (auto j : {-1, 0, 1}) + { + XRPAmount const y(j); + + CHECK((i == j) == (x == y)); + CHECK((i != j) == (x != y)); + CHECK((i < j) == (x < y)); + CHECK((i > j) == (x > y)); + CHECK((i <= j) == (x <= y)); + CHECK((i >= j) == (x >= y)); + } + } +} + +TEST_CASE("Addition & Subtraction") +{ + for (auto i : {-1, 0, 1}) + { + XRPAmount const x(i); + + for (auto j : {-1, 0, 1}) + { + XRPAmount const y(j); + + CHECK(XRPAmount(i + j) == (x + y)); + CHECK(XRPAmount(i - j) == (x - y)); + + CHECK((x + y) == (y + x)); // addition is commutative + } + } +} + +TEST_CASE("decimalXRP") +{ + // Tautology + CHECK(DROPS_PER_XRP.decimalXRP() == 1); + + XRPAmount test{1}; + CHECK(test.decimalXRP() == 0.000001); + + test = -test; + CHECK(test.decimalXRP() == -0.000001); + + test = 100'000'000; + CHECK(test.decimalXRP() == 100); + + test = -test; + CHECK(test.decimalXRP() == -100); +} + +TEST_CASE("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 defaulted; + (void)defaulted; + XRPAmount test{0}; + CHECK(test.drops() == 0); + + test = make(beast::zero); + CHECK(test.drops() == 0); + + test = beast::zero; + CHECK(test.drops() == 0); + + test = make(100); + CHECK(test.drops() == 100); + + test = make(100u); + CHECK(test.drops() == 100); + + XRPAmount const targetSame{200u}; + test = make(targetSame); + CHECK(test.drops() == 200); + CHECK(test == targetSame); + CHECK(test < XRPAmount{1000}); + CHECK(test > XRPAmount{100}); + + test = std::int64_t(200); + CHECK(test.drops() == 200); + test = std::uint32_t(300); + CHECK(test.drops() == 300); + + test = targetSame; + CHECK(test.drops() == 200); + auto testOther = test.dropsAs(); + CHECK(testOther); + CHECK(*testOther == 200); + test = std::numeric_limits::max(); + testOther = test.dropsAs(); + CHECK(!testOther); + test = -1; + testOther = test.dropsAs(); + CHECK(!testOther); + + test = targetSame * 2; + CHECK(test.drops() == 400); + test = 3 * targetSame; + CHECK(test.drops() == 600); + test = 20; + CHECK(test.drops() == 20); + + test += targetSame; + CHECK(test.drops() == 220); + + test -= targetSame; + CHECK(test.drops() == 20); + + test *= 5; + CHECK(test.drops() == 100); + test = 50; + CHECK(test.drops() == 50); + test -= 39; + CHECK(test.drops() == 11); + + // legal with signed + test = -test; + CHECK(test.drops() == -11); + CHECK(test.signum() == -1); + CHECK(to_string(test) == "-11"); + + CHECK(test); + test = 0; + CHECK(!test); + CHECK(test.signum() == 0); + test = targetSame; + CHECK(test.signum() == 1); + CHECK(to_string(test) == "200"); +} + +TEST_CASE("mulRatio") +{ + constexpr auto maxUInt32 = std::numeric_limits::max(); + constexpr auto maxXRP = std::numeric_limits::max(); + constexpr auto minXRP = std::numeric_limits::min(); + + { + // multiply by a number that would overflow then divide by the same + // number, and check we didn't lose any value + XRPAmount big(maxXRP); + CHECK(big == mulRatio(big, maxUInt32, maxUInt32, true)); + // rounding mode shouldn't matter as the result is exact + CHECK(big == mulRatio(big, maxUInt32, maxUInt32, 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 divisable by 4 + CHECK(mulRatio(big, 3, 4, false).value() == (big.value() / 4) * 3); + CHECK(mulRatio(big, 3, 4, true).value() == (big.value() / 4) * 3); + CHECK((big.value() * 3) / 4 != (big.value() / 4) * 3); + } + + { + // Similar test as above, but for negative values + XRPAmount big(minXRP); + CHECK(big == mulRatio(big, maxUInt32, maxUInt32, true)); + // rounding mode shouldn't matter as the result is exact + CHECK(big == mulRatio(big, maxUInt32, maxUInt32, false)); + + // multiply and divide by values that would overflow if done + // naively, and check that it gives the correct answer + CHECK(mulRatio(big, 3, 4, false).value() == (big.value() / 4) * 3); + CHECK(mulRatio(big, 3, 4, true).value() == (big.value() / 4) * 3); + CHECK((big.value() * 3) / 4 != (big.value() / 4) * 3); + } + + { + // small amounts + XRPAmount tiny(1); + // Round up should give the smallest allowable number + CHECK(tiny == mulRatio(tiny, 1, maxUInt32, true)); + // rounding down should be zero + CHECK(beast::zero == mulRatio(tiny, 1, maxUInt32, false)); + CHECK(beast::zero == mulRatio(tiny, maxUInt32 - 1, maxUInt32, false)); + + // tiny negative numbers + XRPAmount tinyNeg(-1); + // Round up should give zero + CHECK(beast::zero == mulRatio(tinyNeg, 1, maxUInt32, true)); + CHECK(beast::zero == mulRatio(tinyNeg, maxUInt32 - 1, maxUInt32, true)); + // rounding down should be tiny + CHECK(tinyNeg == mulRatio(tinyNeg, maxUInt32 - 1, maxUInt32, false)); + } + + { // rounding + { + XRPAmount one(1); + auto const rup = mulRatio(one, maxUInt32 - 1, maxUInt32, true); + auto const rdown = mulRatio(one, maxUInt32 - 1, maxUInt32, false); + CHECK(rup.drops() - rdown.drops() == 1); + } + + { + XRPAmount big(maxXRP); + auto const rup = mulRatio(big, maxUInt32 - 1, maxUInt32, true); + auto const rdown = mulRatio(big, maxUInt32 - 1, maxUInt32, false); + CHECK(rup.drops() - rdown.drops() == 1); + } + + { + XRPAmount negOne(-1); + auto const rup = mulRatio(negOne, maxUInt32 - 1, maxUInt32, true); + auto const rdown = + mulRatio(negOne, maxUInt32 - 1, maxUInt32, false); + CHECK(rup.drops() - rdown.drops() == 1); + } + } + + { + // division by zero + XRPAmount one(1); + CHECK_THROWS([&] { mulRatio(one, 1, 0, true); }()); + } + + { + // overflow + XRPAmount big(maxXRP); + CHECK_THROWS([&] { mulRatio(big, 2, 1, true); }()); + } + + { + // underflow + XRPAmount bigNegative(minXRP + 10); + CHECK(mulRatio(bigNegative, 2, 1, true) == minXRP); + } +} + +TEST_SUITE_END(); diff --git a/src/doctest/basics/base58.cpp b/src/doctest/basics/base58.cpp new file mode 100644 index 0000000000..ac319ec9d0 --- /dev/null +++ b/src/doctest/basics/base58.cpp @@ -0,0 +1,432 @@ +// base58 doctest - converted from src/test/basics/base58_test.cpp + +#ifndef _MSC_VER + +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace xrpl { +namespace test { +namespace { + +[[nodiscard]] inline auto +randEngine() -> std::mt19937& +{ + static std::mt19937 r = [] { + std::random_device rd; + return std::mt19937{rd()}; + }(); + return r; +} + +constexpr int numTokenTypeIndexes = 9; + +[[nodiscard]] inline auto +tokenTypeAndSize(int i) -> std::tuple +{ + assert(i < numTokenTypeIndexes); + + switch (i) + { + using enum xrpl::TokenType; + case 0: + return {None, 20}; + case 1: + return {NodePublic, 32}; + case 2: + return {NodePublic, 33}; + case 3: + return {NodePrivate, 32}; + case 4: + return {AccountID, 20}; + case 5: + return {AccountPublic, 32}; + case 6: + return {AccountPublic, 33}; + case 7: + return {AccountSecret, 32}; + case 8: + return {FamilySeed, 16}; + default: + throw std::invalid_argument( + "Invalid token selection passed to tokenTypeAndSize() " + "in " __FILE__); + } +} + +[[nodiscard]] inline auto +randomTokenTypeAndSize() -> std::tuple +{ + using namespace xrpl; + auto& rng = randEngine(); + std::uniform_int_distribution<> d(0, 8); + return tokenTypeAndSize(d(rng)); +} + +// Return the token type and subspan of `d` to use as test data. +[[nodiscard]] inline auto +randomB256TestData(std::span d) + -> std::tuple> +{ + auto& rng = randEngine(); + std::uniform_int_distribution dist(0, 255); + auto [tokType, tokSize] = randomTokenTypeAndSize(); + std::generate(d.begin(), d.begin() + tokSize, [&] { return dist(rng); }); + return {tokType, d.subspan(0, tokSize)}; +} + +inline void +printAsChar(std::span a, std::span b) +{ + auto asString = [](std::span s) { + std::string r; + r.resize(s.size()); + std::copy(s.begin(), s.end(), r.begin()); + return r; + }; + auto sa = asString(a); + auto sb = asString(b); + std::cerr << "\n\n" << sa << "\n" << sb << "\n"; +} + +inline void +printAsInt(std::span a, std::span b) +{ + auto asString = [](std::span s) -> std::string { + std::stringstream sstr; + for (auto i : s) + { + sstr << std::setw(3) << int(i) << ','; + } + return sstr.str(); + }; + auto sa = asString(a); + auto sb = asString(b); + std::cerr << "\n\n" << sa << "\n" << sb << "\n"; +} + +} // namespace + +namespace multiprecision_utils { + +boost::multiprecision::checked_uint512_t +toBoostMP(std::span in) +{ + boost::multiprecision::checked_uint512_t mbp = 0; + for (auto i = in.rbegin(); i != in.rend(); ++i) + { + mbp <<= 64; + mbp += *i; + } + return mbp; +} + +std::vector +randomBigInt(std::uint8_t minSize = 1, std::uint8_t maxSize = 5) +{ + auto eng = randEngine(); + std::uniform_int_distribution numCoeffDist(minSize, maxSize); + std::uniform_int_distribution dist; + auto const numCoeff = numCoeffDist(eng); + std::vector coeffs; + coeffs.reserve(numCoeff); + for (int i = 0; i < numCoeff; ++i) + { + coeffs.push_back(dist(eng)); + } + return coeffs; +} +} // namespace multiprecision_utils + +} // namespace test +} // namespace xrpl + +TEST_SUITE_BEGIN("base58"); + +TEST_CASE("b58_multiprecision") +{ + using namespace boost::multiprecision; + using namespace xrpl::test; + using namespace xrpl; + + constexpr std::size_t iters = 100000; + auto eng = randEngine(); + std::uniform_int_distribution dist; + std::uniform_int_distribution dist1(1); + + for (int i = 0; i < iters; ++i) + { + std::uint64_t const d = dist(eng); + if (!d) + continue; + auto bigInt = multiprecision_utils::randomBigInt(); + auto const boostBigInt = multiprecision_utils::toBoostMP( + std::span(bigInt.data(), bigInt.size())); + + auto const refDiv = boostBigInt / d; + auto const refMod = boostBigInt % d; + + auto const mod = b58_fast::detail::inplace_bigint_div_rem( + std::span(bigInt.data(), bigInt.size()), d); + auto const foundDiv = multiprecision_utils::toBoostMP(bigInt); + CHECK(refMod.convert_to() == mod); + CHECK(foundDiv == refDiv); + } + for (int i = 0; i < iters; ++i) + { + std::uint64_t const d = dist(eng); + auto bigInt = multiprecision_utils::randomBigInt(/*minSize*/ 2); + if (bigInt[bigInt.size() - 1] == + std::numeric_limits::max()) + { + bigInt[bigInt.size() - 1] -= 1; // Prevent overflow + } + auto const boostBigInt = multiprecision_utils::toBoostMP( + std::span(bigInt.data(), bigInt.size())); + + auto const refAdd = boostBigInt + d; + + auto const result = b58_fast::detail::inplace_bigint_add( + std::span(bigInt.data(), bigInt.size()), d); + CHECK(result == TokenCodecErrc::success); + auto const foundAdd = multiprecision_utils::toBoostMP(bigInt); + CHECK(refAdd == foundAdd); + } + for (int i = 0; i < iters; ++i) + { + std::uint64_t const d = dist1(eng); + // Force overflow + std::vector bigInt( + 5, std::numeric_limits::max()); + + auto const boostBigInt = multiprecision_utils::toBoostMP( + std::span(bigInt.data(), bigInt.size())); + + auto const refAdd = boostBigInt + d; + + auto const result = b58_fast::detail::inplace_bigint_add( + std::span(bigInt.data(), bigInt.size()), d); + CHECK(result == TokenCodecErrc::overflowAdd); + auto const foundAdd = multiprecision_utils::toBoostMP(bigInt); + CHECK(refAdd != foundAdd); + } + for (int i = 0; i < iters; ++i) + { + std::uint64_t const d = dist(eng); + auto bigInt = multiprecision_utils::randomBigInt(/* minSize */ 2); + // inplace mul requires the most significant coeff to be zero to + // hold the result. + bigInt[bigInt.size() - 1] = 0; + auto const boostBigInt = multiprecision_utils::toBoostMP( + std::span(bigInt.data(), bigInt.size())); + + auto const refMul = boostBigInt * d; + + auto const result = b58_fast::detail::inplace_bigint_mul( + std::span(bigInt.data(), bigInt.size()), d); + CHECK(result == TokenCodecErrc::success); + auto const foundMul = multiprecision_utils::toBoostMP(bigInt); + CHECK(refMul == foundMul); + } + for (int i = 0; i < iters; ++i) + { + std::uint64_t const d = dist1(eng); + // Force overflow + std::vector bigInt( + 5, std::numeric_limits::max()); + auto const boostBigInt = multiprecision_utils::toBoostMP( + std::span(bigInt.data(), bigInt.size())); + + auto const refMul = boostBigInt * d; + + auto const result = b58_fast::detail::inplace_bigint_mul( + std::span(bigInt.data(), bigInt.size()), d); + CHECK(result == TokenCodecErrc::inputTooLarge); + auto const foundMul = multiprecision_utils::toBoostMP(bigInt); + CHECK(refMul != foundMul); + } +} + +TEST_CASE("fast_matches_ref") +{ + using namespace xrpl::test; + using namespace xrpl; + + auto testRawEncode = [&](std::span const& b256Data) { + std::array b58ResultBuf[2]; + std::array, 2> b58Result; + + std::array b256ResultBuf[2]; + std::array, 2> b256Result; + for (int i = 0; i < 2; ++i) + { + std::span const outBuf{b58ResultBuf[i]}; + if (i == 0) + { + auto const r = + xrpl::b58_fast::detail::b256_to_b58_be(b256Data, outBuf); + REQUIRE(r); + b58Result[i] = r.value(); + } + else + { + std::array tmpBuf; + std::string const s = xrpl::b58_ref::detail::encodeBase58( + b256Data.data(), + b256Data.size(), + tmpBuf.data(), + tmpBuf.size()); + REQUIRE(s.size()); + b58Result[i] = outBuf.subspan(0, s.size()); + std::copy(s.begin(), s.end(), b58Result[i].begin()); + } + } + REQUIRE(b58Result[0].size() == b58Result[1].size()); + CHECK( + memcmp( + b58Result[0].data(), + b58Result[1].data(), + b58Result[0].size()) == 0); + + 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::b58_to_b256_be(in, outBuf); + REQUIRE(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); + REQUIRE(s.size()); + b256Result[i] = outBuf.subspan(0, s.size()); + std::copy(s.begin(), s.end(), b256Result[i].begin()); + } + } + + REQUIRE(b256Result[0].size() == b256Result[1].size()); + CHECK( + memcmp( + b256Result[0].data(), + b256Result[1].data(), + b256Result[0].size()) == 0); + }; + + auto testTokenEncode = [&](xrpl::TokenType const tokType, + std::span const& b256Data) { + std::array b58ResultBuf[2]; + std::array, 2> b58Result; + + std::array b256ResultBuf[2]; + std::array, 2> b256Result; + for (int i = 0; i < 2; ++i) + { + std::span const outBuf{ + b58ResultBuf[i].data(), b58ResultBuf[i].size()}; + if (i == 0) + { + auto const r = xrpl::b58_fast::encodeBase58Token( + tokType, b256Data, outBuf); + REQUIRE(r); + b58Result[i] = r.value(); + } + else + { + std::string const s = xrpl::b58_ref::encodeBase58Token( + tokType, b256Data.data(), b256Data.size()); + REQUIRE(s.size()); + b58Result[i] = outBuf.subspan(0, s.size()); + std::copy(s.begin(), s.end(), b58Result[i].begin()); + } + } + REQUIRE(b58Result[0].size() == b58Result[1].size()); + CHECK( + memcmp( + b58Result[0].data(), + b58Result[1].data(), + b58Result[0].size()) == 0); + + 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); + REQUIRE(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); + REQUIRE(s.size()); + b256Result[i] = outBuf.subspan(0, s.size()); + std::copy(s.begin(), s.end(), b256Result[i].begin()); + } + } + + REQUIRE(b256Result[0].size() == b256Result[1].size()); + CHECK( + memcmp( + b256Result[0].data(), + b256Result[1].data(), + b256Result[0].size()) == 0); + }; + + auto testIt = [&](xrpl::TokenType const tokType, + std::span const& b256Data) { + testRawEncode(b256Data); + testTokenEncode(tokType, b256Data); + }; + + // test every token type with data where every byte is the same and the + // bytes range from 0-255 + for (int i = 0; i < numTokenTypeIndexes; ++i) + { + std::array b256DataBuf; + auto const [tokType, tokSize] = tokenTypeAndSize(i); + for (int d = 0; d <= 255; ++d) + { + memset(b256DataBuf.data(), d, tokSize); + testIt(tokType, std::span(b256DataBuf.data(), tokSize)); + } + } + + // test with random data + constexpr std::size_t iters = 100000; + for (int i = 0; i < iters; ++i) + { + std::array b256DataBuf; + auto const [tokType, b256Data] = randomB256TestData(b256DataBuf); + testIt(tokType, b256Data); + } +} + +TEST_SUITE_END(); + +#endif // _MSC_VER diff --git a/src/doctest/basics/base_uint.cpp b/src/doctest/basics/base_uint.cpp new file mode 100644 index 0000000000..a9027fb33d --- /dev/null +++ b/src/doctest/basics/base_uint.cpp @@ -0,0 +1,323 @@ +#include +#include +#include + +#include + +#include + +#include +#include +#include + +using namespace xrpl; + +// a non-hashing Hasher that just copies the bytes. +// Used to test hash_append in base_uint +template +struct nonhash +{ + static constexpr auto const endian = boost::endian::order::big; + static constexpr std::size_t WIDTH = Bits / 8; + + std::array data_; + + nonhash() = default; + + void + operator()(void const* key, std::size_t len) noexcept + { + assert(len == WIDTH); + memcpy(data_.data(), key, len); + } + + explicit + operator std::size_t() noexcept + { + return WIDTH; + } +}; + +using test96 = base_uint<96>; +static_assert(std::is_copy_constructible::value); +static_assert(std::is_copy_assignable::value); + +TEST_SUITE_BEGIN("base_uint"); + +TEST_CASE("comparisons 64-bit") +{ + static constexpr std:: + array, 6> + test_args{ + {{"0000000000000000", "0000000000000001"}, + {"0000000000000000", "ffffffffffffffff"}, + {"1234567812345678", "2345678923456789"}, + {"8000000000000000", "8000000000000001"}, + {"aaaaaaaaaaaaaaa9", "aaaaaaaaaaaaaaaa"}, + {"fffffffffffffffe", "ffffffffffffffff"}}}; + + for (auto const& arg : test_args) + { + xrpl::base_uint<64> const u{arg.first}, v{arg.second}; + CHECK(u < v); + CHECK(u <= v); + CHECK(u != v); + CHECK(!(u == v)); + CHECK(!(u > v)); + CHECK(!(u >= v)); + CHECK(!(v < u)); + CHECK(!(v <= u)); + CHECK(v != u); + CHECK(!(v == u)); + CHECK(v > u); + CHECK(v >= u); + CHECK(u == u); + CHECK(v == v); + } +} + +TEST_CASE("comparisons 96-bit") +{ + static constexpr std:: + array, 6> + test_args{{ + {"000000000000000000000000", "000000000000000000000001"}, + {"000000000000000000000000", "ffffffffffffffffffffffff"}, + {"0123456789ab0123456789ab", "123456789abc123456789abc"}, + {"555555555555555555555555", "55555555555a555555555555"}, + {"aaaaaaaaaaaaaaa9aaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaa"}, + {"fffffffffffffffffffffffe", "ffffffffffffffffffffffff"}, + }}; + + for (auto const& arg : test_args) + { + xrpl::base_uint<96> const u{arg.first}, v{arg.second}; + CHECK(u < v); + CHECK(u <= v); + CHECK(u != v); + CHECK(!(u == v)); + CHECK(!(u > v)); + CHECK(!(u >= v)); + CHECK(!(v < u)); + CHECK(!(v <= u)); + CHECK(v != u); + CHECK(!(v == u)); + CHECK(v > u); + CHECK(v >= u); + CHECK(u == u); + CHECK(v == v); + } +} + +TEST_CASE("general purpose tests") +{ + static_assert(!std::is_constructible>::value); + static_assert(!std::is_assignable>::value); + + // used to verify set insertion (hashing required) + std::unordered_set> uset; + + Blob raw{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; + CHECK(test96::bytes == raw.size()); + + test96 u{raw}; + uset.insert(u); + CHECK(raw.size() == u.size()); + CHECK(to_string(u) == "0102030405060708090A0B0C"); + CHECK(to_short_string(u) == "01020304..."); + CHECK(*u.data() == 1); + CHECK(u.signum() == 1); + CHECK(!!u); + CHECK(!u.isZero()); + CHECK(u.isNonZero()); + unsigned char t = 0; + for (auto& d : u) + { + CHECK(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 w{std::vector(h.data_.begin(), h.data_.end())}; + CHECK(w == u); + + test96 v{~u}; + uset.insert(v); + CHECK(to_string(v) == "FEFDFCFBFAF9F8F7F6F5F4F3"); + CHECK(to_short_string(v) == "FEFDFCFB..."); + CHECK(*v.data() == 0xfe); + CHECK(v.signum() == 1); + CHECK(!!v); + CHECK(!v.isZero()); + CHECK(v.isNonZero()); + t = 0xff; + for (auto& d : v) + { + CHECK(d == --t); + } + + CHECK(u < v); + CHECK(v > u); + + v = u; + CHECK(v == u); + + test96 z{beast::zero}; + uset.insert(z); + CHECK(to_string(z) == "000000000000000000000000"); + CHECK(to_short_string(z) == "00000000..."); + CHECK(*z.data() == 0); + CHECK(*z.begin() == 0); + CHECK(*std::prev(z.end(), 1) == 0); + CHECK(z.signum() == 0); + CHECK(!z); + CHECK(z.isZero()); + CHECK(!z.isNonZero()); + for (auto& d : z) + { + CHECK(d == 0); + } + + test96 n{z}; + n++; + CHECK(n == test96(1)); + n--; + CHECK(n == beast::zero); + CHECK(n == z); + n--; + CHECK(to_string(n) == "FFFFFFFFFFFFFFFFFFFFFFFF"); + CHECK(to_short_string(n) == "FFFFFFFF..."); + n = beast::zero; + CHECK(n == z); + + test96 zp1{z}; + zp1++; + test96 zm1{z}; + zm1--; + test96 x{zm1 ^ zp1}; + uset.insert(x); + CHECK(to_string(x) == "FFFFFFFFFFFFFFFFFFFFFFFE"); + CHECK(to_short_string(x) == "FFFFFFFF..."); + + CHECK(uset.size() == 4); + + test96 tmp; + CHECK(tmp.parseHex(to_string(u))); + CHECK(tmp == u); + tmp = z; + + // fails with extra char + CHECK(!tmp.parseHex("A" + to_string(u))); + tmp = z; + + // fails with extra char at end + CHECK(!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)); + CHECK(!tmp.parseHex(x)); + } + + // Walking 1s: + for (std::size_t i = 0; i != 24; ++i) + { + std::string s1 = "000000000000000000000000"; + s1[i] = '1'; + + CHECK(tmp.parseHex(s1)); + CHECK(to_string(tmp) == s1); + } + + // Walking 0s: + for (std::size_t i = 0; i != 24; ++i) + { + std::string s1 = "111111111111111111111111"; + s1[i] = '0'; + + CHECK(tmp.parseHex(s1)); + CHECK(to_string(tmp) == s1); + } +} + +TEST_CASE("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); + + // Using the constexpr constructor in a non-constexpr context + // with an error in the parsing throws an exception. + { + // Invalid length for string. + bool caught = false; + try + { + // Try to prevent constant evaluation. + std::vector str(23, '7'); + std::string_view sView(str.data(), str.size()); + [[maybe_unused]] test96 t96(sView); + } + catch (std::invalid_argument const& e) + { + CHECK(e.what() == std::string("invalid length for hex string")); + caught = true; + } + CHECK(caught); + } + { + // Invalid character in string. + bool caught = false; + try + { + // Try to prevent constant evaluation. + std::vector str(23, '7'); + str.push_back('G'); + std::string_view sView(str.data(), str.size()); + [[maybe_unused]] test96 t96(sView); + } + catch (std::range_error const& e) + { + CHECK(e.what() == std::string("invalid hex character")); + caught = true; + } + CHECK(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) + { + } + }; + constexpr StrBaseUint testCases[] = { + "000000000000000000000000", + "000000000000000000000001", + "fedcba9876543210ABCDEF91", + "19FEDCBA0123456789abcdef", + "800000000000000000000000", + "fFfFfFfFfFfFfFfFfFfFfFfF"}; + + for (StrBaseUint const& t : testCases) + { + test96 t96; + CHECK(t96.parseHex(t.str)); + CHECK(t96 == t.tst); + } +} + +TEST_SUITE_END(); diff --git a/src/doctest/basics/hardened_hash.cpp b/src/doctest/basics/hardened_hash.cpp new file mode 100644 index 0000000000..a443da3844 --- /dev/null +++ b/src/doctest/basics/hardened_hash.cpp @@ -0,0 +1,133 @@ +#include + +#include + +#include +#include +#include +#include +#include + +using namespace xrpl; + +namespace { + +template +class test_user_type_member +{ +private: + T t; + +public: + explicit test_user_type_member(T const& t_ = T()) : t(t_) + { + } + + template + friend void + hash_append(Hasher& h, test_user_type_member const& a) noexcept + { + using beast::hash_append; + hash_append(h, a.t); + } +}; + +template +class test_user_type_free +{ +private: + T t; + +public: + explicit test_user_type_free(T const& t_ = T()) : t(t_) + { + } + + template + friend void + hash_append(Hasher& h, test_user_type_free const& a) noexcept + { + using beast::hash_append; + hash_append(h, a.t); + } +}; + +template +using test_hardened_unordered_set = std::unordered_set>; + +template +using test_hardened_unordered_map = std::unordered_map>; + +template +using test_hardened_unordered_multiset = + std::unordered_multiset>; + +template +using test_hardened_unordered_multimap = + std::unordered_multimap>; + +template +void +check() +{ + T t{}; + hardened_hash<>()(t); +} + +template