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}