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
106impl CandidCallError {
107 #[must_use]
109 pub fn new(message: impl Into<String>) -> Self {
110 Self {
111 message: message.into(),
112 kind: CandidCallErrorKind::Other,
113 context: None,
114 reject_response: None,
115 }
116 }
117
118 #[must_use]
120 pub fn encode(context: CandidCallContext, source: impl std::fmt::Display) -> Self {
121 let message = format!(
122 "candid encode_args failed (operation={}, canister={}, caller={}, method={}): {source}",
123 context.operation, context.canister_id, context.caller, context.method
124 );
125
126 Self {
127 message,
128 kind: CandidCallErrorKind::Encode,
129 context: Some(Box::new(context)),
130 reject_response: None,
131 }
132 }
133
134 #[must_use]
136 pub fn decode(
137 context: CandidCallContext,
138 bytes: usize,
139 source: impl std::fmt::Display,
140 ) -> Self {
141 let message = format!(
142 "candid decode failed (operation={}, canister={}, caller={}, method={}, bytes={}): {source}",
143 context.operation, context.canister_id, context.caller, context.method, bytes
144 );
145
146 Self {
147 message,
148 kind: CandidCallErrorKind::Decode,
149 context: Some(Box::new(context)),
150 reject_response: None,
151 }
152 }
153
154 #[must_use]
156 pub fn canister_reject(context: CandidCallContext, response: RejectResponse) -> Self {
157 let message = format!(
158 "pocket_ic {} was rejected (canister={}, caller={}, method={}): {response}",
159 context.operation, context.canister_id, context.caller, context.method
160 );
161
162 Self {
163 message,
164 kind: CandidCallErrorKind::CanisterReject,
165 context: Some(Box::new(context)),
166 reject_response: Some(Box::new(response)),
167 }
168 }
169
170 #[must_use]
172 pub fn transport(context: CandidCallContext, source: impl std::fmt::Display) -> Self {
173 let message = format!(
174 "pocket_ic {} failed (canister={}, caller={}, method={}): {source}",
175 context.operation, context.canister_id, context.caller, context.method
176 );
177
178 Self {
179 message,
180 kind: CandidCallErrorKind::Transport,
181 context: Some(Box::new(context)),
182 reject_response: None,
183 }
184 }
185}
186
187impl std::fmt::Display for CandidCallError {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 f.write_str(&self.message)
190 }
191}
192
193impl std::error::Error for CandidCallError {}
194
195impl CanisterInstallError {
196 #[must_use]
198 pub const fn new(
199 phase: CanisterInstallPhase,
200 canister_id: Option<Principal>,
201 label: Option<String>,
202 source: PocketIcOperationError,
203 ) -> Self {
204 Self {
205 phase,
206 canister_id,
207 label,
208 source,
209 }
210 }
211
212 #[must_use]
214 pub const fn phase(&self) -> CanisterInstallPhase {
215 self.phase
216 }
217
218 #[must_use]
220 pub const fn canister_id(&self) -> Option<Principal> {
221 self.canister_id
222 }
223
224 #[must_use]
226 pub fn message(&self) -> &str {
227 self.source.message()
228 }
229
230 #[must_use]
232 pub const fn operation_error(&self) -> &PocketIcOperationError {
233 &self.source
234 }
235
236 #[must_use]
238 pub fn label(&self) -> Option<&str> {
239 self.label.as_deref()
240 }
241}
242
243impl std::fmt::Display for CanisterInstallError {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 write!(f, "{} failed", self.phase.operation())?;
246 if let Some(canister_id) = self.canister_id {
247 write!(f, " for canister {canister_id}")?;
248 }
249 if let Some(label) = &self.label {
250 write!(f, " ({label})")?;
251 }
252 write!(f, ": {}", self.source)
253 }
254}
255
256impl std::error::Error for CanisterInstallError {
257 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
258 Some(&self.source)
259 }
260}
261
262impl StandaloneCanisterInstallError {
263 pub(super) fn new(
264 instance: PocketIcInstance<PocketIc>,
265 install_error: CanisterInstallError,
266 ) -> Self {
267 Self {
268 instance: Box::new(instance),
269 install_error,
270 }
271 }
272
273 #[must_use]
275 pub fn pocket_ic(&self) -> &PocketIc {
276 &self.instance.instance
277 }
278
279 #[must_use]
281 pub const fn install_error(&self) -> &CanisterInstallError {
282 &self.install_error
283 }
284
285 pub fn into_parts(self) -> (PocketIcInstance<PocketIc>, CanisterInstallError) {
287 (*self.instance, self.install_error)
288 }
289}
290
291impl std::fmt::Debug for StandaloneCanisterInstallError {
292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293 f.debug_struct("StandaloneCanisterInstallError")
294 .field("install_error", &self.install_error)
295 .finish_non_exhaustive()
296 }
297}
298
299impl std::fmt::Display for StandaloneCanisterInstallError {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 self.install_error.fmt(f)
302 }
303}
304
305impl std::error::Error for StandaloneCanisterInstallError {
306 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
307 Some(&self.install_error)
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use candid::Principal;
314 use pocket_ic::{ErrorCode, RejectCode, RejectResponse};
315
316 use super::{CandidCallContext, CandidCallError, CandidCallErrorKind, CanisterInstallError};
317
318 #[test]
319 fn labeled_install_error_display_includes_label() {
320 let err = CanisterInstallError::new(
321 super::CanisterInstallPhase::InstallCode,
322 Some(Principal::anonymous()),
323 Some("authority".into()),
324 super::PocketIcOperationError::new("trap"),
325 );
326
327 assert_eq!(err.label(), Some("authority"));
328 assert!(err.to_string().contains("(authority): trap"));
329 }
330
331 #[test]
332 fn canister_reject_preserves_the_upstream_response() {
333 let response = RejectResponse {
334 reject_code: RejectCode::DestinationInvalid,
335 reject_message: "missing canister".to_string(),
336 error_code: ErrorCode::CanisterNotFound,
337 certified: true,
338 };
339 let error = CandidCallError::canister_reject(
340 CandidCallContext::new(
341 "query_call",
342 Principal::anonymous(),
343 Principal::management_canister(),
344 "get",
345 ),
346 response.clone(),
347 );
348
349 assert_eq!(error.kind, CandidCallErrorKind::CanisterReject);
350 assert_eq!(error.reject_response.as_deref(), Some(&response));
351 }
352}