ffx_command_error/
context.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::Error;
6use anyhow::Context;
7use errors::{FfxError, IntoExitCode, ResultExt};
8use std::fmt::Display;
9
10/// Adds helpers to result types to produce useful error messages to the user from
11/// the ffx frontend (through [`crate::Error`])
12pub trait FfxContext<T, E> {
13    /// Make this error into a BUG check that will display to the user as an error that
14    /// shouldn't happen.
15    fn bug(self) -> Result<T, Error>;
16
17    /// Make this error into a BUG check that will display to the user as an error that
18    /// shouldn't happen, with the added context.
19    fn bug_context<C: Display + Send + Sync + 'static>(self, context: C) -> Result<T, Error>;
20
21    /// Make this error into a BUG check that will display to the user as an error that
22    /// shouldn't happen, with the added context returned by the closure `f`.
23    fn with_bug_context<C: Display + Send + Sync + 'static>(
24        self,
25        f: impl FnOnce() -> C,
26    ) -> Result<T, Error>;
27
28    /// Make this error into a displayed user error, with the added context for display to the user.
29    /// Use this for errors that happen in the normal course of execution, like files not being found.
30    fn user_message<C: Display + Send + Sync + 'static>(self, context: C) -> Result<T, Error>;
31
32    /// Make this error into a displayed user error, with the added context for display to the user.
33    /// Use this for errors that happen in the normal course of execution, like files not being found.
34    fn with_user_message<C: Display + Send + Sync + 'static>(
35        self,
36        f: impl FnOnce() -> C,
37    ) -> Result<T, Error>;
38}
39
40/// Helper function for preventing duplicate error messages. Takes an error and, if it is of type
41/// `crate::Error` extracts its source. This prevents duplicating error messages by re-wrapping the
42/// error types within `FfxContext` multiple times, as the context chain messages get copied in
43/// each subsequent error wrapping.
44fn unwrap_source(err: anyhow::Error) -> anyhow::Error {
45    match err.downcast::<Error>() {
46        Ok(e) => match e.source() {
47            Ok(source) => source,
48            Err(e) => e.into(),
49        },
50        Err(e) => e,
51    }
52}
53
54impl<T, E> FfxContext<T, E> for Result<T, E>
55where
56    Self: anyhow::Context<T, E>,
57    E: Into<anyhow::Error>,
58{
59    fn bug(self) -> Result<T, Error> {
60        self.map_err(|e| Error::Unexpected(e.into()))
61    }
62
63    fn bug_context<C: Display + Send + Sync + 'static>(self, context: C) -> Result<T, Error> {
64        self.map_err(|e| Error::Unexpected(unwrap_source(e.into()).context(context)))
65    }
66
67    fn with_bug_context<C: Display + Send + Sync + 'static>(
68        self,
69        f: impl FnOnce() -> C,
70    ) -> Result<T, Error> {
71        self.bug_context((f)())
72    }
73
74    fn user_message<C: Display + Send + Sync + 'static>(self, context: C) -> Result<T, Error> {
75        self.map_err(|e| Error::User(unwrap_source(e.into()).context(context)))
76    }
77
78    fn with_user_message<C: Display + Send + Sync + 'static>(
79        self,
80        f: impl FnOnce() -> C,
81    ) -> Result<T, Error> {
82        self.user_message((f)())
83    }
84}
85
86impl<T> FfxContext<T, core::convert::Infallible> for Option<T>
87where
88    Self: anyhow::Context<T, core::convert::Infallible>,
89{
90    fn bug(self) -> Result<T, Error> {
91        self.ok_or_else(|| Error::Unexpected(anyhow::anyhow!("Option is None")))
92    }
93
94    fn bug_context<C: Display + Send + Sync + 'static>(self, context: C) -> Result<T, Error> {
95        self.context(context).map_err(Error::Unexpected)
96    }
97
98    fn with_bug_context<C: Display + Send + Sync + 'static>(
99        self,
100        f: impl FnOnce() -> C,
101    ) -> Result<T, Error> {
102        self.with_context(f).map_err(Error::Unexpected)
103    }
104
105    fn user_message<C: Display + Send + Sync + 'static>(self, context: C) -> Result<T, Error> {
106        self.context(context).map_err(Error::User)
107    }
108
109    fn with_user_message<C: Display + Send + Sync + 'static>(
110        self,
111        f: impl FnOnce() -> C,
112    ) -> Result<T, Error> {
113        self.with_context(f).map_err(Error::User)
114    }
115}
116
117impl ResultExt for Error {
118    fn ffx_error<'a>(&'a self) -> Option<&'a FfxError> {
119        match self {
120            Error::User(err) => err.downcast_ref(),
121            _ => None,
122        }
123    }
124}
125impl IntoExitCode for Error {
126    fn exit_code(&self) -> i32 {
127        use Error::*;
128        match self {
129            Help { code, .. } | ExitWithCode(code) => *code,
130            Unexpected(err) | User(err) | Config(err) => {
131                err.ffx_error().map(FfxError::exit_code).unwrap_or(1)
132            }
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::tests::*;
141    use anyhow::anyhow;
142    use assert_matches::assert_matches;
143
144    #[test]
145    fn error_context_helpers() {
146        assert_matches!(
147            anyhow::Result::<()>::Err(anyhow!(ERR_STR)).bug(),
148            Err(Error::Unexpected(_)),
149            "anyhow.bug() should be a bugcheck error"
150        );
151        assert_matches!(
152            anyhow::Result::<()>::Err(anyhow!(ERR_STR)).bug_context("boom"),
153            Err(Error::Unexpected(_)),
154            "anyhow.bug_context() should be a bugcheck error"
155        );
156        assert_matches!(
157            anyhow::Result::<()>::Err(anyhow!(ERR_STR)).with_bug_context(|| "boom"),
158            Err(Error::Unexpected(_)),
159            "anyhow.bug_context() should be a bugcheck error"
160        );
161        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)");
162        assert_matches!(
163            anyhow::Result::<()>::Err(anyhow!(ERR_STR)).user_message("boom"),
164            Err(Error::User(_)),
165            "anyhow.user_message() should be a user error"
166        );
167        assert_matches!(
168            anyhow::Result::<()>::Err(anyhow!(ERR_STR)).with_user_message(|| "boom"),
169            Err(Error::User(_)),
170            "anyhow.with_user_message() should be a user error"
171        );
172        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.");
173    }
174
175    #[test]
176    fn test_user_error_formats_through_multiple_levels() {
177        let user_err =
178            anyhow::Result::<()>::Err(anyhow!("the wubbler broke")).user_message("broken wubbler");
179        let user_err2 = user_err.user_message("getting wubbler");
180        let err_string = format!("{}", user_err2.unwrap_err());
181        assert_eq!(err_string, "getting wubbler: broken wubbler: the wubbler broke");
182    }
183
184    #[test]
185    fn test_bug_and_user_error_override_each_other_but_add_context() {
186        let user_err =
187            anyhow::Result::<()>::Err(anyhow!("the wubbler broke")).user_message("broken wubbler");
188        let user_err2 = user_err.bug_context("getting wubbler");
189        let user_err3 = user_err2.user_message("delegating wubbler");
190        let err_string = format!("{}", user_err3.unwrap_err());
191        assert_eq!(
192            err_string,
193            "delegating wubbler: getting wubbler: broken wubbler: the wubbler broke"
194        );
195    }
196
197    #[test]
198    fn test_bug_and_user_error_override_each_other_but_add_context_part_two() {
199        let user_err =
200            anyhow::Result::<()>::Err(anyhow!("the wubbler broke")).user_message("broken wubbler");
201        let user_err2 = user_err.bug_context("getting wubbler");
202        let user_err3 = user_err2.bug_context("delegating wubbler");
203        let err_string = format!("{}", user_err3.unwrap_err());
204        assert_eq!(
205            err_string,
206            "BUG: An internal command error occurred.\nError: delegating wubbler\n    1.  getting wubbler\n    2.  broken wubbler\n    3.  the wubbler broke"
207        );
208    }
209}