Skip to main content

ic_testkit/pic/
errors.rs

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