1use std::{io, process::Output};
2
3#[derive(thiserror::Error, Debug)]
5pub enum Error {
6 #[error(
9 "unexpected process output: intent: `{intent}`, stdout: `{stdout}`, stderr: `{stderr}`"
10 )]
11 UnexpectedTmuxOutput {
12 intent: &'static str,
13 stdout: String,
14 stderr: String,
15 },
16
17 #[error("unexpected tmux config: `{0}`")]
19 TmuxConfig(&'static str),
20
21 #[error("failed parsing: `{intent}`")]
23 ParseError {
24 desc: &'static str,
25 intent: &'static str,
26 err: nom::Err<nom::error::Error<String>>,
27 },
28
29 #[error("failed parsing utf-8 string: `{source}`")]
31 Utf8 {
32 #[from]
33 source: std::string::FromUtf8Error,
35 },
36
37 #[error("failed with io: `{source}`")]
39 Io {
40 #[from]
41 source: io::Error,
43 },
44}
45
46#[must_use]
52pub fn map_add_intent(
53 desc: &'static str,
54 intent: &'static str,
55 nom_err: nom::Err<nom::error::Error<&str>>,
56) -> Error {
57 Error::ParseError {
58 desc,
59 intent,
60 err: nom_err.to_owned(),
61 }
62}
63
64pub fn check_empty_process_output(
71 output: &Output,
72 intent: &'static str,
73) -> std::result::Result<(), Error> {
74 if !output.stdout.is_empty() || !output.stderr.is_empty() {
75 let stdout = String::from_utf8_lossy(&output.stdout[..]).to_string();
76 let stderr = String::from_utf8_lossy(&output.stderr[..]).to_string();
77 return Err(Error::UnexpectedTmuxOutput {
78 intent,
79 stdout,
80 stderr,
81 });
82 }
83 Ok(())
84}
85
86pub fn check_process_success(
95 output: &Output,
96 intent: &'static str,
97) -> std::result::Result<(), Error> {
98 if !output.status.success() {
99 let stdout = String::from_utf8_lossy(&output.stdout[..]).to_string();
100 let stderr = String::from_utf8_lossy(&output.stderr[..]).to_string();
101 return Err(Error::UnexpectedTmuxOutput {
102 intent,
103 stdout,
104 stderr,
105 });
106 }
107 Ok(())
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use std::os::unix::process::ExitStatusExt;
114 use std::process::ExitStatus;
115
116 fn make_output(status_code: i32, stdout: &[u8], stderr: &[u8]) -> Output {
117 Output {
118 status: ExitStatus::from_raw(status_code << 8), stdout: stdout.to_vec(),
120 stderr: stderr.to_vec(),
121 }
122 }
123
124 #[test]
125 fn check_empty_process_output_succeeds_when_empty() {
126 let output = make_output(0, b"", b"");
127 let result = check_empty_process_output(&output, "test-intent");
128 assert!(result.is_ok());
129 }
130
131 #[test]
132 fn check_empty_process_output_fails_when_stdout_not_empty() {
133 let output = make_output(0, b"some output", b"");
134 let result = check_empty_process_output(&output, "test-intent");
135 assert!(result.is_err());
136
137 match result.unwrap_err() {
138 Error::UnexpectedTmuxOutput {
139 intent,
140 stdout,
141 stderr,
142 } => {
143 assert_eq!(intent, "test-intent");
144 assert_eq!(stdout, "some output");
145 assert_eq!(stderr, "");
146 }
147 _ => panic!("Expected UnexpectedTmuxOutput error"),
148 }
149 }
150
151 #[test]
152 fn check_empty_process_output_fails_when_stderr_not_empty() {
153 let output = make_output(0, b"", b"error message");
154 let result = check_empty_process_output(&output, "test-intent");
155 assert!(result.is_err());
156
157 match result.unwrap_err() {
158 Error::UnexpectedTmuxOutput {
159 intent,
160 stdout,
161 stderr,
162 } => {
163 assert_eq!(intent, "test-intent");
164 assert_eq!(stdout, "");
165 assert_eq!(stderr, "error message");
166 }
167 _ => panic!("Expected UnexpectedTmuxOutput error"),
168 }
169 }
170
171 #[test]
172 fn check_empty_process_output_fails_when_both_not_empty() {
173 let output = make_output(0, b"stdout", b"stderr");
174 let result = check_empty_process_output(&output, "test-intent");
175 assert!(result.is_err());
176
177 match result.unwrap_err() {
178 Error::UnexpectedTmuxOutput { stdout, stderr, .. } => {
179 assert_eq!(stdout, "stdout");
180 assert_eq!(stderr, "stderr");
181 }
182 _ => panic!("Expected UnexpectedTmuxOutput error"),
183 }
184 }
185
186 #[test]
187 fn check_process_success_succeeds_on_zero_exit() {
188 let output = make_output(0, b"output", b"");
189 let result = check_process_success(&output, "test-intent");
190 assert!(result.is_ok());
191 }
192
193 #[test]
194 fn check_process_success_fails_on_nonzero_exit() {
195 let output = make_output(1, b"", b"command failed");
196 let result = check_process_success(&output, "test-intent");
197 assert!(result.is_err());
198
199 match result.unwrap_err() {
200 Error::UnexpectedTmuxOutput {
201 intent,
202 stdout,
203 stderr,
204 } => {
205 assert_eq!(intent, "test-intent");
206 assert_eq!(stdout, "");
207 assert_eq!(stderr, "command failed");
208 }
209 _ => panic!("Expected UnexpectedTmuxOutput error"),
210 }
211 }
212
213 #[test]
214 fn map_add_intent_creates_parse_error() {
215 use nom::error::{Error as NomError, ErrorKind};
216
217 let nom_err: nom::Err<NomError<&str>> =
218 nom::Err::Error(NomError::new("remaining input", ErrorKind::Tag));
219
220 let error = map_add_intent("description", "expected format", nom_err);
221
222 match error {
223 Error::ParseError { desc, intent, .. } => {
224 assert_eq!(desc, "description");
225 assert_eq!(intent, "expected format");
226 }
227 _ => panic!("Expected ParseError"),
228 }
229 }
230
231 #[test]
232 fn error_display_messages() {
233 let err = Error::UnexpectedTmuxOutput {
235 intent: "test",
236 stdout: "out".to_string(),
237 stderr: "err".to_string(),
238 };
239 let msg = format!("{}", err);
240 assert!(msg.contains("unexpected process output"));
241 assert!(msg.contains("test"));
242
243 let err = Error::TmuxConfig("missing default-shell");
245 let msg = format!("{}", err);
246 assert!(msg.contains("unexpected tmux config"));
247 assert!(msg.contains("missing default-shell"));
248 }
249}