1use candid::Principal;
2use pocket_ic::{PocketIc, RejectResponse};
3
4use super::PocketIcOperationError;
5
6#[non_exhaustive]
8#[derive(Clone, Debug, Eq, PartialEq)]
9pub struct CandidCallError {
10 pub message: String,
12 pub kind: CandidCallErrorKind,
14 pub context: Option<Box<CandidCallContext>>,
16 pub reject_response: Option<Box<RejectResponse>>,
18}
19
20#[non_exhaustive]
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum CandidCallErrorKind {
24 Encode,
26 Decode,
28 CanisterReject,
30 Transport,
32 Other,
34}
35
36#[non_exhaustive]
38#[derive(Clone, Debug, Eq, PartialEq)]
39pub struct CandidCallContext {
40 pub operation: &'static str,
42 pub canister_id: Principal,
44 pub caller: Principal,
46 pub method: String,
48}
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
52pub enum CanisterInstallPhase {
53 CreateCanister,
55 AddCycles,
57 InstallCode,
59}
60
61impl CanisterInstallPhase {
62 #[must_use]
64 pub const fn operation(self) -> &'static str {
65 match self {
66 Self::CreateCanister => "create_canister",
67 Self::AddCycles => "add_cycles",
68 Self::InstallCode => "install_canister",
69 }
70 }
71}
72
73#[derive(Debug, Eq, PartialEq)]
75pub struct CanisterInstallError {
76 phase: CanisterInstallPhase,
77 canister_id: Option<Principal>,
78 label: Option<String>,
79 source: PocketIcOperationError,
80}
81
82pub struct StandaloneCanisterInstallError {
84 pocket_ic: Box<PocketIc>,
85 install_error: CanisterInstallError,
86}
87
88impl CandidCallContext {
89 #[must_use]
91 pub fn new(
92 operation: &'static str,
93 canister_id: Principal,
94 caller: Principal,
95 method: impl Into<String>,
96 ) -> Self {
97 Self {
98 operation,
99 canister_id,
100 caller,
101 method: method.into(),
102 }
103 }
104
105 #[must_use]
107 pub const fn operation(&self) -> &'static str {
108 self.operation
109 }
110
111 #[must_use]
113 pub const fn canister_id(&self) -> Principal {
114 self.canister_id
115 }
116
117 #[must_use]
119 pub const fn caller(&self) -> Principal {
120 self.caller
121 }
122
123 #[must_use]
125 pub fn method(&self) -> &str {
126 &self.method
127 }
128}
129
130impl CandidCallError {
131 #[must_use]
133 pub fn new(message: impl Into<String>) -> Self {
134 Self {
135 message: message.into(),
136 kind: CandidCallErrorKind::Other,
137 context: None,
138 reject_response: None,
139 }
140 }
141
142 #[must_use]
144 pub fn encode(context: CandidCallContext, source: impl std::fmt::Display) -> Self {
145 let message = format!(
146 "candid encode_args failed (operation={}, canister={}, caller={}, method={}): {source}",
147 context.operation, context.canister_id, context.caller, context.method
148 );
149
150 Self {
151 message,
152 kind: CandidCallErrorKind::Encode,
153 context: Some(Box::new(context)),
154 reject_response: None,
155 }
156 }
157
158 #[must_use]
160 pub fn decode(
161 context: CandidCallContext,
162 bytes: usize,
163 source: impl std::fmt::Display,
164 ) -> Self {
165 let message = format!(
166 "candid decode_one failed (operation={}, canister={}, caller={}, method={}, bytes={}): {source}",
167 context.operation, context.canister_id, context.caller, context.method, bytes
168 );
169
170 Self {
171 message,
172 kind: CandidCallErrorKind::Decode,
173 context: Some(Box::new(context)),
174 reject_response: None,
175 }
176 }
177
178 #[must_use]
180 pub fn canister_reject(context: CandidCallContext, response: RejectResponse) -> Self {
181 let message = format!(
182 "pocket_ic {} was rejected (canister={}, caller={}, method={}): {response}",
183 context.operation, context.canister_id, context.caller, context.method
184 );
185
186 Self {
187 message,
188 kind: CandidCallErrorKind::CanisterReject,
189 context: Some(Box::new(context)),
190 reject_response: Some(Box::new(response)),
191 }
192 }
193
194 #[must_use]
196 pub fn transport(context: CandidCallContext, source: impl std::fmt::Display) -> Self {
197 let message = format!(
198 "pocket_ic {} failed (canister={}, caller={}, method={}): {source}",
199 context.operation, context.canister_id, context.caller, context.method
200 );
201
202 Self {
203 message,
204 kind: CandidCallErrorKind::Transport,
205 context: Some(Box::new(context)),
206 reject_response: None,
207 }
208 }
209
210 #[must_use]
212 pub fn message(&self) -> &str {
213 &self.message
214 }
215
216 #[must_use]
218 pub const fn kind(&self) -> CandidCallErrorKind {
219 self.kind
220 }
221
222 #[must_use]
224 pub fn context(&self) -> Option<&CandidCallContext> {
225 self.context.as_deref()
226 }
227
228 #[must_use]
230 pub fn reject_response(&self) -> Option<&RejectResponse> {
231 self.reject_response.as_deref()
232 }
233}
234
235impl std::fmt::Display for CandidCallError {
236 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237 f.write_str(&self.message)
238 }
239}
240
241impl std::error::Error for CandidCallError {}
242
243impl CanisterInstallError {
244 #[must_use]
246 pub const fn new(
247 phase: CanisterInstallPhase,
248 canister_id: Option<Principal>,
249 label: Option<String>,
250 source: PocketIcOperationError,
251 ) -> Self {
252 Self {
253 phase,
254 canister_id,
255 label,
256 source,
257 }
258 }
259
260 #[must_use]
262 pub const fn phase(&self) -> CanisterInstallPhase {
263 self.phase
264 }
265
266 #[must_use]
268 pub const fn canister_id(&self) -> Option<Principal> {
269 self.canister_id
270 }
271
272 #[must_use]
274 pub fn message(&self) -> &str {
275 self.source.message()
276 }
277
278 #[must_use]
280 pub const fn operation_error(&self) -> &PocketIcOperationError {
281 &self.source
282 }
283
284 #[must_use]
286 pub fn label(&self) -> Option<&str> {
287 self.label.as_deref()
288 }
289}
290
291impl std::fmt::Display for CanisterInstallError {
292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293 write!(f, "{} failed", self.phase.operation())?;
294 if let Some(canister_id) = self.canister_id {
295 write!(f, " for canister {canister_id}")?;
296 }
297 if let Some(label) = &self.label {
298 write!(f, " ({label})")?;
299 }
300 write!(f, ": {}", self.source)
301 }
302}
303
304impl std::error::Error for CanisterInstallError {
305 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
306 Some(&self.source)
307 }
308}
309
310impl StandaloneCanisterInstallError {
311 pub(super) fn new(pocket_ic: PocketIc, install_error: CanisterInstallError) -> Self {
312 Self {
313 pocket_ic: Box::new(pocket_ic),
314 install_error,
315 }
316 }
317
318 #[must_use]
320 pub fn pocket_ic(&self) -> &PocketIc {
321 self.pocket_ic.as_ref()
322 }
323
324 #[must_use]
326 pub const fn install_error(&self) -> &CanisterInstallError {
327 &self.install_error
328 }
329
330 #[must_use]
332 pub fn into_parts(self) -> (PocketIc, CanisterInstallError) {
333 (*self.pocket_ic, self.install_error)
334 }
335}
336
337impl std::fmt::Debug for StandaloneCanisterInstallError {
338 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339 f.debug_struct("StandaloneCanisterInstallError")
340 .field("install_error", &self.install_error)
341 .finish_non_exhaustive()
342 }
343}
344
345impl std::fmt::Display for StandaloneCanisterInstallError {
346 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
347 self.install_error.fmt(f)
348 }
349}
350
351impl std::error::Error for StandaloneCanisterInstallError {
352 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
353 Some(&self.install_error)
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use candid::Principal;
360 use pocket_ic::{ErrorCode, RejectCode, RejectResponse};
361
362 use super::{CandidCallContext, CandidCallError, CandidCallErrorKind, CanisterInstallError};
363
364 #[test]
365 fn labeled_install_error_display_includes_label() {
366 let err = CanisterInstallError::new(
367 super::CanisterInstallPhase::InstallCode,
368 Some(Principal::anonymous()),
369 Some("authority".into()),
370 super::PocketIcOperationError::new("trap"),
371 );
372
373 assert_eq!(err.label(), Some("authority"));
374 assert!(err.to_string().contains("(authority): trap"));
375 }
376
377 #[test]
378 fn canister_reject_preserves_the_upstream_response() {
379 let response = RejectResponse {
380 reject_code: RejectCode::DestinationInvalid,
381 reject_message: "missing canister".to_string(),
382 error_code: ErrorCode::CanisterNotFound,
383 certified: true,
384 };
385 let error = CandidCallError::canister_reject(
386 CandidCallContext::new(
387 "query_call",
388 Principal::anonymous(),
389 Principal::management_canister(),
390 "get",
391 ),
392 response.clone(),
393 );
394
395 assert_eq!(error.kind(), CandidCallErrorKind::CanisterReject);
396 assert_eq!(error.reject_response(), Some(&response));
397 }
398}