1use bstr::ByteSlice;
4use std::error::Error;
5use std::fmt;
6use std::process;
7
8pub trait OutputOkExt
26where
27 Self: ::std::marker::Sized,
28{
29 fn ok(self) -> OutputResult;
46
47 #[track_caller]
63 fn unwrap(self) -> process::Output {
64 match self.ok() {
65 Ok(output) => output,
66 Err(err) => panic!("{}", err),
67 }
68 }
69
70 #[track_caller]
86 fn unwrap_err(self) -> OutputError {
87 match self.ok() {
88 Ok(output) => panic!(
89 "Command completed successfully\nstdout=```{}```",
90 DebugBytes::new(&output.stdout)
91 ),
92 Err(err) => err,
93 }
94 }
95}
96
97impl OutputOkExt for process::Output {
98 fn ok(self) -> OutputResult {
99 if self.status.success() {
100 Ok(self)
101 } else {
102 let error = OutputError::new(self);
103 Err(error)
104 }
105 }
106}
107
108impl OutputOkExt for &mut process::Command {
109 fn ok(self) -> OutputResult {
110 let output = self.output().map_err(OutputError::with_cause)?;
111 if output.status.success() {
112 Ok(output)
113 } else {
114 let error = OutputError::new(output).set_cmd(format!("{self:?}"));
115 Err(error)
116 }
117 }
118
119 #[track_caller]
120 fn unwrap_err(self) -> OutputError {
121 match self.ok() {
122 Ok(output) => panic!(
123 "Completed successfully:\ncommand=`{:?}`\nstdout=```{}```",
124 self,
125 DebugBytes::new(&output.stdout)
126 ),
127 Err(err) => err,
128 }
129 }
130}
131
132pub type OutputResult = Result<process::Output, OutputError>;
152
153#[derive(Debug)]
171pub struct OutputError {
172 cmd: Option<String>,
173 stdin: Option<bstr::BString>,
174 cause: OutputCause,
175}
176
177impl OutputError {
178 pub fn new(output: process::Output) -> Self {
183 Self {
184 cmd: None,
185 stdin: None,
186 cause: OutputCause::Expected(Output { output }),
187 }
188 }
189
190 pub fn with_cause<E>(cause: E) -> Self
194 where
195 E: Error + Send + Sync + 'static,
196 {
197 Self {
198 cmd: None,
199 stdin: None,
200 cause: OutputCause::Unexpected(Box::new(cause)),
201 }
202 }
203
204 pub fn set_cmd(mut self, cmd: String) -> Self {
206 self.cmd = Some(cmd);
207 self
208 }
209
210 pub fn set_stdin(mut self, stdin: Vec<u8>) -> Self {
212 self.stdin = Some(bstr::BString::from(stdin));
213 self
214 }
215
216 pub fn as_output(&self) -> Option<&process::Output> {
236 match self.cause {
237 OutputCause::Expected(ref e) => Some(&e.output),
238 OutputCause::Unexpected(_) => None,
239 }
240 }
241}
242
243impl Error for OutputError {}
244
245impl fmt::Display for OutputError {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 let palette = crate::Palette::color();
248 if let Some(ref cmd) = self.cmd {
249 writeln!(f, "{:#}={:#}", palette.key("command"), palette.value(cmd))?;
250 }
251 if let Some(ref stdin) = self.stdin {
252 writeln!(
253 f,
254 "{:#}={:#}",
255 palette.key("stdin"),
256 palette.value(DebugBytes::new(stdin))
257 )?;
258 }
259 write!(f, "{:#}", self.cause)
260 }
261}
262
263#[derive(Debug)]
264enum OutputCause {
265 Expected(Output),
266 Unexpected(Box<dyn Error + Send + Sync + 'static>),
267}
268
269impl fmt::Display for OutputCause {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 match *self {
272 Self::Expected(ref e) => write!(f, "{e:#}"),
273 Self::Unexpected(ref e) => write!(f, "{e:#}"),
274 }
275 }
276}
277
278#[derive(Debug)]
279struct Output {
280 output: process::Output,
281}
282
283impl fmt::Display for Output {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 output_fmt(&self.output, f)
286 }
287}
288
289pub(crate) fn output_fmt(output: &process::Output, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290 let palette = crate::Palette::color();
291 if let Some(code) = output.status.code() {
292 writeln!(f, "{:#}={:#}", palette.key("code"), palette.value(code))?;
293 } else {
294 writeln!(
295 f,
296 "{:#}={:#}",
297 palette.key("code"),
298 palette.value("<interrupted>")
299 )?;
300 }
301
302 write!(
303 f,
304 "{:#}={:#}\n{:#}={:#}\n",
305 palette.key("stdout"),
306 palette.value(DebugBytes::new(&output.stdout)),
307 palette.key("stderr"),
308 palette.value(DebugBytes::new(&output.stderr)),
309 )?;
310 Ok(())
311}
312
313#[derive(Debug)]
314pub(crate) struct DebugBytes<'a> {
315 bytes: &'a [u8],
316}
317
318impl<'a> DebugBytes<'a> {
319 pub(crate) fn new(bytes: &'a [u8]) -> Self {
320 DebugBytes { bytes }
321 }
322}
323
324impl fmt::Display for DebugBytes<'_> {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 format_bytes(self.bytes, f)
327 }
328}
329
330#[derive(Debug)]
331pub(crate) struct DebugBuffer {
332 buffer: bstr::BString,
333}
334
335impl DebugBuffer {
336 pub(crate) fn new(buffer: Vec<u8>) -> Self {
337 Self {
338 buffer: buffer.into(),
339 }
340 }
341}
342
343impl fmt::Display for DebugBuffer {
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 format_bytes(&self.buffer, f)
346 }
347}
348
349fn format_bytes(data: &[u8], f: &mut impl fmt::Write) -> fmt::Result {
350 #![allow(
351 clippy::assertions_on_constants,
352 reason = "enforce constant relationships on edit"
353 )]
354
355 const LINES_MIN_OVERFLOW: usize = 80;
356 const LINES_MAX_START: usize = 20;
357 const LINES_MAX_END: usize = 40;
358 const LINES_MAX_PRINTED: usize = LINES_MAX_START + LINES_MAX_END;
359
360 const BYTES_MIN_OVERFLOW: usize = 8192;
361 const BYTES_MAX_START: usize = 2048;
362 const BYTES_MAX_END: usize = 2048;
363 const BYTES_MAX_PRINTED: usize = BYTES_MAX_START + BYTES_MAX_END;
364
365 assert!(LINES_MAX_PRINTED < LINES_MIN_OVERFLOW);
366 assert!(BYTES_MAX_PRINTED < BYTES_MIN_OVERFLOW);
367
368 let lines_total = data.as_bstr().lines_with_terminator().count();
369 let multiline = 1 < lines_total;
370
371 if LINES_MIN_OVERFLOW <= lines_total {
372 let lines_omitted = lines_total - LINES_MAX_PRINTED;
373 let start_lines = data.as_bstr().lines_with_terminator().take(LINES_MAX_START);
374 let end_lines = data
375 .as_bstr()
376 .lines_with_terminator()
377 .skip(LINES_MAX_START + lines_omitted);
378 writeln!(f, "<{lines_total} lines total>")?;
379 write_debug_bstrs(f, true, start_lines)?;
380 writeln!(f, "<{lines_omitted} lines omitted>")?;
381 write_debug_bstrs(f, true, end_lines)
382 } else if BYTES_MIN_OVERFLOW <= data.len() {
383 write!(
384 f,
385 "<{} bytes total>{}",
386 data.len(),
387 if multiline { "\n" } else { "" }
388 )?;
389 write_debug_bstrs(
390 f,
391 multiline,
392 data[..BYTES_MAX_START].lines_with_terminator(),
393 )?;
394 write!(
395 f,
396 "<{} bytes omitted>{}",
397 data.len() - BYTES_MAX_PRINTED,
398 if multiline { "\n" } else { "" }
399 )?;
400 write_debug_bstrs(
401 f,
402 multiline,
403 data[data.len() - BYTES_MAX_END..].lines_with_terminator(),
404 )
405 } else {
406 write_debug_bstrs(f, multiline, data.lines_with_terminator())
407 }
408}
409
410fn write_debug_bstrs<'a>(
411 f: &mut impl fmt::Write,
412 multiline: bool,
413 mut lines: impl Iterator<Item = &'a [u8]>,
414) -> fmt::Result {
415 if multiline {
416 writeln!(f, "```")?;
417 for mut line in lines {
418 let mut newline = false;
419 if line.last() == Some(&b'\n') {
420 line = &line[..line.len() - 1];
421 newline = true;
422 }
423 let s = format!("{:?}", line.as_bstr());
424 write!(
425 f,
426 "{}{}",
427 &s[1..s.len() - 1],
428 if newline { "\n" } else { "" }
429 )?;
430 }
431 writeln!(f, "```")
432 } else {
433 write!(f, "{:?}", lines.next().unwrap_or(&[]).as_bstr())
434 }
435}
436
437#[cfg(test)]
438mod test {
439 #[test]
440 fn format_bytes() {
441 let mut s = String::new();
442 for i in 0..80 {
443 s.push_str(&format!("{i}\n"));
444 }
445
446 let mut buf = String::new();
447 super::format_bytes(s.as_bytes(), &mut buf).unwrap();
448
449 assert_eq!(
450 "<80 lines total>
451```
4520
4531
4542
4553
4564
4575
4586
4597
4608
4619
46210
46311
46412
46513
46614
46715
46816
46917
47018
47119
472```
473<20 lines omitted>
474```
47540
47641
47742
47843
47944
48045
48146
48247
48348
48449
48550
48651
48752
48853
48954
49055
49156
49257
49358
49459
49560
49661
49762
49863
49964
50065
50166
50267
50368
50469
50570
50671
50772
50873
50974
51075
51176
51277
51378
51479
515```
516",
517 buf
518 );
519 }
520
521 #[test]
522 fn no_trailing_newline() {
523 let s = "no\ntrailing\nnewline";
524
525 let mut buf = String::new();
526 super::format_bytes(s.as_bytes(), &mut buf).unwrap();
527
528 assert_eq!(
529 "```
530no
531trailing
532newline```
533",
534 buf
535 );
536 }
537}