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    #[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    /// Unwrap a [`Output`] but with a prettier message than `ok().err().unwrap()`.
71    ///
72    /// # Examples
73    ///
74    /// ```rust,no_run
75    /// use assert_cmd::prelude::*;
76    ///
77    /// use std::process::Command;
78    ///
79    /// let err = Command::new("a-command")
80    ///     .args(&["--will-fail"])
81    ///     .unwrap_err();
82    /// ```
83    ///
84    /// [`Output`]: std::process::Output
85    #[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
132/// [`Output`] represented as a [`Result`].
133///
134/// Generally produced by [`OutputOkExt`].
135///
136/// # Examples
137///
138/// ```rust
139/// use assert_cmd::prelude::*;
140///
141/// use std::process::Command;
142///
143/// let result = Command::new("echo")
144///     .args(&["42"])
145///     .ok();
146/// assert!(result.is_ok());
147/// ```
148///
149/// [`Output`]: std::process::Output
150/// [`Result`]: std::result::Result
151pub type OutputResult = Result<process::Output, OutputError>;
152
153/// [`Command`] error.
154///
155/// Generally produced by [`OutputOkExt`].
156///
157/// # Examples
158///
159/// ```rust,no_run
160/// use assert_cmd::prelude::*;
161///
162/// use std::process::Command;
163///
164/// let err = Command::new("a-command")
165///     .args(&["--will-fail"])
166///     .unwrap_err();
167/// ```
168///
169/// [`Command`]: std::process::Command
170#[derive(Debug)]
171pub struct OutputError {
172    cmd: Option<String>,
173    stdin: Option<bstr::BString>,
174    cause: OutputCause,
175}
176
177impl OutputError {
178    /// Convert [`Output`] into an [`Error`].
179    ///
180    /// [`Output`]: std::process::Output
181    /// [`Error`]: std::error::Error
182    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    /// For errors that happen in creating a [`Output`].
191    ///
192    /// [`Output`]: std::process::Output
193    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    /// Add the command line for additional context.
205    pub fn set_cmd(mut self, cmd: String) -> Self {
206        self.cmd = Some(cmd);
207        self
208    }
209
210    /// Add the `stdin` for additional context.
211    pub fn set_stdin(mut self, stdin: Vec<u8>) -> Self {
212        self.stdin = Some(bstr::BString::from(stdin));
213        self
214    }
215
216    /// Access the contained [`Output`].
217    ///
218    /// # Examples
219    ///
220    /// ```rust,no_run
221    /// use assert_cmd::prelude::*;
222    ///
223    /// use std::process::Command;
224    ///
225    /// let err = Command::new("a-command")
226    ///     .args(&["--will-fail"])
227    ///     .unwrap_err();
228    /// let output = err
229    ///     .as_output()
230    ///     .unwrap();
231    /// assert_eq!(Some(42), output.status.code());
232    /// ```
233    ///
234    /// [`Output`]: std::process::Output
235    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}