Skip to main content

ic_testkit/pic/
errors.rs

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