1use candid::{
2 CandidType, Principal, encode_args,
3 utils::{ArgumentDecoder, ArgumentEncoder},
4};
5use ic_testkit_candid::{DecodeError, DecodeLimits, decode_args, decode_one};
6use pocket_ic::{PocketIc, RejectResponse};
7use serde::de::DeserializeOwned;
8use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
9
10use super::{CandidCallContext, CandidCallError, PocketIcOperationError, transport};
11
12#[derive(Clone, Copy)]
13struct CallContext<'a> {
14 operation: &'static str,
15 canister_id: Principal,
16 caller: Principal,
17 method: &'a str,
18}
19
20impl CallContext<'_> {
21 fn to_error_context(self) -> CandidCallContext {
22 CandidCallContext::new(self.operation, self.canister_id, self.caller, self.method)
23 }
24}
25
26pub trait CandidCallExt {
31 fn update_candid<T, A>(
33 &self,
34 canister_id: Principal,
35 method: &str,
36 args: A,
37 limits: DecodeLimits,
38 ) -> Result<T, CandidCallError>
39 where
40 T: CandidType + DeserializeOwned,
41 A: ArgumentEncoder,
42 {
43 self.update_candid_as(canister_id, Principal::anonymous(), method, args, limits)
44 }
45
46 fn update_candid_as<T, A>(
48 &self,
49 canister_id: Principal,
50 caller: Principal,
51 method: &str,
52 args: A,
53 limits: DecodeLimits,
54 ) -> Result<T, CandidCallError>
55 where
56 T: CandidType + DeserializeOwned,
57 A: ArgumentEncoder;
58
59 fn query_candid<T, A>(
61 &self,
62 canister_id: Principal,
63 method: &str,
64 args: A,
65 limits: DecodeLimits,
66 ) -> Result<T, CandidCallError>
67 where
68 T: CandidType + DeserializeOwned,
69 A: ArgumentEncoder,
70 {
71 self.query_candid_as(canister_id, Principal::anonymous(), method, args, limits)
72 }
73
74 fn query_candid_as<T, A>(
76 &self,
77 canister_id: Principal,
78 caller: Principal,
79 method: &str,
80 args: A,
81 limits: DecodeLimits,
82 ) -> Result<T, CandidCallError>
83 where
84 T: CandidType + DeserializeOwned,
85 A: ArgumentEncoder;
86}
87
88impl CandidCallExt for PocketIc {
89 fn update_candid_as<T, A>(
91 &self,
92 canister_id: Principal,
93 caller: Principal,
94 method: &str,
95 args: A,
96 limits: DecodeLimits,
97 ) -> Result<T, CandidCallError>
98 where
99 T: CandidType + DeserializeOwned,
100 A: ArgumentEncoder,
101 {
102 let context = CallContext {
103 operation: "update_call",
104 canister_id,
105 caller,
106 method,
107 };
108 call_candid(
109 context,
110 args,
111 |bytes| Self::update_call(self, canister_id, caller, method, bytes),
112 |bytes| decode_one(bytes, limits),
113 )
114 }
115
116 fn query_candid_as<T, A>(
118 &self,
119 canister_id: Principal,
120 caller: Principal,
121 method: &str,
122 args: A,
123 limits: DecodeLimits,
124 ) -> Result<T, CandidCallError>
125 where
126 T: CandidType + DeserializeOwned,
127 A: ArgumentEncoder,
128 {
129 let context = CallContext {
130 operation: "query_call",
131 canister_id,
132 caller,
133 method,
134 };
135 call_candid(
136 context,
137 args,
138 |bytes| Self::query_call(self, canister_id, caller, method, bytes),
139 |bytes| decode_one(bytes, limits),
140 )
141 }
142}
143
144pub fn update_candid_args<T, A>(
151 pic: &PocketIc,
152 canister_id: Principal,
153 caller: Principal,
154 method: &str,
155 args: A,
156 limits: DecodeLimits,
157) -> Result<T, CandidCallError>
158where
159 T: for<'de> ArgumentDecoder<'de>,
160 A: ArgumentEncoder,
161{
162 let context = CallContext {
163 operation: "update_call",
164 canister_id,
165 caller,
166 method,
167 };
168 call_candid(
169 context,
170 args,
171 |bytes| pic.update_call(canister_id, caller, method, bytes),
172 |bytes| decode_args(bytes, limits),
173 )
174}
175
176pub fn query_candid_args<T, A>(
183 pic: &PocketIc,
184 canister_id: Principal,
185 caller: Principal,
186 method: &str,
187 args: A,
188 limits: DecodeLimits,
189) -> Result<T, CandidCallError>
190where
191 T: for<'de> ArgumentDecoder<'de>,
192 A: ArgumentEncoder,
193{
194 let context = CallContext {
195 operation: "query_call",
196 canister_id,
197 caller,
198 method,
199 };
200 call_candid(
201 context,
202 args,
203 |bytes| pic.query_call(canister_id, caller, method, bytes),
204 |bytes| decode_args(bytes, limits),
205 )
206}
207
208fn call_candid<T, A, F, D>(
209 context: CallContext<'_>,
210 args: A,
211 call: F,
212 decode: D,
213) -> Result<T, CandidCallError>
214where
215 A: ArgumentEncoder,
216 F: FnOnce(Vec<u8>) -> Result<Vec<u8>, RejectResponse>,
217 D: FnOnce(&[u8]) -> Result<T, DecodeError>,
218{
219 let bytes = encode_args(args).map_err(|err| CandidCallError::Encode {
220 context: Box::new(context.to_error_context()),
221 message: err.to_string(),
222 })?;
223 let result = run_raw_call(context, || call(bytes))?;
224 decode(&result).map_err(|source| CandidCallError::Decode {
225 context: Box::new(context.to_error_context()),
226 bytes: result.len(),
227 source: Box::new(source),
228 })
229}
230
231fn run_raw_call<F>(context: CallContext<'_>, call: F) -> Result<Vec<u8>, CandidCallError>
232where
233 F: FnOnce() -> Result<Vec<u8>, RejectResponse>,
234{
235 match catch_unwind(AssertUnwindSafe(call)) {
236 Ok(Ok(bytes)) => Ok(bytes),
237 Ok(Err(response)) => Err(CandidCallError::CanisterReject {
238 context: Box::new(context.to_error_context()),
239 response: Box::new(response),
240 }),
241 Err(payload) => {
242 let source =
243 PocketIcOperationError::new(transport::panic_payload_to_string(payload.as_ref()));
244 if source.is_transport() {
245 Err(CandidCallError::Transport {
246 context: Box::new(context.to_error_context()),
247 source,
248 })
249 } else {
250 resume_unwind(payload)
251 }
252 }
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use candid::Principal;
259 use ic_testkit_candid::{DecodeError, DecodeLimits, decode_args, decode_one};
260
261 use super::{CallContext, CandidCallError, call_candid, run_raw_call};
262
263 const LIMITS: DecodeLimits = DecodeLimits {
264 reply_bytes: 65_536,
265 decoding_quota: 1_000_000,
266 skipping_quota: 100_000,
267 type_entries: 1024,
268 header_bytes: 8192,
269 };
270
271 #[test]
272 fn decode_error_includes_call_context() {
273 let context = CallContext {
274 operation: "query_call",
275 canister_id: Principal::anonymous(),
276 caller: Principal::management_canister(),
277 method: "get",
278 };
279
280 let err = call_candid(
281 context,
282 (),
283 |_| Ok(vec![0xde, 0xad]),
284 |bytes| decode_one::<u64>(bytes, LIMITS),
285 )
286 .expect_err("decode fails");
287
288 assert!(err.to_string().contains("candid decode failed"));
289 assert!(err.to_string().contains("operation=query_call"));
290 assert!(err.to_string().contains("method=get"));
291 assert!(err.to_string().contains("bytes=2"));
292 assert!(std::error::Error::source(&err).unwrap().is::<DecodeError>());
293 let CandidCallError::Decode {
294 context,
295 bytes,
296 source,
297 } = err
298 else {
299 panic!("expected decode failure");
300 };
301 assert_eq!(bytes, 2);
302 assert_eq!(context.method, "get");
303 assert!(matches!(*source, DecodeError::Candid(_)));
304 }
305
306 #[test]
307 fn caller_decode_limits_retain_typed_refusals() {
308 let context = CallContext {
309 operation: "query_call",
310 canister_id: Principal::anonymous(),
311 caller: Principal::anonymous(),
312 method: "get",
313 };
314 let bytes = candid::encode_one(42_u64).unwrap();
315 let length = bytes.len();
316 let error = call_candid(
317 context,
318 (),
319 |_| Ok(bytes),
320 |bytes| {
321 decode_one::<u64>(
322 bytes,
323 DecodeLimits {
324 reply_bytes: 0,
325 ..LIMITS
326 },
327 )
328 },
329 )
330 .unwrap_err();
331 let cause = std::error::Error::source(&error)
332 .unwrap()
333 .downcast_ref::<DecodeError>()
334 .unwrap();
335 assert!(
336 matches!(cause, DecodeError::ReplyTooLarge { actual, maximum: 0 } if *actual == length)
337 );
338
339 let bytes = candid::encode_args((vec![(); 100_000],)).unwrap();
342 let error = call_candid(
343 context,
344 (),
345 |_| Ok(bytes),
346 |bytes| {
347 decode_args::<(Vec<()>,)>(
348 bytes,
349 DecodeLimits {
350 decoding_quota: 0,
351 ..LIMITS
352 },
353 )
354 },
355 )
356 .unwrap_err();
357 let cause = std::error::Error::source(&error)
358 .unwrap()
359 .downcast_ref::<DecodeError>()
360 .unwrap();
361 assert!(matches!(cause, DecodeError::Candid(_)));
362 assert!(std::error::Error::source(cause).is_some());
363 }
364
365 #[test]
366 fn reply_arguments_preserve_top_level_arity_and_wide_values() {
367 let context = CallContext {
368 operation: "update_call",
369 canister_id: Principal::anonymous(),
370 caller: Principal::management_canister(),
371 method: "measure",
372 };
373 let values = (u128::MAX, u128::from(u64::MAX) + 1);
374 let bytes = candid::encode_args(values).unwrap();
375 assert_eq!(candid::decode_args::<(u128, u128)>(&bytes).unwrap(), values);
376 assert!(candid::decode_one::<(u128, u128)>(&bytes).is_err());
377 let tuple = candid::encode_one(values).unwrap();
378 assert!(candid::decode_args::<(u128, u128)>(&tuple).is_err());
379 assert_eq!(candid::decode_one::<(u128, u128)>(&tuple).unwrap(), values);
380 let no_replies = candid::encode_args(()).unwrap();
381 candid::decode_args::<()>(&no_replies).unwrap();
382 let one_reply = candid::encode_args((7_u64,)).unwrap();
383 assert_eq!(candid::decode_args::<(u64,)>(&one_reply).unwrap(), (7,));
384 let result: (u64, bool, String) = call_candid(
385 context,
386 (42_u64, false),
387 |args| {
388 assert_eq!(
389 candid::decode_args::<(u64, bool)>(&args).unwrap(),
390 (42, false)
391 );
392 Ok(candid::encode_args((9_u64, true, "ready")).unwrap())
393 },
394 |bytes| decode_args(bytes, LIMITS),
395 )
396 .unwrap();
397 assert_eq!(result, (9, true, "ready".into()));
398 }
399
400 #[test]
401 fn invalid_reply_arguments_keep_decode_context() {
402 let context = CallContext {
403 operation: "query_call",
404 canister_id: Principal::anonymous(),
405 caller: Principal::management_canister(),
406 method: "measure",
407 };
408 for bytes in [vec![0xde, 0xad], candid::encode_args((7_u64,)).unwrap()] {
409 let length = bytes.len();
410 let err = call_candid(
411 context,
412 (),
413 |_| Ok(bytes),
414 |bytes| decode_args::<(u64, bool)>(bytes, LIMITS),
415 )
416 .unwrap_err();
417 assert!(err.to_string().contains(&format!("bytes={length}")));
418 let CandidCallError::Decode {
419 context: observed,
420 bytes,
421 ..
422 } = err
423 else {
424 panic!("expected decode failure");
425 };
426 assert_eq!(bytes, length);
427 assert_eq!(observed.operation, "query_call");
428 assert_eq!(observed.canister_id, context.canister_id);
429 assert_eq!(observed.caller, context.caller);
430 assert_eq!(observed.method, "measure");
431 }
432 }
433
434 #[test]
435 fn dead_instance_panic_is_classified_as_transport() {
436 let context = CallContext {
437 operation: "query_call",
438 canister_id: Principal::anonymous(),
439 caller: Principal::management_canister(),
440 method: "get",
441 };
442 let error = run_raw_call(context, || -> Result<Vec<u8>, pocket_ic::RejectResponse> {
443 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\" }}) }}");
444 })
445 .unwrap_err();
446
447 let source = std::error::Error::source(&error)
448 .unwrap()
449 .downcast_ref::<super::PocketIcOperationError>()
450 .unwrap();
451 assert!(source.is_transport());
452 assert!(source.message().starts_with("HTTP failure: "));
453 assert!(error.to_string().contains("operation=query_call"));
454 assert!(super::super::is_dead_pocket_ic_transport_error(&error));
455 let CandidCallError::Transport { source: owned, .. } = error else {
456 panic!("expected transport failure");
457 };
458 assert!(owned.is_transport());
459 }
460
461 #[test]
462 fn unrelated_panics_resume_with_the_original_payload() {
463 for message in [
464 "application worker channel closed",
465 "fixture expected ConnectionRefused but observed another result",
466 ] {
467 let context = CallContext {
468 operation: "query_call",
469 canister_id: Principal::anonymous(),
470 caller: Principal::management_canister(),
471 method: "get",
472 };
473 let payload = message.to_owned();
474 let original_allocation = payload.as_ptr() as usize;
475 let result = std::panic::catch_unwind(|| {
476 run_raw_call(context, || -> Result<Vec<u8>, pocket_ic::RejectResponse> {
477 std::panic::panic_any(payload);
478 })
479 });
480 let resumed = result
481 .expect_err("application panic must resume")
482 .downcast::<String>()
483 .expect("original panic type must be preserved");
484 assert_eq!(resumed.as_str(), message);
485 assert_eq!(resumed.as_ptr() as usize, original_allocation);
486 }
487 let payload = Box::new(7_u8);
488 let original = std::ptr::from_ref(payload.as_ref());
489 let result = std::panic::catch_unwind(|| {
490 run_raw_call(
491 CallContext {
492 operation: "query_call",
493 canister_id: Principal::anonymous(),
494 caller: Principal::anonymous(),
495 method: "get",
496 },
497 || -> Result<Vec<u8>, pocket_ic::RejectResponse> {
498 std::panic::panic_any(payload);
499 },
500 )
501 });
502 let resumed = result.unwrap_err().downcast::<Box<u8>>().unwrap();
503 assert_eq!(std::ptr::from_ref(resumed.as_ref().as_ref()), original);
504 }
505}