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/// 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, message } => {
137                write!(f, "pocket_ic {} failed: {message}", 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
151impl std::fmt::Display for CanisterInstallError {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        let (operation, canister_id, label, source) = match self {
154            Self::CreateCanister { label, source } => ("create_canister", None, label, source),
155            Self::AddCycles {
156                canister_id,
157                label,
158                source,
159            } => ("add_cycles", Some(canister_id), label, source),
160            Self::InstallCode {
161                canister_id,
162                label,
163                source,
164            } => ("install_canister", Some(canister_id), label, source),
165        };
166        write!(f, "{operation} failed")?;
167        if let Some(canister_id) = canister_id {
168            write!(f, " for canister {canister_id}")?;
169        }
170        if let Some(label) = label {
171            write!(f, " ({label})")?;
172        }
173        write!(f, ": {source}")
174    }
175}
176
177impl std::error::Error for CanisterInstallError {
178    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
179        let (Self::CreateCanister { source, .. }
180        | Self::AddCycles { source, .. }
181        | Self::InstallCode { source, .. }) = self;
182        Some(source)
183    }
184}
185
186impl StandaloneCanisterInstallError {
187    pub(super) fn new(
188        instance: PocketIcInstance<PocketIc>,
189        install_error: CanisterInstallError,
190    ) -> Self {
191        Self {
192            instance: Box::new(instance),
193            install_error,
194        }
195    }
196
197    /// Borrow the caller-created instance retained after the failed install.
198    #[must_use]
199    pub fn pocket_ic(&self) -> &PocketIc {
200        &self.instance.instance
201    }
202
203    /// Inspect the structured install failure.
204    #[must_use]
205    pub const fn install_error(&self) -> &CanisterInstallError {
206        &self.install_error
207    }
208
209    /// Recover the instance/server ownership record and install failure.
210    pub fn into_parts(self) -> (PocketIcInstance<PocketIc>, CanisterInstallError) {
211        (*self.instance, self.install_error)
212    }
213}
214
215impl std::fmt::Debug for StandaloneCanisterInstallError {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        f.debug_struct("StandaloneCanisterInstallError")
218            .field("install_error", &self.install_error)
219            .finish_non_exhaustive()
220    }
221}
222
223impl std::fmt::Display for StandaloneCanisterInstallError {
224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225        self.install_error.fmt(f)
226    }
227}
228
229impl std::error::Error for StandaloneCanisterInstallError {
230    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
231        Some(&self.install_error)
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use candid::Principal;
238
239    use super::CanisterInstallError;
240
241    #[test]
242    fn labeled_install_error_display_includes_label() {
243        let err = CanisterInstallError::InstallCode {
244            canister_id: Principal::anonymous(),
245            label: Some("authority".into()),
246            source: super::PocketIcOperationError::new("trap"),
247        };
248        assert!(err.to_string().contains("(authority): trap"));
249    }
250}