Skip to main content

assert_cmd/
output.rs

1//! Simplify one-off runs of programs.
2
3use bstr::ByteSlice;
4use std::error::Error;
5use std::fmt;
6use std::process;
7
8/// Converts a type to an [`OutputResult`].
9///
10/// This is for example implemented on [`std::process::Output`].
11///
12/// # Examples
13///
14/// ```rust
15/// use assert_cmd::prelude::*;
16///
17/// use std::process::Command;
18///
19/// let result = Command::new("echo")
20///     .args(&["42"])
21///     .ok();
22/// assert!(result.is_ok());
23/// ```
24///
25pub trait OutputOkExt
26where
27    Self: ::std::marker::Sized,
28{
29    /// Convert an [`Output`] to an [`OutputResult`].
30    ///
31    /// # Examples
32    ///
33    /// ```rust
34    /// use assert_cmd::prelude::*;
35    ///
36    /// use std::process::Command;
37    ///
38    /// let result = Command::new("echo")
39    ///     .args(&["42"])
40    ///     .ok();
41    /// assert!(result.is_ok());
42    /// ```
43    ///
44    /// [`Output`]: std::process::Output
45    fn ok(self) -> OutputResult;
46
47    /// Unwrap a [`Output`] but with a prettier message than `.ok().unwrap()`.
48    ///
49    /// # Examples
50    ///
51    /// ```rust
52    /// use assert_cmd::prelude::*;
53    ///
54    /// use std::process::Command;
55    ///
56    /// let output = Command::new("echo")
57    ///     .args(&["42"])
58    ///     .unwrap();
59    /// ```
60    ///
61    /// [`Output`]: std::process::Output
62    fn unwrap(self) -> process::Output {
63        match self.ok() {
64            Ok(output) => output,
65            Err(err) => panic!("{}", err),
66        }
67    }
68
69    /// Unwrap a [`Output`] but with a prettier message than `ok().err().unwrap()`.
70    ///
71    /// # Examples
72    ///
73    /// ```rust,no_run
74    /// use assert_cmd::prelude::*;
75    ///
76    /// use std::process::Command;
77    ///
78    /// let err = Command::new("a-command")
79    ///     .args(&["--will-fail"])
80    ///     .unwrap_err();
81    /// ```
82    ///
83    /// [`Output`]: std::process::Output
84    fn unwrap_err(self) -> OutputError {
85        match self.ok() {
86            Ok(output) => panic!(
87                "Command completed successfully\nstdout=```{}```",
88                DebugBytes::new(&output.stdout)
89            ),
90            Err(err) => err,
91        }
92    }
93}
94
95impl OutputOkExt for process::Output {
96    fn ok(self) -> OutputResult {
97        if self.status.success() {
98            Ok(self)
99        } else {
100            let error = OutputError::new(self);
101            Err(error)
102        }
103    }
104}
105
106impl OutputOkExt for &mut process::Command {
107    fn ok(self) -> OutputResult {
108        let output = self.output().map_err(OutputError::with_cause)?;
109        if output.status.success() {
110            Ok(output)
111        } else {
112            let error = OutputError::new(output).set_cmd(format!("{self:?}"));
113            Err(error)
114        }
115    }
116
117    fn unwrap_err(self) -> OutputError {
118        match self.ok() {
119            Ok(output) => panic!(
120                "Completed successfully:\ncommand=`{:?}`\nstdout=```{}```",
121                self,
122                DebugBytes::new(&output.stdout)
123            ),
124            Err(err) => err,
125        }
126    }
127}
128
129/// [`Output`] represented as a [`Result`].
130///
131/// Generally produced by [`OutputOkExt`].
132///
133/// # Examples
134///
135/// ```rust
136/// use assert_cmd::prelude::*;
137///
138/// use std::process::Command;
139///
140/// let result = Command::new("echo")
141///     .args(&["42"])
142///     .ok();
143/// assert!(result.is_ok());
144/// ```
145///
146/// [`Output`]: std::process::Output
147/// [`Result`]: std::result::Result
148pub type OutputResult = Result<process::Output, OutputError>;
149
150/// [`Command`] error.
151///
152/// Generally produced by [`OutputOkExt`].
153///
154/// # Examples
155///
156/// ```rust,no_run
157/// use assert_cmd::prelude::*;
158///
159/// use std::process::Command;
160///
161/// let err = Command::new("a-command")
162///     .args(&["--will-fail"])
163///     .unwrap_err();
164/// ```
165///
166/// [`Command`]: std::process::Command
167#[derive(Debug)]
168pub struct OutputError {
169    cmd: Option<String>,
170    stdin: Option<bstr::BString>,
171    cause: OutputCause,
172}
173
174impl OutputError {
175    /// Convert [`Output`] into an [`Error`].
176    ///
177    /// [`Output`]: std::process::Output
178    /// [`Error`]: std::error::Error
179    pub fn new(output: process::Output) -> Self {
180        Self {
181            cmd: None,
182            stdin: None,
183            cause: OutputCause::Expected(Output { output }),
184        }
185    }
186
187    /// For errors that happen in creating a [`Output`].
188    ///
189    /// [`Output`]: std::process::Output
190    pub fn with_cause<E>(cause: E) -> Self
191    where
192        E: Error + Send + Sync + 'static,
193    {
194        Self {
195            cmd: None,
196            stdin: None,
197            cause: OutputCause::Unexpected(Box::new(cause)),
198        }
199    }
200
201    /// Add the command line for additional context.
202    pub fn set_cmd(mut self, cmd: String) -> Self {
203        self.cmd = Some(cmd);
204        self
205    }
206
207    /// Add the `stdin` for additional context.
208    pub fn set_stdin(mut self, stdin: Vec<u8>) -> Self {
209        self.stdin = Some(bstr::BString::from(stdin));
210        self
211    }
212
213    /// Access the contained [`Output`].
214    ///
215    /// # Examples
216    ///
217    /// ```rust,no_run
218    /// use assert_cmd::prelude::*;
219    ///
220    /// use std::process::Command;
221    ///
222    /// let err = Command::new("a-command")
223    ///     .args(&["--will-fail"])
224    ///     .unwrap_err();
225    /// let output = err
226    ///     .as_output()
227    ///     .unwrap();
228    /// assert_eq!(Some(42), output.status.code());
229    /// ```
230    ///
231    /// [`Output`]: std::process::Output
232    pub fn as_output(&self) -> Option<&process::Output> {
233        match self.cause {
234            OutputCause::Expected(ref e) => Some(&e.output),
235            OutputCause::Unexpected(_) => None,
236        }
237    }
238}
239
240impl Error for OutputError {}
241
242impl fmt::Display for OutputError {
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244        let palette = crate::Palette::color();
245        if let Some(ref cmd) = self.cmd {
246            writeln!(f, "{:#}={:#}", palette.key("command"), palette.value(cmd))?;
247        }
248        if let Some(ref stdin) = self.stdin {
249            writeln!(
250                f,
251                "{:#}={:#}",
252                palette.key("stdin"),
253                palette.value(DebugBytes::new(stdin))
254            )?;
255        }
256        write!(f, "{:#}", self.cause)
257    }
258}
259
260#[derive(Debug)]
261enum OutputCause {
262    Expected(Output),
263    Unexpected(Box<dyn Error + Send + Sync + 'static>),
264}
265
266impl fmt::Display for OutputCause {
267    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268        match *self {
269            OutputCause::Expected(ref e) => write!(f, "{e:#}"),
270            OutputCause::Unexpected(ref e) => write!(f, "{e:#}"),
271        }
272    }
273}
274
275#[derive(Debug)]
276struct Output {
277    output: process::Output,
278}
279
280impl fmt::Display for Output {
281    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282        output_fmt(&self.output, f)
283    }
284}
285
286pub(crate) fn output_fmt(output: &process::Output, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287    let palette = crate::Palette::color();
288    if let Some(code) = output.status.code() {
289        writeln!(f, "{:#}={:#}", palette.key("code"), palette.value(code))?;
290    } else {
291        writeln!(
292            f,
293            "{:#}={:#}",
294            palette.key("code"),
295            palette.value("<interrupted>")
296        )?;
297    }
298
299    write!(
300        f,
301        "{:#}={:#}\n{:#}={:#}\n",
302        palette.key("stdout"),
303        palette.value(DebugBytes::new(&output.stdout)),
304        palette.key("stderr"),
305        palette.value(DebugBytes::new(&output.stderr)),
306    )?;
307    Ok(())
308}
309
310#[derive(Debug)]
311pub(crate) struct DebugBytes<'a> {
312    bytes: &'a [u8],
313}
314
315impl<'a> DebugBytes<'a> {
316    pub(crate) fn new(bytes: &'a [u8]) -> Self {
317        DebugBytes { bytes }
318    }
319}
320
321impl fmt::Display for DebugBytes<'_> {
322    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323        format_bytes(self.bytes, f)
324    }
325}
326
327#[derive(Debug)]
328pub(crate) struct DebugBuffer {
329    buffer: bstr::BString,
330}
331
332impl DebugBuffer {
333    pub(crate) fn new(buffer: Vec<u8>) -> Self {
334        DebugBuffer {
335            buffer: buffer.into(),
336        }
337    }
338}
339
340impl fmt::Display for DebugBuffer {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        format_bytes(&self.buffer, f)
343    }
344}
345
346fn format_bytes(data: &[u8], f: &mut impl fmt::Write) -> fmt::Result {
347    #![allow(clippy::assertions_on_constants)]
348
349    const LINES_MIN_OVERFLOW: usize = 80;
350    const LINES_MAX_START: usize = 20;
351    const LINES_MAX_END: usize = 40;
352    const LINES_MAX_PRINTED: usize = LINES_MAX_START + LINES_MAX_END;
353
354    const BYTES_MIN_OVERFLOW: usize = 8192;
355    const BYTES_MAX_START: usize = 2048;
356    const BYTES_MAX_END: usize = 2048;
357    const BYTES_MAX_PRINTED: usize = BYTES_MAX_START + BYTES_MAX_END;
358
359    assert!(LINES_MAX_PRINTED < LINES_MIN_OVERFLOW);
360    assert!(BYTES_MAX_PRINTED < BYTES_MIN_OVERFLOW);
361
362    let lines_total = data.as_bstr().lines_with_terminator().count();
363    let multiline = 1 < lines_total;
364
365    if LINES_MIN_OVERFLOW <= lines_total {
366        let lines_omitted = lines_total - LINES_MAX_PRINTED;
367        let start_lines = data.as_bstr().lines_with_terminator().take(LINES_MAX_START);
368        let end_lines = data
369            .as_bstr()
370            .lines_with_terminator()
371            .skip(LINES_MAX_START + lines_omitted);
372        writeln!(f, "<{lines_total} lines total>")?;
373        write_debug_bstrs(f, true, start_lines)?;
374        writeln!(f, "<{lines_omitted} lines omitted>")?;
375        write_debug_bstrs(f, true, end_lines)
376    } else if BYTES_MIN_OVERFLOW <= data.len() {
377        write!(
378            f,
379            "<{} bytes total>{}",
380            data.len(),
381            if multiline { "\n" } else { "" }
382        )?;
383        write_debug_bstrs(
384            f,
385            multiline,
386            data[..BYTES_MAX_START].lines_with_terminator(),
387        )?;
388        write!(
389            f,
390            "<{} bytes omitted>{}",
391            data.len() - BYTES_MAX_PRINTED,
392            if multiline { "\n" } else { "" }
393        )?;
394        write_debug_bstrs(
395            f,
396            multiline,
397            data[data.len() - BYTES_MAX_END..].lines_with_terminator(),
398        )
399    } else {
400        write_debug_bstrs(f, multiline, data.lines_with_terminator())
401    }
402}
403
404fn write_debug_bstrs<'a>(
405    f: &mut impl fmt::Write,
406    multiline: bool,
407    mut lines: impl Iterator<Item = &'a [u8]>,
408) -> fmt::Result {
409    if multiline {
410        writeln!(f, "```")?;
411        for mut line in lines {
412            let mut newline = false;
413            if line.last() == Some(&b'\n') {
414                line = &line[..line.len() - 1];
415                newline = true;
416            }
417            let s = format!("{:?}", line.as_bstr());
418            write!(
419                f,
420                "{}{}",
421                &s[1..s.len() - 1],
422                if newline { "\n" } else { "" }
423            )?;
424        }
425        writeln!(f, "```")
426    } else {
427        write!(f, "{:?}", lines.next().unwrap_or(&[]).as_bstr())
428    }
429}
430
431#[cfg(test)]
432mod test {
433    #[test]
434    fn format_bytes() {
435        let mut s = String::new();
436        for i in 0..80 {
437            s.push_str(&format!("{i}\n"));
438        }
439
440        let mut buf = String::new();
441        super::format_bytes(s.as_bytes(), &mut buf).unwrap();
442
443        assert_eq!(
444            "<80 lines total>
445```
4460
4471
4482
4493
4504
4515
4526
4537
4548
4559
45610
45711
45812
45913
46014
46115
46216
46317
46418
46519
466```
467<20 lines omitted>
468```
46940
47041
47142
47243
47344
47445
47546
47647
47748
47849
47950
48051
48152
48253
48354
48455
48556
48657
48758
48859
48960
49061
49162
49263
49364
49465
49566
49667
49768
49869
49970
50071
50172
50273
50374
50475
50576
50677
50778
50879
509```
510",
511            buf
512        );
513    }
514
515    #[test]
516    fn no_trailing_newline() {
517        let s = "no\ntrailing\nnewline";
518
519        let mut buf = String::new();
520        super::format_bytes(s.as_bytes(), &mut buf).unwrap();
521
522        assert_eq!(
523            "```
524no
525trailing
526newline```
527",
528            buf
529        );
530    }
531}