mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-06 02:07:07 +00:00
Compare commits
15 Commits
vlntb/intr
...
vvysokikh/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1670f5614 | ||
|
|
6056045f2e | ||
|
|
a489708326 | ||
|
|
8b6b075397 | ||
|
|
cdcae49fdb | ||
|
|
763bba2aba | ||
|
|
35521e1065 | ||
|
|
7f2d18f99e | ||
|
|
4ca1c6d97f | ||
|
|
52c6652a56 | ||
|
|
7ed000495c | ||
|
|
89c38e6220 | ||
|
|
00d46c5423 | ||
|
|
7e15621e7b | ||
|
|
99431d7833 |
@@ -3,6 +3,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
|
||||
#include <array>
|
||||
#include <compare>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
@@ -401,40 +402,41 @@ public:
|
||||
x.exponent_ == y.exponent_;
|
||||
}
|
||||
|
||||
friend constexpr bool
|
||||
operator!=(Number const& x, Number const& y) noexcept
|
||||
// operator!=, >, <=, >= are synthesized from operator== and operator<=>.
|
||||
friend constexpr std::strong_ordering
|
||||
operator<=>(Number const& l, Number const& r) noexcept
|
||||
{
|
||||
return !(x == y);
|
||||
}
|
||||
bool const lneg = l.negative_;
|
||||
bool const rneg = r.negative_;
|
||||
|
||||
friend constexpr bool
|
||||
operator<(Number const& x, Number const& y) noexcept
|
||||
{
|
||||
// If the two amounts have different signs (zero is treated as positive)
|
||||
// then the comparison is true iff the left is negative.
|
||||
bool const lneg = x.negative_;
|
||||
bool const rneg = y.negative_;
|
||||
|
||||
// then the negative one is smaller.
|
||||
if (lneg != rneg)
|
||||
return lneg;
|
||||
{
|
||||
return lneg ? std::strong_ordering::less : std::strong_ordering::greater;
|
||||
}
|
||||
|
||||
// Both have same sign and the left is zero: the right must be
|
||||
// greater than 0.
|
||||
if (x.mantissa_ == 0)
|
||||
return y.mantissa_ > 0;
|
||||
// Same sign: compare the unsigned magnitudes |a| <=> |b|. For negative
|
||||
// values the order is reversed (larger magnitude == smaller value), so
|
||||
// swap the operands. A negative value is never zero, so the zero checks
|
||||
// below only ever fire for the non-negative case.
|
||||
Number const& a = lneg ? r : l;
|
||||
Number const& b = lneg ? l : r;
|
||||
|
||||
// Both have same sign, the right is zero and the left is non-zero.
|
||||
if (y.mantissa_ == 0)
|
||||
return false;
|
||||
// A zero mantissa carries a sentinel exponent, so zero has to be handled
|
||||
// before the exponents can be compared.
|
||||
if (a.mantissa_ == 0)
|
||||
{
|
||||
return b.mantissa_ == 0 ? std::strong_ordering::equal : std::strong_ordering::less;
|
||||
}
|
||||
if (b.mantissa_ == 0)
|
||||
return std::strong_ordering::greater;
|
||||
|
||||
// Both have the same sign, compare by exponents:
|
||||
if (x.exponent_ > y.exponent_)
|
||||
return lneg;
|
||||
if (x.exponent_ < y.exponent_)
|
||||
return !lneg;
|
||||
|
||||
// If equal exponents, compare mantissas
|
||||
return x.mantissa_ < y.mantissa_;
|
||||
// Both are non-zero and normalized, so the exponent dominates and the
|
||||
// mantissa breaks ties.
|
||||
if (auto const cmp = a.exponent_ <=> b.exponent_; cmp != 0)
|
||||
return cmp;
|
||||
return a.mantissa_ <=> b.mantissa_;
|
||||
}
|
||||
|
||||
/** Return the sign of the amount */
|
||||
@@ -449,24 +451,6 @@ public:
|
||||
[[nodiscard]] Number
|
||||
truncate() const noexcept;
|
||||
|
||||
friend constexpr bool
|
||||
operator>(Number const& x, Number const& y) noexcept
|
||||
{
|
||||
return y < x;
|
||||
}
|
||||
|
||||
friend constexpr bool
|
||||
operator<=(Number const& x, Number const& y) noexcept
|
||||
{
|
||||
return !(y < x);
|
||||
}
|
||||
|
||||
friend constexpr bool
|
||||
operator>=(Number const& x, Number const& y) noexcept
|
||||
{
|
||||
return !(x < y);
|
||||
}
|
||||
|
||||
friend std::ostream&
|
||||
operator<<(std::ostream& os, Number const& x)
|
||||
{
|
||||
|
||||
@@ -461,6 +461,7 @@ loanAccruedInterest(
|
||||
|
||||
ExtendedPaymentComponents
|
||||
computeOverpaymentComponents(
|
||||
Rules const& rules,
|
||||
Asset const& asset,
|
||||
int32_t const loanScale,
|
||||
Number const& overpayment,
|
||||
|
||||
@@ -559,15 +559,34 @@ tryOverpayment(
|
||||
<< ", new total value: " << newLoanProperties.loanState.valueOutstanding
|
||||
<< ", first payment principal: " << newLoanProperties.firstPaymentPrincipal;
|
||||
|
||||
// Calculate what the new loan state should be with the new periodic payment
|
||||
// including rounding errors
|
||||
auto const newTheoreticalState = computeTheoreticalLoanState(
|
||||
rules,
|
||||
newLoanProperties.periodicPayment,
|
||||
periodicRate,
|
||||
paymentRemaining,
|
||||
managementFeeRate) +
|
||||
errors;
|
||||
// Calculate what the new loan state should be with the new periodic payment,
|
||||
// including the preserved rounding errors.
|
||||
|
||||
auto const newTheoreticalState = [&]() {
|
||||
auto const state = computeTheoreticalLoanState(
|
||||
rules,
|
||||
newLoanProperties.periodicPayment,
|
||||
periodicRate,
|
||||
paymentRemaining,
|
||||
managementFeeRate) +
|
||||
errors;
|
||||
|
||||
if (!rules.enabled(fixCleanup3_2_0))
|
||||
return state;
|
||||
|
||||
// The new principal is known exactly: it is reduced by the overpayment's
|
||||
// principal portion. computeTheoreticalLoanState instead derives the
|
||||
// principal -- and, from it, the management fee and interest -- via a
|
||||
// lossy (P * factor) / factor round-trip. Pin the principal to the exact
|
||||
// value and re-derive the management fee from the exact interest gross
|
||||
// (value - principal), so the intermediate state is fully consistent with
|
||||
// the exact principal rather than the one-scale-unit-high round-trip.
|
||||
Number const principal =
|
||||
roundedOldState.principalOutstanding - overpaymentComponents.trackedPrincipalDelta;
|
||||
Number const managementFee =
|
||||
tenthBipsOfValue(state.valueOutstanding - principal, managementFeeRate);
|
||||
return constructLoanState(state.valueOutstanding, principal, managementFee);
|
||||
}();
|
||||
|
||||
JLOG(j.debug()) << "new theoretical value: " << newTheoreticalState.valueOutstanding
|
||||
<< ", principal: " << newTheoreticalState.principalOutstanding
|
||||
@@ -762,13 +781,6 @@ doOverpayment(
|
||||
// The proxies still hold the original (pre-overpayment) values, which
|
||||
// allows us to compute deltas and verify they match what we expect
|
||||
// from the overpaymentComponents and loanPaymentParts.
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
overpaymentComponents.trackedPrincipalDelta ==
|
||||
principalOutstandingProxy - newRoundedLoanState.principalOutstanding,
|
||||
"xrpl::detail::doOverpayment",
|
||||
"principal change agrees");
|
||||
|
||||
JLOG(j.debug()) << "valueChange: " << loanPaymentParts.valueChange
|
||||
<< ", totalValue before: " << *totalValueOutstandingProxy
|
||||
<< ", totalValue after: " << newRoundedLoanState.valueOutstanding
|
||||
@@ -780,35 +792,50 @@ doOverpayment(
|
||||
<< overpaymentComponents.trackedPrincipalDelta -
|
||||
(totalValueOutstandingProxy - newRoundedLoanState.valueOutstanding);
|
||||
|
||||
// The valueChange returned by tryOverpayment satisfies
|
||||
// valueChange = (newInterestDue - oldInterestDue) + untrackedInterest.
|
||||
// Using the loan-state identity v = p + i + m and the adjacent
|
||||
// `principal change agrees` assertion (dp = oldP - newP), this
|
||||
// rearranges into three independently-computable terms:
|
||||
//
|
||||
// 1. TVO change beyond what principal repayment alone explains:
|
||||
// newTVO - (oldTVO - dp)
|
||||
// 2. Management fee released by re-amortization (positive when
|
||||
// mfee decreased; zero when managementFeeRate == 0):
|
||||
// oldMfee - newMfee
|
||||
// 3. The overpayment's penalty interest part (= untrackedInterest
|
||||
// for the overpayment path; see computeOverpaymentComponents):
|
||||
// trackedInterestPart()
|
||||
[[maybe_unused]] Number const tvoChange = newRoundedLoanState.valueOutstanding -
|
||||
(totalValueOutstandingProxy - overpaymentComponents.trackedPrincipalDelta);
|
||||
[[maybe_unused]] Number const managementFeeReleased =
|
||||
managementFeeOutstandingProxy - newRoundedLoanState.managementFeeDue;
|
||||
[[maybe_unused]] Number const interestPart = overpaymentComponents.trackedInterestPart();
|
||||
// The three assertions below are invariants that only hold once
|
||||
// fixCleanup3_2_0 pins the new principal to the exact reduction
|
||||
// (oldPrincipal - trackedPrincipalDelta). Before the amendment, the lossy
|
||||
// (P * factor) / factor round-trip can leave the new principal one
|
||||
// scale-unit high, so these equalities do not hold on the pre-amendment
|
||||
// code path and must be gated to match the fix they verify.
|
||||
if (rules.enabled(fixCleanup3_2_0))
|
||||
{
|
||||
// The valueChange returned by tryOverpayment satisfies
|
||||
// valueChange = (newInterestDue - oldInterestDue) + untrackedInterest.
|
||||
// Using the loan-state identity v = p + i + m and the adjacent
|
||||
// `principal change agrees` assertion (dp = oldP - newP), this
|
||||
// rearranges into three independently-computable terms:
|
||||
//
|
||||
// 1. TVO change beyond what principal repayment alone explains:
|
||||
// newTVO - (oldTVO - dp)
|
||||
// 2. Management fee released by re-amortization (positive when
|
||||
// mfee decreased; zero when managementFeeRate == 0):
|
||||
// oldMfee - newMfee
|
||||
// 3. The overpayment's penalty interest part (= untrackedInterest
|
||||
// for the overpayment path; see computeOverpaymentComponents):
|
||||
// trackedInterestPart()
|
||||
[[maybe_unused]] Number const tvoChange = newRoundedLoanState.valueOutstanding -
|
||||
(totalValueOutstandingProxy - overpaymentComponents.trackedPrincipalDelta);
|
||||
[[maybe_unused]] Number const managementFeeReleased =
|
||||
managementFeeOutstandingProxy - newRoundedLoanState.managementFeeDue;
|
||||
[[maybe_unused]] Number const interestPart = overpaymentComponents.trackedInterestPart();
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
loanPaymentParts.valueChange == tvoChange + managementFeeReleased + interestPart,
|
||||
"xrpl::detail::doOverpayment",
|
||||
"interest paid agrees");
|
||||
XRPL_ASSERT_PARTS(
|
||||
overpaymentComponents.trackedPrincipalDelta ==
|
||||
principalOutstandingProxy - newRoundedLoanState.principalOutstanding,
|
||||
"xrpl::detail::doOverpayment",
|
||||
"principal change agrees");
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
overpaymentComponents.trackedPrincipalDelta == loanPaymentParts.principalPaid,
|
||||
"xrpl::detail::doOverpayment",
|
||||
"principal payment matches");
|
||||
XRPL_ASSERT_PARTS(
|
||||
loanPaymentParts.valueChange == tvoChange + managementFeeReleased + interestPart,
|
||||
"xrpl::detail::doOverpayment",
|
||||
"interest paid agrees");
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
overpaymentComponents.trackedPrincipalDelta == loanPaymentParts.principalPaid,
|
||||
"xrpl::detail::doOverpayment",
|
||||
"principal payment matches");
|
||||
}
|
||||
|
||||
// All validations passed, so update the proxy objects (which will
|
||||
// modify the actual Loan ledger object)
|
||||
@@ -1144,11 +1171,13 @@ computePaymentComponents(
|
||||
// Cap each component to never exceed what's actually outstanding
|
||||
deltas.principal = std::min(deltas.principal, currentLedgerState.principalOutstanding);
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
deltas.interest <= currentLedgerState.interestDue,
|
||||
"xrpl::detail::computePaymentComponents",
|
||||
"interest due delta not greater than outstanding");
|
||||
|
||||
if (fixCleanup320Enabled)
|
||||
{
|
||||
XRPL_ASSERT_PARTS(
|
||||
deltas.interest <= currentLedgerState.interestDue,
|
||||
"xrpl::detail::computePaymentComponents",
|
||||
"interest due delta not greater than outstanding");
|
||||
}
|
||||
// Cap interest to both the outstanding amount AND what's left of the
|
||||
// periodic payment after principal is paid
|
||||
deltas.interest = std::min(
|
||||
@@ -1289,6 +1318,7 @@ computePaymentComponents(
|
||||
*/
|
||||
ExtendedPaymentComponents
|
||||
computeOverpaymentComponents(
|
||||
Rules const& rules,
|
||||
Asset const& asset,
|
||||
int32_t const loanScale,
|
||||
Number const& overpayment,
|
||||
@@ -1296,10 +1326,13 @@ computeOverpaymentComponents(
|
||||
TenthBips32 const overpaymentFeeRate,
|
||||
TenthBips16 const managementFeeRate)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
overpayment > 0 && isRounded(asset, overpayment, loanScale),
|
||||
"xrpl::detail::computeOverpaymentComponents : valid overpayment "
|
||||
"amount");
|
||||
if (rules.enabled(fixCleanup3_2_0))
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
overpayment > 0 && isRounded(asset, overpayment, loanScale),
|
||||
"xrpl::detail::computeOverpaymentComponents : valid overpayment "
|
||||
"amount");
|
||||
}
|
||||
|
||||
// First, deduct the fixed overpayment fee from the total amount.
|
||||
// This reduces the effective payment that will be applied to the loan.
|
||||
@@ -2055,6 +2088,7 @@ loanMakePayment(
|
||||
{
|
||||
detail::ExtendedPaymentComponents const overpaymentComponents =
|
||||
detail::computeOverpaymentComponents(
|
||||
view.rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
overpayment,
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace {
|
||||
//------------------------------------------------------------------------------
|
||||
// clang-format off
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
char const* const versionString = "3.2.0-rc2"
|
||||
char const* const versionString = "3.2.0-rc3"
|
||||
// clang-format on
|
||||
;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -513,7 +514,9 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
auto const expectedOverpaymentManagementFee = Number{10}; // 10% of 100
|
||||
auto const expectedPrincipalPortion = Number{400}; // 1,000 - 100 - 500
|
||||
|
||||
Env const env{*this};
|
||||
auto const components = xrpl::detail::computeOverpaymentComponents(
|
||||
env.current()->rules(),
|
||||
iou,
|
||||
loanScale,
|
||||
overpayment,
|
||||
@@ -854,7 +857,13 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
Number const overpaymentAmount{50};
|
||||
|
||||
auto const overpaymentComponents = computeOverpaymentComponents(
|
||||
asset, loanScale, overpaymentAmount, TenthBips32(0), TenthBips32(0), managementFeeRate);
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
overpaymentAmount,
|
||||
TenthBips32(0),
|
||||
TenthBips32(0),
|
||||
managementFeeRate);
|
||||
|
||||
auto const loanProperties = computeLoanProperties(
|
||||
env.current()->rules(),
|
||||
@@ -942,6 +951,7 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
|
||||
|
||||
auto const overpaymentComponents = computeOverpaymentComponents(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
Number{50, 0},
|
||||
@@ -1037,6 +1047,7 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
|
||||
|
||||
auto const overpaymentComponents = computeOverpaymentComponents(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
Number{50, 0},
|
||||
@@ -1138,6 +1149,7 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
|
||||
|
||||
auto const overpaymentComponents = computeOverpaymentComponents(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
Number{50, 0},
|
||||
@@ -1247,6 +1259,7 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
|
||||
|
||||
auto const overpaymentComponents = computeOverpaymentComponents(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
Number{50, 0},
|
||||
@@ -1344,7 +1357,6 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
using namespace jtx;
|
||||
using namespace xrpl::detail;
|
||||
|
||||
Env const env{*this};
|
||||
Account const issuer{"issuer"};
|
||||
PrettyAsset const asset = issuer["USD"];
|
||||
std::int32_t const loanScale = -5;
|
||||
@@ -1355,7 +1367,9 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
std::uint32_t const paymentsRemaining = 10;
|
||||
auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
|
||||
|
||||
Env const env{*this};
|
||||
auto const overpaymentComponents = computeOverpaymentComponents(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
Number{50, 0},
|
||||
@@ -1363,87 +1377,97 @@ class LendingHelpers_test : public beast::unit_test::Suite
|
||||
TenthBips32(10'000), // 10% overpayment fee
|
||||
managementFeeRate);
|
||||
|
||||
auto const loanProperties = computeLoanProperties(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanPrincipal,
|
||||
loanInterestRate,
|
||||
paymentInterval,
|
||||
paymentsRemaining,
|
||||
managementFeeRate,
|
||||
loanScale);
|
||||
struct Outcome
|
||||
{
|
||||
LoanPaymentParts parts;
|
||||
LoanState oldState;
|
||||
LoanState newState;
|
||||
};
|
||||
|
||||
auto const ret = tryOverpayment(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
overpaymentComponents,
|
||||
loanProperties.loanState,
|
||||
loanProperties.periodicPayment,
|
||||
periodicRate,
|
||||
paymentsRemaining,
|
||||
managementFeeRate,
|
||||
env.journal);
|
||||
// Run tryOverpayment under a given amendment set. At this (non-near-zero)
|
||||
// rate computeLoanProperties is amendment-independent, so the loan state
|
||||
// is identical across the amendment; only tryOverpayment's fixCleanup3_2_0
|
||||
// behaviour (the exact-principal pin and the management-fee re-derivation
|
||||
// from that principal) differs.
|
||||
auto run = [&](FeatureBitset features) -> std::optional<Outcome> {
|
||||
Env const env{*this, features};
|
||||
auto const loanProperties = computeLoanProperties(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanPrincipal,
|
||||
loanInterestRate,
|
||||
paymentInterval,
|
||||
paymentsRemaining,
|
||||
managementFeeRate,
|
||||
loanScale);
|
||||
auto const ret = tryOverpayment(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
loanScale,
|
||||
overpaymentComponents,
|
||||
loanProperties.loanState,
|
||||
loanProperties.periodicPayment,
|
||||
periodicRate,
|
||||
paymentsRemaining,
|
||||
managementFeeRate,
|
||||
env.journal);
|
||||
if (!BEAST_EXPECT(ret))
|
||||
return std::nullopt;
|
||||
return Outcome{
|
||||
.parts = ret->first,
|
||||
.oldState = loanProperties.loanState,
|
||||
.newState = ret->second.loanState};
|
||||
};
|
||||
|
||||
BEAST_EXPECT(ret);
|
||||
auto const fixedOpt = run(testableAmendments());
|
||||
auto const legacyOpt = run(testableAmendments() - fixCleanup3_2_0);
|
||||
if (!fixedOpt || !legacyOpt)
|
||||
{
|
||||
BEAST_EXPECT(fixedOpt.has_value());
|
||||
BEAST_EXPECT(legacyOpt.has_value());
|
||||
return;
|
||||
}
|
||||
Outcome const& fixed = *fixedOpt;
|
||||
Outcome const& legacy = *legacyOpt;
|
||||
|
||||
auto const& [actualPaymentParts, newLoanProperties] = *ret;
|
||||
auto const& newState = newLoanProperties.loanState;
|
||||
// Components that the amendment does not change. The management fee is
|
||||
// charged against the overpayment interest portion first, so interest
|
||||
// paid stays 4.5 and fee paid 5.5; the principal repaid is 40 in both.
|
||||
auto checkCommon = [&](Outcome const& o, char const* tag) {
|
||||
BEAST_EXPECTS(
|
||||
(o.parts.interestPaid == Number{45, -1}),
|
||||
std::string(tag) + " interestPaid " + to_string(o.parts.interestPaid));
|
||||
BEAST_EXPECTS(
|
||||
(o.parts.feePaid == Number{55, -1}),
|
||||
std::string(tag) + " feePaid " + to_string(o.parts.feePaid));
|
||||
BEAST_EXPECTS(
|
||||
o.parts.principalPaid == 40,
|
||||
std::string(tag) + " principalPaid " + to_string(o.parts.principalPaid));
|
||||
BEAST_EXPECT(
|
||||
o.parts.principalPaid ==
|
||||
o.oldState.principalOutstanding - o.newState.principalOutstanding);
|
||||
// v = p + i + m identity: the non-interest part of valueChange equals
|
||||
// the interest-due change.
|
||||
BEAST_EXPECT(
|
||||
o.parts.valueChange - o.parts.interestPaid ==
|
||||
o.newState.interestDue - o.oldState.interestDue);
|
||||
};
|
||||
checkCommon(fixed, "fixed");
|
||||
checkCommon(legacy, "legacy");
|
||||
|
||||
// =========== VALIDATE PAYMENT PARTS ===========
|
||||
|
||||
// Since there is loan management fee, the fee is charged against
|
||||
// overpayment interest portion first, so interest paid remains 4.5
|
||||
BEAST_EXPECTS(
|
||||
(actualPaymentParts.interestPaid == Number{45, -1}),
|
||||
" interestPaid mismatch: expected 4.5, got " +
|
||||
to_string(actualPaymentParts.interestPaid));
|
||||
|
||||
// With overpayment interest portion, value change should equal the
|
||||
// interest decrease plus overpayment interest portion
|
||||
BEAST_EXPECTS(
|
||||
(actualPaymentParts.valueChange ==
|
||||
Number{-164737, -5} + actualPaymentParts.interestPaid),
|
||||
" valueChange mismatch: expected " +
|
||||
to_string(Number{-164737, -5} + actualPaymentParts.interestPaid) + ", got " +
|
||||
to_string(actualPaymentParts.valueChange));
|
||||
|
||||
// While there is no overpayment fee, fee paid should equal the
|
||||
// management fee charged against the overpayment interest portion
|
||||
BEAST_EXPECTS(
|
||||
(actualPaymentParts.feePaid == Number{55, -1}),
|
||||
" feePaid mismatch: expected 5.5, got " + to_string(actualPaymentParts.feePaid));
|
||||
|
||||
BEAST_EXPECTS(
|
||||
actualPaymentParts.principalPaid == 40,
|
||||
" principalPaid mismatch: expected 40, got `" +
|
||||
to_string(actualPaymentParts.principalPaid));
|
||||
|
||||
// =========== VALIDATE STATE CHANGES ===========
|
||||
|
||||
BEAST_EXPECTS(
|
||||
actualPaymentParts.principalPaid ==
|
||||
loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
|
||||
" principalPaid mismatch: expected " +
|
||||
to_string(
|
||||
loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
|
||||
", got " + to_string(actualPaymentParts.principalPaid));
|
||||
|
||||
// Note that the management fee value change is not captured, as this
|
||||
// value is not needed to correctly update the Vault state.
|
||||
BEAST_EXPECTS(
|
||||
(newState.managementFeeDue - loanProperties.loanState.managementFeeDue ==
|
||||
Number{-18304, -5}),
|
||||
" management fee change mismatch: expected " + to_string(Number{-18304, -5}) +
|
||||
", got " +
|
||||
to_string(newState.managementFeeDue - loanProperties.loanState.managementFeeDue));
|
||||
|
||||
BEAST_EXPECTS(
|
||||
actualPaymentParts.valueChange - actualPaymentParts.interestPaid ==
|
||||
newState.interestDue - loanProperties.loanState.interestDue,
|
||||
" valueChange mismatch: expected " +
|
||||
to_string(newState.interestDue - loanProperties.loanState.interestDue) + ", got " +
|
||||
to_string(actualPaymentParts.valueChange - actualPaymentParts.interestPaid));
|
||||
// With fixCleanup3_2_0 the management fee is re-derived from the exact
|
||||
// principal; without it, from the one-scale-unit-high round-trip
|
||||
// principal. So the management fee outstanding (and hence the value
|
||||
// change, via v = p + i + m) differ by exactly one scale-unit (1e-5 at
|
||||
// loanScale -5) between the two paths.
|
||||
BEAST_EXPECT((fixed.parts.valueChange == Number{-164738, -5} + fixed.parts.interestPaid));
|
||||
BEAST_EXPECT(
|
||||
(fixed.newState.managementFeeDue - fixed.oldState.managementFeeDue ==
|
||||
Number{-18303, -5}));
|
||||
BEAST_EXPECT((legacy.parts.valueChange == Number{-164737, -5} + legacy.parts.interestPaid));
|
||||
BEAST_EXPECT(
|
||||
(legacy.newState.managementFeeDue - legacy.oldState.managementFeeDue ==
|
||||
Number{-18304, -5}));
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
@@ -7580,6 +7580,366 @@ protected:
|
||||
attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS);
|
||||
}
|
||||
|
||||
// A residual overpayment can reduce the stored principal by one scale-unit
|
||||
// *less* than computeOverpaymentComponents predicts, firing the
|
||||
// "principal change agrees" XRPL_ASSERT_PARTS in doOverpayment:
|
||||
//
|
||||
// trackedPrincipalDelta == principalOutstanding - newPrincipalOutstanding
|
||||
//
|
||||
// tryOverpayment re-amortizes the loan at the reduced principal, then
|
||||
// re-derives the theoretical principal from the new periodic payment via
|
||||
// (P * paymentFactor) / paymentFactor. That round-trip is not exact in
|
||||
// Number's 19-digit arithmetic; a positive residual pushes the recomputed
|
||||
// principal a hair above the exact grid point `oldPrincipal - delta`, and
|
||||
// the Upward rounding in tryOverpayment then bumps it a full scale-unit
|
||||
// higher. The principal therefore drops by `delta - 1 unit`, not `delta`.
|
||||
//
|
||||
// Concrete case (isolated, at the tryOverpayment level):
|
||||
// A 100 USD loan at the minimum non-zero rate, 3 payments, loanScale -10.
|
||||
// After one regular payment (principalOutstanding 66.6666666674) a residual overpayment of
|
||||
// 0.049999998 yields trackedPrincipalDelta 0.048999998 but only reduces the principal by
|
||||
// 0.0489999979 (newPrincipal 66.6176666695) — short by 1e-10.
|
||||
//
|
||||
// With fixCleanup3_2_0, tryOverpayment pins the new principal to the exact,
|
||||
// on-grid reduction (oldPrincipal - trackedPrincipalDelta) instead of the
|
||||
// lossy (P*factor)/factor round-trip, so the assertion holds and the
|
||||
// overpayment applies cleanly. The three "principal change agrees" /
|
||||
// "interest paid agrees" / "principal payment matches" assertions are
|
||||
// gated behind the same amendment, so without it they are disabled (the
|
||||
// server does not abort) and the loan keeps the pre-amendment computation.
|
||||
//
|
||||
// The test runs the same scenario under both amendment settings and checks
|
||||
// the stored principal against a ground-truth value derived independently of
|
||||
// the loan-state computation under test.
|
||||
void
|
||||
testBugOverpaymentPrincipalChange()
|
||||
{
|
||||
testcase("bug: doOverpayment asserts 'principal change agrees'");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace loan;
|
||||
using namespace xrpl::detail;
|
||||
|
||||
struct Params
|
||||
{
|
||||
TenthBips32 interestRate;
|
||||
TenthBips16 managementFeeRate;
|
||||
std::uint32_t paymentTotal;
|
||||
std::uint32_t paymentInterval;
|
||||
std::int64_t principal;
|
||||
Number overpayment;
|
||||
TenthBips32 overpaymentInterestRate;
|
||||
TenthBips32 overpaymentFeeRate;
|
||||
std::optional<int> vaultScale;
|
||||
};
|
||||
|
||||
struct Result
|
||||
{
|
||||
Number principalOutstanding; // stored principal after the LoanPay
|
||||
Number expectedNewPrincipal; // ground truth, independent of the fix
|
||||
Number managementFeeChange; // managementFeeOutstanding after - before
|
||||
Number unit; // one scale-unit at the loan scale
|
||||
};
|
||||
|
||||
auto runScenario = [this](FeatureBitset features, Params const& p) -> Result {
|
||||
Env env(*this, features);
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const lender{"vaultOwner"};
|
||||
Account const borrower{"borrower"};
|
||||
|
||||
env.fund(XRP(1'000'000), issuer, lender, borrower);
|
||||
env(fset(issuer, asfDefaultRipple));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const iouAsset = issuer["USD"];
|
||||
Asset const asset = iouAsset.raw();
|
||||
STAmount const iouLimit{asset, Number{9'999'999'999'999'999LL}};
|
||||
env(trust(lender, iouLimit));
|
||||
env(trust(borrower, iouLimit));
|
||||
env(pay(issuer, lender, iouAsset(1'000'000)));
|
||||
env(pay(issuer, borrower, iouAsset(1'000'000)));
|
||||
env.close();
|
||||
|
||||
auto const broker = createVaultAndBroker(
|
||||
env,
|
||||
iouAsset,
|
||||
lender,
|
||||
{.vaultDeposit = 900'000,
|
||||
.debtMax = 0,
|
||||
.managementFeeRate = p.managementFeeRate,
|
||||
.vaultScale = p.vaultScale});
|
||||
|
||||
auto const brokerSle = env.le(broker.brokerKeylet());
|
||||
BEAST_EXPECT(brokerSle);
|
||||
auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0;
|
||||
auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence);
|
||||
|
||||
env(set(borrower, broker.brokerID, Number{p.principal}, tfLoanOverpayment),
|
||||
Sig(sfCounterpartySignature, lender),
|
||||
kInterestRate(p.interestRate),
|
||||
kPaymentTotal(p.paymentTotal),
|
||||
kPaymentInterval(p.paymentInterval),
|
||||
kGracePeriod(p.paymentInterval),
|
||||
kOverpaymentFee(p.overpaymentFeeRate),
|
||||
kOverpaymentInterestRate(p.overpaymentInterestRate),
|
||||
Fee(env.current()->fees().base * 2),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// The single LoanPay below makes one regular payment (the overpayment
|
||||
// is smaller than one period) and leaves the residual as an
|
||||
// overpayment.
|
||||
auto const s = getCurrentState(env, broker, loanKeylet);
|
||||
auto const periodicRate = loanPeriodicRate(s.interestRate, s.paymentInterval);
|
||||
auto const onePeriod = computePaymentComponents(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
s.loanScale,
|
||||
s.totalValue,
|
||||
s.principalOutstanding,
|
||||
s.managementFeeOutstanding,
|
||||
s.periodicPayment,
|
||||
periodicRate,
|
||||
s.paymentRemaining,
|
||||
p.managementFeeRate);
|
||||
|
||||
// Ground truth: the stored principal must drop by exactly the regular
|
||||
// payment's principal portion plus the overpayment's principal
|
||||
// portion. computeOverpaymentComponents depends only on the
|
||||
// overpayment amount and rates (not on the loan-state computation
|
||||
// under test), so it is an independent oracle. Both components are
|
||||
// computed under the same rules as the env so the payment factor
|
||||
// matches.
|
||||
auto const overpaymentComponents = computeOverpaymentComponents(
|
||||
env.current()->rules(),
|
||||
asset,
|
||||
s.loanScale,
|
||||
p.overpayment,
|
||||
p.overpaymentInterestRate,
|
||||
p.overpaymentFeeRate,
|
||||
p.managementFeeRate);
|
||||
Number const expectedNewPrincipal = s.principalOutstanding -
|
||||
onePeriod.trackedPrincipalDelta - overpaymentComponents.trackedPrincipalDelta;
|
||||
|
||||
Number const managementFeeBefore = s.managementFeeOutstanding;
|
||||
|
||||
STAmount const payAmount{asset, onePeriod.trackedValueDelta + p.overpayment};
|
||||
env(pay(borrower, loanKeylet.key, payAmount),
|
||||
Txflags(tfLoanOverpayment),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto const loanSle = env.le(loanKeylet);
|
||||
BEAST_EXPECT(loanSle);
|
||||
|
||||
return Result{
|
||||
.principalOutstanding = loanSle ? Number{loanSle->at(sfPrincipalOutstanding)} : 0,
|
||||
.expectedNewPrincipal = expectedNewPrincipal,
|
||||
.managementFeeChange =
|
||||
(loanSle ? Number{loanSle->at(sfManagementFeeOutstanding)} : Number{0}) -
|
||||
managementFeeBefore,
|
||||
.unit = Number{1, s.loanScale}};
|
||||
};
|
||||
|
||||
// Scenario 1: the original near-zero-rate principal reproduction
|
||||
// (loanScale -10, no management fee). 0.049999998 is smaller than one
|
||||
// period, so it stays a residual overpayment.
|
||||
Params const principalCase{
|
||||
.interestRate = TenthBips32{1},
|
||||
.managementFeeRate = TenthBips16{0},
|
||||
.paymentTotal = 3,
|
||||
.paymentInterval = 60,
|
||||
.principal = 100,
|
||||
.overpayment = Number{49999998, -9},
|
||||
.overpaymentInterestRate = TenthBips32{1000},
|
||||
.overpaymentFeeRate = TenthBips32{1000},
|
||||
.vaultScale = 1};
|
||||
|
||||
// With fixCleanup3_2_0 the stored principal lands exactly on the
|
||||
// ground-truth grid point: it is reduced by exactly the overpayment's
|
||||
// principal portion. This is the key correctness check: if the principal
|
||||
// pin were removed (even with the assertions still gated off), the lossy
|
||||
// (P * factor) / factor round-trip would leave the principal one
|
||||
// scale-unit high and this would fail.
|
||||
Result const fixed = runScenario(all_, principalCase);
|
||||
BEAST_EXPECTS(
|
||||
fixed.principalOutstanding == fixed.expectedNewPrincipal,
|
||||
"fixed principal " + to_string(fixed.principalOutstanding) + " != expected " +
|
||||
to_string(fixed.expectedNewPrincipal));
|
||||
|
||||
// Without the amendment the loan amortizes with the catastrophically
|
||||
// cancelling near-zero payment factor, so its schedule (and ground truth)
|
||||
// differ from the fixed case; the gated assertions keep the server from
|
||||
// aborting and the overpayment still lands exactly on that schedule.
|
||||
Result const legacy = runScenario(all_ - fixCleanup3_2_0, principalCase);
|
||||
BEAST_EXPECTS(
|
||||
legacy.principalOutstanding == legacy.expectedNewPrincipal,
|
||||
"legacy principal " + to_string(legacy.principalOutstanding) + " != expected " +
|
||||
to_string(legacy.expectedNewPrincipal));
|
||||
|
||||
// Scenario 2: a normal-rate loan with a 10% management fee. At a normal
|
||||
// rate the payment factor is identical across the amendment, so toggling
|
||||
// fixCleanup3_2_0 isolates the fix. This overpayment (found by search)
|
||||
// lands on a state where both the principal and the management fee differ
|
||||
// by one scale-unit between the fixed and legacy paths.
|
||||
Params const feeCase{
|
||||
.interestRate = TenthBips32{10000},
|
||||
.managementFeeRate = TenthBips16{10000},
|
||||
.paymentTotal = 6,
|
||||
.paymentInterval = 30u * 24 * 60 * 60,
|
||||
.principal = 1000,
|
||||
.overpayment = Number{214367363, -10},
|
||||
.overpaymentInterestRate = TenthBips32{0},
|
||||
.overpaymentFeeRate = TenthBips32{0},
|
||||
.vaultScale = std::nullopt};
|
||||
|
||||
Result const feeFixed = runScenario(all_, feeCase);
|
||||
Result const feeLegacy = runScenario(all_ - fixCleanup3_2_0, feeCase);
|
||||
|
||||
// With the fix the principal is the exact reduction; without it the lossy
|
||||
// (P * factor) / factor round-trip leaves it one scale-unit high.
|
||||
BEAST_EXPECTS(
|
||||
feeFixed.principalOutstanding == feeFixed.expectedNewPrincipal,
|
||||
"fee-case fixed principal " + to_string(feeFixed.principalOutstanding) +
|
||||
" != expected " + to_string(feeFixed.expectedNewPrincipal));
|
||||
BEAST_EXPECTS(
|
||||
feeLegacy.principalOutstanding == feeLegacy.expectedNewPrincipal + feeLegacy.unit,
|
||||
"fee-case legacy principal " + to_string(feeLegacy.principalOutstanding) +
|
||||
" != expected " + to_string(feeLegacy.expectedNewPrincipal + feeLegacy.unit));
|
||||
|
||||
// Management fee: the overpayment re-amortizes a fee-bearing loan, so the management fee
|
||||
// outstanding drops.
|
||||
//
|
||||
// Unlike the principal that is already at the correct precision, the re-amortized
|
||||
// management fee is tenthBipsOfValue of the new schedule's gross interest, which depends
|
||||
// on the recomputed periodic payment. So the expected change below is a pinned constant
|
||||
// captured from a passing run a magic value only because there is nothing simpler to
|
||||
// compare against.
|
||||
//
|
||||
// At the integration level, toggling the amendment also changes the regular payment's
|
||||
// rounding so a fixed-vs-legacy comparison cannot isolate the overpayment management-fee
|
||||
// fix.
|
||||
BEAST_EXPECT(feeFixed.managementFeeChange == feeLegacy.managementFeeChange);
|
||||
BEAST_EXPECTS(
|
||||
(feeFixed.managementFeeChange == Number{-8219709543, -10}),
|
||||
"fee-case mgmt fee change " + to_string(feeFixed.managementFeeChange));
|
||||
}
|
||||
|
||||
// A LoanSet with InterestRate = 1 (0.001% annualized, the minimum non-zero
|
||||
// rate). At such a near-zero rate the closed-form payment factor
|
||||
// (1 + r)^n - 1 cancels catastrophically.
|
||||
//
|
||||
// Without fixCleanup3_2_0 the resulting amortization is degenerate and the
|
||||
// LoanSet is rejected with tecPRECISION_LOSS (no loan created). With the
|
||||
// amendment, computePowerMinusOneHybrid uses a numerically-stable series
|
||||
// expansion, so the loan is created and the scheduled payments
|
||||
// (2 * periodicPayment) cover the principal — no economic underpayment
|
||||
// (yield theft).
|
||||
//
|
||||
// The test runs the same LoanSet under both amendment settings and pins the
|
||||
// exact outcome for each.
|
||||
void
|
||||
testLoanSetNearZeroInterestRateSucceeds()
|
||||
{
|
||||
testcase("LoanSet near-zero interest rate covers principal");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace loan;
|
||||
|
||||
Number const principalRequested{1000};
|
||||
|
||||
struct Result
|
||||
{
|
||||
TER ter = tesSUCCESS;
|
||||
bool created = false;
|
||||
std::int32_t loanScale = 0;
|
||||
Number principal;
|
||||
Number totalValue;
|
||||
Number managementFee;
|
||||
Number periodicPayment;
|
||||
};
|
||||
|
||||
auto runScenario = [&](FeatureBitset features, TER expectedTer) -> Result {
|
||||
Env env(*this, features);
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const lender{"vaultOwner"};
|
||||
Account const borrower{"borrower"};
|
||||
|
||||
env.fund(XRP(1'000'000), issuer, lender, borrower);
|
||||
env(fset(issuer, asfDefaultRipple));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const iouAsset = issuer["USD"];
|
||||
STAmount const iouLimit{iouAsset.raw(), Number{9'999'999'999'999'999LL}};
|
||||
env(trust(lender, iouLimit));
|
||||
env(trust(borrower, iouLimit));
|
||||
env(pay(issuer, lender, iouAsset(1'000'000)));
|
||||
env(pay(issuer, borrower, iouAsset(1'000'000)));
|
||||
env.close();
|
||||
|
||||
auto const broker = createVaultAndBroker(
|
||||
env,
|
||||
iouAsset,
|
||||
lender,
|
||||
{.vaultDeposit = 100'000, .debtMax = 0, .managementFeeRate = TenthBips16{0}});
|
||||
|
||||
auto const brokerSle = env.le(broker.brokerKeylet());
|
||||
BEAST_EXPECT(brokerSle);
|
||||
auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0;
|
||||
auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence);
|
||||
|
||||
env(set(borrower, broker.brokerID, principalRequested),
|
||||
Sig(sfCounterpartySignature, lender),
|
||||
kInterestRate(TenthBips32{1}),
|
||||
kPaymentTotal(2),
|
||||
kPaymentInterval(400),
|
||||
Fee(env.current()->fees().base * 2),
|
||||
Ter(expectedTer));
|
||||
env.close();
|
||||
|
||||
Result r;
|
||||
r.ter = env.ter();
|
||||
if (auto const loanSle = env.le(loanKeylet))
|
||||
{
|
||||
r.created = true;
|
||||
r.loanScale = loanSle->at(sfLoanScale);
|
||||
r.principal = loanSle->at(sfPrincipalOutstanding);
|
||||
r.totalValue = loanSle->at(sfTotalValueOutstanding);
|
||||
r.managementFee = loanSle->at(sfManagementFeeOutstanding);
|
||||
r.periodicPayment = loanSle->at(sfPeriodicPayment);
|
||||
}
|
||||
return r;
|
||||
};
|
||||
|
||||
Result const fixed = runScenario(all_, tesSUCCESS);
|
||||
Result const legacy = runScenario(all_ - fixCleanup3_2_0, tecPRECISION_LOSS);
|
||||
|
||||
// Without the amendment, the catastrophically-cancelling closed-form
|
||||
// payment factor produces a degenerate amortization that fails
|
||||
// checkLoanGuards: the LoanSet is rejected with tecPRECISION_LOSS and no
|
||||
// loan is created.
|
||||
BEAST_EXPECT(legacy.ter == tecPRECISION_LOSS);
|
||||
BEAST_EXPECT(!legacy.created);
|
||||
|
||||
// With the amendment the stable series expansion produces a valid loan
|
||||
// at loanScale -10.
|
||||
BEAST_EXPECT(fixed.ter == tesSUCCESS);
|
||||
BEAST_EXPECT(fixed.created);
|
||||
BEAST_EXPECT(fixed.loanScale == -10);
|
||||
BEAST_EXPECT(fixed.principal == principalRequested);
|
||||
BEAST_EXPECT((fixed.totalValue == Number{10000000001903, -10}));
|
||||
BEAST_EXPECT(fixed.managementFee == beast::kZero);
|
||||
|
||||
// Periodic payment from the numerically-stable series expansion, and the
|
||||
// scheduled total (2 * periodicPayment) which exceeds the 1000 principal
|
||||
// — no economic underpayment / yield theft.
|
||||
BEAST_EXPECT((fixed.periodicPayment == Number{5000000000951293762, -16}));
|
||||
BEAST_EXPECT((fixed.periodicPayment * 2 == Number{1000000000190258752, -15}));
|
||||
BEAST_EXPECT(fixed.periodicPayment * 2 > principalRequested);
|
||||
}
|
||||
|
||||
// An overpayment whose residual amount has more precision than loanScale
|
||||
// fires the isRounded(asset, overpayment, loanScale) assertion in
|
||||
// computeOverpaymentComponents (and a downstream "interest paid agrees"
|
||||
@@ -8358,12 +8718,14 @@ protected:
|
||||
testLimitExceeded();
|
||||
testLoanSetBlockedLoanPayAllowedWhenCanTransferCleared();
|
||||
testLendingCanTradeClearedNoImpact();
|
||||
testBugOverpaymentPrincipalChange();
|
||||
testBugOverpayUnroundedAmount();
|
||||
|
||||
for (auto const flags : {0u, tfLoanOverpayment})
|
||||
testYieldTheftRounding(flags);
|
||||
testBugInterestDueDeltaCrash();
|
||||
testFullLifecycleVaultPnLNearZeroRate();
|
||||
testLoanSetNearZeroInterestRateSucceeds();
|
||||
}
|
||||
|
||||
// Tests run under each entry in amendmentCombinations().
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <boost/multiprecision/cpp_dec_float.hpp>
|
||||
#include <boost/multiprecision/number.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
@@ -20,6 +21,8 @@
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -1386,10 +1389,103 @@ public:
|
||||
testRelationals()
|
||||
{
|
||||
testcase << "test_relationals " << to_string(Number::getMantissaScale());
|
||||
BEAST_EXPECT(!(Number{100} < Number{10}));
|
||||
BEAST_EXPECT(Number{100} > Number{10});
|
||||
BEAST_EXPECT(Number{100} >= Number{10});
|
||||
BEAST_EXPECT(!(Number{100} <= Number{10}));
|
||||
|
||||
{
|
||||
auto test = [this](auto const& nums) {
|
||||
BEAST_EXPECT(std::ranges::is_sorted(nums));
|
||||
|
||||
for (auto iter1 = nums.begin(); iter1 != nums.end(); ++iter1)
|
||||
{
|
||||
auto iter2 = iter1;
|
||||
for (++iter2; iter2 != nums.end(); ++iter2)
|
||||
{
|
||||
Number const& smaller = *iter1;
|
||||
Number const& larger = *iter2;
|
||||
std::stringstream ss;
|
||||
ss << smaller << " < " << larger;
|
||||
auto const str = ss.str();
|
||||
|
||||
// The ==/!= operators use a completely different code path than <, etc.
|
||||
// This helps detect a breakage in one but not the other. It also helps
|
||||
// verify that the values are being ordered correctly.
|
||||
BEAST_EXPECTS(smaller != larger, str + " (!=)");
|
||||
BEAST_EXPECTS(!(smaller == larger), str + " (==)");
|
||||
|
||||
// true results using operator< and derived operators
|
||||
BEAST_EXPECTS(smaller < larger, str + " (<)");
|
||||
BEAST_EXPECTS(larger > smaller, str + " (>)");
|
||||
BEAST_EXPECTS(larger >= smaller, str + " (>=)");
|
||||
BEAST_EXPECTS(smaller <= larger, str + " (<=)");
|
||||
|
||||
// false results using operator< and derived operators
|
||||
BEAST_EXPECTS(!(larger < smaller), str + " (! <)");
|
||||
BEAST_EXPECTS(!(smaller > larger), str + " (! >)");
|
||||
BEAST_EXPECTS(!(smaller >= larger), str + " (! >=)");
|
||||
BEAST_EXPECTS(!(larger <= smaller), str + " (! <=)");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto const intNums = [this]() {
|
||||
// Inequality test cases are built from a list of sorted integers
|
||||
auto const values =
|
||||
std::to_array<int>({-100, -50, -20, -10, -1, 0, 1, 10, 20, 50, 100});
|
||||
// Check this list is sorted before converting it to Numbers.
|
||||
// That way if any of the other tests fail, we know it's because of code and not the
|
||||
// source data.
|
||||
BEAST_EXPECT(std::ranges::is_sorted(values));
|
||||
|
||||
std::vector<Number> result;
|
||||
result.reserve(values.size());
|
||||
for (auto const v : values)
|
||||
result.emplace_back(v);
|
||||
return result;
|
||||
}();
|
||||
|
||||
auto const otherNums = std::to_array<Number>({
|
||||
Number{-5, 100},
|
||||
Number{-1, 100},
|
||||
Number{-7, -10},
|
||||
Number{-2, -10},
|
||||
Number{0},
|
||||
Number{2, -10},
|
||||
Number{7, -10},
|
||||
Number{1, 100},
|
||||
Number{5, 100},
|
||||
});
|
||||
|
||||
test(intNums);
|
||||
test(otherNums);
|
||||
}
|
||||
|
||||
{
|
||||
// Equality test cases are <Number, __LINE__>. Number will be compared against itself
|
||||
using Case = std::pair<Number, int>;
|
||||
auto const c = std::to_array<Case>({
|
||||
{700, __LINE__},
|
||||
{50, __LINE__},
|
||||
{1, __LINE__},
|
||||
{0, __LINE__},
|
||||
{-1, __LINE__},
|
||||
{-30, __LINE__},
|
||||
{-600, __LINE__},
|
||||
});
|
||||
for (auto const& [n, line] : c)
|
||||
{
|
||||
auto const str = to_string(n);
|
||||
|
||||
// NOLINTBEGIN(misc-redundant-expression) Explicitly testing operators with
|
||||
// equivalent values
|
||||
expect(n == n, str + " ==", __FILE__, line);
|
||||
expect(!(n != n), str + " !=", __FILE__, line);
|
||||
|
||||
expect(!(n < n), str + " < ", __FILE__, line);
|
||||
expect(!(n > n), str + " >", __FILE__, line);
|
||||
expect(n >= n, str + " >=", __FILE__, line);
|
||||
expect(n <= n, str + " <=", __FILE__, line);
|
||||
// NOLINTEND(misc-redundant-expression)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
Reference in New Issue
Block a user