1use candid::Principal;
2use pocket_ic::{PocketIc, RejectResponse};
3
4#[non_exhaustive]
6#[derive(Clone, Debug, Eq, PartialEq)]
7pub struct CandidCallError {
8 pub message: String,
10 pub kind: CandidCallErrorKind,
12 pub context: Option<Box<CandidCallContext>>,
14 pub reject_response: Option<Box<RejectResponse>>,
16}
17
18#[non_exhaustive]
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum CandidCallErrorKind {
22 Encode,
24 Decode,
26 CanisterReject,
28 Transport,
30 Other,
32}
33
34#[non_exhaustive]
36#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct CandidCallContext {
38 pub operation: &'static str,
40 pub canister_id: Principal,
42 pub caller: Principal,
44 pub method: String,
46}
47
48#[derive(Debug, Eq, PartialEq)]
50pub struct CanisterInstallError {
51 canister_id: Principal,
52 label: Option<String>,
53 message: String,
54}
55
56pub struct StandaloneCanisterInstallError {
58 pocket_ic: Box<PocketIc>,
59 install_error: CanisterInstallError,
60}
61
62impl CandidCallContext {
63 #[must_use]
65 pub fn new(
66 operation: &'static str,
67 canister_id: Principal,
68 caller: Principal,
69 method: impl Into<String>,
70 ) -> Self {
71 Self {
72 operation,
73 canister_id,
74 caller,
75 method: method.into(),
76 }
77 }
78
79 #[must_use]
81 pub const fn operation(&self) -> &'static str {
82 self.operation
83 }
84
85 #[must_use]
87 pub const fn canister_id(&self) -> Principal {
88 self.canister_id
89 }
90
91 #[must_use]
93 pub const fn caller(&self) -> Principal {
94 self.caller
95 }
96
97 #[must_use]
99 pub fn method(&self) -> &str {
100 &self.method
101 }
102}
103
104impl CandidCallError {
105 #[must_use]
107 pub fn new(message: impl Into<String>) -> Self {
108 Self {
109 message: message.into(),
110 kind: CandidCallErrorKind::Other,
111 context: None,
112 reject_response: None,
113 }
114 }
115
116 #[must_use]
118 pub fn encode(context: CandidCallContext, source: impl std::fmt::Display) -> Self {
119 let message = format!(
120 "candid encode_args failed (operation={}, canister={}, caller={}, method={}): {source}",
121 context.operation, context.canister_id, context.caller, context.method
122 );
123
124 Self {
125 message,
126 kind: CandidCallErrorKind::Encode,
127 context: Some(Box::new(context)),
128 reject_response: None,
129 }
130 }
131
132 #[must_use]
134 pub fn decode(
135 context: CandidCallContext,
136 bytes: usize,
137 source: impl std::fmt::Display,
138 ) -> Self {
139 let message = format!(
140 "candid decode_one failed (operation={}, canister={}, caller={}, method={}, bytes={}): {source}",
141 context.operation, context.canister_id, context.caller, context.method, bytes
142 );
143
144 Self {
145 message,
146 kind: CandidCallErrorKind::Decode,
147 context: Some(Box::new(context)),
148 reject_response: None,
149 }
150 }
151
152 #[must_use]
154 pub fn canister_reject(context: CandidCallContext, response: RejectResponse) -> Self {
155 let message = format!(
156 "pocket_ic {} was rejected (canister={}, caller={}, method={}): {response}",
157 context.operation, context.canister_id, context.caller, context.method
158 );
159
160 Self {
161 message,
162 kind: CandidCallErrorKind::CanisterReject,
163 context: Some(Box::new(context)),
164 reject_response: Some(Box::new(response)),
165 }
166 }
167
168 #[must_use]
170 pub fn transport(context: CandidCallContext, source: impl std::fmt::Display) -> Self {
171 let message = format!(
172 "pocket_ic {} failed (canister={}, caller={}, method={}): {source}",
173 context.operation, context.canister_id, context.caller, context.method
174 );
175
176 Self {
177 message,
178 kind: CandidCallErrorKind::Transport,
179 context: Some(Box::new(context)),
180 reject_response: None,
181 }
182 }
183
184 #[must_use]
186 pub fn message(&self) -> &str {
187 &self.message
188 }
189
190 #[must_use]
192 pub const fn kind(&self) -> CandidCallErrorKind {
193 self.kind
194 }
195
196 #[must_use]
198 pub fn context(&self) -> Option<&CandidCallContext> {
199 self.context.as_deref()
200 }
201
202 #[must_use]
204 pub fn reject_response(&self) -> Option<&RejectResponse> {
205 self.reject_response.as_deref()
206 }
207}
208
209impl std::fmt::Display for CandidCallError {
210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 f.write_str(&self.message)
212 }
213}
214
215impl std::error::Error for CandidCallError {}
216
217impl CanisterInstallError {
218 #[must_use]
220 pub const fn new(canister_id: Principal, message: String) -> Self {
221 Self {
222 canister_id,
223 label: None,
224 message,
225 }
226 }
227
228 #[must_use]
230 pub fn labeled(
231 canister_id: Principal,
232 label: impl Into<String>,
233 message: impl Into<String>,
234 ) -> Self {
235 Self {
236 canister_id,
237 label: Some(label.into()),
238 message: message.into(),
239 }
240 }
241
242 #[must_use]
244 pub const fn canister_id(&self) -> Principal {
245 self.canister_id
246 }
247
248 #[must_use]
250 pub fn message(&self) -> &str {
251 &self.message
252 }
253
254 #[must_use]
256 pub fn label(&self) -> Option<&str> {
257 self.label.as_deref()
258 }
259}
260
261impl std::fmt::Display for CanisterInstallError {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 if let Some(label) = &self.label {
264 write!(
265 f,
266 "failed to install canister {} ({label}): {}",
267 self.canister_id, self.message
268 )
269 } else {
270 write!(
271 f,
272 "failed to install canister {}: {}",
273 self.canister_id, self.message
274 )
275 }
276 }
277}
278
279impl std::error::Error for CanisterInstallError {}
280
281impl StandaloneCanisterInstallError {
282 pub(super) fn new(pocket_ic: PocketIc, install_error: CanisterInstallError) -> Self {
283 Self {
284 pocket_ic: Box::new(pocket_ic),
285 install_error,
286 }
287 }
288
289 #[must_use]
291 pub fn pocket_ic(&self) -> &PocketIc {
292 self.pocket_ic.as_ref()
293 }
294
295 #[must_use]
297 pub const fn install_error(&self) -> &CanisterInstallError {
298 &self.install_error
299 }
300
301 #[must_use]
303 pub fn into_parts(self) -> (PocketIc, CanisterInstallError) {
304 (*self.pocket_ic, self.install_error)
305 }
306}
307
308impl std::fmt::Debug for StandaloneCanisterInstallError {
309 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310 f.debug_struct("StandaloneCanisterInstallError")
311 .field("install_error", &self.install_error)
312 .finish_non_exhaustive()
313 }
314}
315
316impl std::fmt::Display for StandaloneCanisterInstallError {
317 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318 self.install_error.fmt(f)
319 }
320}
321
322impl std::error::Error for StandaloneCanisterInstallError {
323 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
324 Some(&self.install_error)
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use candid::Principal;
331 use pocket_ic::{ErrorCode, RejectCode, RejectResponse};
332
333 use super::{CandidCallContext, CandidCallError, CandidCallErrorKind, CanisterInstallError};
334
335 #[test]
336 fn labeled_install_error_display_includes_label() {
337 let err = CanisterInstallError::labeled(Principal::anonymous(), "authority", "trap");
338
339 assert_eq!(err.label(), Some("authority"));
340 assert!(err.to_string().contains("(authority): trap"));
341 }
342
343 #[test]
344 fn canister_reject_preserves_the_upstream_response() {
345 let response = RejectResponse {
346 reject_code: RejectCode::DestinationInvalid,
347 reject_message: "missing canister".to_string(),
348 error_code: ErrorCode::CanisterNotFound,
349 certified: true,
350 };
351 let error = CandidCallError::canister_reject(
352 CandidCallContext::new(
353 "query_call",
354 Principal::anonymous(),
355 Principal::management_canister(),
356 "get",
357 ),
358 response.clone(),
359 );
360
361 assert_eq!(error.kind(), CandidCallErrorKind::CanisterReject);
362 assert_eq!(error.reject_response(), Some(&response));
363 }
364}