Skip to main content

kawauso_process/execution/
capture.rs

1//! The bytes that a command wrote to one of its streams
2
3use std::borrow::Cow;
4use std::fmt::Display;
5use std::fmt::Formatter;
6use std::fmt::Result;
7
8/// The bytes that a command wrote to one of its streams
9///
10/// The value holds the bytes as the command wrote them. A command writes what
11/// it wants: the lines of a report, the bytes of an archive, or a sequence
12/// that is not valid UTF-8 at all. A conversion into text would lose the
13/// bytes that have no character, so the type keeps the bytes and offers the
14/// text to the caller that wants it.
15///
16/// # Examples
17///
18/// ```
19/// use kawauso_process::execution::Capture;
20///
21/// let capture = Capture::new("hello\n");
22///
23/// assert_eq!(capture.to_string_lossy().trim(), "hello");
24/// ```
25#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
26pub struct Capture(Vec<u8>);
27
28impl Capture {
29    /// Creates the capture of a stream from the bytes that a command wrote
30    ///
31    /// # Examples
32    ///
33    /// ```
34    /// use kawauso_process::execution::Capture;
35    ///
36    /// let capture = Capture::new(vec![b'h', b'i']);
37    ///
38    /// assert_eq!(capture.get(), b"hi");
39    /// ```
40    pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
41        Self(bytes.into())
42    }
43
44    /// Returns the bytes as the command wrote them
45    pub fn get(&self) -> &[u8] {
46        &self.0
47    }
48
49    /// Returns whether the command wrote nothing to the stream
50    pub fn is_empty(&self) -> bool {
51        self.0.is_empty()
52    }
53
54    /// Returns the capture as text
55    ///
56    /// A byte that is part of no valid character becomes the replacement
57    /// character `U+FFFD`. The text is therefore always valid UTF-8, and a
58    /// caller that needs the bytes of the command asks for [`get`][get]
59    /// instead.
60    ///
61    /// # Examples
62    ///
63    /// ```
64    /// use kawauso_process::execution::Capture;
65    ///
66    /// let capture = Capture::new(vec![0xff]);
67    ///
68    /// assert_eq!(capture.to_string_lossy(), "\u{fffd}");
69    /// ```
70    ///
71    /// [get]: Capture::get
72    pub fn to_string_lossy(&self) -> Cow<'_, str> {
73        String::from_utf8_lossy(&self.0)
74    }
75}
76
77/// Shows the capture for a reader
78///
79/// The text is the lossy conversion that [`to_string_lossy`][to-string-lossy]
80/// returns, because a log line and an error message take text and not bytes.
81///
82/// [to-string-lossy]: Capture::to_string_lossy
83impl Display for Capture {
84    fn fmt(&self, formatter: &mut Formatter<'_>) -> Result {
85        formatter.write_str(&self.to_string_lossy())
86    }
87}
88
89/// Creates the capture of a stream from the bytes that a command wrote
90impl From<Vec<u8>> for Capture {
91    fn from(bytes: Vec<u8>) -> Self {
92        Self::new(bytes)
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    // An assertion in a test panics by design. A `# Panics` section on every
99    // test would repeat that and give the reader no information.
100    #![allow(clippy::missing_panics_doc)]
101
102    use super::*;
103
104    // A caller keeps the capture of a run in a report that another thread
105    // reads. This test holds the type to the auto traits that make this
106    // possible, because a private field of a later version could take them
107    // away without a word from the compiler.
108    #[test]
109    fn capture_is_send_and_sync() {
110        fn assert_send_and_sync<T: Send + Sync>() {}
111
112        assert_send_and_sync::<Capture>();
113    }
114
115    // A command that writes nothing leaves an empty capture, and a caller
116    // that reports only what a command said asks for this state.
117    #[test]
118    fn is_empty_without_bytes_reports_an_empty_capture() {
119        let capture = Capture::default();
120
121        assert!(capture.is_empty());
122    }
123
124    // The text of the capture reaches a log line through `Display`, and a
125    // caller that shows it gets the same text as the lossy conversion.
126    #[test]
127    fn to_string_returns_the_capture_as_text() {
128        let capture = Capture::new("hello");
129
130        assert_eq!(capture.to_string(), "hello");
131    }
132}