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;
6
7/// Represents a recoverable error. Intended to be embedded in `Error`.
8#[derive(thiserror::Error, Debug)]
9#[error("non-fatal error encountered: {}", .0)]
10pub struct NonFatalError(#[source] pub anyhow::Error);
11
12/// A top level error type for ffx tool results
13#[derive(thiserror::Error, Debug)]
14pub enum Error {
15    /// An error that qualifies as a bugcheck
16    Unexpected(#[source] anyhow::Error),
17    /// A known kind of error that can be reported usefully to the user
18    User(#[source] anyhow::Error),
19    /// An early-exit that should result in outputting help to the user (like [`argh::EarlyExit`]),
20    /// but is not itself an error in any meaningful sense.
21    Help {
22        /// The command name (argv[0..]) that should be used in supplemental help output
23        command: Vec<String>,
24        /// The text to output to the user
25        output: String,
26        /// The exit status
27        code: i32,
28    },
29    /// An error from general I/O. Meant mostly to handle things like write!() and such, but also
30    /// for potential issues with piping outputs of other commands into ffx. This isn't something
31    /// that's exactly common, but is a possibility.
32    IoError(#[from] std::io::Error),
33    /// Something failed before ffx's configuration could be loaded (like an
34    /// invalid argument, a failure to read an env config file, etc).
35    ///
36    /// Errors of this type should include any information the user might need
37    /// to recover from the issue, because it will not advise the user to look
38    /// in the log files or anything like that.
39    Config(#[source] anyhow::Error),
40    /// Exit with a specific error code but no output
41    ExitWithCode(i32),
42}
43
44impl Error {
45    /// Attempts to downcast this error into something non-fatal, returning `Ok(e)`
46    /// if able to downcast to something non-fatal, else returning the original error.
47    pub fn downcast_non_fatal(self) -> Result<anyhow::Error, Self> {
48        fn try_downcast(err: anyhow::Error) -> Result<anyhow::Error, anyhow::Error> {
49            match err.downcast::<NonFatalError>() {
50                Ok(NonFatalError(e)) => Ok(e),
51                Err(e) => Err(e),
52            }
53        }
54
55        match self {
56            Self::Help { .. } | Self::ExitWithCode(_) | Self::IoError(_) => Err(self),
57            Self::User(e) => try_downcast(e).map_err(Self::User),
58            Self::Unexpected(e) => try_downcast(e).map_err(Self::Unexpected),
59            Self::Config(e) => try_downcast(e).map_err(Self::Config),
60        }
61    }
62
63    /// Attempts to get the original `anyhow::Error` source (this is useful for chaining context
64    /// errors). If successful, returns `Ok(e)` with the error source, but if there's no error
65    /// source that can be returned, returns `self`.
66    pub fn source(self) -> Result<anyhow::Error, Self> {
67        match self {
68            Self::User(e) | Self::Unexpected(e) | Self::Config(e) => Ok(e),
69            Self::Help { .. } | Self::ExitWithCode(_) | Self::IoError(_) => Err(self),
70        }
71    }
72}
73
74/// Writes a detailed description of an anyhow error to the formatter
75fn write_detailed(f: &mut std::fmt::Formatter<'_>, error: &anyhow::Error) -> std::fmt::Result {
76    write!(f, "Error: {}", error)?;
77    for (i, e) in error.chain().skip(1).enumerate() {
78        write!(f, "\n  {: >3}.  {}", i + 1, e)?;
79    }
80    Ok(())
81}
82
83fn write_display(f: &mut std::fmt::Formatter<'_>, error: &anyhow::Error) -> std::fmt::Result {
84    write!(f, "{error}")?;
85    let mut previous_error = error.to_string();
86    for e in error.chain().skip(1) {
87        // This is a total hack. When errors are chained together through various thiserror
88        // wrappers, what can happen is the error will use this display function to make itself
89        // into a string, and the display function will show duplicates of the context chain.
90        //
91        // If, for example, we have something like `ffx_bail!` which returns an error, and it is
92        // encapsulated into a `thiserror` enum, and then later wrapped into a
93        // `ffx_command::Error::User`, we will have a context chain with the same error multiple
94        // times in a row. For example, say we have something like:
95        //
96        // ```
97        // let err = ffx_error!(anyhow!("this thing broke"));
98        // let err2 = LogError::FfxError(err);
99        // let err3 = ffx_command::Error::User(err2);
100        // eprintln!("{err3}");
101        // ```
102        //
103        // This will print: "this thing broke: this thing broke"
104        //
105        // This check will prevent that from happening without removing the context chain.
106        let err_string = format!("{}", e);
107        // There have been issues with empty strings in the past when formatting errors. Make
108        // sure to explicitly show that an empty string is in one of the errors so that it can
109        // be caught. This sort of thing used to happen with certain SSH errors.
110        let err_string = if err_string.is_empty() { "\"\"".to_owned() } else { err_string };
111        if err_string == previous_error {
112            continue;
113        }
114        write!(f, ": {}", err_string)?;
115        previous_error = err_string;
116    }
117    Ok(())
118}
119
120// LINT.IfChange
121const BUG_LINE: &str = "BUG: An internal command error occurred.";
122// LINT.ThenChange(//src/testing/end_to_end/honeydew/honeydew/affordances/session/session_using_ffx.py)
123impl std::fmt::Display for Error {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        match self {
126            Self::Unexpected(error) => {
127                writeln!(f, "{BUG_LINE}")?;
128                write_detailed(f, error)
129            }
130            Self::User(error) | Self::Config(error) => write_display(f, error),
131            Self::Help { output, .. } => write!(f, "{output}"),
132            Self::ExitWithCode(code) => write!(f, "Exiting with code {code}"),
133            Self::IoError(e) => write!(f, "I/O error: {e}"),
134        }
135    }
136}
137
138impl From<anyhow::Error> for Error {
139    fn from(error: anyhow::Error) -> Self {
140        // If it's already an Error, just return it
141        match error.downcast::<Self>() {
142            Ok(this) => this,
143            // this is just a compatibility shim to extract information out of the way
144            // we've traditionally divided user and unexpected errors.
145            Err(error) => match error.downcast::<FfxError>() {
146                Ok(err) => Self::User(err.into()),
147                Err(err) => Self::Unexpected(err),
148            },
149        }
150    }
151}
152
153impl From<FfxError> for Error {
154    fn from(error: FfxError) -> Self {
155        Error::User(error.into())
156    }
157}
158
159impl Error {
160    /// Map an argh early exit to our kind of error
161    pub fn from_early_exit(command: &[impl AsRef<str>], early_exit: argh::EarlyExit) -> Self {
162        let command = Vec::from_iter(command.iter().map(|s| s.as_ref().to_owned()));
163        let output = early_exit.output;
164        // if argh's early_exit status is Ok() that means it's printing help because
165        // of a `--help` argument or `help` as a subcommand was passed. Otherwise
166        // it's just an error parsing the arguments. So only map `status: Ok(())`
167        // as help output.
168        match early_exit.status {
169            Ok(_) => Error::Help { command, output, code: 0 },
170            Err(_) => Error::Config(anyhow::anyhow!("{}", output)),
171        }
172    }
173
174    /// Get the exit code this error should correspond to if it bubbles up to `main()`
175    pub fn exit_code(&self) -> i32 {
176        match self {
177            Error::User(err) => {
178                if let Some(FfxError::Error(_, code)) = err.downcast_ref() {
179                    *code
180                } else {
181                    1
182                }
183            }
184            Error::Help { code, .. } => *code,
185            Error::ExitWithCode(code) => *code,
186            _ => 1,
187        }
188    }
189}
190
191/// A convenience Result type
192pub type Result<T, E = crate::Error> = core::result::Result<T, E>;
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use crate::tests::*;
198    use anyhow::anyhow;
199    use assert_matches::assert_matches;
200    use errors::{IntoExitCode, ffx_error, ffx_error_with_code};
201    use std::io::{Cursor, Write};
202
203    #[test]
204    fn test_write_result_ffx_error() {
205        let err = Error::from(ffx_error!(FFX_STR));
206        let mut cursor = Cursor::new(Vec::new());
207
208        assert_matches!(write!(&mut cursor, "{err}"), Ok(_));
209
210        assert!(String::from_utf8(cursor.into_inner()).unwrap().contains(FFX_STR));
211    }
212
213    #[test]
214    fn into_error_from_arbitrary_is_unexpected() {
215        let err = anyhow!(ERR_STR);
216        assert_matches!(
217            Error::from(err),
218            Error::Unexpected(_),
219            "an arbitrary anyhow error should convert to an 'unexpected' bug check error"
220        );
221    }
222
223    #[test]
224    fn into_error_from_ffx_error_is_user_error() {
225        let err = FfxError::Error(anyhow!(FFX_STR), 1);
226        assert_matches!(
227            Error::from(err),
228            Error::User(_),
229            "an arbitrary anyhow error should convert to a 'user' error"
230        );
231    }
232
233    #[test]
234    fn into_error_from_contextualized_ffx_error_prints_original_error() {
235        let err = Error::from(anyhow::anyhow!(errors::ffx_error!(FFX_STR)).context("boom"));
236        assert_eq!(
237            &format!("{err}"),
238            FFX_STR,
239            "an anyhow error with context should print the original error, not the context, when stringified."
240        );
241    }
242
243    #[test]
244    fn test_write_result_arbitrary_error() {
245        let err = Error::from(anyhow!(ERR_STR));
246        let mut cursor = Cursor::new(Vec::new());
247
248        assert_matches!(write!(&mut cursor, "{err}"), Ok(_));
249
250        let err_str = String::from_utf8(cursor.into_inner()).unwrap();
251        assert!(err_str.contains(BUG_LINE));
252        assert!(err_str.contains(ERR_STR));
253    }
254
255    #[test]
256    fn test_result_ext_exit_code_ffx_error() {
257        let err = Result::<()>::Err(Error::from(ffx_error_with_code!(42, FFX_STR)));
258        assert_eq!(err.exit_code(), 42);
259    }
260
261    #[test]
262    fn test_from_ok_early_exit() {
263        let command = ["testing", "--help"];
264        let output = "stuff!".to_owned();
265        let status = Ok(());
266        let code = 0;
267
268        let early_exit = argh::EarlyExit { output: output.clone(), status };
269        let err = Error::from_early_exit(&command, early_exit);
270        assert_eq!(err.exit_code(), code);
271        assert_matches!(err, Error::Help { command: error_command, output: error_output, code: error_code } if error_command == command && error_output == output && error_code == code);
272    }
273
274    #[test]
275    fn test_from_error_early_exit() {
276        let command = ["testing", "bad", "command"];
277        let output = "stuff!".to_owned();
278        let status = Err(());
279        let code = 1;
280
281        let early_exit = argh::EarlyExit { output: output.clone(), status };
282        let err = Error::from_early_exit(&command, early_exit);
283        assert_eq!(err.exit_code(), code);
284        assert_matches!(err, Error::Config(err) if format!("{err}") == output);
285    }
286
287    #[test]
288    fn test_downcast_recasts_types() {
289        let err = Error::User(anyhow!("boom"));
290        assert_matches!(err.downcast_non_fatal(), Err(Error::User(_)));
291
292        let err = Error::Unexpected(anyhow!("boom"));
293        assert_matches!(err.downcast_non_fatal(), Err(Error::Unexpected(_)));
294
295        let err = Error::Config(anyhow!("boom"));
296        assert_matches!(err.downcast_non_fatal(), Err(Error::Config(_)));
297
298        let err =
299            Error::Help { command: vec!["foobar".to_owned()], output: "blorp".to_owned(), code: 1 };
300        assert_matches!(err.downcast_non_fatal(), Err(Error::Help { .. }));
301
302        let err = Error::ExitWithCode(2);
303        assert_matches!(err.downcast_non_fatal(), Err(Error::ExitWithCode(2)));
304    }
305
306    #[test]
307    fn test_downcast_non_fatal_recovers_non_fatal_error() {
308        static ERR_STR: &'static str = "Oh look it's non fatal";
309        let constructors = vec![Error::User, Error::Unexpected, Error::Config];
310        for c in constructors.into_iter() {
311            let err = c(NonFatalError(anyhow!(ERR_STR)).into());
312            let res = err.downcast_non_fatal().expect("expected non-fatal downcast");
313            assert_eq!(res.to_string(), ERR_STR.to_owned());
314        }
315    }
316
317    #[test]
318    fn test_error_source() {
319        static ERR_STR: &'static str = "some nonsense";
320        let constructors = vec![Error::User, Error::Unexpected, Error::Config];
321        for cons in constructors.into_iter() {
322            let err = cons(anyhow!(ERR_STR));
323            let res = err.source();
324            assert!(res.is_ok());
325            assert_eq!(res.unwrap().to_string(), ERR_STR.to_owned());
326        }
327    }
328
329    #[test]
330    fn test_error_source_flatten_no_context() {
331        assert_eq!("Some Operation", Error::User(anyhow!("Some Operation")).to_string());
332    }
333
334    // The order of context's is "in-side-out", the root-most error is
335    // created first, and then the context() is attached on all of the
336    // returned values, so they are created in the opposite order that they
337    // are displayed.
338
339    #[test]
340    fn test_error_source_flatten_one_context() {
341        let expected = "Some Other Operation: some failure";
342        let error = anyhow!("some failure");
343        let error = error.context("Some Other Operation");
344        assert_eq!(expected, Error::User(error).to_string());
345    }
346
347    #[test]
348    fn test_error_source_flatten_two_contexts() {
349        let expected = "Some Operation: some context: some failure";
350        let error = anyhow!("some failure");
351        let error = error.context("some context");
352        let error = error.context("Some Operation");
353        assert_eq!(expected, Error::User(error).to_string());
354    }
355
356    #[test]
357    fn test_error_source_flatten_three_contexts() {
358        let expected = "Some Operation: some context: more context: some failure";
359        let error = anyhow!("some failure")
360            .context("more context")
361            .context("some context")
362            .context("Some Operation");
363        assert_eq!(expected, Error::User(error).to_string());
364    }
365
366    #[test]
367    fn test_error_doesnt_duplicate_when_rewrapped() {
368        #[derive(thiserror::Error, Debug)]
369        enum NonsenseErr {
370            #[error(transparent)]
371            Error(#[from] FfxError),
372        }
373        let expected = "This thing broke!";
374        let error = ffx_error!(anyhow!(expected));
375        let error: NonsenseErr = error.into();
376        let error = Error::User(error.into());
377        assert_eq!(
378            error.to_string(),
379            expected.to_owned(),
380            "There should be no duplication from re-wrapping errors"
381        );
382    }
383}