blob: c18a37d4284e413ea3f54016482e52fac648fcc4 [file] [log] [blame] [edit]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::Context;
use errors::{FfxError, IntoExitCode, ResultExt};
use std::fmt::Display;
/// A top level error type for ffx tool results
#[derive(thiserror::Error, Debug)]
pub enum Error {
/// An error that qualifies as a bugcheck
Unexpected(#[source] anyhow::Error),
/// A known kind of error that can be reported usefully to the user
User(#[source] anyhow::Error),
/// An early-exit that should result in outputting help to the user (like [`argh::EarlyExit`]),
/// but is not itself an error in any meaningful sense.
Help {
/// The text to output to the user
output: String,
/// The exit status
code: i32,
},
}
/// Writes a detailed description of an anyhow error to the formatter
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) => write!(f, "{error}"),
Self::Help { output, .. } => write!(f, "{output}"),
}
}
}
impl From<anyhow::Error> for Error {
fn from(error: anyhow::Error) -> Self {
// this is just a compatibility shim to extract information out of the way
// we've traditionally divided user and unexpected errors.
if error.is::<FfxError>() {
Self::User(error)
} else {
Self::Unexpected(error)
}
}
}
impl From<FfxError> for Error {
fn from(error: FfxError) -> Self {
Error::User(error.into())
}
}
impl Error {
/// Get the exit code this error should correspond to if it bubbles up to `main()`
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,
_ => 1,
}
}
}
/// A convenience Result type
pub type Result<T, E = crate::Error> = core::result::Result<T, E>;
pub trait FfxContext<T, E> {
/// Make this error into a BUG check that will display to the user as an error that
/// shouldn't happen.
fn bug(self) -> Result<T, Error>;
/// Make this error into a BUG check that will display to the user as an error that
/// shouldn't happen, with the added context.
fn bug_context<C: Display + Send + Sync + 'static>(self, context: C) -> Result<T, Error>;
/// Make this error into a BUG check that will display to the user as an error that
/// shouldn't happen, with the added context returned by the closure `f`.
fn with_bug_context<C: Display + Send + Sync + 'static>(
self,
f: impl FnOnce() -> C,
) -> Result<T, Error>;
/// Make this error into a displayed user error, with the added context for display to the user.
/// Use this for errors that happen in the normal course of execution, like files not being found.
fn user_message<C: Display + Send + Sync + 'static>(self, context: C) -> Result<T, Error>;
/// Make this error into a displayed user error, with the added context for display to the user.
/// Use this for errors that happen in the normal course of execution, like files not being found.
fn with_user_message<C: Display + Send + Sync + 'static>(
self,
f: impl FnOnce() -> C,
) -> Result<T, Error>;
}
impl<T, E> FfxContext<T, E> for Result<T, E>
where
Self: anyhow::Context<T, E>,
E: Into<anyhow::Error>,
{
fn bug(self) -> Result<T, Error> {
self.map_err(|e| Error::Unexpected(e.into()))
}
fn bug_context<C: Display + Send + Sync + 'static>(self, context: C) -> Result<T, Error> {
self.context(context).map_err(Error::Unexpected)
}
fn with_bug_context<C: Display + Send + Sync + 'static>(
self,
f: impl FnOnce() -> C,
) -> Result<T, Error> {
self.with_context(f).map_err(Error::Unexpected)
}
fn user_message<C: Display + Send + Sync + 'static>(self, context: C) -> Result<T, Error> {
self.context(context).map_err(Error::User)
}
fn with_user_message<C: Display + Send + Sync + 'static>(
self,
f: impl FnOnce() -> C,
) -> Result<T, Error> {
self.with_context(f).map_err(Error::User)
}
}
impl ResultExt for Error {
fn ffx_error<'a>(&'a self) -> Option<&'a FfxError> {
match self {
Error::User(err) => err.downcast_ref(),
_ => None,
}
}
}
impl IntoExitCode for Error {
fn exit_code(&self) -> i32 {
match self.ffx_error() {
Some(err) => err.exit_code(),
None => 1,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::anyhow;
use assert_matches::assert_matches;
use errors::{ffx_error, ffx_error_with_code};
use std::io::{Cursor, Write};
const FFX_STR: &str = "I am an ffx error";
const ERR_STR: &str = "I am not an ffx error";
#[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 error_context_helpers() {
assert_matches!(
anyhow::Result::<()>::Err(anyhow!(ERR_STR)).bug(),
Err(Error::Unexpected(_)),
"anyhow.bug() should be a bugcheck error"
);
assert_matches!(
anyhow::Result::<()>::Err(anyhow!(ERR_STR)).bug_context("boom"),
Err(Error::Unexpected(_)),
"anyhow.bug_context() should be a bugcheck error"
);
assert_matches!(
anyhow::Result::<()>::Err(anyhow!(ERR_STR)).with_bug_context(|| "boom"),
Err(Error::Unexpected(_)),
"anyhow.bug_context() should be a bugcheck error"
);
assert_matches!(anyhow::Result::<()>::Err(anyhow!(ERR_STR)).bug_context(FfxError::TestingError), Err(Error::Unexpected(_)), "anyhow.bug_context() should create a bugcheck error even if given an ffx error (magic reduction)");
assert_matches!(
anyhow::Result::<()>::Err(anyhow!(ERR_STR)).user_message("boom"),
Err(Error::User(_)),
"anyhow.user_message() should be a user error"
);
assert_matches!(
anyhow::Result::<()>::Err(anyhow!(ERR_STR)).with_user_message(|| "boom"),
Err(Error::User(_)),
"anyhow.with_user_message() should be a user error"
);
assert_matches!(anyhow::Result::<()>::Err(anyhow!(ERR_STR)).with_user_message(|| FfxError::TestingError).ffx_error(), Some(FfxError::TestingError), "anyhow.with_user_message should be a user error that properly extracts to the ffx error.");
}
#[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);
}
}