Skip to main content

kawauso_process/execution/
mod.rs

1//! The result of one run of an external command
2//!
3//! A run produces one value that describes what happened: how the command
4//! ended, what it wrote to each of its streams, and how long it took. This
5//! module holds that value and the capture of one stream.
6//!
7//! A command that ends without success is not a failure of the run. The exit
8//! status is part of the result, and the caller decides what it means.
9
10pub mod capture;
11
12use std::process::ExitStatus;
13use std::time::Duration;
14
15pub use self::capture::Capture;
16use crate::error::RequireSuccessError;
17use crate::invocation::Invocation;
18use crate::process_id::ProcessId;
19
20/// The result of one run of an external command
21///
22/// The value holds the command that ran, the exit status of the command, what
23/// the command wrote to its standard output and to its standard error, and
24/// the time that the run took. It also holds the identifier that the
25/// operating system gave the command. The two streams stay apart, because a
26/// program separates its result from its diagnostics.
27///
28/// A status that is not a success is data. The check mode of a formatter ends
29/// without success when it finds a file to format, and that is the answer
30/// that the caller asked for. A caller that must not accept such a status
31/// calls [`require_success`][require-success].
32///
33/// # Examples
34///
35/// ```no_run
36/// use kawauso_process::Invocation;
37///
38/// # #[tokio::main(flavor = "current_thread")]
39/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
40/// let execution = Invocation::new("git").arg("status").run().await?;
41///
42/// println!("{}", execution.stdout());
43/// # Ok(())
44/// # }
45/// ```
46///
47/// [require-success]: Execution::require_success
48#[derive(Clone, Eq, PartialEq, Debug)]
49pub struct Execution {
50    /// The command that ran
51    ///
52    /// The result carries it so that a log line and an error message can name
53    /// the command without the caller holding it a second time.
54    invocation: Invocation,
55
56    /// The identifier that the operating system gave the command
57    ///
58    /// `None` when no command produced the result, which is the case for a
59    /// result that a caller built.
60    id: Option<ProcessId>,
61
62    /// The status with which the command ended
63    status: ExitStatus,
64
65    /// What the command wrote to its standard output
66    stdout: Capture,
67
68    /// What the command wrote to its standard error
69    stderr: Capture,
70
71    /// The time that the run took
72    duration: Duration,
73}
74
75impl Execution {
76    /// Creates the result of a run
77    ///
78    /// A run builds this value, and a caller builds one where a test stands
79    /// in for a command that no one starts. The identifier is the one that
80    /// the operating system gave the command, and a result that no command
81    /// produced carries none.
82    pub fn new(
83        invocation: Invocation,
84        id: Option<ProcessId>,
85        status: ExitStatus,
86        stdout: Capture,
87        stderr: Capture,
88        duration: Duration,
89    ) -> Self {
90        Self {
91            invocation,
92            id,
93            status,
94            stdout,
95            stderr,
96            duration,
97        }
98    }
99
100    /// Returns the time that the run took
101    ///
102    /// The time covers the whole run, from the start of the program to the
103    /// end of the capture. A caller that reports a slow step, or that logs
104    /// how long a tool ran, reads it here instead of measuring the call.
105    // process[impl run.duration]
106    pub fn duration(&self) -> Duration {
107        self.duration
108    }
109
110    /// Returns the identifier that the operating system gave the command
111    ///
112    /// An application that reports which program ran, or that writes the
113    /// identifier to a log, reads it here. The value is `None` when no run
114    /// produced the result, such as a result that a test built.
115    ///
116    /// The command of a result has ended, and the operating system can give
117    /// the identifier to another command. The value therefore names the
118    /// command that ran. It does not name a command that runs now.
119    // process[impl run.identity]
120    pub fn id(&self) -> Option<ProcessId> {
121        self.id
122    }
123
124    /// Returns the command that ran
125    pub fn invocation(&self) -> &Invocation {
126        &self.invocation
127    }
128
129    /// Returns the result of a run that ended with success
130    ///
131    /// A caller that must not accept a command that failed makes the check
132    /// with this method instead of reading the status itself. The result
133    /// travels on, so a caller can take the output of the command from it.
134    ///
135    /// # Errors
136    ///
137    /// Returns [`UnsuccessfulCommand`][unsuccessful] when the command ended
138    /// without success. The error names the command, the status, and what the
139    /// command wrote to its standard error, which is where a program states
140    /// why it stopped.
141    ///
142    /// # Examples
143    ///
144    /// ```no_run
145    /// use kawauso_process::Invocation;
146    ///
147    /// # #[tokio::main(flavor = "current_thread")]
148    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
149    /// let execution = Invocation::new("git")
150    ///     .arg("rev-parse")
151    ///     .arg("HEAD")
152    ///     .run()
153    ///     .await?
154    ///     .require_success()?;
155    ///
156    /// let commit = execution.stdout().to_string_lossy().trim().to_owned();
157    /// # Ok(())
158    /// # }
159    /// ```
160    ///
161    /// [unsuccessful]: RequireSuccessError::UnsuccessfulCommand
162    // process[impl run.success]
163    pub fn require_success(self) -> Result<Self, RequireSuccessError> {
164        if self.status.success() {
165            Ok(self)
166        } else {
167            Err(RequireSuccessError::UnsuccessfulCommand {
168                invocation: Box::new(self.invocation),
169                status: self.status,
170                stderr: self.stderr,
171            })
172        }
173    }
174
175    /// Returns the status with which the command ended
176    ///
177    /// A status that is not a success is a normal result. A command that ran
178    /// and failed reports its failure here, and the run itself returns an
179    /// error only when the command could not run at all.
180    // process[impl run.exit]
181    pub fn status(&self) -> ExitStatus {
182        self.status
183    }
184
185    /// Returns what the command wrote to its standard error
186    // process[impl run.output]
187    pub fn stderr(&self) -> &Capture {
188        &self.stderr
189    }
190
191    /// Returns what the command wrote to its standard output
192    // process[impl run.output]
193    pub fn stdout(&self) -> &Capture {
194        &self.stdout
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    // An assertion in a test panics by design. A `# Panics` section on every
201    // test would repeat that and give the reader no information.
202    #![allow(clippy::missing_panics_doc)]
203
204    use super::*;
205
206    // A caller keeps the result of a run in a report that another thread
207    // reads. This test holds the type to the auto traits that make this
208    // possible, because a private field of a later version could take them
209    // away without a word from the compiler.
210    #[test]
211    fn execution_is_send_and_sync() {
212        fn assert_send_and_sync<T: Send + Sync>() {}
213
214        assert_send_and_sync::<Execution>();
215    }
216}