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