1use ic_platform::candid::{DecodeError, DecodeLimits, decode_args};
2use ic_platform::{
3 Principal,
4 candid::contract::{ArgumentDecoder, ArgumentEncoder, encode_args},
5};
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
25pub 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
57pub 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 ic_platform::Principal;
140 use ic_platform::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 = ic_platform::candid::contract::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 let bytes = ic_platform::candid::contract::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 = ic_platform::candid::contract::encode_args(values).unwrap();
256 assert_eq!(
257 ic_platform::candid::decode_args::<(u128, u128)>(&bytes, LIMITS).unwrap(),
258 values
259 );
260 assert!(ic_platform::candid::decode_one::<(u128, u128)>(&bytes, LIMITS).is_err());
261 let tuple = ic_platform::candid::contract::encode_one(values).unwrap();
262 assert!(ic_platform::candid::decode_args::<(u128, u128)>(&tuple, LIMITS).is_err());
263 assert_eq!(
264 ic_platform::candid::decode_one::<(u128, u128)>(&tuple, LIMITS).unwrap(),
265 values
266 );
267 let no_replies = ic_platform::candid::contract::encode_args(()).unwrap();
268 ic_platform::candid::decode_args::<()>(&no_replies, LIMITS).unwrap();
269 let one_reply = ic_platform::candid::contract::encode_args((7_u64,)).unwrap();
270 assert_eq!(
271 ic_platform::candid::decode_args::<(u64,)>(&one_reply, LIMITS).unwrap(),
272 (7,)
273 );
274 let result: (u64, bool, String) = call_candid(
275 context,
276 (42_u64, false),
277 |args| {
278 assert_eq!(
279 ic_platform::candid::decode_args::<(u64, bool)>(&args, LIMITS).unwrap(),
280 (42, false)
281 );
282 Ok(ic_platform::candid::contract::encode_args((9_u64, true, "ready")).unwrap())
283 },
284 |bytes| decode_args(bytes, LIMITS),
285 )
286 .unwrap();
287 assert_eq!(result, (9, true, "ready".into()));
288 }
289
290 #[test]
291 fn invalid_reply_arguments_keep_decode_context() {
292 let context = CallContext {
293 operation: "query_call",
294 canister_id: Principal::anonymous(),
295 caller: Principal::management_canister(),
296 method: "measure",
297 };
298 for bytes in [
299 vec![0xde, 0xad],
300 ic_platform::candid::contract::encode_args((7_u64,)).unwrap(),
301 ] {
302 let length = bytes.len();
303 let err = call_candid(
304 context,
305 (),
306 |_| Ok(bytes),
307 |bytes| decode_args::<(u64, bool)>(bytes, LIMITS),
308 )
309 .unwrap_err();
310 assert!(err.to_string().contains(&format!("bytes={length}")));
311 let CandidCallError::Decode {
312 context: observed,
313 bytes,
314 ..
315 } = err
316 else {
317 panic!("expected decode failure");
318 };
319 assert_eq!(bytes, length);
320 assert_eq!(observed.operation, "query_call");
321 assert_eq!(observed.canister_id, context.canister_id);
322 assert_eq!(observed.caller, context.caller);
323 assert_eq!(observed.method, "measure");
324 }
325 }
326
327 #[test]
328 fn dead_instance_panic_is_classified_as_transport() {
329 let context = CallContext {
330 operation: "query_call",
331 canister_id: Principal::anonymous(),
332 caller: Principal::management_canister(),
333 method: "get",
334 };
335 let error = run_raw_call(context, || -> Result<Vec<u8>, pocket_ic::RejectResponse> {
336 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\" }}) }}");
337 })
338 .unwrap_err();
339
340 let source = std::error::Error::source(&error)
341 .unwrap()
342 .downcast_ref::<super::PocketIcOperationError>()
343 .unwrap();
344 assert!(source.is_transport());
345 assert!(source.message().starts_with("HTTP failure: "));
346 assert!(error.to_string().contains("operation=query_call"));
347 assert!(super::super::is_dead_pocket_ic_transport_error(&error));
348 let CandidCallError::Transport { source: owned, .. } = error else {
349 panic!("expected transport failure");
350 };
351 assert!(owned.is_transport());
352 }
353
354 #[test]
355 fn unrelated_panics_resume_with_the_original_payload() {
356 for message in [
357 "application worker channel closed",
358 "fixture expected ConnectionRefused but observed another result",
359 ] {
360 let context = CallContext {
361 operation: "query_call",
362 canister_id: Principal::anonymous(),
363 caller: Principal::management_canister(),
364 method: "get",
365 };
366 let payload = message.to_owned();
367 let original_allocation = payload.as_ptr() as usize;
368 let result = std::panic::catch_unwind(|| {
369 run_raw_call(context, || -> Result<Vec<u8>, pocket_ic::RejectResponse> {
370 std::panic::panic_any(payload);
371 })
372 });
373 let resumed = result
374 .expect_err("application panic must resume")
375 .downcast::<String>()
376 .expect("original panic type must be preserved");
377 assert_eq!(resumed.as_str(), message);
378 assert_eq!(resumed.as_ptr() as usize, original_allocation);
379 }
380 let payload = Box::new(7_u8);
381 let original = std::ptr::from_ref(payload.as_ref());
382 let result = std::panic::catch_unwind(|| {
383 run_raw_call(
384 CallContext {
385 operation: "query_call",
386 canister_id: Principal::anonymous(),
387 caller: Principal::anonymous(),
388 method: "get",
389 },
390 || -> Result<Vec<u8>, pocket_ic::RejectResponse> {
391 std::panic::panic_any(payload);
392 },
393 )
394 });
395 let resumed = result.unwrap_err().downcast::<Box<u8>>().unwrap();
396 assert_eq!(std::ptr::from_ref(resumed.as_ref().as_ref()), original);
397 }
398}