Skip to main content

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}