1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! The error type used throughout the tool.
use core::error::Error as StdError;
use core::fmt::{self, Display, Formatter};
use std::backtrace::Backtrace;
use std::io;
/// An error carrying a human-readable message and an optional cause.
///
/// Messages are written for the person who ran the command, not for a log aggregator: they say
/// what was being attempted and what to do about it.
#[derive(Debug)]
pub struct Error {
message: String,
cause: Option<Box<dyn StdError + Send + Sync>>,
usage: bool,
skippable: bool,
/// Captured at construction, unconditionally.
///
/// Every path that produces an `Error` funnels through [`Self::new`] or through a conversion
/// that carries the origin's own capture across, so the frames recorded are the ones where the
/// failure happened rather than where it was rewrapped. Whether frames are recorded at all is
/// controlled the way the standard library controls it everywhere else, by
/// `RUST_BACKTRACE`/`RUST_LIB_BACKTRACE`, so this costs nothing when they are unset.
backtrace: Backtrace,
}
impl Error {
/// Creates an error with the given message.
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
cause: None,
usage: false,
skippable: false,
backtrace: Backtrace::capture(),
}
}
/// Marks this as a usage error: something the user typed or configured, not something that
/// went wrong while running.
///
/// The distinction is the whole point of the exit-code scheme. A CI script needs to tell "you
/// invoked me wrongly" from "I ran and could not proceed", and collapsing the two forces it to
/// parse the message text to find out which happened.
#[must_use]
pub const fn usage(mut self) -> Self {
self.usage = true;
self
}
/// Returns whether this is a usage error.
#[must_use]
pub const fn is_usage(&self) -> bool {
self.usage
}
/// Marks this as an error the caller may step over: one file could not be handled, and the
/// work the caller is doing is still worth finishing without it.
///
/// Only the *producer* of an error knows whether its subject is the whole job or one item of
/// it, and only the *consumer* knows whether stepping over an item is acceptable there. This
/// flag is how the first tells the second, instead of the second matching on message text. It
/// is never permission to be quiet: a caller that skips must say what it skipped, because a
/// file dropped from a mutation population silently raises the score.
#[must_use]
pub const fn skippable(mut self) -> Self {
self.skippable = true;
self
}
/// Returns whether the caller may step over this error and carry on with the rest of the job.
#[must_use]
pub const fn is_skippable(&self) -> bool {
self.skippable
}
/// Attaches an underlying cause.
#[must_use]
pub fn caused_by(mut self, cause: impl StdError + Send + Sync + 'static) -> Self {
self.cause = Some(Box::new(cause));
self
}
/// Returns the message, without the cause chain.
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
/// Returns the backtrace captured where this error was first constructed.
///
/// Not printed to the user: a mutation run reports what it could not do and what to do about
/// it, and a stack of this tool's own frames answers neither question. It is here for the case
/// the message cannot cover, a failure that should have been impossible, where the only useful
/// next question is which code path produced it.
pub const fn backtrace(&self) -> &Backtrace {
&self.backtrace
}
}
impl Display for Error {
#[expect(clippy::renamed_function_params, reason = "`f` is less clear than `formatter`")]
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.message)?;
if let Some(cause) = &self.cause {
write!(formatter, ": {cause}")?;
}
Ok(())
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.cause.as_ref().map(|cause| &**cause as &(dyn StdError + 'static))
}
}
impl From<io::Error> for Error {
fn from(value: io::Error) -> Self {
Self::new("I/O error").caused_by(value)
}
}
impl From<cargo_gamma_engine::Error> for Error {
fn from(value: cargo_gamma_engine::Error) -> Self {
// The engine's capture is carried across rather than replaced. Capturing here would record
// this conversion, which every engine error passes through and which therefore identifies
// nothing.
let cargo_gamma_engine::Parts {
message,
cause,
usage,
skippable,
backtrace,
} = value.into_parts();
Self {
message,
cause,
usage,
skippable,
backtrace,
}
}
}
/// Creates an [`Error`] from a format string.
macro_rules! error {
($($arg:tt)*) => { $crate::error::Error::new(format!($($arg)*)) };
}
pub(crate) use error;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn message_is_preserved() {
let error = Error::new("could not read the manifest");
assert_eq!(error.message(), "could not read the manifest");
assert_eq!(error.to_string(), "could not read the manifest");
}
#[test]
fn cause_is_appended_to_the_display_form() {
let cause = io::Error::new(io::ErrorKind::NotFound, "no such file");
let error = Error::new("could not read the manifest").caused_by(cause);
assert_eq!(error.to_string(), "could not read the manifest: no such file");
}
#[test]
fn source_is_exposed_for_the_error_trait() {
use core::error::Error as _;
let cause = io::Error::new(io::ErrorKind::NotFound, "no such file");
let error = Error::new("outer").caused_by(cause);
assert!(error.source().is_some());
assert!(Error::new("outer").source().is_none());
}
#[test]
fn errors_are_not_usage_errors_by_default() {
assert!(!Error::new("something went wrong").is_usage());
}
#[test]
fn usage_errors_are_marked() {
// The exit-code scheme depends on this: a caller must be able to tell "you invoked me
// wrongly" from "I ran and could not proceed" without parsing the message.
assert!(Error::new("bad selector").usage().is_usage());
}
#[test]
fn marking_a_usage_error_preserves_the_message_and_cause() {
let cause = io::Error::new(io::ErrorKind::NotFound, "no such file");
let error = Error::new("outer").caused_by(cause).usage();
assert_eq!(error.to_string(), "outer: no such file");
assert!(error.is_usage());
}
#[test]
fn io_errors_convert() {
let error: Error = io::Error::new(io::ErrorKind::PermissionDenied, "denied").into();
assert!(error.to_string().contains("denied"));
}
#[test]
fn engine_errors_preserve_classification_and_causes() {
let engine = cargo_gamma_engine::Error::new("could not read source")
.caused_by(io::Error::new(io::ErrorKind::PermissionDenied, "denied"))
.usage()
.skippable();
let error = Error::from(engine);
assert_eq!(error.to_string(), "could not read source: denied");
assert!(error.is_usage());
assert!(error.is_skippable());
assert!(error.source().is_some());
}
}