Skip to main content

ic_testkit/pic/
errors.rs

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