ic_canister_runtime/stub/mod.rs
1#[cfg(test)]
2mod tests;
3
4use crate::{IcError, Runtime};
5use async_trait::async_trait;
6use candid::{utils::ArgumentEncoder, CandidType, Decode, Encode, Principal};
7use serde::de::DeserializeOwned;
8use std::sync::Arc;
9use std::{collections::VecDeque, sync::Mutex};
10
11/// An implementation of [`Runtime`] that returns pre-defined results from a queue.
12/// This runtime is primarily intended for testing purposes.
13///
14/// # Examples
15///
16/// ```rust
17/// # #[tokio::main]
18/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
19/// use candid::Principal;
20/// use ic_canister_runtime::{IcError, Runtime, StubRuntime};
21///
22/// const PRINCIPAL: Principal = Principal::from_slice(&[0x9d, 0xf7, 0x01]);
23/// const METHOD: &str = "method";
24/// const ARGS: (&str,) = ("args",);
25///
26/// let runtime = StubRuntime::new()
27/// .add_stub_response(1_u64)
28/// .add_stub_response("two")
29/// .add_stub_error(IcError::CallPerformFailed);
30///
31/// let result_1: Result<u64, IcError> = runtime
32/// .update_call(PRINCIPAL, METHOD, ARGS, 0)
33/// .await;
34/// assert_eq!(result_1, Ok(1_u64));
35///
36/// let result_2: Result<String, IcError> = runtime
37/// .query_call(PRINCIPAL, METHOD, ARGS)
38/// .await;
39/// assert_eq!(result_2, Ok("two".to_string()));
40///
41/// let result_3: Result<Option<u128>, IcError> = runtime
42/// .query_call(PRINCIPAL, METHOD, ARGS)
43/// .await;
44/// assert_eq!(result_3, Err(IcError::CallPerformFailed));
45/// # Ok(())
46/// # }
47/// ```
48#[derive(Debug, Default, Clone)]
49pub struct StubRuntime {
50 // Use a mutex so that this struct is Send and Sync
51 #[allow(clippy::type_complexity)]
52 call_results: Arc<Mutex<VecDeque<Result<Vec<u8>, IcError>>>>,
53}
54
55impl StubRuntime {
56 /// Create a new empty [`StubRuntime`].
57 pub fn new() -> Self {
58 Self::default()
59 }
60
61 /// Mutate the [`StubRuntime`] instance to add the given stub response.
62 ///
63 /// Panics if the stub response cannot be encoded using Candid.
64 pub fn add_stub_response<Out: CandidType>(self, stub_response: Out) -> Self {
65 let result = Encode!(&stub_response).expect("Failed to encode Candid stub response");
66 self.call_results.try_lock().unwrap().push_back(Ok(result));
67 self
68 }
69
70 /// Mutate the [`StubRuntime`] instance to add the given stub error.
71 pub fn add_stub_error(self, stub_error: impl Into<IcError>) -> Self {
72 self.call_results
73 .try_lock()
74 .unwrap()
75 .push_back(Err(stub_error.into()));
76 self
77 }
78
79 fn call<Out>(&self) -> Result<Out, IcError>
80 where
81 Out: CandidType + DeserializeOwned,
82 {
83 self.call_results
84 .try_lock()
85 .unwrap()
86 .pop_front()
87 .unwrap_or_else(|| panic!("No available call response"))
88 .map(|bytes| Decode!(&bytes, Out).expect("Failed to decode Candid stub response"))
89 }
90}
91
92#[async_trait]
93impl Runtime for StubRuntime {
94 async fn update_call<In, Out>(
95 &self,
96 _id: Principal,
97 _method: &str,
98 _args: In,
99 _cycles: u128,
100 ) -> Result<Out, IcError>
101 where
102 In: ArgumentEncoder + Send,
103 Out: CandidType + DeserializeOwned,
104 {
105 self.call()
106 }
107
108 async fn query_call<In, Out>(
109 &self,
110 _id: Principal,
111 _method: &str,
112 _args: In,
113 ) -> Result<Out, IcError>
114 where
115 In: ArgumentEncoder + Send,
116 Out: CandidType + DeserializeOwned,
117 {
118 self.call()
119 }
120}