Skip to main content

ic_testkit/pic/
calls.rs

1use candid::{
2    Principal, encode_args,
3    utils::{ArgumentDecoder, ArgumentEncoder},
4};
5use ic_testkit_candid::{DecodeError, DecodeLimits, decode_args};
6use pocket_ic::{PocketIc, RejectResponse};
7use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
8
9use super::{CandidCallContext, CandidCallError, PocketIcOperationError, transport};
10
11#[derive(Clone, Copy)]
12struct CallContext<'a> {
13    operation: &'static str,
14    canister_id: Principal,
15    caller: Principal,
16    method: &'a str,
17}
18
19impl CallContext<'_> {
20    fn to_error_context(self) -> CandidCallContext {
21        CandidCallContext::new(self.operation, self.canister_id, self.caller, self.method)
22    }
23}
24
25/// Execute an update as `caller`, decoding the top-level Candid reply arguments.
26///
27/// `(u64, bool)` means two reply arguments; `((u64, bool),)` means one
28/// tuple-valued reply. Use `()` for no replies or
29/// `(T,)` for one reply. Pass [`Principal::anonymous()`] for an anonymous caller.
30/// Application-level `Result<T, E>` values are returned without unwrapping.
31pub fn update_candid_args<T, A>(
32    pic: &PocketIc,
33    canister_id: Principal,
34    caller: Principal,
35    method: &str,
36    args: A,
37    limits: DecodeLimits,
38) -> Result<T, CandidCallError>
39where
40    T: for<'de> ArgumentDecoder<'de>,
41    A: ArgumentEncoder,
42{
43    let context = CallContext {
44        operation: "update_call",
45        canister_id,
46        caller,
47        method,
48    };
49    call_candid(
50        context,
51        args,
52        |bytes| pic.update_call(canister_id, caller, method, bytes),
53        |bytes| decode_args(bytes, limits),
54    )
55}
56
57/// Execute a query as `caller`, decoding the top-level Candid reply arguments.
58///
59/// `(u64, bool)` means two reply arguments; `((u64, bool),)` means one
60/// tuple-valued reply. Use `()` for no replies or
61/// `(T,)` for one reply. Pass [`Principal::anonymous()`] for an anonymous caller.
62/// Application-level `Result<T, E>` values are returned without unwrapping.
63pub fn query_candid_args<T, A>(
64    pic: &PocketIc,
65    canister_id: Principal,
66    caller: Principal,
67    method: &str,
68    args: A,
69    limits: DecodeLimits,
70) -> Result<T, CandidCallError>
71where
72    T: for<'de> ArgumentDecoder<'de>,
73    A: ArgumentEncoder,
74{
75    let context = CallContext {
76        operation: "query_call",
77        canister_id,
78        caller,
79        method,
80    };
81    call_candid(
82        context,
83        args,
84        |bytes| pic.query_call(canister_id, caller, method, bytes),
85        |bytes| decode_args(bytes, limits),
86    )
87}
88
89fn call_candid<T, A, F, D>(
90    context: CallContext<'_>,
91    args: A,
92    call: F,
93    decode: D,
94) -> Result<T, CandidCallError>
95where
96    A: ArgumentEncoder,
97    F: FnOnce(Vec<u8>) -> Result<Vec<u8>, RejectResponse>,
98    D: FnOnce(&[u8]) -> Result<T, DecodeError>,
99{
100    let bytes = encode_args(args).map_err(|err| CandidCallError::Encode {
101        context: Box::new(context.to_error_context()),
102        message: err.to_string(),
103    })?;
104    let result = run_raw_call(context, || call(bytes))?;
105    decode(&result).map_err(|source| CandidCallError::Decode {
106        context: Box::new(context.to_error_context()),
107        bytes: result.len(),
108        source: Box::new(source),
109    })
110}
111
112fn run_raw_call<F>(context: CallContext<'_>, call: F) -> Result<Vec<u8>, CandidCallError>
113where
114    F: FnOnce() -> Result<Vec<u8>, RejectResponse>,
115{
116    match catch_unwind(AssertUnwindSafe(call)) {
117        Ok(Ok(bytes)) => Ok(bytes),
118        Ok(Err(response)) => Err(CandidCallError::CanisterReject {
119            context: Box::new(context.to_error_context()),
120            response: Box::new(response),
121        }),
122        Err(payload) => {
123            let source =
124                PocketIcOperationError::new(transport::panic_payload_to_string(payload.as_ref()));
125            if source.is_transport() {
126                Err(CandidCallError::Transport {
127                    context: Box::new(context.to_error_context()),
128                    source,
129                })
130            } else {
131                resume_unwind(payload)
132            }
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use candid::Principal;
140    use ic_testkit_candid::{DecodeError, DecodeLimits, decode_args};
141
142    use super::{CallContext, CandidCallError, call_candid, run_raw_call};
143
144    const LIMITS: DecodeLimits = DecodeLimits {
145        reply_bytes: 65_536,
146        decoding_quota: 1_000_000,
147        skipping_quota: 100_000,
148        type_entries: 1024,
149        header_bytes: 8192,
150    };
151
152    #[test]
153    fn decode_error_includes_call_context() {
154        let context = CallContext {
155            operation: "query_call",
156            canister_id: Principal::anonymous(),
157            caller: Principal::management_canister(),
158            method: "get",
159        };
160
161        let err = call_candid(
162            context,
163            (),
164            |_| Ok(vec![0xde, 0xad]),
165            |bytes| decode_args::<(u64,)>(bytes, LIMITS),
166        )
167        .expect_err("decode fails");
168
169        assert!(err.to_string().contains("candid decode failed"));
170        assert!(err.to_string().contains("operation=query_call"));
171        assert!(err.to_string().contains("method=get"));
172        assert!(err.to_string().contains("bytes=2"));
173        assert!(std::error::Error::source(&err).unwrap().is::<DecodeError>());
174        let CandidCallError::Decode {
175            context,
176            bytes,
177            source,
178        } = err
179        else {
180            panic!("expected decode failure");
181        };
182        assert_eq!(bytes, 2);
183        assert_eq!(context.method, "get");
184        assert!(matches!(*source, DecodeError::Candid(_)));
185    }
186
187    #[test]
188    fn caller_decode_limits_retain_typed_refusals() {
189        let context = CallContext {
190            operation: "query_call",
191            canister_id: Principal::anonymous(),
192            caller: Principal::anonymous(),
193            method: "get",
194        };
195        let bytes = candid::encode_one(42_u64).unwrap();
196        let length = bytes.len();
197        let error = call_candid(
198            context,
199            (),
200            |_| Ok(bytes),
201            |bytes| {
202                decode_args::<(u64,)>(
203                    bytes,
204                    DecodeLimits {
205                        reply_bytes: 0,
206                        ..LIMITS
207                    },
208                )
209            },
210        )
211        .unwrap_err();
212        let cause = std::error::Error::source(&error)
213            .unwrap()
214            .downcast_ref::<DecodeError>()
215            .unwrap();
216        assert!(
217            matches!(cause, DecodeError::ReplyTooLarge { actual, maximum: 0 } if *actual == length)
218        );
219
220        // Tiny wire data can still describe expensive values: work is bounded
221        // independently of the reply length, including the arguments API.
222        let bytes = candid::encode_args((vec![(); 100_000],)).unwrap();
223        let error = call_candid(
224            context,
225            (),
226            |_| Ok(bytes),
227            |bytes| {
228                decode_args::<(Vec<()>,)>(
229                    bytes,
230                    DecodeLimits {
231                        decoding_quota: 0,
232                        ..LIMITS
233                    },
234                )
235            },
236        )
237        .unwrap_err();
238        let cause = std::error::Error::source(&error)
239            .unwrap()
240            .downcast_ref::<DecodeError>()
241            .unwrap();
242        assert!(matches!(cause, DecodeError::Candid(_)));
243        assert!(std::error::Error::source(cause).is_some());
244    }
245
246    #[test]
247    fn reply_arguments_preserve_top_level_arity_and_wide_values() {
248        let context = CallContext {
249            operation: "update_call",
250            canister_id: Principal::anonymous(),
251            caller: Principal::management_canister(),
252            method: "measure",
253        };
254        let values = (u128::MAX, u128::from(u64::MAX) + 1);
255        let bytes = candid::encode_args(values).unwrap();
256        assert_eq!(candid::decode_args::<(u128, u128)>(&bytes).unwrap(), values);
257        assert!(candid::decode_one::<(u128, u128)>(&bytes).is_err());
258        let tuple = candid::encode_one(values).unwrap();
259        assert!(candid::decode_args::<(u128, u128)>(&tuple).is_err());
260        assert_eq!(candid::decode_one::<(u128, u128)>(&tuple).unwrap(), values);
261        let no_replies = candid::encode_args(()).unwrap();
262        candid::decode_args::<()>(&no_replies).unwrap();
263        let one_reply = candid::encode_args((7_u64,)).unwrap();
264        assert_eq!(candid::decode_args::<(u64,)>(&one_reply).unwrap(), (7,));
265        let result: (u64, bool, String) = call_candid(
266            context,
267            (42_u64, false),
268            |args| {
269                assert_eq!(
270                    candid::decode_args::<(u64, bool)>(&args).unwrap(),
271                    (42, false)
272                );
273                Ok(candid::encode_args((9_u64, true, "ready")).unwrap())
274            },
275            |bytes| decode_args(bytes, LIMITS),
276        )
277        .unwrap();
278        assert_eq!(result, (9, true, "ready".into()));
279    }
280
281    #[test]
282    fn invalid_reply_arguments_keep_decode_context() {
283        let context = CallContext {
284            operation: "query_call",
285            canister_id: Principal::anonymous(),
286            caller: Principal::management_canister(),
287            method: "measure",
288        };
289        for bytes in [vec![0xde, 0xad], candid::encode_args((7_u64,)).unwrap()] {
290            let length = bytes.len();
291            let err = call_candid(
292                context,
293                (),
294                |_| Ok(bytes),
295                |bytes| decode_args::<(u64, bool)>(bytes, LIMITS),
296            )
297            .unwrap_err();
298            assert!(err.to_string().contains(&format!("bytes={length}")));
299            let CandidCallError::Decode {
300                context: observed,
301                bytes,
302                ..
303            } = err
304            else {
305                panic!("expected decode failure");
306            };
307            assert_eq!(bytes, length);
308            assert_eq!(observed.operation, "query_call");
309            assert_eq!(observed.canister_id, context.canister_id);
310            assert_eq!(observed.caller, context.caller);
311            assert_eq!(observed.method, "measure");
312        }
313    }
314
315    #[test]
316    fn dead_instance_panic_is_classified_as_transport() {
317        let context = CallContext {
318            operation: "query_call",
319            canister_id: Principal::anonymous(),
320            caller: Principal::management_canister(),
321            method: "get",
322        };
323        let error = run_raw_call(context, || -> Result<Vec<u8>, pocket_ic::RejectResponse> {
324            panic!("HTTP failure: reqwest::Error {{ kind: Request, url: \"http://127.0.0.1:1234/instances/0/update/execute_ingress_message\", source: ConnectError(\"tcp connect error\", Os {{ code: 111, kind: ConnectionRefused, message: \"Connection refused\" }}) }}");
325        })
326        .unwrap_err();
327
328        let source = std::error::Error::source(&error)
329            .unwrap()
330            .downcast_ref::<super::PocketIcOperationError>()
331            .unwrap();
332        assert!(source.is_transport());
333        assert!(source.message().starts_with("HTTP failure: "));
334        assert!(error.to_string().contains("operation=query_call"));
335        assert!(super::super::is_dead_pocket_ic_transport_error(&error));
336        let CandidCallError::Transport { source: owned, .. } = error else {
337            panic!("expected transport failure");
338        };
339        assert!(owned.is_transport());
340    }
341
342    #[test]
343    fn unrelated_panics_resume_with_the_original_payload() {
344        for message in [
345            "application worker channel closed",
346            "fixture expected ConnectionRefused but observed another result",
347        ] {
348            let context = CallContext {
349                operation: "query_call",
350                canister_id: Principal::anonymous(),
351                caller: Principal::management_canister(),
352                method: "get",
353            };
354            let payload = message.to_owned();
355            let original_allocation = payload.as_ptr() as usize;
356            let result = std::panic::catch_unwind(|| {
357                run_raw_call(context, || -> Result<Vec<u8>, pocket_ic::RejectResponse> {
358                    std::panic::panic_any(payload);
359                })
360            });
361            let resumed = result
362                .expect_err("application panic must resume")
363                .downcast::<String>()
364                .expect("original panic type must be preserved");
365            assert_eq!(resumed.as_str(), message);
366            assert_eq!(resumed.as_ptr() as usize, original_allocation);
367        }
368        let payload = Box::new(7_u8);
369        let original = std::ptr::from_ref(payload.as_ref());
370        let result = std::panic::catch_unwind(|| {
371            run_raw_call(
372                CallContext {
373                    operation: "query_call",
374                    canister_id: Principal::anonymous(),
375                    caller: Principal::anonymous(),
376                    method: "get",
377                },
378                || -> Result<Vec<u8>, pocket_ic::RejectResponse> {
379                    std::panic::panic_any(payload);
380                },
381            )
382        });
383        let resumed = result.unwrap_err().downcast::<Box<u8>>().unwrap();
384        assert_eq!(std::ptr::from_ref(resumed.as_ref().as_ref()), original);
385    }
386}