use errors::FfxError;
#[derive(thiserror::Error, Debug)]
#[error("non-fatal error encountered: {}", .0)]
pub struct NonFatalError(#[source] pub anyhow::Error);
#[derive(thiserror::Error, Debug)]
pub enum Error {
Unexpected(#[source] anyhow::Error),
User(#[source] anyhow::Error),
Help {
command: Vec<String>,
output: String,
code: i32,
},
Config(#[source] anyhow::Error),
ExitWithCode(i32),
}
impl Error {
pub fn downcast_non_fatal(self) -> Result<anyhow::Error, Self> {
fn try_downcast(err: anyhow::Error) -> Result<anyhow::Error, anyhow::Error> {
match err.downcast::<NonFatalError>() {
Ok(NonFatalError(e)) => Ok(e),
Err(e) => Err(e),
}
}
match self {
Self::Help { .. } | Self::ExitWithCode(_) => Err(self),
Self::User(e) => try_downcast(e).map_err(Self::User),
Self::Unexpected(e) => try_downcast(e).map_err(Self::Unexpected),
Self::Config(e) => try_downcast(e).map_err(Self::Config),
}
}
}
fn write_detailed(f: &mut std::fmt::Formatter<'_>, error: &anyhow::Error) -> std::fmt::Result {
write!(f, "Error: {}", error)?;
for (i, e) in error.chain().skip(1).enumerate() {
write!(f, "\n {: >3}. {}", i + 1, e)?;
}
Ok(())
}
const BUG_LINE: &str = "BUG: An internal command error occurred.";
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unexpected(error) => {
writeln!(f, "{BUG_LINE}")?;
write_detailed(f, error)
}
Self::User(error) | Self::Config(error) => write!(f, "{error}"),
Self::Help { output, .. } => write!(f, "{output}"),
Self::ExitWithCode(code) => write!(f, "Exiting with code {code}"),
}
}
}
impl From<anyhow::Error> for Error {
fn from(error: anyhow::Error) -> Self {
match error.downcast::<Self>() {
Ok(this) => this,
Err(error) => match error.downcast::<FfxError>() {
Ok(err) => Self::User(err.into()),
Err(err) => Self::Unexpected(err),
},
}
}
}
impl From<FfxError> for Error {
fn from(error: FfxError) -> Self {
Error::User(error.into())
}
}
impl Error {
pub fn from_early_exit(command: &[impl AsRef<str>], early_exit: argh::EarlyExit) -> Self {
let command = Vec::from_iter(command.iter().map(|s| s.as_ref().to_owned()));
let output = early_exit.output;
match early_exit.status {
Ok(_) => Error::Help { command, output, code: 0 },
Err(_) => Error::Config(anyhow::anyhow!("{}", output)),
}
}
pub fn exit_code(&self) -> i32 {
match self {
Error::User(err) => {
if let Some(FfxError::Error(_, code)) = err.downcast_ref() {
*code
} else {
1
}
}
Error::Help { code, .. } => *code,
Error::ExitWithCode(code) => *code,
_ => 1,
}
}
}
pub type Result<T, E = crate::Error> = core::result::Result<T, E>;
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::*;
use anyhow::anyhow;
use assert_matches::assert_matches;
use errors::{ffx_error, ffx_error_with_code, IntoExitCode};
use std::io::{Cursor, Write};
#[test]
fn test_write_result_ffx_error() {
let err = Error::from(ffx_error!(FFX_STR));
let mut cursor = Cursor::new(Vec::new());
assert_matches!(write!(&mut cursor, "{err}"), Ok(_));
assert!(String::from_utf8(cursor.into_inner()).unwrap().contains(FFX_STR));
}
#[test]
fn into_error_from_arbitrary_is_unexpected() {
let err = anyhow!(ERR_STR);
assert_matches!(
Error::from(err),
Error::Unexpected(_),
"an arbitrary anyhow error should convert to an 'unexpected' bug check error"
);
}
#[test]
fn into_error_from_ffx_error_is_user_error() {
let err = FfxError::Error(anyhow!(FFX_STR), 1);
assert_matches!(
Error::from(err),
Error::User(_),
"an arbitrary anyhow error should convert to a 'user' error"
);
}
#[test]
fn into_error_from_contextualized_ffx_error_prints_original_error() {
let err = Error::from(anyhow::anyhow!(errors::ffx_error!(FFX_STR)).context("boom"));
assert_eq!(
&format!("{err}"),
FFX_STR,
"an anyhow error with context should print the original error, not the context, when stringified."
);
}
#[test]
fn test_write_result_arbitrary_error() {
let err = Error::from(anyhow!(ERR_STR));
let mut cursor = Cursor::new(Vec::new());
assert_matches!(write!(&mut cursor, "{err}"), Ok(_));
let err_str = String::from_utf8(cursor.into_inner()).unwrap();
assert!(err_str.contains(BUG_LINE));
assert!(err_str.contains(ERR_STR));
}
#[test]
fn test_result_ext_exit_code_ffx_error() {
let err = Result::<()>::Err(Error::from(ffx_error_with_code!(42, FFX_STR)));
assert_eq!(err.exit_code(), 42);
}
#[test]
fn test_from_ok_early_exit() {
let command = ["testing", "--help"];
let output = "stuff!".to_owned();
let status = Ok(());
let code = 0;
let early_exit = argh::EarlyExit { output: output.clone(), status };
let err = Error::from_early_exit(&command, early_exit);
assert_eq!(err.exit_code(), code);
assert_matches!(err, Error::Help { command: error_command, output: error_output, code: error_code } if error_command == command && error_output == output && error_code == code);
}
#[test]
fn test_from_error_early_exit() {
let command = ["testing", "bad", "command"];
let output = "stuff!".to_owned();
let status = Err(());
let code = 1;
let early_exit = argh::EarlyExit { output: output.clone(), status };
let err = Error::from_early_exit(&command, early_exit);
assert_eq!(err.exit_code(), code);
assert_matches!(err, Error::Config(err) if format!("{err}") == output);
}
#[test]
fn test_downcast_recasts_types() {
let err = Error::User(anyhow!("boom"));
assert_matches!(err.downcast_non_fatal(), Err(Error::User(_)));
let err = Error::Unexpected(anyhow!("boom"));
assert_matches!(err.downcast_non_fatal(), Err(Error::Unexpected(_)));
let err = Error::Config(anyhow!("boom"));
assert_matches!(err.downcast_non_fatal(), Err(Error::Config(_)));
let err =
Error::Help { command: vec!["foobar".to_owned()], output: "blorp".to_owned(), code: 1 };
assert_matches!(err.downcast_non_fatal(), Err(Error::Help { .. }));
let err = Error::ExitWithCode(2);
assert_matches!(err.downcast_non_fatal(), Err(Error::ExitWithCode(2)));
}
#[test]
fn test_downcast_non_fatal_recovers_non_fatal_error() {
static ERR_STR: &'static str = "Oh look it's non fatal";
let constructors = vec![Error::User, Error::Unexpected, Error::Config];
for c in constructors.into_iter() {
let err = c(NonFatalError(anyhow!(ERR_STR)).into());
let res = err.downcast_non_fatal().expect("expected non-fatal downcast");
assert_eq!(res.to_string(), ERR_STR.to_owned());
}
}
}