Merge branch 'pratik/otel-phase2-rpc-tracing' into pratik/otel-phase3-tx-tracing

# Conflicts:
#	include/xrpl/telemetry/SpanGuard.h
#	src/libxrpl/telemetry/SpanGuard.cpp
#	src/libxrpl/tx/applySteps.cpp
This commit is contained in:
Pratik Mankawde
2026-07-20 12:03:57 +01:00
885 changed files with 39622 additions and 16871 deletions

View File

@@ -83,7 +83,7 @@ TEST_F(BufferTest, buffer)
x = b0;
EXPECT_EQ(x, b0);
EXPECT_TRUE(sane(x));
#if defined(__clang__)
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wself-assign-overloaded"
#endif
@@ -95,7 +95,7 @@ TEST_F(BufferTest, buffer)
EXPECT_EQ(y, b3);
EXPECT_TRUE(sane(y));
#if defined(__clang__)
#ifdef __clang__
#pragma clang diagnostic pop
#endif
}

File diff suppressed because it is too large Load Diff

View File

@@ -122,6 +122,73 @@ struct BaseUintTest : public ::testing::Test
}
};
using BaseUintDeathTest = BaseUintTest;
TEST_F(BaseUintDeathTest, fromRaw_size_mismatch)
{
// ENABLE_VOIDSTAR is a debug build, but does not crash on failed asserts. Rather than twist
// these tests into knots to make them work, just skip them.
#ifdef ENABLE_VOIDSTAR
GTEST_SKIP() << "ENABLE_VOIDSTAR is a debug build, but does not crash on failed asserts.";
#else
auto smallConstruct = [] {
// Container smaller than the base_uint (8 bytes vs 12 bytes for
// test96). Only the first 8 bytes are copied; the remaining 4 bytes
// stay zero.
Blob const tooSmall{1, 2, 3, 4, 5, 6, 7, 8};
BaseUInt96 const result = BaseUInt96::fromRaw(tooSmall);
auto const resultText = to_string(result);
EXPECT_EQ(resultText, "010203040506070800000000") << resultText;
};
EXPECT_DEBUG_DEATH(smallConstruct(), "input size match");
auto largeConstruct = [] {
// Container larger than the base_uint (16 bytes vs 12 bytes for
// test96). Only the first 12 bytes are copied; the extra bytes are
// ignored.
Blob const tooBig{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
BaseUInt96 const result = BaseUInt96::fromRaw(tooBig);
auto const resultText = to_string(result);
EXPECT_EQ(resultText, "0102030405060708090A0B0C") << resultText;
};
EXPECT_DEBUG_DEATH(largeConstruct(), "input size match");
auto smallCopy = [] {
// Container smaller than the base_uint (8 bytes vs 12 bytes for
// test96). Only the first 8 bytes are copied; the remaining 4 bytes
// stay zero.
Blob const tooSmall{1, 2, 3, 4, 5, 6, 7, 8};
BaseUInt96 result{};
--result;
{
auto const originalText = to_string(result);
EXPECT_EQ(originalText, "FFFFFFFFFFFFFFFFFFFFFFFF") << originalText;
}
result = tooSmall;
auto const resultText = to_string(result);
EXPECT_EQ(resultText, "010203040506070800000000") << resultText;
};
EXPECT_DEBUG_DEATH(smallCopy(), "input size match");
auto const largeCopy = [] {
// Container larger than the base_uint (16 bytes vs 12 bytes for
// test96). Only the first 12 bytes are copied; the extra bytes are
// ignored.
Blob const tooBig{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
BaseUInt96 result{};
--result;
{
auto const originalText = to_string(result);
EXPECT_EQ(originalText, "FFFFFFFFFFFFFFFFFFFFFFFF") << originalText;
}
result = tooBig;
auto const resultText = to_string(result);
EXPECT_EQ(resultText, "0102030405060708090A0B0C") << resultText;
};
EXPECT_DEBUG_DEATH(largeCopy(), "input size match");
#endif
}
TEST_F(BaseUintTest, base_uint)
{
static_assert(!std::is_constructible_v<BaseUInt96, std::complex<double>>);
@@ -198,6 +265,19 @@ TEST_F(BaseUintTest, base_uint)
EXPECT_EQ(d, 0);
}
{
// There are several ways to create a zero. beast::kZero is tested above. Test some
// others.
BaseUInt96 const z1;
EXPECT_EQ(z1, z) << to_string(z1);
BaseUInt96 const z2{};
EXPECT_EQ(z2, z) << to_string(z2);
BaseUInt96 const z3{0u};
EXPECT_EQ(z3, z) << to_string(z3);
}
BaseUInt96 n{z};
n++;
EXPECT_EQ(n, BaseUInt96(1));
@@ -271,25 +351,6 @@ TEST_F(BaseUintTest, base_uint)
static_assert(BaseUInt96("000000000000000000000001").signum() == 1);
static_assert(BaseUInt96("800000000000000000000000").signum() == 1);
// Everything within the #if should fail during compilation.
#if 0
// Too few characters
static_assert(BaseUInt96("00000000000000000000000").signum() == 0);
// Too many characters
static_assert(BaseUInt96("0000000000000000000000000").signum() == 0);
// Non-hex characters
static_assert(BaseUInt96("00000000000000000000000 ").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000/").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000:").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000@").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000G").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000`").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000g").signum() == 1);
static_assert(BaseUInt96("00000000000000000000000~").signum() == 1);
#endif // 0
// Using the constexpr constructor in a non-constexpr context
// with an error in the parsing throws an exception.
{

View File

@@ -38,35 +38,45 @@ public:
*/
explicit Account(std::string_view name, KeyType type = KeyType::Secp256k1);
/** @brief Return the human-readable name. */
/**
* @brief Return the human-readable name.
*/
[[nodiscard]] std::string const&
name() const noexcept
{
return name_;
}
/** @brief Return the AccountID. */
/**
* @brief Return the AccountID.
*/
[[nodiscard]] AccountID const&
id() const noexcept
{
return id_;
}
/** @brief Return the public key. */
/**
* @brief Return the public key.
*/
[[nodiscard]] PublicKey const&
pk() const noexcept
{
return keyPair_.first;
}
/** @brief Return the secret key. */
/**
* @brief Return the secret key.
*/
[[nodiscard]] SecretKey const&
sk() const noexcept
{
return keyPair_.second;
}
/** @brief Implicit conversion to AccountID. */
/**
* @brief Implicit conversion to AccountID.
*/
operator AccountID const&() const noexcept
{
return id_;

View File

@@ -20,11 +20,12 @@
namespace xrpl::test {
/** Test implementation of Family for unit tests.
Uses an in-memory NodeStore database and simple caches.
The missingNode methods throw since tests shouldn't encounter missing nodes.
*/
/**
* Test implementation of Family for unit tests.
*
* Uses an in-memory NodeStore database and simple caches.
* The missingNode methods throw since tests shouldn't encounter missing nodes.
*/
class TestFamily : public Family
{
private:
@@ -109,7 +110,9 @@ public:
(*tnCache_).reset();
}
/** Access the test clock for time manipulation in tests. */
/**
* Access the test clock for time manipulation in tests.
*/
TestStopwatch&
clock()
{

View File

@@ -24,7 +24,9 @@
namespace xrpl::test {
/** Logs implementation that creates TestSink instances. */
/**
* Logs implementation that creates TestSink instances.
*/
class TestLogs : public Logs
{
public:
@@ -39,7 +41,9 @@ public:
}
};
/** Simple NetworkIDService implementation for tests. */
/**
* Simple NetworkIDService implementation for tests.
*/
class TestNetworkIDService final : public NetworkIDService
{
public:
@@ -57,14 +61,15 @@ private:
std::uint32_t networkID_;
};
/** Test implementation of ServiceRegistry for unit tests.
This class provides real implementations for services that can be
instantiated from libxrpl (such as Logs, io_context, caches), and
throws std::logic_error for services that require the full Application.
Tests can subclass this to provide additional services they need.
*/
/**
* Test implementation of ServiceRegistry for unit tests.
*
* This class provides real implementations for services that can be
* instantiated from libxrpl (such as Logs, io_context, caches), and
* throws std::logic_error for services that require the full Application.
*
* Tests can subclass this to provide additional services they need.
*/
class TestServiceRegistry : public ServiceRegistry
{
TestLogs logs_{beast::Severity::Warning};

View File

@@ -157,10 +157,10 @@ allFeatures();
*/
struct TxResult
{
TER ter; /**< The transaction engine result code. */
bool applied; /**< Whether the transaction was applied to the ledger. */
std::optional<TxMeta> metadata; /**< Transaction metadata, if available. */
std::shared_ptr<STTx const> tx; /**< Pointer to the submitted transaction. */
TER ter; ///< The transaction engine result code.
bool applied; ///< Whether the transaction was applied to the ledger.
std::optional<TxMeta> metadata; ///< Transaction metadata, if available.
std::shared_ptr<STTx const> tx; ///< Pointer to the submitted transaction.
};
/**
@@ -360,10 +360,14 @@ private:
std::shared_ptr<Ledger const> closedLedger_;
std::shared_ptr<OpenView> openLedger_;
/** Transactions submitted to the open ledger, for canonical reordering on close. */
/**
* Transactions submitted to the open ledger, for canonical reordering on close.
*/
std::vector<std::shared_ptr<STTx const>> pendingTxs_;
/** Current time (can be advanced arbitrarily for testing). */
/**
* Current time (can be advanced arbitrarily for testing).
*/
NetClock::time_point now_;
};

View File

@@ -40,6 +40,9 @@ TEST(AccountRootTests, BuilderSettersRoundTrip)
auto const mintedNFTokensValue = canonical_UINT32();
auto const burnedNFTokensValue = canonical_UINT32();
auto const firstNFTokenSequenceValue = canonical_UINT32();
auto const sponsoredOwnerCountValue = canonical_UINT32();
auto const sponsoringOwnerCountValue = canonical_UINT32();
auto const sponsoringAccountCountValue = canonical_UINT32();
auto const aMMIDValue = canonical_UINT256();
auto const vaultIDValue = canonical_UINT256();
auto const loanBrokerIDValue = canonical_UINT256();
@@ -67,6 +70,9 @@ TEST(AccountRootTests, BuilderSettersRoundTrip)
builder.setMintedNFTokens(mintedNFTokensValue);
builder.setBurnedNFTokens(burnedNFTokensValue);
builder.setFirstNFTokenSequence(firstNFTokenSequenceValue);
builder.setSponsoredOwnerCount(sponsoredOwnerCountValue);
builder.setSponsoringOwnerCount(sponsoringOwnerCountValue);
builder.setSponsoringAccountCount(sponsoringAccountCountValue);
builder.setAMMID(aMMIDValue);
builder.setVaultID(vaultIDValue);
builder.setLoanBrokerID(loanBrokerIDValue);
@@ -228,6 +234,30 @@ TEST(AccountRootTests, BuilderSettersRoundTrip)
EXPECT_TRUE(entry.hasFirstNFTokenSequence());
}
{
auto const& expected = sponsoredOwnerCountValue;
auto const actualOpt = entry.getSponsoredOwnerCount();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfSponsoredOwnerCount");
EXPECT_TRUE(entry.hasSponsoredOwnerCount());
}
{
auto const& expected = sponsoringOwnerCountValue;
auto const actualOpt = entry.getSponsoringOwnerCount();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfSponsoringOwnerCount");
EXPECT_TRUE(entry.hasSponsoringOwnerCount());
}
{
auto const& expected = sponsoringAccountCountValue;
auto const actualOpt = entry.getSponsoringAccountCount();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfSponsoringAccountCount");
EXPECT_TRUE(entry.hasSponsoringAccountCount());
}
{
auto const& expected = aMMIDValue;
auto const actualOpt = entry.getAMMID();
@@ -285,6 +315,9 @@ TEST(AccountRootTests, BuilderFromSleRoundTrip)
auto const mintedNFTokensValue = canonical_UINT32();
auto const burnedNFTokensValue = canonical_UINT32();
auto const firstNFTokenSequenceValue = canonical_UINT32();
auto const sponsoredOwnerCountValue = canonical_UINT32();
auto const sponsoringOwnerCountValue = canonical_UINT32();
auto const sponsoringAccountCountValue = canonical_UINT32();
auto const aMMIDValue = canonical_UINT256();
auto const vaultIDValue = canonical_UINT256();
auto const loanBrokerIDValue = canonical_UINT256();
@@ -311,6 +344,9 @@ TEST(AccountRootTests, BuilderFromSleRoundTrip)
sle->at(sfMintedNFTokens) = mintedNFTokensValue;
sle->at(sfBurnedNFTokens) = burnedNFTokensValue;
sle->at(sfFirstNFTokenSequence) = firstNFTokenSequenceValue;
sle->at(sfSponsoredOwnerCount) = sponsoredOwnerCountValue;
sle->at(sfSponsoringOwnerCount) = sponsoringOwnerCountValue;
sle->at(sfSponsoringAccountCount) = sponsoringAccountCountValue;
sle->at(sfAMMID) = aMMIDValue;
sle->at(sfVaultID) = vaultIDValue;
sle->at(sfLoanBrokerID) = loanBrokerIDValue;
@@ -566,6 +602,45 @@ TEST(AccountRootTests, BuilderFromSleRoundTrip)
expectEqualField(expected, *fromBuilderOpt, "sfFirstNFTokenSequence");
}
{
auto const& expected = sponsoredOwnerCountValue;
auto const fromSleOpt = entryFromSle.getSponsoredOwnerCount();
auto const fromBuilderOpt = entryFromBuilder.getSponsoredOwnerCount();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfSponsoredOwnerCount");
expectEqualField(expected, *fromBuilderOpt, "sfSponsoredOwnerCount");
}
{
auto const& expected = sponsoringOwnerCountValue;
auto const fromSleOpt = entryFromSle.getSponsoringOwnerCount();
auto const fromBuilderOpt = entryFromBuilder.getSponsoringOwnerCount();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfSponsoringOwnerCount");
expectEqualField(expected, *fromBuilderOpt, "sfSponsoringOwnerCount");
}
{
auto const& expected = sponsoringAccountCountValue;
auto const fromSleOpt = entryFromSle.getSponsoringAccountCount();
auto const fromBuilderOpt = entryFromBuilder.getSponsoringAccountCount();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfSponsoringAccountCount");
expectEqualField(expected, *fromBuilderOpt, "sfSponsoringAccountCount");
}
{
auto const& expected = aMMIDValue;
@@ -697,6 +772,12 @@ TEST(AccountRootTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(entry.getBurnedNFTokens().has_value());
EXPECT_FALSE(entry.hasFirstNFTokenSequence());
EXPECT_FALSE(entry.getFirstNFTokenSequence().has_value());
EXPECT_FALSE(entry.hasSponsoredOwnerCount());
EXPECT_FALSE(entry.getSponsoredOwnerCount().has_value());
EXPECT_FALSE(entry.hasSponsoringOwnerCount());
EXPECT_FALSE(entry.getSponsoringOwnerCount().has_value());
EXPECT_FALSE(entry.hasSponsoringAccountCount());
EXPECT_FALSE(entry.getSponsoringAccountCount().has_value());
EXPECT_FALSE(entry.hasAMMID());
EXPECT_FALSE(entry.getAMMID().has_value());
EXPECT_FALSE(entry.hasVaultID());

View File

@@ -31,6 +31,8 @@ TEST(RippleStateTests, BuilderSettersRoundTrip)
auto const highNodeValue = canonical_UINT64();
auto const highQualityInValue = canonical_UINT32();
auto const highQualityOutValue = canonical_UINT32();
auto const highSponsorValue = canonical_ACCOUNT();
auto const lowSponsorValue = canonical_ACCOUNT();
RippleStateBuilder builder{
balanceValue,
@@ -46,6 +48,8 @@ TEST(RippleStateTests, BuilderSettersRoundTrip)
builder.setHighNode(highNodeValue);
builder.setHighQualityIn(highQualityInValue);
builder.setHighQualityOut(highQualityOutValue);
builder.setHighSponsor(highSponsorValue);
builder.setLowSponsor(lowSponsorValue);
builder.setLedgerIndex(index);
builder.setFlags(0x1u);
@@ -134,6 +138,22 @@ TEST(RippleStateTests, BuilderSettersRoundTrip)
EXPECT_TRUE(entry.hasHighQualityOut());
}
{
auto const& expected = highSponsorValue;
auto const actualOpt = entry.getHighSponsor();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfHighSponsor");
EXPECT_TRUE(entry.hasHighSponsor());
}
{
auto const& expected = lowSponsorValue;
auto const actualOpt = entry.getLowSponsor();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfLowSponsor");
EXPECT_TRUE(entry.hasLowSponsor());
}
EXPECT_TRUE(entry.hasLedgerIndex());
auto const ledgerIndex = entry.getLedgerIndex();
ASSERT_TRUE(ledgerIndex.has_value());
@@ -158,6 +178,8 @@ TEST(RippleStateTests, BuilderFromSleRoundTrip)
auto const highNodeValue = canonical_UINT64();
auto const highQualityInValue = canonical_UINT32();
auto const highQualityOutValue = canonical_UINT32();
auto const highSponsorValue = canonical_ACCOUNT();
auto const lowSponsorValue = canonical_ACCOUNT();
auto sle = std::make_shared<SLE>(RippleState::entryType, index);
@@ -172,6 +194,8 @@ TEST(RippleStateTests, BuilderFromSleRoundTrip)
sle->at(sfHighNode) = highNodeValue;
sle->at(sfHighQualityIn) = highQualityInValue;
sle->at(sfHighQualityOut) = highQualityOutValue;
sle->at(sfHighSponsor) = highSponsorValue;
sle->at(sfLowSponsor) = lowSponsorValue;
RippleStateBuilder builderFromSle{sle};
EXPECT_TRUE(builderFromSle.validate());
@@ -310,6 +334,32 @@ TEST(RippleStateTests, BuilderFromSleRoundTrip)
expectEqualField(expected, *fromBuilderOpt, "sfHighQualityOut");
}
{
auto const& expected = highSponsorValue;
auto const fromSleOpt = entryFromSle.getHighSponsor();
auto const fromBuilderOpt = entryFromBuilder.getHighSponsor();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfHighSponsor");
expectEqualField(expected, *fromBuilderOpt, "sfHighSponsor");
}
{
auto const& expected = lowSponsorValue;
auto const fromSleOpt = entryFromSle.getLowSponsor();
auto const fromBuilderOpt = entryFromBuilder.getLowSponsor();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfLowSponsor");
expectEqualField(expected, *fromBuilderOpt, "sfLowSponsor");
}
EXPECT_EQ(entryFromSle.getKey(), index);
EXPECT_EQ(entryFromBuilder.getKey(), index);
}
@@ -384,5 +434,9 @@ TEST(RippleStateTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(entry.getHighQualityIn().has_value());
EXPECT_FALSE(entry.hasHighQualityOut());
EXPECT_FALSE(entry.getHighQualityOut().has_value());
EXPECT_FALSE(entry.hasHighSponsor());
EXPECT_FALSE(entry.getHighSponsor().has_value());
EXPECT_FALSE(entry.hasLowSponsor());
EXPECT_FALSE(entry.getLowSponsor().has_value());
}
}

View File

@@ -0,0 +1,329 @@
// Auto-generated unit tests for ledger entry Sponsorship
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol_autogen/ledger_entries/Sponsorship.h>
#include <xrpl/protocol_autogen/ledger_entries/Ticket.h>
#include <string>
namespace xrpl::ledger_entries {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed for both the
// builder's STObject and the wrapper's SLE.
TEST(SponsorshipTests, BuilderSettersRoundTrip)
{
uint256 const index{1u};
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const ownerValue = canonical_ACCOUNT();
auto const sponseeValue = canonical_ACCOUNT();
auto const feeAmountValue = canonical_AMOUNT();
auto const maxFeeValue = canonical_AMOUNT();
auto const remainingOwnerCountValue = canonical_UINT32();
auto const ownerNodeValue = canonical_UINT64();
auto const sponseeNodeValue = canonical_UINT64();
SponsorshipBuilder builder{
previousTxnIDValue,
previousTxnLgrSeqValue,
ownerValue,
sponseeValue,
ownerNodeValue,
sponseeNodeValue
};
builder.setFeeAmount(feeAmountValue);
builder.setMaxFee(maxFeeValue);
builder.setRemainingOwnerCount(remainingOwnerCountValue);
builder.setLedgerIndex(index);
builder.setFlags(0x1u);
EXPECT_TRUE(builder.validate());
auto const entry = builder.build(index);
EXPECT_TRUE(entry.validate());
{
auto const& expected = previousTxnIDValue;
auto const actual = entry.getPreviousTxnID();
expectEqualField(expected, actual, "sfPreviousTxnID");
}
{
auto const& expected = previousTxnLgrSeqValue;
auto const actual = entry.getPreviousTxnLgrSeq();
expectEqualField(expected, actual, "sfPreviousTxnLgrSeq");
}
{
auto const& expected = ownerValue;
auto const actual = entry.getOwner();
expectEqualField(expected, actual, "sfOwner");
}
{
auto const& expected = sponseeValue;
auto const actual = entry.getSponsee();
expectEqualField(expected, actual, "sfSponsee");
}
{
auto const& expected = ownerNodeValue;
auto const actual = entry.getOwnerNode();
expectEqualField(expected, actual, "sfOwnerNode");
}
{
auto const& expected = sponseeNodeValue;
auto const actual = entry.getSponseeNode();
expectEqualField(expected, actual, "sfSponseeNode");
}
{
auto const& expected = feeAmountValue;
auto const actualOpt = entry.getFeeAmount();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfFeeAmount");
EXPECT_TRUE(entry.hasFeeAmount());
}
{
auto const& expected = maxFeeValue;
auto const actualOpt = entry.getMaxFee();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfMaxFee");
EXPECT_TRUE(entry.hasMaxFee());
}
{
auto const& expected = remainingOwnerCountValue;
auto const actualOpt = entry.getRemainingOwnerCount();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfRemainingOwnerCount");
EXPECT_TRUE(entry.hasRemainingOwnerCount());
}
EXPECT_TRUE(entry.hasLedgerIndex());
auto const ledgerIndex = entry.getLedgerIndex();
ASSERT_TRUE(ledgerIndex.has_value());
EXPECT_EQ(*ledgerIndex, index);
EXPECT_EQ(entry.getKey(), index);
}
// 2 & 4) Start from an SLE, set fields directly on it, construct a builder
// from that SLE, build a new wrapper, and verify all fields (and validate()).
TEST(SponsorshipTests, BuilderFromSleRoundTrip)
{
uint256 const index{2u};
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const ownerValue = canonical_ACCOUNT();
auto const sponseeValue = canonical_ACCOUNT();
auto const feeAmountValue = canonical_AMOUNT();
auto const maxFeeValue = canonical_AMOUNT();
auto const remainingOwnerCountValue = canonical_UINT32();
auto const ownerNodeValue = canonical_UINT64();
auto const sponseeNodeValue = canonical_UINT64();
auto sle = std::make_shared<SLE>(Sponsorship::entryType, index);
sle->at(sfPreviousTxnID) = previousTxnIDValue;
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
sle->at(sfOwner) = ownerValue;
sle->at(sfSponsee) = sponseeValue;
sle->at(sfFeeAmount) = feeAmountValue;
sle->at(sfMaxFee) = maxFeeValue;
sle->at(sfRemainingOwnerCount) = remainingOwnerCountValue;
sle->at(sfOwnerNode) = ownerNodeValue;
sle->at(sfSponseeNode) = sponseeNodeValue;
SponsorshipBuilder builderFromSle{sle};
EXPECT_TRUE(builderFromSle.validate());
auto const entryFromBuilder = builderFromSle.build(index);
Sponsorship entryFromSle{sle};
EXPECT_TRUE(entryFromBuilder.validate());
EXPECT_TRUE(entryFromSle.validate());
{
auto const& expected = previousTxnIDValue;
auto const fromSle = entryFromSle.getPreviousTxnID();
auto const fromBuilder = entryFromBuilder.getPreviousTxnID();
expectEqualField(expected, fromSle, "sfPreviousTxnID");
expectEqualField(expected, fromBuilder, "sfPreviousTxnID");
}
{
auto const& expected = previousTxnLgrSeqValue;
auto const fromSle = entryFromSle.getPreviousTxnLgrSeq();
auto const fromBuilder = entryFromBuilder.getPreviousTxnLgrSeq();
expectEqualField(expected, fromSle, "sfPreviousTxnLgrSeq");
expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq");
}
{
auto const& expected = ownerValue;
auto const fromSle = entryFromSle.getOwner();
auto const fromBuilder = entryFromBuilder.getOwner();
expectEqualField(expected, fromSle, "sfOwner");
expectEqualField(expected, fromBuilder, "sfOwner");
}
{
auto const& expected = sponseeValue;
auto const fromSle = entryFromSle.getSponsee();
auto const fromBuilder = entryFromBuilder.getSponsee();
expectEqualField(expected, fromSle, "sfSponsee");
expectEqualField(expected, fromBuilder, "sfSponsee");
}
{
auto const& expected = ownerNodeValue;
auto const fromSle = entryFromSle.getOwnerNode();
auto const fromBuilder = entryFromBuilder.getOwnerNode();
expectEqualField(expected, fromSle, "sfOwnerNode");
expectEqualField(expected, fromBuilder, "sfOwnerNode");
}
{
auto const& expected = sponseeNodeValue;
auto const fromSle = entryFromSle.getSponseeNode();
auto const fromBuilder = entryFromBuilder.getSponseeNode();
expectEqualField(expected, fromSle, "sfSponseeNode");
expectEqualField(expected, fromBuilder, "sfSponseeNode");
}
{
auto const& expected = feeAmountValue;
auto const fromSleOpt = entryFromSle.getFeeAmount();
auto const fromBuilderOpt = entryFromBuilder.getFeeAmount();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfFeeAmount");
expectEqualField(expected, *fromBuilderOpt, "sfFeeAmount");
}
{
auto const& expected = maxFeeValue;
auto const fromSleOpt = entryFromSle.getMaxFee();
auto const fromBuilderOpt = entryFromBuilder.getMaxFee();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfMaxFee");
expectEqualField(expected, *fromBuilderOpt, "sfMaxFee");
}
{
auto const& expected = remainingOwnerCountValue;
auto const fromSleOpt = entryFromSle.getRemainingOwnerCount();
auto const fromBuilderOpt = entryFromBuilder.getRemainingOwnerCount();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfRemainingOwnerCount");
expectEqualField(expected, *fromBuilderOpt, "sfRemainingOwnerCount");
}
EXPECT_EQ(entryFromSle.getKey(), index);
EXPECT_EQ(entryFromBuilder.getKey(), index);
}
// 3) Verify wrapper throws when constructed from wrong ledger entry type.
TEST(SponsorshipTests, WrapperThrowsOnWrongEntryType)
{
uint256 const index{3u};
// Build a valid ledger entry of a different type
// Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq
// Check requires: Account, Destination, SendMax, Sequence, OwnerNode, DestinationNode, PreviousTxnID, PreviousTxnLgrSeq
TicketBuilder wrongBuilder{
canonical_ACCOUNT(),
canonical_UINT64(),
canonical_UINT32(),
canonical_UINT256(),
canonical_UINT32()};
auto wrongEntry = wrongBuilder.build(index);
EXPECT_THROW(Sponsorship{wrongEntry.getSle()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong ledger entry type.
TEST(SponsorshipTests, BuilderThrowsOnWrongEntryType)
{
uint256 const index{4u};
// Build a valid ledger entry of a different type
TicketBuilder wrongBuilder{
canonical_ACCOUNT(),
canonical_UINT64(),
canonical_UINT32(),
canonical_UINT256(),
canonical_UINT32()};
auto wrongEntry = wrongBuilder.build(index);
EXPECT_THROW(SponsorshipBuilder{wrongEntry.getSle()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(SponsorshipTests, OptionalFieldsReturnNullopt)
{
uint256 const index{3u};
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const ownerValue = canonical_ACCOUNT();
auto const sponseeValue = canonical_ACCOUNT();
auto const ownerNodeValue = canonical_UINT64();
auto const sponseeNodeValue = canonical_UINT64();
SponsorshipBuilder builder{
previousTxnIDValue,
previousTxnLgrSeqValue,
ownerValue,
sponseeValue,
ownerNodeValue,
sponseeNodeValue
};
auto const entry = builder.build(index);
// Verify optional fields are not present
EXPECT_FALSE(entry.hasFeeAmount());
EXPECT_FALSE(entry.getFeeAmount().has_value());
EXPECT_FALSE(entry.hasMaxFee());
EXPECT_FALSE(entry.getMaxFee().has_value());
EXPECT_FALSE(entry.hasRemainingOwnerCount());
EXPECT_FALSE(entry.getRemainingOwnerCount().has_value());
}
}

View File

@@ -0,0 +1,261 @@
// Auto-generated unit tests for transaction SponsorshipSet
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/SponsorshipSet.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipSet"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 1;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const counterpartySponsorValue = canonical_ACCOUNT();
auto const sponseeValue = canonical_ACCOUNT();
auto const feeAmountValue = canonical_AMOUNT();
auto const maxFeeValue = canonical_AMOUNT();
auto const remainingOwnerCountValue = canonical_UINT32();
SponsorshipSetBuilder builder{
accountValue,
sequenceValue,
feeValue
};
// Set optional fields
builder.setCounterpartySponsor(counterpartySponsorValue);
builder.setSponsee(sponseeValue);
builder.setFeeAmount(feeAmountValue);
builder.setMaxFee(maxFeeValue);
builder.setRemainingOwnerCount(remainingOwnerCountValue);
auto tx = builder.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(tx.validate(reason)) << reason;
// Verify signing was applied
EXPECT_FALSE(tx.getSigningPubKey().empty());
EXPECT_TRUE(tx.hasTxnSignature());
// Verify common fields
EXPECT_EQ(tx.getAccount(), accountValue);
EXPECT_EQ(tx.getSequence(), sequenceValue);
EXPECT_EQ(tx.getFee(), feeValue);
// Verify required fields
// Verify optional fields
{
auto const& expected = counterpartySponsorValue;
auto const actualOpt = tx.getCounterpartySponsor();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCounterpartySponsor should be present";
expectEqualField(expected, *actualOpt, "sfCounterpartySponsor");
EXPECT_TRUE(tx.hasCounterpartySponsor());
}
{
auto const& expected = sponseeValue;
auto const actualOpt = tx.getSponsee();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSponsee should be present";
expectEqualField(expected, *actualOpt, "sfSponsee");
EXPECT_TRUE(tx.hasSponsee());
}
{
auto const& expected = feeAmountValue;
auto const actualOpt = tx.getFeeAmount();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFeeAmount should be present";
expectEqualField(expected, *actualOpt, "sfFeeAmount");
EXPECT_TRUE(tx.hasFeeAmount());
}
{
auto const& expected = maxFeeValue;
auto const actualOpt = tx.getMaxFee();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMaxFee should be present";
expectEqualField(expected, *actualOpt, "sfMaxFee");
EXPECT_TRUE(tx.hasMaxFee());
}
{
auto const& expected = remainingOwnerCountValue;
auto const actualOpt = tx.getRemainingOwnerCount();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCount should be present";
expectEqualField(expected, *actualOpt, "sfRemainingOwnerCount");
EXPECT_TRUE(tx.hasRemainingOwnerCount());
}
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipSetFromTx"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 2;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const counterpartySponsorValue = canonical_ACCOUNT();
auto const sponseeValue = canonical_ACCOUNT();
auto const feeAmountValue = canonical_AMOUNT();
auto const maxFeeValue = canonical_AMOUNT();
auto const remainingOwnerCountValue = canonical_UINT32();
// Build an initial transaction
SponsorshipSetBuilder initialBuilder{
accountValue,
sequenceValue,
feeValue
};
initialBuilder.setCounterpartySponsor(counterpartySponsorValue);
initialBuilder.setSponsee(sponseeValue);
initialBuilder.setFeeAmount(feeAmountValue);
initialBuilder.setMaxFee(maxFeeValue);
initialBuilder.setRemainingOwnerCount(remainingOwnerCountValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
SponsorshipSetBuilder builderFromTx{initialTx.getSTTx()};
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
// Verify common fields
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
// Verify required fields
// Verify optional fields
{
auto const& expected = counterpartySponsorValue;
auto const actualOpt = rebuiltTx.getCounterpartySponsor();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCounterpartySponsor should be present";
expectEqualField(expected, *actualOpt, "sfCounterpartySponsor");
}
{
auto const& expected = sponseeValue;
auto const actualOpt = rebuiltTx.getSponsee();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSponsee should be present";
expectEqualField(expected, *actualOpt, "sfSponsee");
}
{
auto const& expected = feeAmountValue;
auto const actualOpt = rebuiltTx.getFeeAmount();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFeeAmount should be present";
expectEqualField(expected, *actualOpt, "sfFeeAmount");
}
{
auto const& expected = maxFeeValue;
auto const actualOpt = rebuiltTx.getMaxFee();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMaxFee should be present";
expectEqualField(expected, *actualOpt, "sfMaxFee");
}
{
auto const& expected = remainingOwnerCountValue;
auto const actualOpt = rebuiltTx.getRemainingOwnerCount();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCount should be present";
expectEqualField(expected, *actualOpt, "sfRemainingOwnerCount");
}
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsSponsorshipSetTests, WrapperThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(SponsorshipSet{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsSponsorshipSetTests, BuilderThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(SponsorshipSetBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(TransactionsSponsorshipSetTests, OptionalFieldsReturnNullopt)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipSetNullopt"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 3;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific required field values
SponsorshipSetBuilder builder{
accountValue,
sequenceValue,
feeValue
};
// Do NOT set optional fields
auto tx = builder.build(publicKey, secretKey);
// Verify optional fields are not present
EXPECT_FALSE(tx.hasCounterpartySponsor());
EXPECT_FALSE(tx.getCounterpartySponsor().has_value());
EXPECT_FALSE(tx.hasSponsee());
EXPECT_FALSE(tx.getSponsee().has_value());
EXPECT_FALSE(tx.hasFeeAmount());
EXPECT_FALSE(tx.getFeeAmount().has_value());
EXPECT_FALSE(tx.hasMaxFee());
EXPECT_FALSE(tx.getMaxFee().has_value());
EXPECT_FALSE(tx.hasRemainingOwnerCount());
EXPECT_FALSE(tx.getRemainingOwnerCount().has_value());
}
}

View File

@@ -0,0 +1,198 @@
// Auto-generated unit tests for transaction SponsorshipTransfer
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/SponsorshipTransfer.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(TransactionsSponsorshipTransferTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipTransfer"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 1;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const objectIDValue = canonical_UINT256();
auto const sponseeValue = canonical_ACCOUNT();
SponsorshipTransferBuilder builder{
accountValue,
sequenceValue,
feeValue
};
// Set optional fields
builder.setObjectID(objectIDValue);
builder.setSponsee(sponseeValue);
auto tx = builder.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(tx.validate(reason)) << reason;
// Verify signing was applied
EXPECT_FALSE(tx.getSigningPubKey().empty());
EXPECT_TRUE(tx.hasTxnSignature());
// Verify common fields
EXPECT_EQ(tx.getAccount(), accountValue);
EXPECT_EQ(tx.getSequence(), sequenceValue);
EXPECT_EQ(tx.getFee(), feeValue);
// Verify required fields
// Verify optional fields
{
auto const& expected = objectIDValue;
auto const actualOpt = tx.getObjectID();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfObjectID should be present";
expectEqualField(expected, *actualOpt, "sfObjectID");
EXPECT_TRUE(tx.hasObjectID());
}
{
auto const& expected = sponseeValue;
auto const actualOpt = tx.getSponsee();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSponsee should be present";
expectEqualField(expected, *actualOpt, "sfSponsee");
EXPECT_TRUE(tx.hasSponsee());
}
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsSponsorshipTransferTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipTransferFromTx"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 2;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const objectIDValue = canonical_UINT256();
auto const sponseeValue = canonical_ACCOUNT();
// Build an initial transaction
SponsorshipTransferBuilder initialBuilder{
accountValue,
sequenceValue,
feeValue
};
initialBuilder.setObjectID(objectIDValue);
initialBuilder.setSponsee(sponseeValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
SponsorshipTransferBuilder builderFromTx{initialTx.getSTTx()};
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
// Verify common fields
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
// Verify required fields
// Verify optional fields
{
auto const& expected = objectIDValue;
auto const actualOpt = rebuiltTx.getObjectID();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfObjectID should be present";
expectEqualField(expected, *actualOpt, "sfObjectID");
}
{
auto const& expected = sponseeValue;
auto const actualOpt = rebuiltTx.getSponsee();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSponsee should be present";
expectEqualField(expected, *actualOpt, "sfSponsee");
}
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsSponsorshipTransferTests, WrapperThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(SponsorshipTransfer{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsSponsorshipTransferTests, BuilderThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(SponsorshipTransferBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(TransactionsSponsorshipTransferTests, OptionalFieldsReturnNullopt)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testSponsorshipTransferNullopt"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 3;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific required field values
SponsorshipTransferBuilder builder{
accountValue,
sequenceValue,
feeValue
};
// Do NOT set optional fields
auto tx = builder.build(publicKey, secretKey);
// Verify optional fields are not present
EXPECT_FALSE(tx.hasObjectID());
EXPECT_FALSE(tx.getObjectID().has_value());
EXPECT_FALSE(tx.hasSponsee());
EXPECT_FALSE(tx.getSponsee().has_value());
}
}

View File

@@ -0,0 +1,420 @@
// Tests for SpanGuard::rootSpan() and SpanGuard::detached().
//
// These verify the cross-thread scope-leak fix at the trace level using an
// in-memory span exporter: rootSpan() must start a fresh trace root (ignoring
// the ambient active span), and detached() must strip the thread-local Scope
// on the origin thread so the guard can be moved to and ended on another
// thread without corrupting the origin thread's context stack.
//
// The whole file is telemetry-only: when XRPL_ENABLE_TELEMETRY is not defined
// SpanGuard is a no-op stub and the OpenTelemetry SDK headers are unavailable,
// so the translation unit compiles empty.
#ifdef XRPL_ENABLE_TELEMETRY
#include <xrpl/telemetry/SpanGuard.h>
#include <xrpl/telemetry/Telemetry.h>
#include <gtest/gtest.h>
#include <opentelemetry/context/context.h>
#include <opentelemetry/exporters/memory/in_memory_span_data.h>
#include <opentelemetry/exporters/memory/in_memory_span_exporter_factory.h>
#include <opentelemetry/nostd/shared_ptr.h>
#include <opentelemetry/sdk/resource/resource.h>
#include <opentelemetry/sdk/trace/samplers/always_on_factory.h>
#include <opentelemetry/sdk/trace/simple_processor_factory.h>
#include <opentelemetry/sdk/trace/span_data.h>
#include <opentelemetry/sdk/trace/tracer_provider.h>
#include <opentelemetry/sdk/trace/tracer_provider_factory.h>
#include <opentelemetry/trace/span.h>
#include <opentelemetry/trace/span_id.h>
#include <opentelemetry/trace/span_metadata.h>
#include <opentelemetry/trace/span_startoptions.h>
#include <opentelemetry/trace/trace_id.h>
#include <opentelemetry/trace/tracer.h>
#include <cstddef>
#include <memory>
#include <string>
#include <string_view>
#include <thread>
#include <utility>
#include <vector>
namespace xrpl::telemetry {
namespace {
namespace otel_sdk_trace = opentelemetry::sdk::trace;
namespace otel_memory = opentelemetry::exporter::memory;
/**
* In-memory Telemetry backing for SpanGuard scope tests.
*
* Reports every trace category as enabled and creates spans through an SDK
* TracerProvider whose SimpleSpanProcessor forwards ended spans to an
* InMemorySpanExporter, so a test can read the exact exported SpanData
* (trace id, span id, parent id, name).
*
* Inheritance:
*
* +-----------+
* | Telemetry | (abstract interface)
* +-----+-----+
* |
* +-----+---------+
* | TestTelemetry | in-memory exporter pipeline
* +---------------+
*
* @note Test-only. Install with Telemetry::setInstance() and clear it in
* teardown. The exporter buffer is drained by InMemorySpanData::GetSpans(),
* so each GetSpans() call returns only spans exported since the previous one.
*/
class TestTelemetry : public Telemetry
{
public:
/**
* Build the SDK export pipeline and keep the exporter's span buffer.
*/
TestTelemetry()
{
// Factory populates spanData_ with the exporter's shared buffer.
auto exporter = otel_memory::InMemorySpanExporterFactory::Create(spanData_);
auto processor = otel_sdk_trace::SimpleSpanProcessorFactory::Create(std::move(exporter));
provider_ = otel_sdk_trace::TracerProviderFactory::Create(
std::move(processor),
opentelemetry::sdk::resource::Resource::Create({}),
otel_sdk_trace::AlwaysOnSamplerFactory::Create());
}
/**
* @return The exporter's span buffer (drained by GetSpans()).
*/
std::shared_ptr<otel_memory::InMemorySpanData>
spanData() const
{
return spanData_;
}
void
start() override
{
}
void
stop() override
{
}
[[nodiscard]] bool
isEnabled() const override
{
return true;
}
[[nodiscard]] bool
shouldTraceTransactions() const override
{
return true;
}
[[nodiscard]] bool
shouldTraceConsensus() const override
{
return true;
}
[[nodiscard]] bool
shouldTraceRpc() const override
{
return true;
}
[[nodiscard]] bool
shouldTracePeer() const override
{
return true;
}
[[nodiscard]] bool
shouldTraceLedger() const override
{
return true;
}
opentelemetry::nostd::shared_ptr<opentelemetry::trace::Tracer>
getTracer(std::string_view name) override
{
return provider_->GetTracer(std::string(name));
}
opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>
startSpan(std::string_view name, opentelemetry::trace::SpanKind kind) override
{
opentelemetry::trace::StartSpanOptions opts;
opts.kind = kind;
return getTracer(kTracerName)->StartSpan(std::string(name), opts);
}
opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>
startSpan(
std::string_view name,
opentelemetry::context::Context const& parentContext,
opentelemetry::trace::SpanKind kind) override
{
opentelemetry::trace::StartSpanOptions opts;
opts.kind = kind;
opts.parent = parentContext;
return getTracer(kTracerName)->StartSpan(std::string(name), opts);
}
private:
/**
* SDK provider owning the export pipeline.
*/
std::shared_ptr<otel_sdk_trace::TracerProvider> provider_;
/**
* Shared buffer that receives ended spans from the exporter.
*/
std::shared_ptr<otel_memory::InMemorySpanData> spanData_;
};
/**
* @return The span's name as a std::string for equality checks.
*/
std::string
nameOf(otel_sdk_trace::SpanData const& span)
{
auto view = span.GetName();
return std::string(view.data(), view.size());
}
/**
* @return Pointer to the first exported span with the given name, or null.
*/
otel_sdk_trace::SpanData*
findSpan(std::vector<std::unique_ptr<otel_sdk_trace::SpanData>> const& spans, std::string_view name)
{
for (auto const& span : spans)
{
if (nameOf(*span) == name)
return span.get();
}
return nullptr;
}
/**
* @return Number of exported spans with the given name.
*/
std::size_t
countSpans(
std::vector<std::unique_ptr<otel_sdk_trace::SpanData>> const& spans,
std::string_view name)
{
std::size_t count = 0;
for (auto const& span : spans)
{
if (nameOf(*span) == name)
++count;
}
return count;
}
/**
* Installs a TestTelemetry as the global instance for each test and clears it
* afterwards so the singleton never dangles between cases.
*/
class SpanGuardScopeTest : public ::testing::Test
{
protected:
void
SetUp() override
{
telemetry_ = std::make_unique<TestTelemetry>();
Telemetry::setInstance(telemetry_.get());
}
void
TearDown() override
{
Telemetry::setInstance(nullptr);
telemetry_.reset();
}
/**
* @return The exporter's span buffer for the active TestTelemetry.
*/
std::shared_ptr<otel_memory::InMemorySpanData>
spanData() const
{
return telemetry_->spanData();
}
/**
* The in-memory Telemetry installed for the duration of a test.
*/
std::unique_ptr<TestTelemetry> telemetry_;
};
// rootSpan() must ignore the ambient active span and start a brand-new trace.
TEST_F(SpanGuardScopeTest, rootSpan_ignores_ambient_parent)
{
{
// Ambient span becomes the active span on this thread.
auto ambient = SpanGuard::span(TraceCategory::Rpc, "rpc", "command");
ASSERT_TRUE(static_cast<bool>(ambient));
// rootSpan must NOT inherit the ambient span as its parent.
auto root = SpanGuard::rootSpan(TraceCategory::Peer, "peer", "validation.receive");
ASSERT_TRUE(static_cast<bool>(root));
} // root ends first, then ambient -- both on this thread.
auto spans = spanData()->GetSpans();
auto* ambient = findSpan(spans, "rpc.command");
auto* root = findSpan(spans, "peer.validation.receive");
ASSERT_NE(ambient, nullptr);
ASSERT_NE(root, nullptr);
// The ambient span is itself a root (first span, no parent).
EXPECT_FALSE(ambient->GetParentSpanId().IsValid());
EXPECT_TRUE(ambient->GetTraceId().IsValid());
// The rootSpan has NO parent and lives in a DIFFERENT trace than ambient.
EXPECT_FALSE(root->GetParentSpanId().IsValid());
EXPECT_TRUE(root->GetTraceId().IsValid());
EXPECT_NE(root->GetTraceId(), ambient->GetTraceId());
}
// detached() pops the Scope on the origin thread, so ending the guard on
// another thread leaves the origin thread's context stack clean.
TEST_F(SpanGuardScopeTest, detached_does_not_corrupt_origin_stack)
{
// Scoped guard is active on this (origin) thread.
auto guard = SpanGuard::span(TraceCategory::Ledger, "ledger", "build");
ASSERT_TRUE(static_cast<bool>(guard));
// Strip the thread-local Scope here on the origin thread.
auto detached = std::move(guard).detached();
ASSERT_TRUE(static_cast<bool>(detached));
// End the detached span on a worker thread.
std::thread worker([d = std::move(detached)]() mutable {});
worker.join();
// Back on the origin thread: a new span must be a fresh root, proving the
// origin stack top was restored by detached() (not left holding
// ledger.build).
{
auto after = SpanGuard::span(TraceCategory::Rpc, "rpc", "command");
ASSERT_TRUE(static_cast<bool>(after));
}
auto spans = spanData()->GetSpans();
auto* build = findSpan(spans, "ledger.build");
auto* after = findSpan(spans, "rpc.command");
ASSERT_NE(build, nullptr);
ASSERT_NE(after, nullptr);
// 'after' is a new root: zero parent and a different trace than
// ledger.build.
EXPECT_FALSE(after->GetParentSpanId().IsValid());
EXPECT_NE(after->GetTraceId(), build->GetTraceId());
}
// A detached span is ended exactly once, by the worker thread that owns it.
TEST_F(SpanGuardScopeTest, detached_span_ends_once)
{
auto guard = SpanGuard::span(TraceCategory::Ledger, "ledger", "build");
auto detached = std::move(guard).detached();
ASSERT_TRUE(static_cast<bool>(detached));
// Before the worker ends it, the span is still open: nothing exported yet.
auto before = spanData()->GetSpans();
EXPECT_EQ(countSpans(before, "ledger.build"), 0u);
// Ending happens once, on the worker thread, when the guard is destroyed.
{
std::thread worker([d = std::move(detached)]() mutable {});
worker.join();
}
auto after = spanData()->GetSpans();
EXPECT_EQ(countSpans(after, "ledger.build"), 1u);
}
// rootSpan().detached() is the combination used at RPC coroutine entry points
// (e.g. RipplePathFind) that hold a span across a coro yield: the span must be
// a fresh root AND must not leave a Scope on the worker's context stack.
TEST_F(SpanGuardScopeTest, root_then_detached_is_root_and_leaves_stack_clean)
{
{
// Ambient span active on this (origin) thread.
auto ambient = SpanGuard::span(TraceCategory::Rpc, "rpc", "command");
ASSERT_TRUE(static_cast<bool>(ambient));
// Fresh root, then detach the Scope on the origin thread.
auto req = SpanGuard::rootSpan(TraceCategory::Rpc, "pathfind", "request").detached();
ASSERT_TRUE(static_cast<bool>(req));
// End the detached root span on a worker thread (mimics coro resume).
std::thread worker([r = std::move(req)]() mutable {});
worker.join();
// Origin stack must be clean: a new span here is still parented by the
// ambient span (proving the detached root did not linger on the stack).
{
auto child = SpanGuard::span(TraceCategory::Rpc, "rpc", "sub");
ASSERT_TRUE(static_cast<bool>(child));
}
}
auto spans = spanData()->GetSpans();
auto* ambient = findSpan(spans, "rpc.command");
auto* req = findSpan(spans, "pathfind.request");
auto* child = findSpan(spans, "rpc.sub");
ASSERT_NE(ambient, nullptr);
ASSERT_NE(req, nullptr);
ASSERT_NE(child, nullptr);
// The rooted-then-detached span has NO parent and a DIFFERENT trace.
EXPECT_FALSE(req->GetParentSpanId().IsValid());
EXPECT_NE(req->GetTraceId(), ambient->GetTraceId());
// The detached root did not corrupt the origin stack: 'child' still nests
// under the ambient span (same trace, parent = ambient), NOT under req.
EXPECT_EQ(child->GetParentSpanId(), ambient->GetSpanId());
EXPECT_EQ(child->GetTraceId(), ambient->GetTraceId());
}
// Negative control (documents the bug detached() fixes): a scoped guard NOT
// detached, moved to and destroyed on a worker thread, pops the WRONG thread's
// stack, so a later span on the origin thread inherits the stale span. The
// whole scenario runs on a dedicated origin thread so the corrupted
// thread-local stack cannot leak into other tests.
TEST_F(SpanGuardScopeTest, non_detached_cross_thread_corrupts_origin_stack)
{
std::thread origin([&] {
auto guard = SpanGuard::span(TraceCategory::Ledger, "ledger", "build");
{
// ~SpanGuard on the worker ends the span and Detaches on the
// WORKER stack -- the origin stack is never popped.
std::thread worker([d = std::move(guard)]() mutable {});
worker.join();
}
// Origin stack still holds the stale ledger.build as active, so this
// span inherits it as parent instead of being a root.
auto after = SpanGuard::span(TraceCategory::Rpc, "rpc", "command");
ASSERT_TRUE(static_cast<bool>(after));
});
origin.join();
auto spans = spanData()->GetSpans();
auto* build = findSpan(spans, "ledger.build");
auto* after = findSpan(spans, "rpc.command");
ASSERT_NE(build, nullptr);
ASSERT_NE(after, nullptr);
// BUG: 'after' inherited the stale span -- non-zero parent, same trace.
EXPECT_TRUE(after->GetParentSpanId().IsValid());
EXPECT_EQ(after->GetParentSpanId(), build->GetSpanId());
EXPECT_EQ(after->GetTraceId(), build->GetTraceId());
}
} // namespace
} // namespace xrpl::telemetry
#endif // XRPL_ENABLE_TELEMETRY