Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions .github/workflows/unit_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,32 @@ jobs:
RUST_BACKTRACE: 1
Comment thread
kuan121 marked this conversation as resolved.

- name: Test for no_std
run: cargo test --release --no-default-features --features embassy-rt,core,utils,wallet,models,helpers,websocket,json-rpc
run: cargo test --release --no-default-features --features
embassy-rt,core,utils,wallet,models,helpers,websocket,json-rpc
env:
RUST_BACKTRACE: 1

- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov

# Integration-territory files (CLI, async network clients, sync wrappers
# around network calls, faucet) are excluded from unit-test coverage.
# They are exercised by tests under tests/ behind the `integration`
# feature flag, which run against a live rippled in a separate workflow.
# Mirrors xrpl-py's split between tests/unit/ (--fail-under=85) and
# tests/integration/ (--fail-under=70).
Comment thread
pdp2121 marked this conversation as resolved.
Outdated
- name: Generate coverage report
run: cargo llvm-cov --lcov --output-path lcov.info
run: |
cargo llvm-cov --lcov --output-path lcov.info \
--ignore-filename-regex 'src/(bin|cli|clients|account|ledger|asynch/clients|asynch/account|asynch/ledger)/|src/transaction/mod\.rs|src/wallet/faucet_generation\.rs'

- name: Check coverage thresholds
run: |
cargo llvm-cov --summary-only \
--fail-under-lines 73 \
--fail-under-regions 75 \
--fail-under-functions 67
--ignore-filename-regex 'src/(bin|cli|clients|account|ledger|asynch/clients|asynch/account|asynch/ledger)/|src/transaction/mod\.rs|src/wallet/faucet_generation\.rs' \
--fail-under-lines 85 \
--fail-under-regions 85 \
--fail-under-functions 75

@kuan121 kuan121 May 12, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to fail the check when coverage for new or modified code introduced by a PR falls below a certain percentage?

I’m less concerned about the current coverage of existing code. If we want to maintain a high bar for test coverage, we should apply that standard to new code. Over time, that will naturally improve the overall coverage of the repo.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, patch coverage is possible to do, but would require Codecov (similar to the requirements for removing integration test regex) since cargo-llvm-cov alone would not suffice. Probably worth a follow-up PR since we continue to find more needs to integrate

@kuan121 kuan121 May 14, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you create a follow-up story to track this work so it doesn’t get lost?

I think enforcing patch coverage with a high threshold could be a better approach than setting a project-wide threshold, since it focuses on improving new and modified code without relying on broad exclusions. Over time, this should help gradually improve coverage in a repo with low overall coverage.

BTW, xrpl4j uses Codecov (see code). Example PR - XRPLF/xrpl4j#786

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created the issue: #307

Nice! that means the setup for Codecov in XRPLF has already been done, making our task easier without the need for admin privilege

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually let me just add it directly into this PR since Codecov is enabled already in XRPLF so there's no actual blocker

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 22372c3


- name: Upload coverage report
uses: actions/upload-artifact@v4
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Expanded unit-test coverage and raised CI thresholds: lines `73 → 85`, regions `75 → 86`, functions `67 → 76`.
- Split unit-test and integration-test coverage measurement in CI; integration-territory files (CLI, async network clients, sync wrappers, faucet) are excluded from the unit-test gate.

### Fixed

- `RipplePathFind::destination_amount` changed from `Currency<'a>` to `Amount<'a>` to match the XRPL wire format.
- `NoRippleCheckRole` no longer serializes with the `#[serde(tag = "role")]` discriminator; now emits a plain `snake_case` string matching the XRPL wire format.

## [[v1.1.0]]

### Added
Expand Down
3 changes: 2 additions & 1 deletion src/asynch/transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ where
}
}

#[cfg(all(feature = "websocket", feature = "std"))]
#[cfg(all(feature = "websocket", feature = "std", feature = "integration"))]
#[cfg(test)]
mod test_autofill {
use super::autofill;
Expand Down Expand Up @@ -594,6 +594,7 @@ mod test_sign {
assertions::assert_transaction_multisigned(&tx);
}

#[cfg(feature = "integration")]
#[tokio::test]
async fn test_autofill_and_sign() {
let client = AsyncJsonRpcClient::connect(test_constants::TESTNET_URL.parse().unwrap());
Expand Down
1 change: 1 addition & 0 deletions src/asynch/transaction/submit_and_wait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ mod tests {
},
};

#[cfg(feature = "integration")]
#[tokio::test]
async fn test_submit_and_wait() {
let client = AsyncJsonRpcClient::connect(test_constants::TESTNET_URL.parse().unwrap());
Expand Down
1 change: 1 addition & 0 deletions src/asynch/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ mod test_faucet_wallet_generation {
},
};

#[cfg(feature = "integration")]
#[tokio::test]
async fn test_generate_faucet_wallet() {
let client = AsyncJsonRpcClient::connect(test_constants::TESTNET_URL.parse().unwrap());
Expand Down
34 changes: 34 additions & 0 deletions src/models/ledger/objects/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,37 @@ impl<'a> Bridge<'a> {
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::models::currency::XRP;

#[test]
fn test_bridge_serde_round_trip() {
let bridge = Bridge::new(
Some("AABBCC".into()),
Some("DDEEFF".into()),
"rPV4mZjsXfH2HvUSPLNmqz1J8d3Lpv7tpe".into(),
"100".into(),
7,
3,
XChainBridge {
locking_chain_door: "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4".into(),
locking_chain_issue: XRP::new().into(),
issuing_chain_door: "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh".into(),
issuing_chain_issue: XRP::new().into(),
},
"13f".into(),
Some("100000".into()),
);
let serialized = serde_json::to_string(&bridge).unwrap();
let deserialized: Bridge = serde_json::from_str(&serialized).unwrap();
assert_eq!(bridge, deserialized);
assert!(serialized.contains("\"LedgerEntryType\":\"Bridge\""));
assert!(serialized.contains("\"XChainAccountClaimCount\":7"));
assert!(serialized.contains("\"XChainAccountCreateCount\":3"));
assert!(serialized.contains("\"XChainClaimID\":\"13f\""));
assert_eq!(bridge.get_ledger_entry_type(), LedgerEntryType::Bridge);
}
}
61 changes: 61 additions & 0 deletions src/models/ledger/objects/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,64 @@ where

fn get_ledger_entry_type(&self) -> LedgerEntryType;
}

#[cfg(test)]
mod tests {
use super::*;
use crate::models::amount::XRPAmount;
use crate::models::currency::XRP;
use crate::models::NoFlags;
use alloc::string::ToString;

#[test]
fn test_common_fields_new() {
let fields: CommonFields<'_, NoFlags> = CommonFields::new(
FlagCollection::default(),
LedgerEntryType::Bridge,
Some("AABBCC".into()),
Some("DDEEFF".into()),
);
assert_eq!(fields.get_ledger_entry_type(), LedgerEntryType::Bridge);
// The default impl on `LedgerObject` should return `false` here - no
// flag set is true. Pull a flag from the enum to satisfy IntoEnumIter.
// Because NoFlags has no variants, simply check the trait wiring works.
assert_eq!(fields.flags.0.len(), 0);
}

#[test]
fn test_ledger_entry_type_display() {
// The Display impl is auto-derived from `strum_macros::Display`.
assert_eq!(LedgerEntryType::AccountRoot.to_string(), "AccountRoot");
assert_eq!(LedgerEntryType::Bridge.to_string(), "Bridge");
assert_eq!(
LedgerEntryType::XChainOwnedClaimID.to_string(),
"XChainOwnedClaimID"
);
}

#[test]
fn test_ledger_entry_enum_serde_round_trip() {
let bridge = Bridge::new(
Some("AABBCC".into()),
Some("DDEEFF".into()),
"rPV4mZjsXfH2HvUSPLNmqz1J8d3Lpv7tpe".into(),
XRPAmount::from("100"),
0,
0,
crate::models::XChainBridge {
locking_chain_door: "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4".into(),
locking_chain_issue: XRP::new().into(),
issuing_chain_door: "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh".into(),
issuing_chain_issue: XRP::new().into(),
},
"1".into(),
None,
);
let entry = LedgerEntry::Bridge(bridge);
let serialized = serde_json::to_string(&entry).unwrap();
// The enum is untagged-ish; default behaviour is externally tagged on
// variant name. Either way, round-trip must work for the same JSON.
let deserialized: LedgerEntry = serde_json::from_str(&serialized).unwrap();
assert_eq!(entry, deserialized);
}
}
44 changes: 44 additions & 0 deletions src/models/ledger/objects/xchain_owned_claim_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,47 @@ impl<'a> XChainOwnedClaimID<'a> {
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::models::amount::XRPAmount;
use crate::models::currency::XRP;
use alloc::vec;

#[test]
fn test_xchain_owned_claim_id_serde_round_trip() {
let attestation = XChainClaimProofSig {
amount: Amount::XRPAmount(XRPAmount::from("10000")),
attestation_reward_account: "rPV4mZjsXfH2HvUSPLNmqz1J8d3Lpv7tpe".into(),
attestation_signer_account: "rPV4mZjsXfH2HvUSPLNmqz1J8d3Lpv7tpe".into(),
destination: "rDest11111111111111111111111111111".into(),
public_key: "ED1234567890ABCDEF".into(),
was_locking_chain_send: 1,
};
let entry = XChainOwnedClaimID::new(
Some("AABBCC".into()),
None,
"rPV4mZjsXfH2HvUSPLNmqz1J8d3Lpv7tpe".into(),
"rSrc111111111111111111111111111111".into(),
Amount::XRPAmount(XRPAmount::from("100")),
XChainBridge {
locking_chain_door: "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4".into(),
locking_chain_issue: XRP::new().into(),
issuing_chain_door: "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh".into(),
issuing_chain_issue: XRP::new().into(),
},
vec![attestation],
"13f".into(),
);
let serialized = serde_json::to_string(&entry).unwrap();
let deserialized: XChainOwnedClaimID = serde_json::from_str(&serialized).unwrap();
assert_eq!(entry, deserialized);
assert!(serialized.contains("\"LedgerEntryType\":\"XChainOwnedClaimID\""));
assert!(serialized.contains("\"XChainClaimAttestations\""));
assert_eq!(
entry.get_ledger_entry_type(),
LedgerEntryType::XChainOwnedClaimID
);
}
}
23 changes: 23 additions & 0 deletions src/models/requests/account_channels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,26 @@ impl<'a> Request<'a> for AccountChannels<'a> {
&mut self.common_fields
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_serde_round_trip() {
let req = AccountChannels::new(
Some("ac-1".into()),
"rH6ZiHU1PGamME2LvVTxrgvfjQpppWKGmr".into(),
Some("rDest11111111111111111111111111111".into()),
None,
Some(LedgerIndex::Str("validated".into())),
Some(50),
Some(Marker::Int(12345)),
);
let serialized = serde_json::to_string(&req).unwrap();
let deserialized: AccountChannels = serde_json::from_str(&serialized).unwrap();
assert_eq!(req, deserialized);
assert!(serialized.contains("\"command\":\"account_channels\""));
assert!(serialized.contains("\"account\":\"rH6ZiHU1PGamME2LvVTxrgvfjQpppWKGmr\""));
}
}
21 changes: 21 additions & 0 deletions src/models/requests/account_currencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,24 @@ impl<'a> AccountCurrencies<'a> {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_serde_round_trip() {
let req = AccountCurrencies::new(
Some("acur-1".into()),
"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh".into(),
None,
Some(LedgerIndex::Int(123)),
Some(true),
);
let serialized = serde_json::to_string(&req).unwrap();
let deserialized: AccountCurrencies = serde_json::from_str(&serialized).unwrap();
assert_eq!(req, deserialized);
assert!(serialized.contains("\"command\":\"account_currencies\""));
assert!(serialized.contains("\"strict\":true"));
}
}
22 changes: 22 additions & 0 deletions src/models/requests/account_lines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,25 @@ impl<'a> AccountLines<'a> {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_serde_round_trip() {
let req = AccountLines::new(
Some("al-1".into()),
"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh".into(),
None,
Some(LedgerIndex::Str("validated".into())),
Some(100),
Some("rPeer11111111111111111111111111111".into()),
);
let serialized = serde_json::to_string(&req).unwrap();
let deserialized: AccountLines = serde_json::from_str(&serialized).unwrap();
assert_eq!(req, deserialized);
assert!(serialized.contains("\"command\":\"account_lines\""));
assert!(serialized.contains("\"limit\":100"));
}
}
24 changes: 24 additions & 0 deletions src/models/requests/account_objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,27 @@ impl<'a> AccountObjects<'a> {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_serde_round_trip() {
let req = AccountObjects::new(
Some("ao-1".into()),
"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh".into(),
None,
None,
Some(AccountObjectType::Escrow),
Some(true),
Some(20),
None,
);
let serialized = serde_json::to_string(&req).unwrap();
let deserialized: AccountObjects = serde_json::from_str(&serialized).unwrap();
assert_eq!(req, deserialized);
assert!(serialized.contains("\"command\":\"account_objects\""));
assert!(serialized.contains("\"type\":\"escrow\""));
}
}
22 changes: 22 additions & 0 deletions src/models/requests/account_offers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,25 @@ impl<'a> AccountOffers<'a> {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_serde_round_trip() {
let req = AccountOffers::new(
Some("aoff-1".into()),
"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh".into(),
None,
Some(LedgerIndex::Int(456)),
Some(50),
Some(true),
Some(Marker::Str("abc".into())),
);
let serialized = serde_json::to_string(&req).unwrap();
let deserialized: AccountOffers = serde_json::from_str(&serialized).unwrap();
assert_eq!(req, deserialized);
assert!(serialized.contains("\"command\":\"account_offers\""));
}
}
26 changes: 26 additions & 0 deletions src/models/requests/account_tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,29 @@ impl<'a> AccountTx<'a> {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_serde_round_trip() {
let req = AccountTx::new(
Some("atx-1".into()),
"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh".into(),
None,
None,
Some(false),
Some(true),
Some(1),
Some(99999),
Some(25),
None,
);
let serialized = serde_json::to_string(&req).unwrap();
let deserialized: AccountTx = serde_json::from_str(&serialized).unwrap();
assert_eq!(req, deserialized);
assert!(serialized.contains("\"command\":\"account_tx\""));
assert!(serialized.contains("\"forward\":true"));
}
}
Loading
Loading