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 failure text.
40        message: String,
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/// Stage at which a canister creation and installation failed.
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60pub enum CanisterInstallPhase {
61    /// PocketIC could not create the canister.
62    CreateCanister,
63    /// PocketIC could not add the requested cycles.
64    AddCycles,
65    /// PocketIC could not install the Wasm module.
66    InstallCode,
67}
68
69impl CanisterInstallPhase {
70    /// Upstream operation attempted at this stage.
71    #[must_use]
72    pub const fn operation(self) -> &'static str {
73        match self {
74            Self::CreateCanister => "create_canister",
75            Self::AddCycles => "add_cycles",
76            Self::InstallCode => "install_canister",
77        }
78    }
79}
80
81/// Failed creation, funding, or installation with its original operation error.
82#[derive(Debug, Eq, PartialEq)]
83pub struct CanisterInstallError {
84    phase: CanisterInstallPhase,
85    canister_id: Option<Principal>,
86    label: Option<String>,
87    source: PocketIcOperationError,
88}
89
90/// Failed standalone install retaining the caller-created instance and optional server.
91pub struct StandaloneCanisterInstallError {
92    instance: Box<PocketIcInstance<PocketIc>>,
93    install_error: CanisterInstallError,
94}
95
96impl CandidCallContext {
97    /// Capture the stable call metadata attached to one call failure.
98    #[must_use]
99    pub fn new(
100        operation: &'static str,
101        canister_id: Principal,
102        caller: Principal,
103        method: impl Into<String>,
104    ) -> Self {
105        Self {
106            operation,
107            canister_id,
108            caller,
109            method: method.into(),
110        }
111    }
112}
113
114impl std::fmt::Display for CandidCallError {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        let context = match self {
117            Self::Encode { context, message } => {
118                write!(f, "candid encode_args failed: {message}")?;
119                context
120            }
121            Self::Decode {
122                context,
123                bytes,
124                message,
125            } => {
126                write!(f, "candid decode failed (bytes={bytes}): {message}")?;
127                context
128            }
129            Self::CanisterReject { context, response } => {
130                write!(
131                    f,
132                    "pocket_ic {} was rejected: {response}",
133                    context.operation
134                )?;
135                context
136            }
137            Self::Transport { context, message } => {
138                write!(f, "pocket_ic {} failed: {message}", context.operation)?;
139                context
140            }
141        };
142        write!(
143            f,
144            " (operation={}, canister={}, caller={}, method={})",
145            context.operation, context.canister_id, context.caller, context.method
146        )
147    }
148}
149
150impl std::error::Error for CandidCallError {}
151
152impl CanisterInstallError {
153    /// Capture an operation failure and all context available at that stage.
154    #[must_use]
155    pub const fn new(
156        phase: CanisterInstallPhase,
157        canister_id: Option<Principal>,
158        label: Option<String>,
159        source: PocketIcOperationError,
160    ) -> Self {
161        Self {
162            phase,
163            canister_id,
164            label,
165            source,
166        }
167    }
168
169    /// Read the failed stage.
170    #[must_use]
171    pub const fn phase(&self) -> CanisterInstallPhase {
172        self.phase
173    }
174
175    /// Read the created canister id, absent when creation failed.
176    #[must_use]
177    pub const fn canister_id(&self) -> Option<Principal> {
178        self.canister_id
179    }
180
181    /// Read the original upstream panic message.
182    #[must_use]
183    pub fn message(&self) -> &str {
184        self.source.message()
185    }
186
187    /// Inspect the original operation failure and its transport classification.
188    #[must_use]
189    pub const fn operation_error(&self) -> &PocketIcOperationError {
190        &self.source
191    }
192
193    /// Read the optional caller-provided install label.
194    #[must_use]
195    pub fn label(&self) -> Option<&str> {
196        self.label.as_deref()
197    }
198}
199
200impl std::fmt::Display for CanisterInstallError {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        write!(f, "{} failed", self.phase.operation())?;
203        if let Some(canister_id) = self.canister_id {
204            write!(f, " for canister {canister_id}")?;
205        }
206        if let Some(label) = &self.label {
207            write!(f, " ({label})")?;
208        }
209        write!(f, ": {}", self.source)
210    }
211}
212
213impl std::error::Error for CanisterInstallError {
214    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
215        Some(&self.source)
216    }
217}
218
219impl StandaloneCanisterInstallError {
220    pub(super) fn new(
221        instance: PocketIcInstance<PocketIc>,
222        install_error: CanisterInstallError,
223    ) -> Self {
224        Self {
225            instance: Box::new(instance),
226            install_error,
227        }
228    }
229
230    /// Borrow the caller-created instance retained after the failed install.
231    #[must_use]
232    pub fn pocket_ic(&self) -> &PocketIc {
233        &self.instance.instance
234    }
235
236    /// Inspect the structured install failure.
237    #[must_use]
238    pub const fn install_error(&self) -> &CanisterInstallError {
239        &self.install_error
240    }
241
242    /// Recover the instance/server ownership record and install failure.
243    pub fn into_parts(self) -> (PocketIcInstance<PocketIc>, CanisterInstallError) {
244        (*self.instance, self.install_error)
245    }
246}
247
248impl std::fmt::Debug for StandaloneCanisterInstallError {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        f.debug_struct("StandaloneCanisterInstallError")
251            .field("install_error", &self.install_error)
252            .finish_non_exhaustive()
253    }
254}
255
256impl std::fmt::Display for StandaloneCanisterInstallError {
257    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258        self.install_error.fmt(f)
259    }
260}
261
262impl std::error::Error for StandaloneCanisterInstallError {
263    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
264        Some(&self.install_error)
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use candid::Principal;
271
272    use super::CanisterInstallError;
273
274    #[test]
275    fn labeled_install_error_display_includes_label() {
276        let err = CanisterInstallError::new(
277            super::CanisterInstallPhase::InstallCode,
278            Some(Principal::anonymous()),
279            Some("authority".into()),
280            super::PocketIcOperationError::new("trap"),
281        );
282
283        assert_eq!(err.label(), Some("authority"));
284        assert!(err.to_string().contains("(authority): trap"));
285    }
286}