1use errors::{FfxError, IntoExitCode};
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 if error.backtrace().status() == std::backtrace::BacktraceStatus::Captured {
83 write!(f, "\n\nStack backtrace:\n{}", error.backtrace().to_string().trim_end())?;
84 }
85 Ok(())
86}
87
88fn write_display(f: &mut std::fmt::Formatter<'_>, error: &anyhow::Error) -> std::fmt::Result {
89 write!(f, "{error}")?;
90 let mut previous_error = error.to_string();
91 for e in error.chain().skip(1) {
92 let err_string = format!("{}", e);
115 let err_string = if err_string.is_empty() { "\"\"".to_owned() } else { err_string };
119 if previous_error.contains(&err_string) {
120 continue;
121 }
122 write!(f, ": {}", err_string)?;
123 previous_error = err_string;
124 }
125 Ok(())
126}
127
128const BUG_LINE: &str = "BUG: An internal command error occurred.";
130impl std::fmt::Display for Error {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 match self {
134 Self::Unexpected(error) => {
135 writeln!(f, "{BUG_LINE}")?;
136 write_detailed(f, error)
137 }
138 Self::User(error) | Self::Config(error) => write_display(f, error),
139 Self::Help { output, .. } => write!(f, "{output}"),
140 Self::ExitWithCode(code) => write!(f, "Exiting with code {code}"),
141 Self::IoError(e) => write!(f, "I/O error: {e}"),
142 }
143 }
144}
145
146impl From<anyhow::Error> for Error {
147 fn from(error: anyhow::Error) -> Self {
148 match error.downcast::<Self>() {
150 Ok(this) => this,
151 Err(error) => match error.downcast::<FfxError>() {
154 Ok(err) => {
155 Self::User(anyhow::Error::from(traceable_error::TraceableBox::from(err)))
156 }
157 Err(err) => Self::Unexpected(err),
158 },
159 }
160 }
161}
162
163impl From<FfxError> for Error {
164 fn from(error: FfxError) -> Self {
165 Error::User(anyhow::Error::from(traceable_error::TraceableBox::from(error)))
166 }
167}
168
169impl Error {
170 pub fn from_early_exit(command: &[impl AsRef<str>], early_exit: argh::EarlyExit) -> Self {
172 let command = Vec::from_iter(command.iter().map(|s| s.as_ref().to_owned()));
173 let output = early_exit.output;
174 match early_exit.status {
179 Ok(_) => Error::Help { command, output, code: 0 },
180 Err(_) => Error::Config(anyhow::anyhow!("{}", output)),
181 }
182 }
183
184 pub fn exit_code(&self) -> i32 {
186 match self {
187 Error::User(err) => err.exit_code(),
188 Error::Help { code, .. } => *code,
189 Error::ExitWithCode(code) => *code,
190 _ => 1,
191 }
192 }
193}
194
195pub type Result<T, E = crate::Error> = core::result::Result<T, E>;
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use crate::tests::*;
202 use anyhow::anyhow;
203 use assert_matches::assert_matches;
204 use errors::{IntoExitCode, ffx_error, ffx_error_with_code};
205 use std::io::{Cursor, Write};
206
207 #[test]
208 fn test_write_result_ffx_error() {
209 let err = Error::from(ffx_error!(FFX_STR));
210 let mut cursor = Cursor::new(Vec::new());
211
212 assert_matches!(write!(&mut cursor, "{err}"), Ok(_));
213
214 assert!(String::from_utf8(cursor.into_inner()).unwrap().contains(FFX_STR));
215 }
216
217 #[test]
218 fn into_error_from_arbitrary_is_unexpected() {
219 let err = anyhow!(ERR_STR);
220 assert_matches!(
221 Error::from(err),
222 Error::Unexpected(_),
223 "an arbitrary anyhow error should convert to an 'unexpected' bug check error"
224 );
225 }
226
227 #[test]
228 fn into_error_from_ffx_error_is_user_error() {
229 let err = FfxError::Error(anyhow!(FFX_STR), 1);
230 assert_matches!(
231 Error::from(err),
232 Error::User(_),
233 "an arbitrary anyhow error should convert to a 'user' error"
234 );
235 }
236
237 #[test]
238 fn into_error_from_contextualized_ffx_error_prints_original_error() {
239 let err = Error::from(anyhow::anyhow!(errors::ffx_error!(FFX_STR)).context("boom"));
240 assert_eq!(
241 &format!("{err}"),
242 FFX_STR,
243 "an anyhow error with context should print the original error, not the context, when stringified."
244 );
245 }
246
247 #[test]
248 fn test_write_result_arbitrary_error() {
249 let err = Error::from(anyhow!(ERR_STR));
250 let mut cursor = Cursor::new(Vec::new());
251
252 assert_matches!(write!(&mut cursor, "{err}"), Ok(_));
253
254 let err_str = String::from_utf8(cursor.into_inner()).unwrap();
255 assert!(err_str.contains(BUG_LINE));
256 assert!(err_str.contains(ERR_STR));
257 }
258
259 #[test]
260 fn test_result_ext_exit_code_ffx_error() {
261 let err = Result::<()>::Err(Error::from(ffx_error_with_code!(42, FFX_STR)));
262 assert_eq!(err.exit_code(), 42);
263 }
264
265 #[test]
266 fn test_from_ok_early_exit() {
267 let command = ["testing", "--help"];
268 let output = "stuff!".to_owned();
269 let status = Ok(());
270 let code = 0;
271
272 let early_exit = argh::EarlyExit { output: output.clone(), status };
273 let err = Error::from_early_exit(&command, early_exit);
274 assert_eq!(err.exit_code(), code);
275 assert_matches!(err, Error::Help { command: error_command, output: error_output, code: error_code } if error_command == command && error_output == output && error_code == code);
276 }
277
278 #[test]
279 fn test_from_error_early_exit() {
280 let command = ["testing", "bad", "command"];
281 let output = "stuff!".to_owned();
282 let status = Err(());
283 let code = 1;
284
285 let early_exit = argh::EarlyExit { output: output.clone(), status };
286 let err = Error::from_early_exit(&command, early_exit);
287 assert_eq!(err.exit_code(), code);
288 assert_matches!(err, Error::Config(err) if format!("{err}") == output);
289 }
290
291 #[test]
292 fn test_downcast_recasts_types() {
293 let err = Error::User(anyhow!("boom"));
294 assert_matches!(err.downcast_non_fatal(), Err(Error::User(_)));
295
296 let err = Error::Unexpected(anyhow!("boom"));
297 assert_matches!(err.downcast_non_fatal(), Err(Error::Unexpected(_)));
298
299 let err = Error::Config(anyhow!("boom"));
300 assert_matches!(err.downcast_non_fatal(), Err(Error::Config(_)));
301
302 let err =
303 Error::Help { command: vec!["foobar".to_owned()], output: "blorp".to_owned(), code: 1 };
304 assert_matches!(err.downcast_non_fatal(), Err(Error::Help { .. }));
305
306 let err = Error::ExitWithCode(2);
307 assert_matches!(err.downcast_non_fatal(), Err(Error::ExitWithCode(2)));
308 }
309
310 #[test]
311 fn test_downcast_non_fatal_recovers_non_fatal_error() {
312 static ERR_STR: &'static str = "Oh look it's non fatal";
313 let constructors = vec![Error::User, Error::Unexpected, Error::Config];
314 for c in constructors.into_iter() {
315 let err = c(NonFatalError(anyhow!(ERR_STR)).into());
316 let res = err.downcast_non_fatal().expect("expected non-fatal downcast");
317 assert_eq!(res.to_string(), ERR_STR.to_owned());
318 }
319 }
320
321 #[test]
322 fn test_error_source() {
323 static ERR_STR: &'static str = "some nonsense";
324 let constructors = vec![Error::User, Error::Unexpected, Error::Config];
325 for cons in constructors.into_iter() {
326 let err = cons(anyhow!(ERR_STR));
327 let res = err.source();
328 assert!(res.is_ok());
329 assert_eq!(res.unwrap().to_string(), ERR_STR.to_owned());
330 }
331 }
332
333 #[test]
334 fn test_error_source_flatten_no_context() {
335 assert_eq!("Some Operation", Error::User(anyhow!("Some Operation")).to_string());
336 }
337
338 #[test]
344 fn test_error_source_flatten_one_context() {
345 let expected = "Some Other Operation: some failure";
346 let error = anyhow!("some failure");
347 let error = error.context("Some Other Operation");
348 assert_eq!(expected, Error::User(error).to_string());
349 }
350
351 #[test]
352 fn test_error_source_flatten_two_contexts() {
353 let expected = "Some Operation: some context: some failure";
354 let error = anyhow!("some failure");
355 let error = error.context("some context");
356 let error = error.context("Some Operation");
357 assert_eq!(expected, Error::User(error).to_string());
358 }
359
360 #[test]
361 fn test_error_source_flatten_three_contexts() {
362 let expected = "Some Operation: some context: more context: some failure";
363 let error = anyhow!("some failure")
364 .context("more context")
365 .context("some context")
366 .context("Some Operation");
367 assert_eq!(expected, Error::User(error).to_string());
368 }
369
370 #[test]
371 fn test_error_doesnt_duplicate_when_rewrapped() {
372 #[derive(thiserror::Error, Debug)]
373 enum NonsenseErr {
374 #[error(transparent)]
375 Error(#[from] FfxError),
376 }
377 let expected = "This thing broke!";
378 let error = ffx_error!(anyhow!(expected));
379 let error: NonsenseErr = error.into();
380 let error = Error::User(error.into());
381 assert_eq!(
382 error.to_string(),
383 expected.to_owned(),
384 "There should be no duplication from re-wrapping errors"
385 );
386 }
387
388 #[test]
389 fn test_non_fatal_error_formatting() {
390 let inner = anyhow!("inner error");
391 let non_fatal = NonFatalError(inner);
392 let err = Error::User(anyhow!(non_fatal));
393 assert_eq!(format!("{}", err), "non-fatal error encountered: inner error");
394 }
395
396 #[test]
397 fn test_error_doesnt_duplicate_alternating() {
398 #[derive(thiserror::Error, Debug)]
399 #[error("Prefix: {0}")]
400 struct OuterError(#[source] anyhow::Error);
401
402 #[derive(thiserror::Error, Debug)]
403 #[error("NonFatal")]
404 struct NonFatal(#[source] anyhow::Error);
405
406 #[derive(thiserror::Error, Debug)]
407 #[error("TargetNotFound")]
408 struct TargetNotFound;
409
410 let leaf = TargetNotFound;
411 let non_fatal = NonFatal(anyhow::Error::new(leaf));
412 let fho_err = Error::User(anyhow::Error::new(non_fatal));
413 let outer = OuterError(anyhow::Error::new(fho_err));
414 let top = Error::User(anyhow::Error::new(outer));
415
416 let formatted = top.to_string();
417 assert_eq!(formatted, "Prefix: NonFatal: TargetNotFound");
418 }
419
420 #[test]
421 fn test_unexpected_error_backtrace_captured() {
422 if std::env::var("FFX_TEST_BACKTRACE_CHILD").as_deref() == Ok("1") {
423 let err = Error::Unexpected(anyhow!("root cause").context("top context"));
424 let formatted = format!("{err}");
425 assert!(
426 formatted.starts_with(
427 "BUG: An internal command error occurred.\nError: top context\n 1. root cause\n\nStack backtrace:\n"
428 ),
429 "unexpected format: {formatted}"
430 );
431 return;
432 }
433 let status = std::process::Command::new(std::env::current_exe().unwrap())
434 .arg("--exact")
435 .arg("error::tests::test_unexpected_error_backtrace_captured")
436 .env("FFX_TEST_BACKTRACE_CHILD", "1")
437 .env("RUST_BACKTRACE", "1")
438 .env_remove("RUST_LIB_BACKTRACE")
439 .status()
440 .unwrap();
441 assert!(status.success());
442 }
443
444 #[test]
445 fn test_unexpected_error_backtrace_disabled() {
446 if std::env::var("FFX_TEST_BACKTRACE_CHILD").as_deref() == Ok("1") {
447 let err = Error::Unexpected(anyhow!("root cause").context("top context"));
448 let formatted = format!("{err}");
449 assert_eq!(
450 formatted,
451 "BUG: An internal command error occurred.\nError: top context\n 1. root cause"
452 );
453 return;
454 }
455 let status = std::process::Command::new(std::env::current_exe().unwrap())
456 .arg("--exact")
457 .arg("error::tests::test_unexpected_error_backtrace_disabled")
458 .env("FFX_TEST_BACKTRACE_CHILD", "1")
459 .env("RUST_BACKTRACE", "0")
460 .env("RUST_LIB_BACKTRACE", "0")
461 .status()
462 .unwrap();
463 assert!(status.success());
464 }
465}