Skip to main content

ic_testkit/pic/
errors.rs

1use candid::Principal;
2
3use super::{PicSerialGuardError, startup::PicStartError};
4
5///
6/// PicCallError
7///
8
9#[non_exhaustive]
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct PicCallError {
12    pub message: String,
13    pub kind: PicCallErrorKind,
14    pub context: Option<Box<PicCallContext>>,
15}
16
17#[non_exhaustive]
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum PicCallErrorKind {
20    Encode,
21    Decode,
22    Transport,
23    Other,
24}
25
26#[non_exhaustive]
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct PicCallContext {
29    pub operation: &'static str,
30    pub canister_id: Principal,
31    pub caller: Principal,
32    pub method: String,
33}
34
35///
36/// PicInstallError
37///
38
39#[derive(Debug, Eq, PartialEq)]
40pub struct PicInstallError {
41    canister_id: Principal,
42    label: Option<String>,
43    message: String,
44}
45
46///
47/// StandaloneCanisterFixtureError
48///
49
50#[derive(Debug)]
51pub enum StandaloneCanisterFixtureError {
52    SerialGuard(PicSerialGuardError),
53    Start(PicStartError),
54    Install(PicInstallError),
55}
56
57impl PicCallContext {
58    /// Capture the stable call metadata attached to one call failure.
59    #[must_use]
60    pub fn new(
61        operation: &'static str,
62        canister_id: Principal,
63        caller: Principal,
64        method: impl Into<String>,
65    ) -> Self {
66        Self {
67            operation,
68            canister_id,
69            caller,
70            method: method.into(),
71        }
72    }
73
74    /// Read the PocketIC operation name, such as `update_call` or `query_call`.
75    #[must_use]
76    pub const fn operation(&self) -> &'static str {
77        self.operation
78    }
79
80    /// Read the target canister id.
81    #[must_use]
82    pub const fn canister_id(&self) -> Principal {
83        self.canister_id
84    }
85
86    /// Read the caller principal used for the call.
87    #[must_use]
88    pub const fn caller(&self) -> Principal {
89        self.caller
90    }
91
92    /// Read the called method name.
93    #[must_use]
94    pub fn method(&self) -> &str {
95        &self.method
96    }
97}
98
99impl PicCallError {
100    /// Capture one PocketIC call/codec failure.
101    #[must_use]
102    pub fn new(message: impl Into<String>) -> Self {
103        Self {
104            message: message.into(),
105            kind: PicCallErrorKind::Other,
106            context: None,
107        }
108    }
109
110    /// Capture one contextual Candid encode failure.
111    #[must_use]
112    pub fn encode(context: PicCallContext, source: impl std::fmt::Display) -> Self {
113        let message = format!(
114            "candid encode_args failed (operation={}, canister={}, caller={}, method={}): {source}",
115            context.operation, context.canister_id, context.caller, context.method
116        );
117
118        Self {
119            message,
120            kind: PicCallErrorKind::Encode,
121            context: Some(Box::new(context)),
122        }
123    }
124
125    /// Capture one contextual Candid decode failure.
126    #[must_use]
127    pub fn decode(context: PicCallContext, bytes: usize, source: impl std::fmt::Display) -> Self {
128        let message = format!(
129            "candid decode_one failed (operation={}, canister={}, caller={}, method={}, bytes={}): {source}",
130            context.operation, context.canister_id, context.caller, context.method, bytes
131        );
132
133        Self {
134            message,
135            kind: PicCallErrorKind::Decode,
136            context: Some(Box::new(context)),
137        }
138    }
139
140    /// Capture one contextual PocketIC transport failure.
141    #[must_use]
142    pub fn transport(context: PicCallContext, source: impl std::fmt::Display) -> Self {
143        let message = format!(
144            "pocket_ic {} failed (canister={}, caller={}, method={}): {source}",
145            context.operation, context.canister_id, context.caller, context.method
146        );
147
148        Self {
149            message,
150            kind: PicCallErrorKind::Transport,
151            context: Some(Box::new(context)),
152        }
153    }
154
155    /// Read the rendered error message.
156    #[must_use]
157    pub fn message(&self) -> &str {
158        &self.message
159    }
160
161    /// Read the structured failure kind.
162    #[must_use]
163    pub const fn kind(&self) -> PicCallErrorKind {
164        self.kind
165    }
166
167    /// Read the structured call context, when available.
168    #[must_use]
169    pub fn context(&self) -> Option<&PicCallContext> {
170        self.context.as_deref()
171    }
172}
173
174impl std::fmt::Display for PicCallError {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        f.write_str(&self.message)
177    }
178}
179
180impl std::error::Error for PicCallError {}
181
182impl PicInstallError {
183    /// Capture one install failure for a specific canister id.
184    #[must_use]
185    pub const fn new(canister_id: Principal, message: String) -> Self {
186        Self {
187            canister_id,
188            label: None,
189            message,
190        }
191    }
192
193    /// Capture one labeled install failure for a specific canister id.
194    #[must_use]
195    pub fn labeled(
196        canister_id: Principal,
197        label: impl Into<String>,
198        message: impl Into<String>,
199    ) -> Self {
200        Self {
201            canister_id,
202            label: Some(label.into()),
203            message: message.into(),
204        }
205    }
206
207    /// Read the canister id that failed to install.
208    #[must_use]
209    pub const fn canister_id(&self) -> Principal {
210        self.canister_id
211    }
212
213    /// Read the captured panic message from the install attempt.
214    #[must_use]
215    pub fn message(&self) -> &str {
216        &self.message
217    }
218
219    /// Read the optional caller-provided install label.
220    #[must_use]
221    pub fn label(&self) -> Option<&str> {
222        self.label.as_deref()
223    }
224}
225
226impl std::fmt::Display for PicInstallError {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228        if let Some(label) = &self.label {
229            write!(
230                f,
231                "failed to install canister {} ({label}): {}",
232                self.canister_id, self.message
233            )
234        } else {
235            write!(
236                f,
237                "failed to install canister {}: {}",
238                self.canister_id, self.message
239            )
240        }
241    }
242}
243
244impl std::error::Error for PicInstallError {}
245
246impl std::fmt::Display for StandaloneCanisterFixtureError {
247    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248        match self {
249            Self::SerialGuard(err) => write!(f, "{err}"),
250            Self::Start(err) => write!(f, "{err}"),
251            Self::Install(err) => write!(f, "{err}"),
252        }
253    }
254}
255
256impl std::error::Error for StandaloneCanisterFixtureError {
257    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
258        match self {
259            Self::SerialGuard(err) => Some(err),
260            Self::Start(err) => Some(err),
261            Self::Install(err) => Some(err),
262        }
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use candid::Principal;
269
270    use super::PicInstallError;
271
272    #[test]
273    fn labeled_install_error_display_includes_label() {
274        let err = PicInstallError::labeled(Principal::anonymous(), "authority", "trap");
275
276        assert_eq!(err.label(), Some("authority"));
277        assert!(err.to_string().contains("(authority): trap"));
278    }
279}