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 pub fn source(self) -> Result<anyhow::Error, Self> {
63 match self {
64 Self::User(e) | Self::Unexpected(e) | Self::Config(e) => Ok(e),
65 Self::Help { .. } | Self::ExitWithCode(_) => Err(self),
66 }
67 }
68}
69
70fn write_detailed(f: &mut std::fmt::Formatter<'_>, error: &anyhow::Error) -> std::fmt::Result {
72 write!(f, "Error: {}", error)?;
73 for (i, e) in error.chain().skip(1).enumerate() {
74 write!(f, "\n {: >3}. {}", i + 1, e)?;
75 }
76 Ok(())
77}
78
79fn write_display(f: &mut std::fmt::Formatter<'_>, error: &anyhow::Error) -> std::fmt::Result {
80 write!(f, "{error}")?;
81 let mut previous_error = error.to_string();
82 for e in error.chain().skip(1) {
83 let err_string = format!("{}", e);
103 let err_string = if err_string.is_empty() { "\"\"".to_owned() } else { err_string };
107 if err_string == previous_error {
108 continue;
109 }
110 write!(f, ": {}", err_string)?;
111 previous_error = err_string;
112 }
113 Ok(())
114}
115
116const BUG_LINE: &str = "BUG: An internal command error occurred.";
117impl std::fmt::Display for Error {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 match self {
120 Self::Unexpected(error) => {
121 writeln!(f, "{BUG_LINE}")?;
122 write_detailed(f, error)
123 }
124 Self::User(error) | Self::Config(error) => write_display(f, error),
125 Self::Help { output, .. } => write!(f, "{output}"),
126 Self::ExitWithCode(code) => write!(f, "Exiting with code {code}"),
127 }
128 }
129}
130
131impl From<anyhow::Error> for Error {
132 fn from(error: anyhow::Error) -> Self {
133 match error.downcast::<Self>() {
135 Ok(this) => this,
136 Err(error) => match error.downcast::<FfxError>() {
139 Ok(err) => Self::User(err.into()),
140 Err(err) => Self::Unexpected(err),
141 },
142 }
143 }
144}
145
146impl From<FfxError> for Error {
147 fn from(error: FfxError) -> Self {
148 Error::User(error.into())
149 }
150}
151
152impl Error {
153 pub fn from_early_exit(command: &[impl AsRef<str>], early_exit: argh::EarlyExit) -> Self {
155 let command = Vec::from_iter(command.iter().map(|s| s.as_ref().to_owned()));
156 let output = early_exit.output;
157 match early_exit.status {
162 Ok(_) => Error::Help { command, output, code: 0 },
163 Err(_) => Error::Config(anyhow::anyhow!("{}", output)),
164 }
165 }
166
167 pub fn exit_code(&self) -> i32 {
169 match self {
170 Error::User(err) => {
171 if let Some(FfxError::Error(_, code)) = err.downcast_ref() {
172 *code
173 } else {
174 1
175 }
176 }
177 Error::Help { code, .. } => *code,
178 Error::ExitWithCode(code) => *code,
179 _ => 1,
180 }
181 }
182}
183
184pub type Result<T, E = crate::Error> = core::result::Result<T, E>;
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use crate::tests::*;
191 use anyhow::anyhow;
192 use assert_matches::assert_matches;
193 use errors::{ffx_error, ffx_error_with_code, IntoExitCode};
194 use std::io::{Cursor, Write};
195
196 #[test]
197 fn test_write_result_ffx_error() {
198 let err = Error::from(ffx_error!(FFX_STR));
199 let mut cursor = Cursor::new(Vec::new());
200
201 assert_matches!(write!(&mut cursor, "{err}"), Ok(_));
202
203 assert!(String::from_utf8(cursor.into_inner()).unwrap().contains(FFX_STR));
204 }
205
206 #[test]
207 fn into_error_from_arbitrary_is_unexpected() {
208 let err = anyhow!(ERR_STR);
209 assert_matches!(
210 Error::from(err),
211 Error::Unexpected(_),
212 "an arbitrary anyhow error should convert to an 'unexpected' bug check error"
213 );
214 }
215
216 #[test]
217 fn into_error_from_ffx_error_is_user_error() {
218 let err = FfxError::Error(anyhow!(FFX_STR), 1);
219 assert_matches!(
220 Error::from(err),
221 Error::User(_),
222 "an arbitrary anyhow error should convert to a 'user' error"
223 );
224 }
225
226 #[test]
227 fn into_error_from_contextualized_ffx_error_prints_original_error() {
228 let err = Error::from(anyhow::anyhow!(errors::ffx_error!(FFX_STR)).context("boom"));
229 assert_eq!(
230 &format!("{err}"),
231 FFX_STR,
232 "an anyhow error with context should print the original error, not the context, when stringified."
233 );
234 }
235
236 #[test]
237 fn test_write_result_arbitrary_error() {
238 let err = Error::from(anyhow!(ERR_STR));
239 let mut cursor = Cursor::new(Vec::new());
240
241 assert_matches!(write!(&mut cursor, "{err}"), Ok(_));
242
243 let err_str = String::from_utf8(cursor.into_inner()).unwrap();
244 assert!(err_str.contains(BUG_LINE));
245 assert!(err_str.contains(ERR_STR));
246 }
247
248 #[test]
249 fn test_result_ext_exit_code_ffx_error() {
250 let err = Result::<()>::Err(Error::from(ffx_error_with_code!(42, FFX_STR)));
251 assert_eq!(err.exit_code(), 42);
252 }
253
254 #[test]
255 fn test_from_ok_early_exit() {
256 let command = ["testing", "--help"];
257 let output = "stuff!".to_owned();
258 let status = Ok(());
259 let code = 0;
260
261 let early_exit = argh::EarlyExit { output: output.clone(), status };
262 let err = Error::from_early_exit(&command, early_exit);
263 assert_eq!(err.exit_code(), code);
264 assert_matches!(err, Error::Help { command: error_command, output: error_output, code: error_code } if error_command == command && error_output == output && error_code == code);
265 }
266
267 #[test]
268 fn test_from_error_early_exit() {
269 let command = ["testing", "bad", "command"];
270 let output = "stuff!".to_owned();
271 let status = Err(());
272 let code = 1;
273
274 let early_exit = argh::EarlyExit { output: output.clone(), status };
275 let err = Error::from_early_exit(&command, early_exit);
276 assert_eq!(err.exit_code(), code);
277 assert_matches!(err, Error::Config(err) if format!("{err}") == output);
278 }
279
280 #[test]
281 fn test_downcast_recasts_types() {
282 let err = Error::User(anyhow!("boom"));
283 assert_matches!(err.downcast_non_fatal(), Err(Error::User(_)));
284
285 let err = Error::Unexpected(anyhow!("boom"));
286 assert_matches!(err.downcast_non_fatal(), Err(Error::Unexpected(_)));
287
288 let err = Error::Config(anyhow!("boom"));
289 assert_matches!(err.downcast_non_fatal(), Err(Error::Config(_)));
290
291 let err =
292 Error::Help { command: vec!["foobar".to_owned()], output: "blorp".to_owned(), code: 1 };
293 assert_matches!(err.downcast_non_fatal(), Err(Error::Help { .. }));
294
295 let err = Error::ExitWithCode(2);
296 assert_matches!(err.downcast_non_fatal(), Err(Error::ExitWithCode(2)));
297 }
298
299 #[test]
300 fn test_downcast_non_fatal_recovers_non_fatal_error() {
301 static ERR_STR: &'static str = "Oh look it's non fatal";
302 let constructors = vec![Error::User, Error::Unexpected, Error::Config];
303 for c in constructors.into_iter() {
304 let err = c(NonFatalError(anyhow!(ERR_STR)).into());
305 let res = err.downcast_non_fatal().expect("expected non-fatal downcast");
306 assert_eq!(res.to_string(), ERR_STR.to_owned());
307 }
308 }
309
310 #[test]
311 fn test_error_source() {
312 static ERR_STR: &'static str = "some nonsense";
313 let constructors = vec![Error::User, Error::Unexpected, Error::Config];
314 for cons in constructors.into_iter() {
315 let err = cons(anyhow!(ERR_STR));
316 let res = err.source();
317 assert!(res.is_ok());
318 assert_eq!(res.unwrap().to_string(), ERR_STR.to_owned());
319 }
320 }
321
322 #[test]
323 fn test_error_source_flatten_no_context() {
324 assert_eq!("Some Operation", Error::User(anyhow!("Some Operation")).to_string());
325 }
326
327 #[test]
333 fn test_error_source_flatten_one_context() {
334 let expected = "Some Other Operation: some failure";
335 let error = anyhow!("some failure");
336 let error = error.context("Some Other Operation");
337 assert_eq!(expected, Error::User(error).to_string());
338 }
339
340 #[test]
341 fn test_error_source_flatten_two_contexts() {
342 let expected = "Some Operation: some context: some failure";
343 let error = anyhow!("some failure");
344 let error = error.context("some context");
345 let error = error.context("Some Operation");
346 assert_eq!(expected, Error::User(error).to_string());
347 }
348
349 #[test]
350 fn test_error_source_flatten_three_contexts() {
351 let expected = "Some Operation: some context: more context: some failure";
352 let error = anyhow!("some failure")
353 .context("more context")
354 .context("some context")
355 .context("Some Operation");
356 assert_eq!(expected, Error::User(error).to_string());
357 }
358
359 #[test]
360 fn test_error_doesnt_duplicate_when_rewrapped() {
361 #[derive(thiserror::Error, Debug)]
362 enum NonsenseErr {
363 #[error(transparent)]
364 Error(#[from] FfxError),
365 }
366 let expected = "This thing broke!";
367 let error = ffx_error!(anyhow!(expected));
368 let error: NonsenseErr = error.into();
369 let error = Error::User(error.into());
370 assert_eq!(
371 error.to_string(),
372 expected.to_owned(),
373 "There should be no duplication from re-wrapping errors"
374 );
375 }
376}