1use candid::Principal;
2use pocket_ic::{PocketIc, RejectResponse};
3
4use super::{PocketIcInstance, 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 instance: Box<PocketIcInstance<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(
312 instance: PocketIcInstance<PocketIc>,
313 install_error: CanisterInstallError,
314 ) -> Self {
315 Self {
316 instance: Box::new(instance),
317 install_error,
318 }
319 }
320
321 #[must_use]
323 pub fn pocket_ic(&self) -> &PocketIc {
324 &self.instance.instance
325 }
326
327 #[must_use]
329 pub const fn install_error(&self) -> &CanisterInstallError {
330 &self.install_error
331 }
332
333 pub fn into_parts(self) -> (PocketIcInstance<PocketIc>, CanisterInstallError) {
335 (*self.instance, self.install_error)
336 }
337}
338
339impl std::fmt::Debug for StandaloneCanisterInstallError {
340 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
341 f.debug_struct("StandaloneCanisterInstallError")
342 .field("install_error", &self.install_error)
343 .finish_non_exhaustive()
344 }
345}
346
347impl std::fmt::Display for StandaloneCanisterInstallError {
348 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349 self.install_error.fmt(f)
350 }
351}
352
353impl std::error::Error for StandaloneCanisterInstallError {
354 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
355 Some(&self.install_error)
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use candid::Principal;
362 use pocket_ic::{ErrorCode, RejectCode, RejectResponse};
363
364 use super::{CandidCallContext, CandidCallError, CandidCallErrorKind, CanisterInstallError};
365
366 #[test]
367 fn labeled_install_error_display_includes_label() {
368 let err = CanisterInstallError::new(
369 super::CanisterInstallPhase::InstallCode,
370 Some(Principal::anonymous()),
371 Some("authority".into()),
372 super::PocketIcOperationError::new("trap"),
373 );
374
375 assert_eq!(err.label(), Some("authority"));
376 assert!(err.to_string().contains("(authority): trap"));
377 }
378
379 #[test]
380 fn canister_reject_preserves_the_upstream_response() {
381 let response = RejectResponse {
382 reject_code: RejectCode::DestinationInvalid,
383 reject_message: "missing canister".to_string(),
384 error_code: ErrorCode::CanisterNotFound,
385 certified: true,
386 };
387 let error = CandidCallError::canister_reject(
388 CandidCallContext::new(
389 "query_call",
390 Principal::anonymous(),
391 Principal::management_canister(),
392 "get",
393 ),
394 response.clone(),
395 );
396
397 assert_eq!(error.kind(), CandidCallErrorKind::CanisterReject);
398 assert_eq!(error.reject_response(), Some(&response));
399 }
400}