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