1use errors::FfxError;
6
7#[derive(thiserror::Error, Debug)]
9#[error("non-fatal error encountered: {}", .0)]
10pub struct NonFatalError(#[source] pub anyhow::Error);
11
12#[derive(thiserror::Error, Debug)]
14pub enum Error {
15 Unexpected(#[source] anyhow::Error),
17 User(#[source] anyhow::Error),
19 Help {
22 command: Vec<String>,
24 output: String,
26 code: i32,
28 },
29 Config(#[source] anyhow::Error),
36 ExitWithCode(i32),
38}
39
40impl Error {
41 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
60fn 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 match error.downcast::<Self>() {
88 Ok(this) => this,
89 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 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 match early_exit.status {
115 Ok(_) => Error::Help { command, output, code: 0 },
116 Err(_) => Error::Config(anyhow::anyhow!("{}", output)),
117 }
118 }
119
120 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
137pub 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}