Skip to main content

ic_testkit/pic/
calls.rs

1use candid::{CandidType, Principal, decode_one, encode_args, utils::ArgumentEncoder};
2use serde::de::DeserializeOwned;
3
4use super::{Pic, PicCallError};
5
6#[derive(Clone, Copy)]
7struct CallContext<'a> {
8    operation: &'static str,
9    canister_id: Principal,
10    caller: Principal,
11    method: &'a str,
12}
13
14impl Pic {
15    /// Generic update call helper (serializes args + decodes result).
16    pub fn update_call<T, A>(
17        &self,
18        canister_id: Principal,
19        method: &str,
20        args: A,
21    ) -> Result<T, PicCallError>
22    where
23        T: CandidType + DeserializeOwned,
24        A: ArgumentEncoder,
25    {
26        self.update_call_as(canister_id, Principal::anonymous(), method, args)
27    }
28
29    /// Generic update call helper that panics on transport or Candid codec failure.
30    ///
31    /// This does not unwrap application-level results. For example,
32    /// `update_call_or_panic::<Result<T, E>, _>(...)` returns `Result<T, E>`.
33    #[track_caller]
34    pub fn update_call_or_panic<T, A>(&self, canister_id: Principal, method: &str, args: A) -> T
35    where
36        T: CandidType + DeserializeOwned,
37        A: ArgumentEncoder,
38    {
39        self.update_call(canister_id, method, args)
40            .unwrap_or_else(|err| panic!("{err}"))
41    }
42
43    /// Generic update call helper with an explicit caller principal.
44    pub fn update_call_as<T, A>(
45        &self,
46        canister_id: Principal,
47        caller: Principal,
48        method: &str,
49        args: A,
50    ) -> Result<T, PicCallError>
51    where
52        T: CandidType + DeserializeOwned,
53        A: ArgumentEncoder,
54    {
55        let context = CallContext {
56            operation: "update_call",
57            canister_id,
58            caller,
59            method,
60        };
61        let bytes = encode_call_args(args, context)?;
62        let result = self
63            .inner
64            .update_call(canister_id, caller, method, bytes)
65            .map_err(|err| {
66                PicCallError::new(format!(
67                    "pocket_ic update_call failed (canister={canister_id}, caller={caller}, method={method}): {err}"
68                ))
69            })?;
70
71        decode_call_result(&result, context)
72    }
73
74    /// Generic update call helper with an explicit caller principal that panics
75    /// on transport or Candid codec failure.
76    ///
77    /// This does not unwrap application-level results. For example,
78    /// `update_call_as_or_panic::<Result<T, E>, _>(...)` returns `Result<T, E>`.
79    #[track_caller]
80    pub fn update_call_as_or_panic<T, A>(
81        &self,
82        canister_id: Principal,
83        caller: Principal,
84        method: &str,
85        args: A,
86    ) -> T
87    where
88        T: CandidType + DeserializeOwned,
89        A: ArgumentEncoder,
90    {
91        self.update_call_as(canister_id, caller, method, args)
92            .unwrap_or_else(|err| panic!("{err}"))
93    }
94
95    /// Generic query call helper.
96    pub fn query_call<T, A>(
97        &self,
98        canister_id: Principal,
99        method: &str,
100        args: A,
101    ) -> Result<T, PicCallError>
102    where
103        T: CandidType + DeserializeOwned,
104        A: ArgumentEncoder,
105    {
106        self.query_call_as(canister_id, Principal::anonymous(), method, args)
107    }
108
109    /// Generic query call helper that panics on transport or Candid codec failure.
110    ///
111    /// This does not unwrap application-level results. For example,
112    /// `query_call_or_panic::<Result<T, E>, _>(...)` returns `Result<T, E>`.
113    #[track_caller]
114    pub fn query_call_or_panic<T, A>(&self, canister_id: Principal, method: &str, args: A) -> T
115    where
116        T: CandidType + DeserializeOwned,
117        A: ArgumentEncoder,
118    {
119        self.query_call(canister_id, method, args)
120            .unwrap_or_else(|err| panic!("{err}"))
121    }
122
123    /// Generic query call helper with an explicit caller principal.
124    pub fn query_call_as<T, A>(
125        &self,
126        canister_id: Principal,
127        caller: Principal,
128        method: &str,
129        args: A,
130    ) -> Result<T, PicCallError>
131    where
132        T: CandidType + DeserializeOwned,
133        A: ArgumentEncoder,
134    {
135        let context = CallContext {
136            operation: "query_call",
137            canister_id,
138            caller,
139            method,
140        };
141        let bytes = encode_call_args(args, context)?;
142        let result = self
143            .inner
144            .query_call(canister_id, caller, method, bytes)
145            .map_err(|err| {
146                PicCallError::new(format!(
147                    "pocket_ic query_call failed (canister={canister_id}, caller={caller}, method={method}): {err}"
148                ))
149            })?;
150
151        decode_call_result(&result, context)
152    }
153
154    /// Generic query call helper with an explicit caller principal that panics
155    /// on transport or Candid codec failure.
156    ///
157    /// This does not unwrap application-level results. For example,
158    /// `query_call_as_or_panic::<Result<T, E>, _>(...)` returns `Result<T, E>`.
159    #[track_caller]
160    pub fn query_call_as_or_panic<T, A>(
161        &self,
162        canister_id: Principal,
163        caller: Principal,
164        method: &str,
165        args: A,
166    ) -> T
167    where
168        T: CandidType + DeserializeOwned,
169        A: ArgumentEncoder,
170    {
171        self.query_call_as(canister_id, caller, method, args)
172            .unwrap_or_else(|err| panic!("{err}"))
173    }
174
175    /// Advance PocketIC by a fixed number of ticks.
176    pub fn tick_n(&self, times: usize) {
177        for _ in 0..times {
178            self.tick();
179        }
180    }
181}
182
183fn encode_call_args<A>(args: A, context: CallContext<'_>) -> Result<Vec<u8>, PicCallError>
184where
185    A: ArgumentEncoder,
186{
187    encode_args(args).map_err(|err| {
188        PicCallError::new(format!(
189            "candid encode_args failed (operation={}, canister={}, caller={}, method={}): {err}",
190            context.operation, context.canister_id, context.caller, context.method
191        ))
192    })
193}
194
195fn decode_call_result<T>(result: &[u8], context: CallContext<'_>) -> Result<T, PicCallError>
196where
197    T: CandidType + DeserializeOwned,
198{
199    decode_one(result).map_err(|err| {
200        PicCallError::new(format!(
201            "candid decode_one failed (operation={}, canister={}, caller={}, method={}, bytes={}): {err}",
202            context.operation,
203            context.canister_id,
204            context.caller,
205            context.method,
206            result.len()
207        ))
208    })
209}
210
211#[cfg(test)]
212mod tests {
213    use candid::Principal;
214
215    use super::{CallContext, decode_call_result};
216
217    #[test]
218    fn decode_error_includes_call_context() {
219        let context = CallContext {
220            operation: "query_call",
221            canister_id: Principal::anonymous(),
222            caller: Principal::management_canister(),
223            method: "get",
224        };
225
226        let err = decode_call_result::<u64>(&[0xde, 0xad], context).expect_err("decode fails");
227
228        assert!(err.message.contains("candid decode_one failed"));
229        assert!(err.message.contains("operation=query_call"));
230        assert!(err.message.contains("method=get"));
231        assert!(err.message.contains("bytes=2"));
232    }
233}