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 IoError(#[from] std::io::Error),
33 Config(#[source] anyhow::Error),
40 ExitWithCode(i32),
42}
43
44impl Error {
45 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 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
74fn 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 let err_string = format!("{}", e);
107 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
120const BUG_LINE: &str = "BUG: An internal command error occurred.";
122impl 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 match error.downcast::<Self>() {
142 Ok(this) => this,
143 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 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 match early_exit.status {
169 Ok(_) => Error::Help { command, output, code: 0 },
170 Err(_) => Error::Config(anyhow::anyhow!("{}", output)),
171 }
172 }
173
174 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
191pub 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 #[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}