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/// The invocation travels in a box. A result carries its error by value, so
23/// the size of the error reaches every function that returns one. The box
24/// keeps the error small, and a caller that wraps it in an error of its own
25/// needs no box for it.
26///
27/// A later release can add variants, and it can add fields to a variant.
28/// Match with a wildcard arm, and bind the fields of a variant with `..`.
29#[derive(Clone, Eq, PartialEq, Debug)]
30#[non_exhaustive]
31pub enum RequireSuccessError {
32 /// The command ended without success
33 ///
34 /// The exit status says how the command ended. A command that ran to its
35 /// end reports a code, and a command that a signal stopped reports no
36 /// code at all.
37 #[non_exhaustive]
38 UnsuccessfulCommand {
39 /// The command that ended without success
40 invocation: Box<Invocation>,
41
42 /// The status with which the command ended
43 status: ExitStatus,
44
45 /// What the command wrote to its standard error
46 stderr: Capture,
47 },
48}
49
50/// States which command ended how, and what it wrote about it
51///
52/// The message holds the capture of the standard error, because the reader of
53/// a report has only this message and the command wrote its reason there. A
54/// command that wrote nothing says so, so that no reader looks for a text
55/// that does not exist.
56// process[impl run.success.message]
57impl Display for RequireSuccessError {
58 fn fmt(&self, formatter: &mut Formatter<'_>) -> Result {
59 match self {
60 Self::UnsuccessfulCommand {
61 invocation,
62 status,
63 stderr,
64 } => {
65 write!(formatter, "the command `{invocation}` ended with {status}")?;
66
67 let text = stderr.to_string_lossy();
68 let reason = text.trim();
69
70 if reason.is_empty() {
71 formatter.write_str(", and it wrote nothing to its standard error")
72 } else {
73 write!(formatter, ", and it wrote `{reason}` to its standard error")
74 }
75 }
76 }
77 }
78}
79
80/// Reports the error to the tools of the ecosystem
81///
82/// The error has no source. The command ran, and no other operation failed
83/// under this one.
84impl Error for RequireSuccessError {}
85
86#[cfg(test)]
87mod tests {
88 // An assertion in a test panics by design. A `# Panics` section on every
89 // test would repeat that and give the reader no information.
90 #![allow(clippy::missing_panics_doc)]
91
92 use super::*;
93
94 // A caller sends the error between threads and keeps it in a report that
95 // another thread reads. This test holds the error to the auto traits that
96 // make this possible, because a private field of a later version could
97 // take them away without a word from the compiler.
98 #[test]
99 fn require_success_error_is_send_and_sync() {
100 fn assert_send_and_sync<T: Send + Sync>() {}
101
102 assert_send_and_sync::<RequireSuccessError>();
103 }
104
105 // A result carries its error by value, and a consumer wraps the error in
106 // an error of its own. An error that grows makes every such wrapper grow
107 // with it, until the wrapper needs a box that the consumer writes. This
108 // test holds the error to a size that leaves the consumer that room.
109 #[test]
110 fn require_success_error_stays_small() {
111 assert!(size_of::<RequireSuccessError>() <= 6 * size_of::<usize>());
112 }
113}