Skip to main content

ffx_command_error/
error.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use errors::{FfxError, IntoExitCode};
6use traceable_error_derive::TraceableError;
7
8/// Represents a recoverable error. Intended to be embedded in `Error`.
9#[derive(thiserror::Error, Debug)]
10#[error("non-fatal error encountered")]
11pub struct NonFatalError(#[source] pub anyhow::Error);
12
13/// A top level error type for ffx tool results
14#[derive(thiserror::Error, Debug, TraceableError)]
15pub enum Error {
16    /// An error that qualifies as a bugcheck
17    Unexpected(#[source] anyhow::Error),
18    /// A known kind of error that can be reported usefully to the user
19    User(#[source] anyhow::Error),
20    /// An early-exit that should result in outputting help to the user (like [`argh::EarlyExit`]),
21    /// but is not itself an error in any meaningful sense.
22    Help {
23        /// The command name (argv[0..]) that should be used in supplemental help output
24        command: Vec<String>,
25        /// The text to output to the user
26        output: String,
27        /// The exit status
28        code: i32,
29    },
30    /// An error from general I/O. Meant mostly to handle things like write!() and such, but also
31    /// for potential issues with piping outputs of other commands into ffx. This isn't something
32    /// that's exactly common, but is a possibility.
33    #[trace(opaque)]
34    IoError(#[from] std::io::Error),
35    /// Something failed before ffx's configuration could be loaded (like an
36    /// invalid argument, a failure to read an env config file, etc).
37    ///
38    /// Errors of this type should include any information the user might need
39    /// to recover from the issue, because it will not advise the user to look
40    /// in the log files or anything like that.
41    Config(#[source] anyhow::Error),
42    /// Exit with a specific error code but no output
43    ExitWithCode(i32),
44}
45
46impl Error {
47    /// Attempts to downcast this error into something non-fatal, returning `Ok(e)`
48    /// if able to downcast to something non-fatal, else returning the original error.
49    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    /// Attempts to get the original `anyhow::Error` source (this is useful for chaining context
66    /// errors). If successful, returns `Ok(e)` with the error source, but if there's no error
67    /// source that can be returned, returns `self`.
68    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
76/// Writes a detailed description of an anyhow error to the formatter
77fn 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        // This is a total hack. When errors are chained together through various thiserror
93        // wrappers, what can happen is the error will use this display function to make itself
94        // into a string, and the display function will show duplicates of the context chain.
95        //
96        // If, for example, we have something like `ffx_bail!` which returns an error, and it is
97        // encapsulated into a `thiserror` enum, and then later wrapped into a
98        // `ffx_command::Error::User`, we will have a context chain with the same error multiple
99        // times in a row. For example, say we have something like:
100        //
101        // ```
102        // let err = ffx_error!(anyhow!("this thing broke"));
103        // let err2 = LogError::FfxError(err);
104        // let err3 = ffx_command::Error::User(err2);
105        // eprintln!("{err3}");
106        // ```
107        //
108        // This will print: "this thing broke: this thing broke"
109        //
110        // This check will prevent that from happening without removing the context chain.
111        // We check for containment using `.contains()` (rather than exact equality or
112        // ends_with) to robustly handle cases where a wrapper's message includes its
113        // source's message (which is common with `#[error("...: {0}")]`).
114        let err_string = format!("{}", e);
115        // There have been issues with empty strings in the past when formatting errors. Make
116        // sure to explicitly show that an empty string is in one of the errors so that it can
117        // be caught. This sort of thing used to happen with certain SSH errors.
118        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
128// LINT.IfChange
129const BUG_LINE: &str = "BUG: An internal command error occurred.";
130// LINT.ThenChange(//src/testing/end_to_end/honeydew/honeydew/affordances/session/session_using_ffx.py)
131impl 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        // If it's already an Error, just return it
149        match error.downcast::<Self>() {
150            Ok(this) => this,
151            // this is just a compatibility shim to extract information out of the way
152            // we've traditionally divided user and unexpected errors.
153            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    /// Map an argh early exit to our kind of error
171    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        // if argh's early_exit status is Ok() that means it's printing help because
175        // of a `--help` argument or `help` as a subcommand was passed. Otherwise
176        // it's just an error parsing the arguments. So only map `status: Ok(())`
177        // as help output.
178        match early_exit.status {
179            Ok(_) => Error::Help { command, output, code: 0 },
180            Err(_) => Error::Config(anyhow::anyhow!("{}", output)),
181        }
182    }
183
184    /// Get the exit code this error should correspond to if it bubbles up to `main()`
185    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
195/// A convenience Result type
196pub 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    // The order of context's is "in-side-out", the root-most error is
339    // created first, and then the context() is attached on all of the
340    // returned values, so they are created in the opposite order that they
341    // are displayed.
342
343    #[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}