Skip to main content

ic_testkit/pic/
errors.rs

1use candid::Principal;
2use pocket_ic::{PocketIc, RejectResponse};
3
4///
5/// CandidCallError
6///
7
8#[non_exhaustive]
9#[derive(Clone, Debug, Eq, PartialEq)]
10pub struct CandidCallError {
11    pub message: String,
12    pub kind: CandidCallErrorKind,
13    pub context: Option<Box<CandidCallContext>>,
14    pub reject_response: Option<Box<RejectResponse>>,
15}
16
17#[non_exhaustive]
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum CandidCallErrorKind {
20    Encode,
21    Decode,
22    CanisterReject,
23    Transport,
24    Other,
25}
26
27#[non_exhaustive]
28#[derive(Clone, Debug, Eq, PartialEq)]
29pub struct CandidCallContext {
30    pub operation: &'static str,
31    pub canister_id: Principal,
32    pub caller: Principal,
33    pub method: String,
34}
35
36///
37/// CanisterInstallError
38///
39
40#[derive(Debug, Eq, PartialEq)]
41pub struct CanisterInstallError {
42    canister_id: Principal,
43    label: Option<String>,
44    message: String,
45}
46
47/// A failed standalone install that returns ownership of the caller's instance.
48pub struct StandaloneCanisterInstallError {
49    pocket_ic: Box<PocketIc>,
50    install_error: CanisterInstallError,
51}
52
53impl CandidCallContext {
54    /// Capture the stable call metadata attached to one call failure.
55    #[must_use]
56    pub fn new(
57        operation: &'static str,
58        canister_id: Principal,
59        caller: Principal,
60        method: impl Into<String>,
61    ) -> Self {
62        Self {
63            operation,
64            canister_id,
65            caller,
66            method: method.into(),
67        }
68    }
69
70    /// Read the PocketIC operation name, such as `update_call` or `query_call`.
71    #[must_use]
72    pub const fn operation(&self) -> &'static str {
73        self.operation
74    }
75
76    /// Read the target canister id.
77    #[must_use]
78    pub const fn canister_id(&self) -> Principal {
79        self.canister_id
80    }
81
82    /// Read the caller principal used for the call.
83    #[must_use]
84    pub const fn caller(&self) -> Principal {
85        self.caller
86    }
87
88    /// Read the called method name.
89    #[must_use]
90    pub fn method(&self) -> &str {
91        &self.method
92    }
93}
94
95impl CandidCallError {
96    /// Capture one PocketIC call/codec failure.
97    #[must_use]
98    pub fn new(message: impl Into<String>) -> Self {
99        Self {
100            message: message.into(),
101            kind: CandidCallErrorKind::Other,
102            context: None,
103            reject_response: None,
104        }
105    }
106
107    /// Capture one contextual Candid encode failure.
108    #[must_use]
109    pub fn encode(context: CandidCallContext, source: impl std::fmt::Display) -> Self {
110        let message = format!(
111            "candid encode_args failed (operation={}, canister={}, caller={}, method={}): {source}",
112            context.operation, context.canister_id, context.caller, context.method
113        );
114
115        Self {
116            message,
117            kind: CandidCallErrorKind::Encode,
118            context: Some(Box::new(context)),
119            reject_response: None,
120        }
121    }
122
123    /// Capture one contextual Candid decode failure.
124    #[must_use]
125    pub fn decode(
126        context: CandidCallContext,
127        bytes: usize,
128        source: impl std::fmt::Display,
129    ) -> Self {
130        let message = format!(
131            "candid decode_one failed (operation={}, canister={}, caller={}, method={}, bytes={}): {source}",
132            context.operation, context.canister_id, context.caller, context.method, bytes
133        );
134
135        Self {
136            message,
137            kind: CandidCallErrorKind::Decode,
138            context: Some(Box::new(context)),
139            reject_response: None,
140        }
141    }
142
143    /// Capture one structured rejection returned by PocketIC.
144    #[must_use]
145    pub fn canister_reject(context: CandidCallContext, response: RejectResponse) -> Self {
146        let message = format!(
147            "pocket_ic {} was rejected (canister={}, caller={}, method={}): {response}",
148            context.operation, context.canister_id, context.caller, context.method
149        );
150
151        Self {
152            message,
153            kind: CandidCallErrorKind::CanisterReject,
154            context: Some(Box::new(context)),
155            reject_response: Some(Box::new(response)),
156        }
157    }
158
159    /// Capture one contextual PocketIC transport failure.
160    #[must_use]
161    pub fn transport(context: CandidCallContext, source: impl std::fmt::Display) -> Self {
162        let message = format!(
163            "pocket_ic {} failed (canister={}, caller={}, method={}): {source}",
164            context.operation, context.canister_id, context.caller, context.method
165        );
166
167        Self {
168            message,
169            kind: CandidCallErrorKind::Transport,
170            context: Some(Box::new(context)),
171            reject_response: None,
172        }
173    }
174
175    /// Read the rendered error message.
176    #[must_use]
177    pub fn message(&self) -> &str {
178        &self.message
179    }
180
181    /// Read the structured failure kind.
182    #[must_use]
183    pub const fn kind(&self) -> CandidCallErrorKind {
184        self.kind
185    }
186
187    /// Read the structured call context, when available.
188    #[must_use]
189    pub fn context(&self) -> Option<&CandidCallContext> {
190        self.context.as_deref()
191    }
192
193    /// Read the structured PocketIC rejection, when the call reached the IC.
194    #[must_use]
195    pub fn reject_response(&self) -> Option<&RejectResponse> {
196        self.reject_response.as_deref()
197    }
198}
199
200impl std::fmt::Display for CandidCallError {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        f.write_str(&self.message)
203    }
204}
205
206impl std::error::Error for CandidCallError {}
207
208impl CanisterInstallError {
209    /// Capture one install failure for a specific canister id.
210    #[must_use]
211    pub const fn new(canister_id: Principal, message: String) -> Self {
212        Self {
213            canister_id,
214            label: None,
215            message,
216        }
217    }
218
219    /// Capture one labeled install failure for a specific canister id.
220    #[must_use]
221    pub fn labeled(
222        canister_id: Principal,
223        label: impl Into<String>,
224        message: impl Into<String>,
225    ) -> Self {
226        Self {
227            canister_id,
228            label: Some(label.into()),
229            message: message.into(),
230        }
231    }
232
233    /// Read the canister id that failed to install.
234    #[must_use]
235    pub const fn canister_id(&self) -> Principal {
236        self.canister_id
237    }
238
239    /// Read the captured panic message from the install attempt.
240    #[must_use]
241    pub fn message(&self) -> &str {
242        &self.message
243    }
244
245    /// Read the optional caller-provided install label.
246    #[must_use]
247    pub fn label(&self) -> Option<&str> {
248        self.label.as_deref()
249    }
250}
251
252impl std::fmt::Display for CanisterInstallError {
253    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254        if let Some(label) = &self.label {
255            write!(
256                f,
257                "failed to install canister {} ({label}): {}",
258                self.canister_id, self.message
259            )
260        } else {
261            write!(
262                f,
263                "failed to install canister {}: {}",
264                self.canister_id, self.message
265            )
266        }
267    }
268}
269
270impl std::error::Error for CanisterInstallError {}
271
272impl StandaloneCanisterInstallError {
273    pub(super) fn new(pocket_ic: PocketIc, install_error: CanisterInstallError) -> Self {
274        Self {
275            pocket_ic: Box::new(pocket_ic),
276            install_error,
277        }
278    }
279
280    /// Borrow the caller-created instance retained after the failed install.
281    #[must_use]
282    pub fn pocket_ic(&self) -> &PocketIc {
283        self.pocket_ic.as_ref()
284    }
285
286    /// Inspect the structured install failure.
287    #[must_use]
288    pub const fn install_error(&self) -> &CanisterInstallError {
289        &self.install_error
290    }
291
292    /// Recover ownership of the instance and install failure.
293    #[must_use]
294    pub fn into_parts(self) -> (PocketIc, CanisterInstallError) {
295        (*self.pocket_ic, self.install_error)
296    }
297}
298
299impl std::fmt::Debug for StandaloneCanisterInstallError {
300    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301        f.debug_struct("StandaloneCanisterInstallError")
302            .field("install_error", &self.install_error)
303            .finish_non_exhaustive()
304    }
305}
306
307impl std::fmt::Display for StandaloneCanisterInstallError {
308    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309        self.install_error.fmt(f)
310    }
311}
312
313impl std::error::Error for StandaloneCanisterInstallError {
314    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
315        Some(&self.install_error)
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use candid::Principal;
322    use pocket_ic::{ErrorCode, RejectCode, RejectResponse};
323
324    use super::{CandidCallContext, CandidCallError, CandidCallErrorKind, CanisterInstallError};
325
326    #[test]
327    fn labeled_install_error_display_includes_label() {
328        let err = CanisterInstallError::labeled(Principal::anonymous(), "authority", "trap");
329
330        assert_eq!(err.label(), Some("authority"));
331        assert!(err.to_string().contains("(authority): trap"));
332    }
333
334    #[test]
335    fn canister_reject_preserves_the_upstream_response() {
336        let response = RejectResponse {
337            reject_code: RejectCode::DestinationInvalid,
338            reject_message: "missing canister".to_string(),
339            error_code: ErrorCode::CanisterNotFound,
340            certified: true,
341        };
342        let error = CandidCallError::canister_reject(
343            CandidCallContext::new(
344                "query_call",
345                Principal::anonymous(),
346                Principal::management_canister(),
347                "get",
348            ),
349            response.clone(),
350        );
351
352        assert_eq!(error.kind(), CandidCallErrorKind::CanisterReject);
353        assert_eq!(error.reject_response(), Some(&response));
354    }
355}