Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
47 changes: 47 additions & 0 deletions xrpl-wasm-stdlib/src/core/types/nft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,14 +583,61 @@ mod tests {

#[test]
fn test_nft_uri_method() {
use crate::mock_host;

let nft_id = [0u8; 32];
let nft = NFToken::new(nft_id);
let owner = AccountID([0u8; ACCOUNT_ID_SIZE]);

// Mock the get_nft function to return a successful result
mock_host! {
get_nft(_account_ptr, _account_len, _nft_id_ptr, _nft_id_len, _out_buff_ptr, _out_buff_len) => 42
};

// Positive case: should return Ok with URI blob
let result = nft.uri(&owner);
assert!(result.is_ok());
let uri = result.unwrap();
assert!(uri.len <= NFT_URI_MAX_SIZE);
}

#[test]
fn test_nft_uri_method_error() {
use crate::host::error_codes;
use crate::mock_host;

let nft_id = [0u8; 32];
let nft = NFToken::new(nft_id);
let owner = AccountID([0u8; ACCOUNT_ID_SIZE]);

// Mock the get_nft function to return an error (NFT not found)
mock_host! {
get_nft(_account_ptr, _account_len, _nft_id_ptr, _nft_id_len, _out_buff_ptr, _out_buff_len) => error_codes::LEDGER_OBJ_NOT_FOUND
};

// Negative case: should return Err when NFT is not found
let result = nft.uri(&owner);
assert!(result.is_err());
let error = result.err().unwrap();
assert_eq!(error.code(), error_codes::LEDGER_OBJ_NOT_FOUND);
}

#[test]
fn test_nft_flags_mocking() {
use crate::mock_host;

let nft_id = [0u8; 32];
let nft = NFToken::new(nft_id);

// Mock get_nft_flags to return a specific flag (e.g., BURNABLE = 1)
mock_host! {
get_nft_flags(_nft_id_ptr, _nft_id_len) => 1
};

let result = nft.flags();
assert!(result.is_ok());
let flags = result.unwrap();
assert!(flags.is_burnable());
assert_eq!(flags.as_u16(), 1);
}
}
19 changes: 19 additions & 0 deletions xrpl-wasm-stdlib/src/host/host_bindings_for_testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,17 @@ pub unsafe fn get_nft(
_out_buff_ptr: *mut u8,
_out_buff_len: usize,
) -> i32 {
#[cfg(test)]
if let Some(mock_fn) = mock::get_mock("get_nft") {
return mock_fn(&[
_account_ptr,
_account_len as *const u8,
_nft_id_ptr,
_nft_id_len as *const u8,
_out_buff_ptr as *const u8,
_out_buff_len as *const u8,
]);
}
_out_buff_len as i32
}

Expand Down Expand Up @@ -458,12 +469,20 @@ pub unsafe fn get_nft_taxon(
#[allow(unused)]
#[allow(clippy::missing_safety_doc)]
pub unsafe fn get_nft_flags(_nft_id_ptr: *const u8, _nft_id_len: usize) -> i32 {
#[cfg(test)]
if let Some(mock_fn) = mock::get_mock("get_nft_flags") {
return mock_fn(&[_nft_id_ptr, _nft_id_len as *const u8]);
}
_nft_id_len as i32
}

#[allow(unused)]
#[allow(clippy::missing_safety_doc)]
pub unsafe fn get_nft_transfer_fee(_nft_id_ptr: *const u8, _nft_id_len: usize) -> i32 {
#[cfg(test)]
if let Some(mock_fn) = mock::get_mock("get_nft_transfer_fee") {
return mock_fn(&[_nft_id_ptr, _nft_id_len as *const u8]);
}
_nft_id_len as i32
}

Expand Down
160 changes: 160 additions & 0 deletions xrpl-wasm-stdlib/src/host/mock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
//! Simple macro-based mocking for host functions.
//!
//! # Usage
//!
//! ```rust
//! use xrpl_wasm_stdlib::mock_host;
//! use xrpl_wasm_stdlib::core::types::nft::NFToken;
//!
//! #[test]
//! fn test_nft_transfer_fee() {
//! mock_host! {
//! get_nft_transfer_fee(_ptr, _len) => 5000
//! };
//!
//! let nft = NFToken::new([0u8; 32]);
//! assert_eq!(nft.transfer_fee().unwrap(), 5000);
//! }
//! ```

extern crate std;

use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::thread_local;

type MockFn = Rc<dyn Fn(&[*const u8]) -> i32>;

thread_local! {
static MOCKS: RefCell<HashMap<&'static str, MockFn>> = RefCell::new(HashMap::new());
}

pub fn set_mock<F>(name: &'static str, f: F)
where
F: Fn(&[*const u8]) -> i32 + 'static,
{
MOCKS.with(|m| {
m.borrow_mut().insert(name, Rc::new(f));
});
}

/// Clear a specific mock.
pub fn clear_mock(name: &'static str) {
MOCKS.with(|m| {
m.borrow_mut().remove(name);
});
}

/// Clear all mocks.
pub fn clear_all_mocks() {
MOCKS.with(|m| {
m.borrow_mut().clear();
});
}

/// Get a mock function if it exists.
pub(crate) fn get_mock(name: &'static str) -> Option<MockFn> {
MOCKS.with(|m| m.borrow().get(name).cloned())
}

/// Guard that clears mocks when dropped.
pub struct MockGuard;

impl Drop for MockGuard {
fn drop(&mut self) {
clear_all_mocks();
}
}

/// Macro for easily setting up mocks in tests.
///
/// This macro sets up the specified mocks and keeps them active until the end of the current scope.
/// It expands to a `let` binding internally, so you don't need to assign the result to a variable.
///
/// # Usage
///
/// ```rust
/// mock_host! {
/// get_nft(...) => 42
/// };
/// ```
#[macro_export]
macro_rules! mock_host {
// Simple value return
($($name:ident($($arg:ident),*) => $ret:expr),+ $(,)?) => {
let _mock_guard = {
$({
$crate::host::mock::set_mock(stringify!($name), move |_args| $ret);
})+
$crate::host::mock::MockGuard
};
};
}

#[cfg(test)]
mod tests {

use core::ptr;

#[test]
fn test_simple_mock() {
mock_host! {
get_nft_transfer_fee(_ptr, _len) => 42
};

let result = unsafe { super::super::get_nft_transfer_fee(ptr::null(), 0) };
assert_eq!(result, 42);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would you mind adding a test to check actually passing some data into the pointer (the way the host functions actually work)?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Bump @tekvyy

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mvadari Pushed the test, please review

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mvadari let me know if anything else is needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mvadari Let me know if we can merge this, i will create a followup PR with tests for other modules :)


#[test]
fn test_multiple_mocks() {
mock_host! {
get_nft_transfer_fee(_ptr, _len) => 100,
get_nft_flags(_ptr, _len) => 200
};

assert_eq!(
unsafe { super::super::get_nft_transfer_fee(ptr::null(), 0) },
100
);
assert_eq!(unsafe { super::super::get_nft_flags(ptr::null(), 0) }, 200);
}

#[test]
fn test_mock_cleanup() {
{
mock_host! {
get_nft_flags(_ptr, _len) => 200
};
assert_eq!(unsafe { super::super::get_nft_flags(ptr::null(), 0) }, 200);
}
// Should return 0 (the length passed) when not mocked
assert_eq!(unsafe { super::super::get_nft_flags(ptr::null(), 0) }, 0);
}

#[test]
fn test_mock_with_verification() {
use core::slice;

let expected_data = [1u8, 2, 3, 4];

let _guard = {
super::set_mock("get_nft_transfer_fee", move |args| {
// args[0] is pointer, args[1] is length
let ptr = args[0];
let len = args[1] as usize;
let actual_data = unsafe { slice::from_raw_parts(ptr, len) };
assert_eq!(actual_data, &[1, 2, 3, 4]);

42
});
super::MockGuard
};

let result = unsafe {
super::super::get_nft_transfer_fee(expected_data.as_ptr(), expected_data.len())
};
assert_eq!(result, 42);
}
}
6 changes: 6 additions & 0 deletions xrpl-wasm-stdlib/src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ pub mod error_codes;
pub mod field_helpers;
pub mod trace;

/// Mocking infrastructure for testing host functions.
///
/// This module is only available when running tests (not in WASM builds).
#[cfg(all(test, not(target_arch = "wasm32")))]
pub mod mock;

//////////////////////////////////////
// Host functions (defined by the host)
//////////////////////////////////////
Expand Down