Skip to main content

ic_testkit/pic/
errors.rs

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