Skip to content

Commit 11045e2

Browse files
authored
feat : allow rpc mock to insert multiple duplicate requests (#5757)
* feat: allow rpc mock to insert multiple same requests by migrating from Hashmap to Vec * chore : add test spec for duplicate rpc mock req * chore : resolve comments and refactor * chore : cleanup nits
1 parent 64e19be commit 11045e2

File tree

3 files changed

+393
-10
lines changed

3 files changed

+393
-10
lines changed

rpc-client/src/mock_sender.rs

+50-3
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,54 @@ use {
3636
UiRawMessage, UiTransaction, UiTransactionStatusMeta,
3737
},
3838
solana_version::Version,
39-
std::{collections::HashMap, net::SocketAddr, str::FromStr, sync::RwLock},
39+
std::{
40+
collections::{HashMap, VecDeque},
41+
net::SocketAddr,
42+
str::FromStr,
43+
sync::RwLock,
44+
},
4045
};
4146

4247
pub const PUBKEY: &str = "7RoSF9fUmdphVCpabEoefH81WwrW7orsWonXWqTXkKV8";
4348

4449
pub type Mocks = HashMap<RpcRequest, Value>;
50+
51+
impl From<Mocks> for MocksMap {
52+
fn from(mocks: Mocks) -> Self {
53+
let mut map = HashMap::new();
54+
for (key, value) in mocks {
55+
map.insert(key, [value].into());
56+
}
57+
MocksMap(map)
58+
}
59+
}
60+
61+
#[derive(Default, Clone)]
62+
pub struct MocksMap(pub HashMap<RpcRequest, VecDeque<Value>>);
63+
64+
impl FromIterator<(RpcRequest, Value)> for MocksMap {
65+
fn from_iter<T: IntoIterator<Item = (RpcRequest, Value)>>(iter: T) -> Self {
66+
let mut map = MocksMap::default();
67+
for (request, value) in iter {
68+
map.insert(request, value);
69+
}
70+
map
71+
}
72+
}
73+
74+
impl MocksMap {
75+
pub fn insert(&mut self, request: RpcRequest, value: Value) {
76+
let queue = self.0.entry(request).or_default();
77+
queue.push_back(value)
78+
}
79+
80+
pub fn pop_front_with_request(&mut self, request: &RpcRequest) -> Option<Value> {
81+
self.0.get_mut(request).and_then(|queue| queue.pop_front())
82+
}
83+
}
84+
4585
pub struct MockSender {
46-
mocks: RwLock<Mocks>,
86+
mocks: RwLock<MocksMap>,
4787
url: String,
4888
}
4989

@@ -78,6 +118,13 @@ impl MockSender {
78118
}
79119

80120
pub fn new_with_mocks<U: ToString>(url: U, mocks: Mocks) -> Self {
121+
Self {
122+
url: url.to_string(),
123+
mocks: RwLock::new(MocksMap::from(mocks)),
124+
}
125+
}
126+
127+
pub fn new_with_mocks_map<U: ToString>(url: U, mocks: MocksMap) -> Self {
81128
Self {
82129
url: url.to_string(),
83130
mocks: RwLock::new(mocks),
@@ -96,7 +143,7 @@ impl RpcSender for MockSender {
96143
request: RpcRequest,
97144
params: serde_json::Value,
98145
) -> Result<serde_json::Value> {
99-
if let Some(value) = self.mocks.write().unwrap().remove(&request) {
146+
if let Some(value) = self.mocks.write().unwrap().pop_front_with_request(&request) {
100147
return Ok(value);
101148
}
102149
if self.url == "fails" {

rpc-client/src/nonblocking/rpc_client.rs

+97-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use {crate::spinner, solana_clock::MAX_HASH_AGE_IN_SECONDS, std::cmp::min};
1212
use {
1313
crate::{
1414
http_sender::HttpSender,
15-
mock_sender::{mock_encoded_account, MockSender},
15+
mock_sender::{mock_encoded_account, MockSender, MocksMap},
1616
rpc_client::{
1717
GetConfirmedSignaturesForAddress2Config, RpcClientConfig, SerializableMessage,
1818
SerializableTransaction,
@@ -439,6 +439,101 @@ impl RpcClient {
439439
RpcClientConfig::with_commitment(CommitmentConfig::default()),
440440
)
441441
}
442+
/// Create a mock `RpcClient`.
443+
///
444+
/// A mock `RpcClient` contains an implementation of [`RpcSender`] that does
445+
/// not use the network, and instead returns synthetic responses, for use in
446+
/// tests.
447+
///
448+
/// It is primarily for internal use, with limited customizability, and
449+
/// behaviors determined by internal Solana test cases. New users should
450+
/// consider implementing `RpcSender` themselves and constructing
451+
/// `RpcClient` with [`RpcClient::new_sender`] to get mock behavior.
452+
///
453+
/// Unless directed otherwise, a mock `RpcClient` will generally return a
454+
/// reasonable default response to any request, at least for [`RpcRequest`]
455+
/// values for which responses have been implemented.
456+
///
457+
/// This mock can be customized in two ways:
458+
///
459+
/// 1) By changing the `url` argument, which is not actually a URL, but a
460+
/// simple string directive that changes the mock behavior in specific
461+
/// scenarios.
462+
///
463+
/// It is customary to set the `url` to "succeeds" for mocks that should
464+
/// return successfully, though this value is not actually interpreted.
465+
///
466+
/// If `url` is "fails" then any call to `send` will return `Ok(Value::Null)`.
467+
///
468+
/// Other possible values of `url` are specific to different `RpcRequest`
469+
/// values. Read the implementation of `MockSender` (which is non-public)
470+
/// for details.
471+
///
472+
/// 2) Custom responses can be configured by providing [`MocksMap`]. This type
473+
/// is a [`HashMap`] from [`RpcRequest`] to a [`Vec`] of JSON [`Value`] responses,
474+
/// Any entries in this map override the default behavior for the given
475+
/// request.
476+
///
477+
/// The [`RpcClient::new_mock_with_mocks_map`] function offers further
478+
/// customization options.
479+
///
480+
///
481+
/// # Examples
482+
///
483+
/// ```
484+
/// # use solana_rpc_client_api::{
485+
/// # request::RpcRequest,
486+
/// # response::{Response, RpcResponseContext},
487+
/// # };
488+
/// # use solana_rpc_client::{rpc_client::RpcClient, mock_sender::MocksMap};
489+
/// # use serde_json::json;
490+
/// // Create a mock with a custom response to the `GetBalance` request
491+
/// let account_balance_x = 50;
492+
/// let account_balance_y = 100;
493+
/// let account_balance_z = 150;
494+
/// let account_balance_req_responses = vec![
495+
/// (
496+
/// RpcRequest::GetBalance,
497+
/// json!(Response {
498+
/// context: RpcResponseContext {
499+
/// slot: 1,
500+
/// api_version: None,
501+
/// },
502+
/// value: json!(account_balance_x),
503+
/// })
504+
/// ),
505+
/// (
506+
/// RpcRequest::GetBalance,
507+
/// json!(Response {
508+
/// context: RpcResponseContext {
509+
/// slot: 1,
510+
/// api_version: None,
511+
/// },
512+
/// value: json!(account_balance_y),
513+
/// })
514+
/// ),
515+
/// ];
516+
///
517+
/// let mut mocks = MocksMap::from_iter(account_balance_req_responses);
518+
/// mocks.insert(
519+
/// RpcRequest::GetBalance,
520+
/// json!(Response {
521+
/// context: RpcResponseContext {
522+
/// slot: 1,
523+
/// api_version: None,
524+
/// },
525+
/// value: json!(account_balance_z),
526+
/// }),
527+
/// );
528+
/// let url = "succeeds".to_string();
529+
/// let client = RpcClient::new_mock_with_mocks_map(url, mocks);
530+
/// ```
531+
pub fn new_mock_with_mocks_map<U: ToString>(url: U, mocks: MocksMap) -> Self {
532+
Self::new_sender(
533+
MockSender::new_with_mocks_map(url, mocks),
534+
RpcClientConfig::with_commitment(CommitmentConfig::default()),
535+
)
536+
}
442537

443538
/// Create an HTTP `RpcClient` from a [`SocketAddr`].
444539
///
@@ -4707,7 +4802,7 @@ pub(crate) fn parse_keyed_accounts(
47074802

47084803
#[doc(hidden)]
47094804
pub fn create_rpc_client_mocks() -> crate::mock_sender::Mocks {
4710-
let mut mocks = std::collections::HashMap::new();
4805+
let mut mocks = crate::mock_sender::Mocks::default();
47114806

47124807
let get_account_request = RpcRequest::GetAccountInfo;
47134808
let get_account_response = serde_json::to_value(Response {

0 commit comments

Comments
 (0)