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