Skip to main content

kawauso_process/error/
require_success.rs

1//! The error of requiring the success of a command
2
3use std::error::Error;
4use std::fmt::Display;
5use std::fmt::Formatter;
6use std::fmt::Result;
7use std::process::ExitStatus;
8
9use crate::execution::Capture;
10use crate::invocation::Invocation;
11
12/// The error returned when a command ended without success
13///
14/// The error carries the three answers that the reader of a report needs:
15/// which command ran, how it ended, and what it said. A program states why it
16/// stopped on its standard error, so the error carries that capture and its
17/// message shows it.
18///
19/// The command ran. Nothing failed inside the crate, and the error exists
20/// because the caller stated that only success is acceptable.
21///
22/// A later release can add variants, and it can add fields to a variant.
23/// Match with a wildcard arm, and bind the fields of a variant with `..`.
24#[derive(Clone, Eq, PartialEq, Debug)]
25#[non_exhaustive]
26pub enum RequireSuccessError {
27    /// The command ended without success
28    ///
29    /// The exit status says how the command ended. A command that ran to its
30    /// end reports a code, and a command that a signal stopped reports no
31    /// code at all.
32    #[non_exhaustive]
33    UnsuccessfulCommand {
34        /// The command that ended without success
35        invocation: Invocation,
36
37        /// The status with which the command ended
38        status: ExitStatus,
39
40        /// What the command wrote to its standard error
41        stderr: Capture,
42    },
43}
44
45/// States which command ended how, and what it wrote about it
46///
47/// The message holds the capture of the standard error, because the reader of
48/// a report has only this message and the command wrote its reason there. A
49/// command that wrote nothing says so, so that no reader looks for a text
50/// that does not exist.
51// process[impl run.success.message]
52impl Display for RequireSuccessError {
53    fn fmt(&self, formatter: &mut Formatter<'_>) -> Result {
54        match self {
55            Self::UnsuccessfulCommand {
56                invocation,
57                status,
58                stderr,
59            } => {
60                write!(formatter, "the command `{invocation}` ended with {status}")?;
61
62                let text = stderr.to_string_lossy();
63                let reason = text.trim();
64
65                if reason.is_empty() {
66                    formatter.write_str(", and it wrote nothing to its standard error")
67                } else {
68                    write!(formatter, ", and it wrote `{reason}` to its standard error")
69                }
70            }
71        }
72    }
73}
74
75/// Reports the error to the tools of the ecosystem
76///
77/// The error has no source. The command ran, and no other operation failed
78/// under this one.
79impl Error for RequireSuccessError {}
80
81#[cfg(test)]
82mod tests {
83    // An assertion in a test panics by design. A `# Panics` section on every
84    // test would repeat that and give the reader no information.
85    #![allow(clippy::missing_panics_doc)]
86
87    use super::*;
88
89    // A caller sends the error between threads and keeps it in a report that
90    // another thread reads. This test holds the error to the auto traits that
91    // make this possible, because a private field of a later version could
92    // take them away without a word from the compiler.
93    #[test]
94    fn require_success_error_is_send_and_sync() {
95        fn assert_send_and_sync<T: Send + Sync>() {}
96
97        assert_send_and_sync::<RequireSuccessError>();
98    }
99}