@@ -15,34 +15,81 @@ use crate::{
15
15
/// This is a custom command wrapper that simplifies working with commands and makes it easier to
16
16
/// ensure that we check the exit status of executed processes.
17
17
///
18
- /// # A [`Command`] must be executed
18
+ /// # A [`Command`] must be executed exactly once
19
19
///
20
20
/// A [`Command`] is armed by a [`DropBomb`] on construction to enforce that it will be executed. If
21
21
/// a [`Command`] is constructed but never executed, the drop bomb will explode and cause the test
22
22
/// to panic. Execution methods [`run`] and [`run_fail`] will defuse the drop bomb. A test
23
23
/// containing constructed but never executed commands is dangerous because it can give a false
24
24
/// sense of confidence.
25
25
///
26
+ /// Each [`Command`] invocation can also only be executed once, because we want to enforce
27
+ /// `std{in,out,err}` config via [`std::process::Stdio`] but [`std::process::Stdio`] is not
28
+ /// cloneable.
29
+ ///
30
+ /// In this sense, [`Command`] exhibits linear type semantics but enforced at run-time.
31
+ ///
26
32
/// [`run`]: Self::run
27
33
/// [`run_fail`]: Self::run_fail
28
34
/// [`run_unchecked`]: Self::run_unchecked
29
35
#[ derive( Debug ) ]
30
36
pub struct Command {
31
37
cmd : StdCommand ,
32
- stdin : Option < Box < [ u8 ] > > ,
38
+ // Convience for providing a quick stdin buffer.
39
+ stdin_buf : Option < Box < [ u8 ] > > ,
40
+
41
+ // Configurations for child process's std{in,out,err} handles.
42
+ stdin : Option < Stdio > ,
43
+ stdout : Option < Stdio > ,
44
+ stderr : Option < Stdio > ,
45
+
46
+ // Emulate linear type semantics.
33
47
drop_bomb : DropBomb ,
48
+ already_executed : bool ,
34
49
}
35
50
36
51
impl Command {
37
52
#[ track_caller]
38
53
pub fn new < P : AsRef < OsStr > > ( program : P ) -> Self {
39
54
let program = program. as_ref ( ) ;
40
- Self { cmd : StdCommand :: new ( program) , stdin : None , drop_bomb : DropBomb :: arm ( program) }
55
+ Self {
56
+ cmd : StdCommand :: new ( program) ,
57
+ stdin_buf : None ,
58
+ drop_bomb : DropBomb :: arm ( program) ,
59
+ stdin : None ,
60
+ stdout : None ,
61
+ stderr : None ,
62
+ already_executed : false ,
63
+ }
41
64
}
42
65
43
- /// Specify a stdin input
44
- pub fn stdin < I : AsRef < [ u8 ] > > ( & mut self , input : I ) -> & mut Self {
45
- self . stdin = Some ( input. as_ref ( ) . to_vec ( ) . into_boxed_slice ( ) ) ;
66
+ /// Specify a stdin input buffer. This is a convenience helper,
67
+ pub fn stdin_buf < I : AsRef < [ u8 ] > > ( & mut self , input : I ) -> & mut Self {
68
+ self . stdin_buf = Some ( input. as_ref ( ) . to_vec ( ) . into_boxed_slice ( ) ) ;
69
+ self
70
+ }
71
+
72
+ /// Configuration for the child process’s standard input (stdin) handle.
73
+ ///
74
+ /// See [`std::process::Command::stdin`].
75
+ pub fn stdin < T : Into < Stdio > > ( & mut self , cfg : T ) -> & mut Self {
76
+ self . stdin = Some ( cfg. into ( ) ) ;
77
+ self
78
+ }
79
+
80
+ /// Configuration for the child process’s standard output (stdout) handle.
81
+ ///
82
+ /// See [`std::process::Command::stdout`].
83
+ pub fn stdout < T : Into < Stdio > > ( & mut self , cfg : T ) -> & mut Self {
84
+ self . stdout = Some ( cfg. into ( ) ) ;
85
+ self
86
+ }
87
+
88
+ /// Configuration for the child process’s standard error (stderr) handle.
89
+ ///
90
+ /// See [`std::process::Command::stderr`].
91
+ pub fn stderr < T : Into < Stdio > > ( & mut self , cfg : T ) -> & mut Self {
92
+ self . stderr = Some ( cfg. into ( ) ) ;
46
93
self
47
94
}
48
95
@@ -105,6 +152,8 @@ impl Command {
105
152
}
106
153
107
154
/// Run the constructed command and assert that it is successfully run.
155
+ ///
156
+ /// By default, std{in,out,err} are [`Stdio::piped()`].
108
157
#[ track_caller]
109
158
pub fn run ( & mut self ) -> CompletedProcess {
110
159
let output = self . command_output ( ) ;
@@ -115,6 +164,8 @@ impl Command {
115
164
}
116
165
117
166
/// Run the constructed command and assert that it does not successfully run.
167
+ ///
168
+ /// By default, std{in,out,err} are [`Stdio::piped()`].
118
169
#[ track_caller]
119
170
pub fn run_fail ( & mut self ) -> CompletedProcess {
120
171
let output = self . command_output ( ) ;
@@ -124,24 +175,30 @@ impl Command {
124
175
output
125
176
}
126
177
127
- /// Run the command but do not check its exit status.
128
- /// Only use if you explicitly don't care about the exit status.
129
- /// Prefer to use [`Self::run`] and [`Self::run_fail`]
130
- /// whenever possible.
178
+ /// Run the command but do not check its exit status. Only use if you explicitly don't care
179
+ /// about the exit status.
180
+ ///
181
+ /// Prefer to use [`Self::run`] and [`Self::run_fail`] whenever possible.
131
182
#[ track_caller]
132
183
pub fn run_unchecked ( & mut self ) -> CompletedProcess {
133
184
self . command_output ( )
134
185
}
135
186
136
187
#[ track_caller]
137
188
fn command_output ( & mut self ) -> CompletedProcess {
189
+ if self . already_executed {
190
+ panic ! ( "command was already executed" ) ;
191
+ } else {
192
+ self . already_executed = true ;
193
+ }
194
+
138
195
self . drop_bomb . defuse ( ) ;
139
196
// let's make sure we piped all the input and outputs
140
- self . cmd . stdin ( Stdio :: piped ( ) ) ;
141
- self . cmd . stdout ( Stdio :: piped ( ) ) ;
142
- self . cmd . stderr ( Stdio :: piped ( ) ) ;
197
+ self . cmd . stdin ( self . stdin . take ( ) . unwrap_or ( Stdio :: piped ( ) ) ) ;
198
+ self . cmd . stdout ( self . stdout . take ( ) . unwrap_or ( Stdio :: piped ( ) ) ) ;
199
+ self . cmd . stderr ( self . stderr . take ( ) . unwrap_or ( Stdio :: piped ( ) ) ) ;
143
200
144
- let output = if let Some ( input) = & self . stdin {
201
+ let output = if let Some ( input) = & self . stdin_buf {
145
202
let mut child = self . cmd . spawn ( ) . unwrap ( ) ;
146
203
147
204
{
0 commit comments