1use errors::{FfxError, IntoExitCode};
6use traceable_error_derive::TraceableError;
7
8#[derive(thiserror::Error, Debug)]
10#[error("non-fatal error encountered")]
11pub struct NonFatalError(#[source] pub anyhow::Error);
12
13#[derive(thiserror::Error, Debug, TraceableError)]
15pub enum Error {
16 Unexpected(#[source] anyhow::Error),
18 User(#[source] anyhow::Error),
20 Help {
23 command: Vec<String>,
25 output: String,
27 code: i32,
29 },
30 #[trace(opaque)]
34 IoError(#[from] std::io::Error),
35 Config(#[source] anyhow::Error),
42 ExitWithCode(i32),
44}
45
46impl Error {
47 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 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
76fn 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 Ok(())
83}
84
85fn write_display(f: &mut std::fmt::Formatter<'_>, error: &anyhow::Error) -> std::fmt::Result {
86 write!(f, "{error}")?;
87 let mut previous_error = error.to_string();
88 for e in error.chain().skip(1) {
89 let err_string = format!("{}", e);
112 let err_string = if err_string.is_empty() { "\"\"".to_owned() } else { err_string };
116 if previous_error.contains(&err_string) {
117 continue;
118 }
119 write!(f, ": {}", err_string)?;
120 previous_error = err_string;
121 }
122 Ok(())
123}
124
125const BUG_LINE: &str = "BUG: An internal command error occurred.";
127impl std::fmt::Display for Error {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 match self {
131 Self::Unexpected(error) => {
132 writeln!(f, "{BUG_LINE}")?;
133 write_detailed(f, error)
134 }
135 Self::User(error) | Self::Config(error) => write_display(f, error),
136 Self::Help { output, .. } => write!(f, "{output}"),
137 Self::ExitWithCode(code) => write!(f, "Exiting with code {code}"),
138 Self::IoError(e) => write!(f, "I/O error: {e}"),
139 }
140 }
141}
142
143impl From<anyhow::Error> for Error {
144 fn from(error: anyhow::Error) -> Self {
145 match error.downcast::<Self>() {
147 Ok(this) => this,
148 Err(error) => match error.downcast::<FfxError>() {
151 Ok(err) => {
152 Self::User(anyhow::Error::from(traceable_error::TraceableBox::from(err)))
153 }
154 Err(err) => Self::Unexpected(err),
155 },
156 }
157 }
158}
159
160impl From<FfxError> for Error {
161 fn from(error: FfxError) -> Self {
162 Error::User(anyhow::Error::from(traceable_error::TraceableBox::from(error)))
163 }
164}
165
166impl Error {
167 pub fn from_early_exit(command: &[impl AsRef<str>], early_exit: argh::EarlyExit) -> Self {
169 let command = Vec::from_iter(command.iter().map(|s| s.as_ref().to_owned()));
170 let output = early_exit.output;
171 match early_exit.status {
176 Ok(_) => Error::Help { command, output, code: 0 },
177 Err(_) => Error::Config(anyhow::anyhow!("{}", output)),
178 }
179 }
180
181 pub fn exit_code(&self) -> i32 {
183 match self {
184 Error::User(err) => err.exit_code(),
185 Error::Help { code, .. } => *code,
186 Error::ExitWithCode(code) => *code,
187 _ => 1,
188 }
189 }
190}
191
192pub type Result<T, E = crate::Error> = core::result::Result<T, E>;
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::tests::*;
199 use anyhow::anyhow;
200 use assert_matches::assert_matches;
201 use errors::{IntoExitCode, ffx_error, ffx_error_with_code};
202 use std::io::{Cursor, Write};
203
204 #[test]
205 fn test_write_result_ffx_error() {
206 let err = Error::from(ffx_error!(FFX_STR));
207 let mut cursor = Cursor::new(Vec::new());
208
209 assert_matches!(write!(&mut cursor, "{err}"), Ok(_));
210
211 assert!(String::from_utf8(cursor.into_inner()).unwrap().contains(FFX_STR));
212 }
213
214 #[test]
215 fn into_error_from_arbitrary_is_unexpected() {
216 let err = anyhow!(ERR_STR);
217 assert_matches!(
218 Error::from(err),
219 Error::Unexpected(_),
220 "an arbitrary anyhow error should convert to an 'unexpected' bug check error"
221 );
222 }
223
224 #[test]
225 fn into_error_from_ffx_error_is_user_error() {
226 let err = FfxError::Error(anyhow!(FFX_STR), 1);
227 assert_matches!(
228 Error::from(err),
229 Error::User(_),
230 "an arbitrary anyhow error should convert to a 'user' error"
231 );
232 }
233
234 #[test]
235 fn into_error_from_contextualized_ffx_error_prints_original_error() {
236 let err = Error::from(anyhow::anyhow!(errors::ffx_error!(FFX_STR)).context("boom"));
237 assert_eq!(
238 &format!("{err}"),
239 FFX_STR,
240 "an anyhow error with context should print the original error, not the context, when stringified."
241 );
242 }
243
244 #[test]
245 fn test_write_result_arbitrary_error() {
246 let err = Error::from(anyhow!(ERR_STR));
247 let mut cursor = Cursor::new(Vec::new());
248
249 assert_matches!(write!(&mut cursor, "{err}"), Ok(_));
250
251 let err_str = String::from_utf8(cursor.into_inner()).unwrap();
252 assert!(err_str.contains(BUG_LINE));
253 assert!(err_str.contains(ERR_STR));
254 }
255
256 #[test]
257 fn test_result_ext_exit_code_ffx_error() {
258 let err = Result::<()>::Err(Error::from(ffx_error_with_code!(42, FFX_STR)));
259 assert_eq!(err.exit_code(), 42);
260 }
261
262 #[test]
263 fn test_from_ok_early_exit() {
264 let command = ["testing", "--help"];
265 let output = "stuff!".to_owned();
266 let status = Ok(());
267 let code = 0;
268
269 let early_exit = argh::EarlyExit { output: output.clone(), status };
270 let err = Error::from_early_exit(&command, early_exit);
271 assert_eq!(err.exit_code(), code);
272 assert_matches!(err, Error::Help { command: error_command, output: error_output, code: error_code } if error_command == command && error_output == output && error_code == code);
273 }
274
275 #[test]
276 fn test_from_error_early_exit() {
277 let command = ["testing", "bad", "command"];
278 let output = "stuff!".to_owned();
279 let status = Err(());
280 let code = 1;
281
282 let early_exit = argh::EarlyExit { output: output.clone(), status };
283 let err = Error::from_early_exit(&command, early_exit);
284 assert_eq!(err.exit_code(), code);
285 assert_matches!(err, Error::Config(err) if format!("{err}") == output);
286 }
287
288 #[test]
289 fn test_downcast_recasts_types() {
290 let err = Error::User(anyhow!("boom"));
291 assert_matches!(err.downcast_non_fatal(), Err(Error::User(_)));
292
293 let err = Error::Unexpected(anyhow!("boom"));
294 assert_matches!(err.downcast_non_fatal(), Err(Error::Unexpected(_)));
295
296 let err = Error::Config(anyhow!("boom"));
297 assert_matches!(err.downcast_non_fatal(), Err(Error::Config(_)));
298
299 let err =
300 Error::Help { command: vec!["foobar".to_owned()], output: "blorp".to_owned(), code: 1 };
301 assert_matches!(err.downcast_non_fatal(), Err(Error::Help { .. }));
302
303 let err = Error::ExitWithCode(2);
304 assert_matches!(err.downcast_non_fatal(), Err(Error::ExitWithCode(2)));
305 }
306
307 #[test]
308 fn test_downcast_non_fatal_recovers_non_fatal_error() {
309 static ERR_STR: &'static str = "Oh look it's non fatal";
310 let constructors = vec![Error::User, Error::Unexpected, Error::Config];
311 for c in constructors.into_iter() {
312 let err = c(NonFatalError(anyhow!(ERR_STR)).into());
313 let res = err.downcast_non_fatal().expect("expected non-fatal downcast");
314 assert_eq!(res.to_string(), ERR_STR.to_owned());
315 }
316 }
317
318 #[test]
319 fn test_error_source() {
320 static ERR_STR: &'static str = "some nonsense";
321 let constructors = vec![Error::User, Error::Unexpected, Error::Config];
322 for cons in constructors.into_iter() {
323 let err = cons(anyhow!(ERR_STR));
324 let res = err.source();
325 assert!(res.is_ok());
326 assert_eq!(res.unwrap().to_string(), ERR_STR.to_owned());
327 }
328 }
329
330 #[test]
331 fn test_error_source_flatten_no_context() {
332 assert_eq!("Some Operation", Error::User(anyhow!("Some Operation")).to_string());
333 }
334
335 #[test]
341 fn test_error_source_flatten_one_context() {
342 let expected = "Some Other Operation: some failure";
343 let error = anyhow!("some failure");
344 let error = error.context("Some Other Operation");
345 assert_eq!(expected, Error::User(error).to_string());
346 }
347
348 #[test]
349 fn test_error_source_flatten_two_contexts() {
350 let expected = "Some Operation: some context: some failure";
351 let error = anyhow!("some failure");
352 let error = error.context("some context");
353 let error = error.context("Some Operation");
354 assert_eq!(expected, Error::User(error).to_string());
355 }
356
357 #[test]
358 fn test_error_source_flatten_three_contexts() {
359 let expected = "Some Operation: some context: more context: some failure";
360 let error = anyhow!("some failure")
361 .context("more context")
362 .context("some context")
363 .context("Some Operation");
364 assert_eq!(expected, Error::User(error).to_string());
365 }
366
367 #[test]
368 fn test_error_doesnt_duplicate_when_rewrapped() {
369 #[derive(thiserror::Error, Debug)]
370 enum NonsenseErr {
371 #[error(transparent)]
372 Error(#[from] FfxError),
373 }
374 let expected = "This thing broke!";
375 let error = ffx_error!(anyhow!(expected));
376 let error: NonsenseErr = error.into();
377 let error = Error::User(error.into());
378 assert_eq!(
379 error.to_string(),
380 expected.to_owned(),
381 "There should be no duplication from re-wrapping errors"
382 );
383 }
384
385 #[test]
386 fn test_non_fatal_error_formatting() {
387 let inner = anyhow!("inner error");
388 let non_fatal = NonFatalError(inner);
389 let err = Error::User(anyhow!(non_fatal));
390 assert_eq!(format!("{}", err), "non-fatal error encountered: inner error");
391 }
392
393 #[test]
394 fn test_error_doesnt_duplicate_alternating() {
395 #[derive(thiserror::Error, Debug)]
396 #[error("Prefix: {0}")]
397 struct OuterError(#[source] anyhow::Error);
398
399 #[derive(thiserror::Error, Debug)]
400 #[error("NonFatal")]
401 struct NonFatal(#[source] anyhow::Error);
402
403 #[derive(thiserror::Error, Debug)]
404 #[error("TargetNotFound")]
405 struct TargetNotFound;
406
407 let leaf = TargetNotFound;
408 let non_fatal = NonFatal(anyhow::Error::new(leaf));
409 let fho_err = Error::User(anyhow::Error::new(non_fatal));
410 let outer = OuterError(anyhow::Error::new(fho_err));
411 let top = Error::User(anyhow::Error::new(outer));
412
413 let formatted = top.to_string();
414 assert_eq!(formatted, "Prefix: NonFatal: TargetNotFound");
415 }
416}