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///
8/// Match the variant to inspect its required context and original failure details.
9#[non_exhaustive]
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub enum CandidCallError {
12    /// Arguments could not be Candid-encoded.
13    Encode {
14        /// Target, caller, method and raw PocketIC operation.
15        context: Box<CandidCallContext>,
16        /// Original codec failure text.
17        message: String,
18    },
19    /// Successful response bytes could not be Candid-decoded.
20    Decode {
21        /// Target, caller, method and raw PocketIC operation.
22        context: Box<CandidCallContext>,
23        /// Number of reply bytes presented to the decoder.
24        bytes: usize,
25        /// Original codec failure text.
26        message: String,
27    },
28    /// PocketIC returned a structured canister rejection.
29    CanisterReject {
30        /// Target, caller, method and raw PocketIC operation.
31        context: Box<CandidCallContext>,
32        /// Complete upstream rejection.
33        response: Box<RejectResponse>,
34    },
35    /// The PocketIC instance became unreachable.
36    Transport {
37        /// Target, caller, method and raw PocketIC operation.
38        context: Box<CandidCallContext>,
39        /// Original upstream operation failure and transport classification.
40        source: PocketIcOperationError,
41    },
42}
43
44/// Stable call metadata attached to a contextual call failure.
45#[non_exhaustive]
46#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct CandidCallContext {
48    /// Underlying PocketIC operation, such as `update_call` or `query_call`.
49    pub operation: &'static str,
50    /// Target canister.
51    pub canister_id: Principal,
52    /// Effective message caller.
53    pub caller: Principal,
54    /// Canister method name.
55    pub method: String,
56}
57
58/// Failed creation, funding, or installation with its original operation error.
59#[non_exhaustive]
60#[derive(Debug, Eq, PartialEq)]
61pub enum CanisterInstallError {
62    /// Canister creation failed before an id was returned.
63    CreateCanister {
64        /// Optional caller-provided install label.
65        label: Option<String>,
66        /// Original upstream operation failure.
67        source: PocketIcOperationError,
68    },
69    /// Cycle funding failed for the created canister.
70    AddCycles {
71        /// Created canister, retained for caller inspection.
72        canister_id: Principal,
73        /// Optional caller-provided install label.
74        label: Option<String>,
75        /// Original upstream operation failure.
76        source: PocketIcOperationError,
77    },
78    /// Wasm installation failed for the created canister.
79    InstallCode {
80        /// Created canister, retained for caller inspection.
81        canister_id: Principal,
82        /// Optional caller-provided install label.
83        label: Option<String>,
84        /// Original upstream operation failure.
85        source: PocketIcOperationError,
86    },
87}
88
89/// Failed standalone install retaining the caller-created instance and optional server.
90pub struct StandaloneCanisterInstallError {
91    instance: Box<PocketIcInstance<PocketIc>>,
92    install_error: CanisterInstallError,
93}
94
95impl CandidCallContext {
96    /// Capture the stable call metadata attached to one call failure.
97    #[must_use]
98    pub fn new(
99        operation: &'static str,
100        canister_id: Principal,
101        caller: Principal,
102        method: impl Into<String>,
103    ) -> Self {
104        Self {
105            operation,
106            canister_id,
107            caller,
108            method: method.into(),
109        }
110    }
111}
112
113impl std::fmt::Display for CandidCallError {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        let context = match self {
116            Self::Encode { context, message } => {
117                write!(f, "candid encode_args failed: {message}")?;
118                context
119            }
120            Self::Decode {
121                context,
122                bytes,
123                message,
124            } => {
125                write!(f, "candid decode failed (bytes={bytes}): {message}")?;
126                context
127            }
128            Self::CanisterReject { context, response } => {
129                write!(
130                    f,
131                    "pocket_ic {} was rejected: {response}",
132                    context.operation
133                )?;
134                context
135            }
136            Self::Transport { context, source } => {
137                write!(f, "pocket_ic {} failed: {source}", context.operation)?;
138                context
139            }
140        };
141        write!(
142            f,
143            " (operation={}, canister={}, caller={}, method={})",
144            context.operation, context.canister_id, context.caller, context.method
145        )
146    }
147}
148
149impl std::error::Error for CandidCallError {
150    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
151        match self {
152            Self::Transport { source, .. } => Some(source),
153            _ => None,
154        }
155    }
156}
157
158impl std::fmt::Display for CanisterInstallError {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        let (operation, canister_id, label, source) = match self {
161            Self::CreateCanister { label, source } => ("create_canister", None, label, source),
162            Self::AddCycles {
163                canister_id,
164                label,
165                source,
166            } => ("add_cycles", Some(canister_id), label, source),
167            Self::InstallCode {
168                canister_id,
169                label,
170                source,
171            } => ("install_canister", Some(canister_id), label, source),
172        };
173        write!(f, "{operation} failed")?;
174        if let Some(canister_id) = canister_id {
175            write!(f, " for canister {canister_id}")?;
176        }
177        if let Some(label) = label {
178            write!(f, " ({label})")?;
179        }
180        write!(f, ": {source}")
181    }
182}
183
184impl std::error::Error for CanisterInstallError {
185    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
186        let (Self::CreateCanister { source, .. }
187        | Self::AddCycles { source, .. }
188        | Self::InstallCode { source, .. }) = self;
189        Some(source)
190    }
191}
192
193impl StandaloneCanisterInstallError {
194    pub(super) fn new(
195        instance: PocketIcInstance<PocketIc>,
196        install_error: CanisterInstallError,
197    ) -> Self {
198        Self {
199            instance: Box::new(instance),
200            install_error,
201        }
202    }
203
204    /// Borrow the caller-created instance retained after the failed install.
205    #[must_use]
206    pub fn pocket_ic(&self) -> &PocketIc {
207        &self.instance.instance
208    }
209
210    /// Inspect the structured install failure.
211    #[must_use]
212    pub const fn install_error(&self) -> &CanisterInstallError {
213        &self.install_error
214    }
215
216    /// Recover the instance/server ownership record and install failure.
217    pub fn into_parts(self) -> (PocketIcInstance<PocketIc>, CanisterInstallError) {
218        (*self.instance, self.install_error)
219    }
220}
221
222impl std::fmt::Debug for StandaloneCanisterInstallError {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        f.debug_struct("StandaloneCanisterInstallError")
225            .field("install_error", &self.install_error)
226            .finish_non_exhaustive()
227    }
228}
229
230impl std::fmt::Display for StandaloneCanisterInstallError {
231    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232        self.install_error.fmt(f)
233    }
234}
235
236impl std::error::Error for StandaloneCanisterInstallError {
237    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
238        Some(&self.install_error)
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use candid::Principal;
245
246    use super::CanisterInstallError;
247
248    #[test]
249    fn labeled_install_error_display_includes_label() {
250        let err = CanisterInstallError::InstallCode {
251            canister_id: Principal::anonymous(),
252            label: Some("authority".into()),
253            source: super::PocketIcOperationError::new("trap"),
254        };
255        assert!(err.to_string().contains("(authority): trap"));
256    }
257}