1use std::fmt;
2use std::io;
3use std::process::ExitStatus;
4
5#[derive(Debug)]
26pub enum RunError {
27 Spawn {
30 program: String,
31 source: io::Error,
32 },
33 NonZeroExit {
37 program: String,
38 args: Vec<String>,
39 status: ExitStatus,
40 stdout: Vec<u8>,
41 stderr: String,
42 },
43}
44
45impl RunError {
46 pub fn program(&self) -> &str {
48 match self {
49 Self::Spawn { program, .. } => program,
50 Self::NonZeroExit { program, .. } => program,
51 }
52 }
53
54 pub fn stderr(&self) -> Option<&str> {
56 match self {
57 Self::NonZeroExit { stderr, .. } => Some(stderr),
58 Self::Spawn { .. } => None,
59 }
60 }
61
62 pub fn exit_status(&self) -> Option<ExitStatus> {
64 match self {
65 Self::NonZeroExit { status, .. } => Some(*status),
66 Self::Spawn { .. } => None,
67 }
68 }
69
70 pub fn is_non_zero_exit(&self) -> bool {
72 matches!(self, Self::NonZeroExit { .. })
73 }
74
75 pub fn is_spawn_failure(&self) -> bool {
77 matches!(self, Self::Spawn { .. })
78 }
79}
80
81impl fmt::Display for RunError {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 match self {
84 Self::Spawn { program, source } => {
85 write!(f, "failed to spawn {program}: {source}")
86 }
87 Self::NonZeroExit {
88 program,
89 args,
90 status,
91 stderr,
92 ..
93 } => {
94 let trimmed = stderr.trim();
95 if trimmed.is_empty() {
96 write!(f, "{program} {} exited with {status}", args.join(" "))
97 } else {
98 write!(
99 f,
100 "{program} {} exited with {status}: {trimmed}",
101 args.join(" ")
102 )
103 }
104 }
105 }
106 }
107}
108
109impl std::error::Error for RunError {
110 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
111 match self {
112 Self::Spawn { source, .. } => Some(source),
113 Self::NonZeroExit { .. } => None,
114 }
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 fn spawn_error() -> RunError {
123 RunError::Spawn {
124 program: "git".into(),
125 source: io::Error::new(io::ErrorKind::NotFound, "not found"),
126 }
127 }
128
129 fn non_zero_exit(stderr: &str) -> RunError {
130 let status = std::process::Command::new("false")
133 .status()
134 .expect("false should be runnable");
135 RunError::NonZeroExit {
136 program: "git".into(),
137 args: vec!["status".into()],
138 status,
139 stdout: Vec::new(),
140 stderr: stderr.to_string(),
141 }
142 }
143
144 #[test]
145 fn program_returns_name() {
146 assert_eq!(spawn_error().program(), "git");
147 assert_eq!(non_zero_exit("").program(), "git");
148 }
149
150 #[test]
151 fn stderr_only_for_non_zero_exit() {
152 assert_eq!(spawn_error().stderr(), None);
153 assert_eq!(non_zero_exit("boom").stderr(), Some("boom"));
154 }
155
156 #[test]
157 fn exit_status_only_for_non_zero_exit() {
158 assert!(spawn_error().exit_status().is_none());
159 assert!(non_zero_exit("").exit_status().is_some());
160 }
161
162 #[test]
163 fn is_non_zero_exit_predicate() {
164 assert!(!spawn_error().is_non_zero_exit());
165 assert!(non_zero_exit("").is_non_zero_exit());
166 }
167
168 #[test]
169 fn is_spawn_failure_predicate() {
170 assert!(spawn_error().is_spawn_failure());
171 assert!(!non_zero_exit("").is_spawn_failure());
172 }
173
174 #[test]
175 fn display_spawn_failure() {
176 let msg = format!("{}", spawn_error());
177 assert!(msg.contains("spawn"));
178 assert!(msg.contains("git"));
179 assert!(msg.contains("not found"));
180 }
181
182 #[test]
183 fn display_non_zero_exit_with_stderr() {
184 let msg = format!("{}", non_zero_exit("something broke"));
185 assert!(msg.contains("git status"));
186 assert!(msg.contains("something broke"));
187 }
188
189 #[test]
190 fn display_non_zero_exit_empty_stderr() {
191 let msg = format!("{}", non_zero_exit(""));
192 assert!(msg.contains("git status"));
193 assert!(msg.contains("exited"));
194 }
195
196 #[test]
197 fn error_source_for_spawn() {
198 use std::error::Error;
199 let err = spawn_error();
200 assert!(err.source().is_some());
201 }
202
203 #[test]
204 fn error_source_none_for_exit() {
205 use std::error::Error;
206 let err = non_zero_exit("");
207 assert!(err.source().is_none());
208 }
209
210 #[test]
211 fn wraps_into_anyhow() {
212 let err = spawn_error();
214 let _: anyhow::Error = err.into();
215 }
216}