1use candid::{
2 CandidType, Principal, decode_args, decode_one, encode_args,
3 utils::{ArgumentDecoder, ArgumentEncoder},
4};
5use pocket_ic::{PocketIc, RejectResponse};
6use serde::de::DeserializeOwned;
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
25pub trait CandidCallExt {
27 fn update_candid<T, A>(
29 &self,
30 canister_id: Principal,
31 method: &str,
32 args: A,
33 ) -> Result<T, CandidCallError>
34 where
35 T: CandidType + DeserializeOwned,
36 A: ArgumentEncoder,
37 {
38 self.update_candid_as(canister_id, Principal::anonymous(), method, args)
39 }
40
41 fn update_candid_as<T, A>(
43 &self,
44 canister_id: Principal,
45 caller: Principal,
46 method: &str,
47 args: A,
48 ) -> Result<T, CandidCallError>
49 where
50 T: CandidType + DeserializeOwned,
51 A: ArgumentEncoder;
52
53 fn query_candid<T, A>(
55 &self,
56 canister_id: Principal,
57 method: &str,
58 args: A,
59 ) -> Result<T, CandidCallError>
60 where
61 T: CandidType + DeserializeOwned,
62 A: ArgumentEncoder,
63 {
64 self.query_candid_as(canister_id, Principal::anonymous(), method, args)
65 }
66
67 fn query_candid_as<T, A>(
69 &self,
70 canister_id: Principal,
71 caller: Principal,
72 method: &str,
73 args: A,
74 ) -> Result<T, CandidCallError>
75 where
76 T: CandidType + DeserializeOwned,
77 A: ArgumentEncoder;
78}
79
80impl CandidCallExt for PocketIc {
81 fn update_candid_as<T, A>(
83 &self,
84 canister_id: Principal,
85 caller: Principal,
86 method: &str,
87 args: A,
88 ) -> Result<T, CandidCallError>
89 where
90 T: CandidType + DeserializeOwned,
91 A: ArgumentEncoder,
92 {
93 let context = CallContext {
94 operation: "update_call",
95 canister_id,
96 caller,
97 method,
98 };
99 call_candid(
100 context,
101 args,
102 |bytes| Self::update_call(self, canister_id, caller, method, bytes),
103 |bytes| decode_one(bytes),
104 )
105 }
106
107 fn query_candid_as<T, A>(
109 &self,
110 canister_id: Principal,
111 caller: Principal,
112 method: &str,
113 args: A,
114 ) -> Result<T, CandidCallError>
115 where
116 T: CandidType + DeserializeOwned,
117 A: ArgumentEncoder,
118 {
119 let context = CallContext {
120 operation: "query_call",
121 canister_id,
122 caller,
123 method,
124 };
125 call_candid(
126 context,
127 args,
128 |bytes| Self::query_call(self, canister_id, caller, method, bytes),
129 |bytes| decode_one(bytes),
130 )
131 }
132}
133
134pub fn update_candid_args<T, A>(
141 pic: &PocketIc,
142 canister_id: Principal,
143 caller: Principal,
144 method: &str,
145 args: A,
146) -> Result<T, CandidCallError>
147where
148 T: for<'de> ArgumentDecoder<'de>,
149 A: ArgumentEncoder,
150{
151 let context = CallContext {
152 operation: "update_call",
153 canister_id,
154 caller,
155 method,
156 };
157 call_candid(
158 context,
159 args,
160 |bytes| pic.update_call(canister_id, caller, method, bytes),
161 |bytes| decode_args(bytes),
162 )
163}
164
165pub fn query_candid_args<T, A>(
172 pic: &PocketIc,
173 canister_id: Principal,
174 caller: Principal,
175 method: &str,
176 args: A,
177) -> Result<T, CandidCallError>
178where
179 T: for<'de> ArgumentDecoder<'de>,
180 A: ArgumentEncoder,
181{
182 let context = CallContext {
183 operation: "query_call",
184 canister_id,
185 caller,
186 method,
187 };
188 call_candid(
189 context,
190 args,
191 |bytes| pic.query_call(canister_id, caller, method, bytes),
192 |bytes| decode_args(bytes),
193 )
194}
195
196fn call_candid<T, A, F, D>(
197 context: CallContext<'_>,
198 args: A,
199 call: F,
200 decode: D,
201) -> Result<T, CandidCallError>
202where
203 A: ArgumentEncoder,
204 F: FnOnce(Vec<u8>) -> Result<Vec<u8>, RejectResponse>,
205 D: FnOnce(&[u8]) -> Result<T, candid::Error>,
206{
207 let bytes = encode_args(args).map_err(|err| CandidCallError::Encode {
208 context: Box::new(context.to_error_context()),
209 message: err.to_string(),
210 })?;
211 let result = run_raw_call(context, || call(bytes))?;
212 decode(&result).map_err(|err| CandidCallError::Decode {
213 context: Box::new(context.to_error_context()),
214 bytes: result.len(),
215 message: err.to_string(),
216 })
217}
218
219fn run_raw_call<F>(context: CallContext<'_>, call: F) -> Result<Vec<u8>, CandidCallError>
220where
221 F: FnOnce() -> Result<Vec<u8>, RejectResponse>,
222{
223 match catch_unwind(AssertUnwindSafe(call)) {
224 Ok(Ok(bytes)) => Ok(bytes),
225 Ok(Err(response)) => Err(CandidCallError::CanisterReject {
226 context: Box::new(context.to_error_context()),
227 response: Box::new(response),
228 }),
229 Err(payload) => {
230 let source =
231 PocketIcOperationError::new(transport::panic_payload_to_string(payload.as_ref()));
232 if source.is_transport() {
233 Err(CandidCallError::Transport {
234 context: Box::new(context.to_error_context()),
235 source,
236 })
237 } else {
238 resume_unwind(payload)
239 }
240 }
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use candid::Principal;
247
248 use super::{CallContext, CandidCallError, call_candid, run_raw_call};
249
250 #[test]
251 fn decode_error_includes_call_context() {
252 let context = CallContext {
253 operation: "query_call",
254 canister_id: Principal::anonymous(),
255 caller: Principal::management_canister(),
256 method: "get",
257 };
258
259 let err = call_candid(
260 context,
261 (),
262 |_| Ok(vec![0xde, 0xad]),
263 |bytes| candid::decode_one::<u64>(bytes),
264 )
265 .expect_err("decode fails");
266
267 assert!(err.to_string().contains("candid decode failed"));
268 assert!(err.to_string().contains("operation=query_call"));
269 assert!(err.to_string().contains("method=get"));
270 assert!(err.to_string().contains("bytes=2"));
271 let CandidCallError::Decode { context, bytes, .. } = err else {
272 panic!("expected decode failure");
273 };
274 assert_eq!(bytes, 2);
275 assert_eq!(context.method, "get");
276 }
277
278 #[test]
279 fn reply_arguments_preserve_top_level_arity_and_wide_values() {
280 let context = CallContext {
281 operation: "update_call",
282 canister_id: Principal::anonymous(),
283 caller: Principal::management_canister(),
284 method: "measure",
285 };
286 let values = (u128::MAX, u128::from(u64::MAX) + 1);
287 let bytes = candid::encode_args(values).unwrap();
288 assert_eq!(candid::decode_args::<(u128, u128)>(&bytes).unwrap(), values);
289 assert!(candid::decode_one::<(u128, u128)>(&bytes).is_err());
290 let tuple = candid::encode_one(values).unwrap();
291 assert!(candid::decode_args::<(u128, u128)>(&tuple).is_err());
292 assert_eq!(candid::decode_one::<(u128, u128)>(&tuple).unwrap(), values);
293 let no_replies = candid::encode_args(()).unwrap();
294 candid::decode_args::<()>(&no_replies).unwrap();
295 let one_reply = candid::encode_args((7_u64,)).unwrap();
296 assert_eq!(candid::decode_args::<(u64,)>(&one_reply).unwrap(), (7,));
297 let result: (u64, bool, String) = call_candid(
298 context,
299 (42_u64, false),
300 |args| {
301 assert_eq!(
302 candid::decode_args::<(u64, bool)>(&args).unwrap(),
303 (42, false)
304 );
305 Ok(candid::encode_args((9_u64, true, "ready")).unwrap())
306 },
307 |bytes| candid::decode_args(bytes),
308 )
309 .unwrap();
310 assert_eq!(result, (9, true, "ready".into()));
311 }
312
313 #[test]
314 fn invalid_reply_arguments_keep_decode_context() {
315 let context = CallContext {
316 operation: "query_call",
317 canister_id: Principal::anonymous(),
318 caller: Principal::management_canister(),
319 method: "measure",
320 };
321 for bytes in [vec![0xde, 0xad], candid::encode_args((7_u64,)).unwrap()] {
322 let length = bytes.len();
323 let err = call_candid(
324 context,
325 (),
326 |_| Ok(bytes),
327 |bytes| candid::decode_args::<(u64, bool)>(bytes),
328 )
329 .unwrap_err();
330 assert!(err.to_string().contains(&format!("bytes={length}")));
331 let CandidCallError::Decode {
332 context: observed,
333 bytes,
334 ..
335 } = err
336 else {
337 panic!("expected decode failure");
338 };
339 assert_eq!(bytes, length);
340 assert_eq!(observed.operation, "query_call");
341 assert_eq!(observed.canister_id, context.canister_id);
342 assert_eq!(observed.caller, context.caller);
343 assert_eq!(observed.method, "measure");
344 }
345 }
346
347 #[test]
348 fn dead_instance_panic_is_classified_as_transport() {
349 let context = CallContext {
350 operation: "query_call",
351 canister_id: Principal::anonymous(),
352 caller: Principal::management_canister(),
353 method: "get",
354 };
355 let error = run_raw_call(context, || -> Result<Vec<u8>, pocket_ic::RejectResponse> {
356 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\" }}) }}");
357 })
358 .unwrap_err();
359
360 let source = std::error::Error::source(&error)
361 .unwrap()
362 .downcast_ref::<super::PocketIcOperationError>()
363 .unwrap();
364 assert!(source.is_transport());
365 assert!(source.message().starts_with("HTTP failure: "));
366 assert!(error.to_string().contains("operation=query_call"));
367 assert!(super::super::is_dead_pocket_ic_transport_error(&error));
368 let CandidCallError::Transport { source: owned, .. } = error else {
369 panic!("expected transport failure");
370 };
371 assert!(owned.is_transport());
372 }
373
374 #[test]
375 fn unrelated_panics_resume_with_the_original_payload() {
376 for message in [
377 "application worker channel closed",
378 "fixture expected ConnectionRefused but observed another result",
379 ] {
380 let context = CallContext {
381 operation: "query_call",
382 canister_id: Principal::anonymous(),
383 caller: Principal::management_canister(),
384 method: "get",
385 };
386 let payload = message.to_owned();
387 let original_allocation = payload.as_ptr() as usize;
388 let result = std::panic::catch_unwind(|| {
389 run_raw_call(context, || -> Result<Vec<u8>, pocket_ic::RejectResponse> {
390 std::panic::panic_any(payload);
391 })
392 });
393 let resumed = result
394 .expect_err("application panic must resume")
395 .downcast::<String>()
396 .expect("original panic type must be preserved");
397 assert_eq!(resumed.as_str(), message);
398 assert_eq!(resumed.as_ptr() as usize, original_allocation);
399 }
400 let payload = Box::new(7_u8);
401 let original = std::ptr::from_ref(payload.as_ref());
402 let result = std::panic::catch_unwind(|| {
403 run_raw_call(
404 CallContext {
405 operation: "query_call",
406 canister_id: Principal::anonymous(),
407 caller: Principal::anonymous(),
408 method: "get",
409 },
410 || -> Result<Vec<u8>, pocket_ic::RejectResponse> {
411 std::panic::panic_any(payload);
412 },
413 )
414 });
415 let resumed = result.unwrap_err().downcast::<Box<u8>>().unwrap();
416 assert_eq!(std::ptr::from_ref(resumed.as_ref().as_ref()), original);
417 }
418}