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    /// Something failed before ffx's configuration could be loaded (like an
30    /// invalid argument, a failure to read an env config file, etc).
31    ///
32    /// Errors of this type should include any information the user might need
33    /// to recover from the issue, because it will not advise the user to look
34    /// in the log files or anything like that.
35    Config(#[source] anyhow::Error),
36    /// Exit with a specific error code but no output
37    ExitWithCode(i32),
38}
39
40impl Error {
41    /// Attempts to downcast this error into something non-fatal, returning `Ok(e)`
42    /// if able to downcast to something non-fatal, else returning the original error.
43    pub fn downcast_non_fatal(self) -> Result<anyhow::Error, Self> {
44        fn try_downcast(err: anyhow::Error) -> Result<anyhow::Error, anyhow::Error> {
45            match err.downcast::<NonFatalError>() {
46                Ok(NonFatalError(e)) => Ok(e),
47                Err(e) => Err(e),
48            }
49        }
50
51        match self {
52            Self::Help { .. } | Self::ExitWithCode(_) => Err(self),
53            Self::User(e) => try_downcast(e).map_err(Self::User),
54            Self::Unexpected(e) => try_downcast(e).map_err(Self::Unexpected),
55            Self::Config(e) => try_downcast(e).map_err(Self::Config),
56        }
57    }
58}
59
60/// Writes a detailed description of an anyhow error to the formatter
61fn write_detailed(f: &mut std::fmt::Formatter<'_>, error: &anyhow::Error) -> std::fmt::Result {
62    write!(f, "Error: {}", error)?;
63    for (i, e) in error.chain().skip(1).enumerate() {
64        write!(f, "\n  {: >3}.  {}", i + 1, e)?;
65    }
66    Ok(())
67}
68
69const BUG_LINE: &str = "BUG: An internal command error occurred.";
70impl std::fmt::Display for Error {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        match self {
73            Self::Unexpected(error) => {
74                writeln!(f, "{BUG_LINE}")?;
75                write_detailed(f, error)
76            }
77            Self::User(error) | Self::Config(error) => write!(f, "{error}"),
78            Self::Help { output, .. } => write!(f, "{output}"),
79            Self::ExitWithCode(code) => write!(f, "Exiting with code {code}"),
80        }
81    }
82}
83
84impl From<anyhow::Error> for Error {
85    fn from(error: anyhow::Error) -> Self {
86        // If it's already an Error, just return it
87        match error.downcast::<Self>() {
88            Ok(this) => this,
89            // this is just a compatibility shim to extract information out of the way
90            // we've traditionally divided user and unexpected errors.
91            Err(error) => match error.downcast::<FfxError>() {
92                Ok(err) => Self::User(err.into()),
93                Err(err) => Self::Unexpected(err),
94            },
95        }
96    }
97}
98
99impl From<FfxError> for Error {
100    fn from(error: FfxError) -> Self {
101        Error::User(error.into())
102    }
103}
104
105impl Error {
106    /// Map an argh early exit to our kind of error
107    pub fn from_early_exit(command: &[impl AsRef<str>], early_exit: argh::EarlyExit) -> Self {
108        let command = Vec::from_iter(command.iter().map(|s| s.as_ref().to_owned()));
109        let output = early_exit.output;
110        // if argh's early_exit status is Ok() that means it's printing help because
111        // of a `--help` argument or `help` as a subcommand was passed. Otherwise
112        // it's just an error parsing the arguments. So only map `status: Ok(())`
113        // as help output.
114        match early_exit.status {
115            Ok(_) => Error::Help { command, output, code: 0 },
116            Err(_) => Error::Config(anyhow::anyhow!("{}", output)),
117        }
118    }
119
120    /// Get the exit code this error should correspond to if it bubbles up to `main()`
121    pub fn exit_code(&self) -> i32 {
122        match self {
123            Error::User(err) => {
124                if let Some(FfxError::Error(_, code)) = err.downcast_ref() {
125                    *code
126                } else {
127                    1
128                }
129            }
130            Error::Help { code, .. } => *code,
131            Error::ExitWithCode(code) => *code,
132            _ => 1,
133        }
134    }
135}
136
137/// A convenience Result type
138pub type Result<T, E = crate::Error> = core::result::Result<T, E>;
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crate::tests::*;
144    use anyhow::anyhow;
145    use assert_matches::assert_matches;
146    use errors::{ffx_error, ffx_error_with_code, IntoExitCode};
147    use std::io::{Cursor, Write};
148
149    #[test]
150    fn test_write_result_ffx_error() {
151        let err = Error::from(ffx_error!(FFX_STR));
152        let mut cursor = Cursor::new(Vec::new());
153
154        assert_matches!(write!(&mut cursor, "{err}"), Ok(_));
155
156        assert!(String::from_utf8(cursor.into_inner()).unwrap().contains(FFX_STR));
157    }
158
159    #[test]
160    fn into_error_from_arbitrary_is_unexpected() {
161        let err = anyhow!(ERR_STR);
162        assert_matches!(
163            Error::from(err),
164            Error::Unexpected(_),
165            "an arbitrary anyhow error should convert to an 'unexpected' bug check error"
166        );
167    }
168
169    #[test]
170    fn into_error_from_ffx_error_is_user_error() {
171        let err = FfxError::Error(anyhow!(FFX_STR), 1);
172        assert_matches!(
173            Error::from(err),
174            Error::User(_),
175            "an arbitrary anyhow error should convert to a 'user' error"
176        );
177    }
178
179    #[test]
180    fn into_error_from_contextualized_ffx_error_prints_original_error() {
181        let err = Error::from(anyhow::anyhow!(errors::ffx_error!(FFX_STR)).context("boom"));
182        assert_eq!(
183            &format!("{err}"),
184            FFX_STR,
185            "an anyhow error with context should print the original error, not the context, when stringified."
186        );
187    }
188
189    #[test]
190    fn test_write_result_arbitrary_error() {
191        let err = Error::from(anyhow!(ERR_STR));
192        let mut cursor = Cursor::new(Vec::new());
193
194        assert_matches!(write!(&mut cursor, "{err}"), Ok(_));
195
196        let err_str = String::from_utf8(cursor.into_inner()).unwrap();
197        assert!(err_str.contains(BUG_LINE));
198        assert!(err_str.contains(ERR_STR));
199    }
200
201    #[test]
202    fn test_result_ext_exit_code_ffx_error() {
203        let err = Result::<()>::Err(Error::from(ffx_error_with_code!(42, FFX_STR)));
204        assert_eq!(err.exit_code(), 42);
205    }
206
207    #[test]
208    fn test_from_ok_early_exit() {
209        let command = ["testing", "--help"];
210        let output = "stuff!".to_owned();
211        let status = Ok(());
212        let code = 0;
213
214        let early_exit = argh::EarlyExit { output: output.clone(), status };
215        let err = Error::from_early_exit(&command, early_exit);
216        assert_eq!(err.exit_code(), code);
217        assert_matches!(err, Error::Help { command: error_command, output: error_output, code: error_code } if error_command == command && error_output == output && error_code == code);
218    }
219
220    #[test]
221    fn test_from_error_early_exit() {
222        let command = ["testing", "bad", "command"];
223        let output = "stuff!".to_owned();
224        let status = Err(());
225        let code = 1;
226
227        let early_exit = argh::EarlyExit { output: output.clone(), status };
228        let err = Error::from_early_exit(&command, early_exit);
229        assert_eq!(err.exit_code(), code);
230        assert_matches!(err, Error::Config(err) if format!("{err}") == output);
231    }
232
233    #[test]
234    fn test_downcast_recasts_types() {
235        let err = Error::User(anyhow!("boom"));
236        assert_matches!(err.downcast_non_fatal(), Err(Error::User(_)));
237
238        let err = Error::Unexpected(anyhow!("boom"));
239        assert_matches!(err.downcast_non_fatal(), Err(Error::Unexpected(_)));
240
241        let err = Error::Config(anyhow!("boom"));
242        assert_matches!(err.downcast_non_fatal(), Err(Error::Config(_)));
243
244        let err =
245            Error::Help { command: vec!["foobar".to_owned()], output: "blorp".to_owned(), code: 1 };
246        assert_matches!(err.downcast_non_fatal(), Err(Error::Help { .. }));
247
248        let err = Error::ExitWithCode(2);
249        assert_matches!(err.downcast_non_fatal(), Err(Error::ExitWithCode(2)));
250    }
251
252    #[test]
253    fn test_downcast_non_fatal_recovers_non_fatal_error() {
254        static ERR_STR: &'static str = "Oh look it's non fatal";
255        let constructors = vec![Error::User, Error::Unexpected, Error::Config];
256        for c in constructors.into_iter() {
257            let err = c(NonFatalError(anyhow!(ERR_STR)).into());
258            let res = err.downcast_non_fatal().expect("expected non-fatal downcast");
259            assert_eq!(res.to_string(), ERR_STR.to_owned());
260        }
261    }
262}