1use errors::FfxError;
6use traceable_error_derive::TraceableError;
7
8#[derive(thiserror::Error, Debug)]
10#[error("non-fatal error encountered")]
11pub struct NonFatalError(#[source] pub anyhow::Error);
12
13#[derive(thiserror::Error, Debug, TraceableError)]
15pub enum Error {
16 Unexpected(#[source] anyhow::Error),
18 User(#[source] anyhow::Error),
20 Help {
23 command: Vec<String>,
25 output: String,
27 code: i32,
29 },
30 #[trace(opaque)]
34 IoError(#[from] std::io::Error),
35 Config(#[source] anyhow::Error),
42 ExitWithCode(i32),
44}
45
46impl Error {
47 pub fn downcast_non_fatal(self) -> Result<anyhow::Error, Self> {
50 fn try_downcast(err: anyhow::Error) -> Result<anyhow::Error, anyhow::Error> {
51 match err.downcast::<NonFatalError>() {
52 Ok(NonFatalError(e)) => Ok(e),
53 Err(e) => Err(e),
54 }
55 }
56
57 match self {
58 Self::Help { .. } | Self::ExitWithCode(_) | Self::IoError(_) => Err(self),
59 Self::User(e) => try_downcast(e).map_err(Self::User),
60 Self::Unexpected(e) => try_downcast(e).map_err(Self::Unexpected),
61 Self::Config(e) => try_downcast(e).map_err(Self::Config),
62 }
63 }
64
65 pub fn source(self) -> Result<anyhow::Error, Self> {
69 match self {
70 Self::User(e) | Self::Unexpected(e) | Self::Config(e) => Ok(e),
71 Self::Help { .. } | Self::ExitWithCode(_) | Self::IoError(_) => Err(self),
72 }
73 }
74}
75
76fn write_detailed(f: &mut std::fmt::Formatter<'_>, error: &anyhow::Error) -> std::fmt::Result {
78 write!(f, "Error: {}", error)?;
79 for (i, e) in error.chain().skip(1).enumerate() {
80 write!(f, "\n {: >3}. {}", i + 1, e)?;
81 }
82 Ok(())
83}
84
85fn write_display(f: &mut std::fmt::Formatter<'_>, error: &anyhow::Error) -> std::fmt::Result {
86 write!(f, "{error}")?;
87 let mut previous_error = error.to_string();
88 for e in error.chain().skip(1) {
89 let err_string = format!("{}", e);
109 let err_string = if err_string.is_empty() { "\"\"".to_owned() } else { err_string };
113 if err_string == previous_error {
114 continue;
115 }
116 write!(f, ": {}", err_string)?;
117 previous_error = err_string;
118 }
119 Ok(())
120}
121
122const BUG_LINE: &str = "BUG: An internal command error occurred.";
124impl std::fmt::Display for Error {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 match self {
128 Self::Unexpected(error) => {
129 writeln!(f, "{BUG_LINE}")?;
130 write_detailed(f, error)
131 }
132 Self::User(error) | Self::Config(error) => write_display(f, error),
133 Self::Help { output, .. } => write!(f, "{output}"),
134 Self::ExitWithCode(code) => write!(f, "Exiting with code {code}"),
135 Self::IoError(e) => write!(f, "I/O error: {e}"),
136 }
137 }
138}
139
140impl From<anyhow::Error> for Error {
141 fn from(error: anyhow::Error) -> Self {
142 match error.downcast::<Self>() {
144 Ok(this) => this,
145 Err(error) => match error.downcast::<FfxError>() {
148 Ok(err) => Self::User(err.into()),
149 Err(err) => Self::Unexpected(err),
150 },
151 }
152 }
153}
154
155impl From<FfxError> for Error {
156 fn from(error: FfxError) -> Self {
157 Error::User(error.into())
158 }
159}
160
161impl Error {
162 pub fn from_early_exit(command: &[impl AsRef<str>], early_exit: argh::EarlyExit) -> Self {
164 let command = Vec::from_iter(command.iter().map(|s| s.as_ref().to_owned()));
165 let output = early_exit.output;
166 match early_exit.status {
171 Ok(_) => Error::Help { command, output, code: 0 },
172 Err(_) => Error::Config(anyhow::anyhow!("{}", output)),
173 }
174 }
175
176 pub fn exit_code(&self) -> i32 {
178 match self {
179 Error::User(err) => {
180 if let Some(FfxError::Error(_, code)) = err.downcast_ref() {
181 *code
182 } else {
183 1
184 }
185 }
186 Error::Help { code, .. } => *code,
187 Error::ExitWithCode(code) => *code,
188 _ => 1,
189 }
190 }
191}
192
193pub type Result<T, E = crate::Error> = core::result::Result<T, E>;
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use crate::tests::*;
200 use anyhow::anyhow;
201 use assert_matches::assert_matches;
202 use errors::{IntoExitCode, ffx_error, ffx_error_with_code};
203 use std::io::{Cursor, Write};
204
205 #[test]
206 fn test_write_result_ffx_error() {
207 let err = Error::from(ffx_error!(FFX_STR));
208 let mut cursor = Cursor::new(Vec::new());
209
210 assert_matches!(write!(&mut cursor, "{err}"), Ok(_));
211
212 assert!(String::from_utf8(cursor.into_inner()).unwrap().contains(FFX_STR));
213 }
214
215 #[test]
216 fn into_error_from_arbitrary_is_unexpected() {
217 let err = anyhow!(ERR_STR);
218 assert_matches!(
219 Error::from(err),
220 Error::Unexpected(_),
221 "an arbitrary anyhow error should convert to an 'unexpected' bug check error"
222 );
223 }
224
225 #[test]
226 fn into_error_from_ffx_error_is_user_error() {
227 let err = FfxError::Error(anyhow!(FFX_STR), 1);
228 assert_matches!(
229 Error::from(err),
230 Error::User(_),
231 "an arbitrary anyhow error should convert to a 'user' error"
232 );
233 }
234
235 #[test]
236 fn into_error_from_contextualized_ffx_error_prints_original_error() {
237 let err = Error::from(anyhow::anyhow!(errors::ffx_error!(FFX_STR)).context("boom"));
238 assert_eq!(
239 &format!("{err}"),
240 FFX_STR,
241 "an anyhow error with context should print the original error, not the context, when stringified."
242 );
243 }
244
245 #[test]
246 fn test_write_result_arbitrary_error() {
247 let err = Error::from(anyhow!(ERR_STR));
248 let mut cursor = Cursor::new(Vec::new());
249
250 assert_matches!(write!(&mut cursor, "{err}"), Ok(_));
251
252 let err_str = String::from_utf8(cursor.into_inner()).unwrap();
253 assert!(err_str.contains(BUG_LINE));
254 assert!(err_str.contains(ERR_STR));
255 }
256
257 #[test]
258 fn test_result_ext_exit_code_ffx_error() {
259 let err = Result::<()>::Err(Error::from(ffx_error_with_code!(42, FFX_STR)));
260 assert_eq!(err.exit_code(), 42);
261 }
262
263 #[test]
264 fn test_from_ok_early_exit() {
265 let command = ["testing", "--help"];
266 let output = "stuff!".to_owned();
267 let status = Ok(());
268 let code = 0;
269
270 let early_exit = argh::EarlyExit { output: output.clone(), status };
271 let err = Error::from_early_exit(&command, early_exit);
272 assert_eq!(err.exit_code(), code);
273 assert_matches!(err, Error::Help { command: error_command, output: error_output, code: error_code } if error_command == command && error_output == output && error_code == code);
274 }
275
276 #[test]
277 fn test_from_error_early_exit() {
278 let command = ["testing", "bad", "command"];
279 let output = "stuff!".to_owned();
280 let status = Err(());
281 let code = 1;
282
283 let early_exit = argh::EarlyExit { output: output.clone(), status };
284 let err = Error::from_early_exit(&command, early_exit);
285 assert_eq!(err.exit_code(), code);
286 assert_matches!(err, Error::Config(err) if format!("{err}") == output);
287 }
288
289 #[test]
290 fn test_downcast_recasts_types() {
291 let err = Error::User(anyhow!("boom"));
292 assert_matches!(err.downcast_non_fatal(), Err(Error::User(_)));
293
294 let err = Error::Unexpected(anyhow!("boom"));
295 assert_matches!(err.downcast_non_fatal(), Err(Error::Unexpected(_)));
296
297 let err = Error::Config(anyhow!("boom"));
298 assert_matches!(err.downcast_non_fatal(), Err(Error::Config(_)));
299
300 let err =
301 Error::Help { command: vec!["foobar".to_owned()], output: "blorp".to_owned(), code: 1 };
302 assert_matches!(err.downcast_non_fatal(), Err(Error::Help { .. }));
303
304 let err = Error::ExitWithCode(2);
305 assert_matches!(err.downcast_non_fatal(), Err(Error::ExitWithCode(2)));
306 }
307
308 #[test]
309 fn test_downcast_non_fatal_recovers_non_fatal_error() {
310 static ERR_STR: &'static str = "Oh look it's non fatal";
311 let constructors = vec![Error::User, Error::Unexpected, Error::Config];
312 for c in constructors.into_iter() {
313 let err = c(NonFatalError(anyhow!(ERR_STR)).into());
314 let res = err.downcast_non_fatal().expect("expected non-fatal downcast");
315 assert_eq!(res.to_string(), ERR_STR.to_owned());
316 }
317 }
318
319 #[test]
320 fn test_error_source() {
321 static ERR_STR: &'static str = "some nonsense";
322 let constructors = vec![Error::User, Error::Unexpected, Error::Config];
323 for cons in constructors.into_iter() {
324 let err = cons(anyhow!(ERR_STR));
325 let res = err.source();
326 assert!(res.is_ok());
327 assert_eq!(res.unwrap().to_string(), ERR_STR.to_owned());
328 }
329 }
330
331 #[test]
332 fn test_error_source_flatten_no_context() {
333 assert_eq!("Some Operation", Error::User(anyhow!("Some Operation")).to_string());
334 }
335
336 #[test]
342 fn test_error_source_flatten_one_context() {
343 let expected = "Some Other Operation: some failure";
344 let error = anyhow!("some failure");
345 let error = error.context("Some Other Operation");
346 assert_eq!(expected, Error::User(error).to_string());
347 }
348
349 #[test]
350 fn test_error_source_flatten_two_contexts() {
351 let expected = "Some Operation: some context: some failure";
352 let error = anyhow!("some failure");
353 let error = error.context("some context");
354 let error = error.context("Some Operation");
355 assert_eq!(expected, Error::User(error).to_string());
356 }
357
358 #[test]
359 fn test_error_source_flatten_three_contexts() {
360 let expected = "Some Operation: some context: more context: some failure";
361 let error = anyhow!("some failure")
362 .context("more context")
363 .context("some context")
364 .context("Some Operation");
365 assert_eq!(expected, Error::User(error).to_string());
366 }
367
368 #[test]
369 fn test_error_doesnt_duplicate_when_rewrapped() {
370 #[derive(thiserror::Error, Debug)]
371 enum NonsenseErr {
372 #[error(transparent)]
373 Error(#[from] FfxError),
374 }
375 let expected = "This thing broke!";
376 let error = ffx_error!(anyhow!(expected));
377 let error: NonsenseErr = error.into();
378 let error = Error::User(error.into());
379 assert_eq!(
380 error.to_string(),
381 expected.to_owned(),
382 "There should be no duplication from re-wrapping errors"
383 );
384 }
385
386 #[test]
387 fn test_non_fatal_error_formatting() {
388 let inner = anyhow!("inner error");
389 let non_fatal = NonFatalError(inner);
390 let err = Error::User(anyhow!(non_fatal));
391 assert_eq!(format!("{}", err), "non-fatal error encountered: inner error");
392 }
393}