shell_tunnel/execution/
command.rs1use std::collections::HashMap;
4use std::path::PathBuf;
5use std::time::Duration;
6
7#[derive(Debug, Clone)]
9pub struct Command {
10 pub command_line: String,
12 pub working_dir: Option<PathBuf>,
14 pub env: HashMap<String, String>,
16 pub timeout: Option<Duration>,
18 pub capture_output: bool,
20 pub max_output_bytes: Option<u64>,
25}
26
27impl Command {
28 pub fn new(command_line: impl Into<String>) -> Self {
30 Self {
31 command_line: command_line.into(),
32 working_dir: None,
33 env: HashMap::new(),
34 timeout: None,
35 capture_output: true,
36 max_output_bytes: None,
37 }
38 }
39
40 pub fn working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
42 self.working_dir = Some(dir.into());
43 self
44 }
45
46 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
48 self.env.insert(key.into(), value.into());
49 self
50 }
51
52 pub fn envs<I, K, V>(mut self, vars: I) -> Self
54 where
55 I: IntoIterator<Item = (K, V)>,
56 K: Into<String>,
57 V: Into<String>,
58 {
59 for (k, v) in vars {
60 self.env.insert(k.into(), v.into());
61 }
62 self
63 }
64
65 pub fn timeout(mut self, duration: Duration) -> Self {
67 self.timeout = Some(duration);
68 self
69 }
70
71 pub fn capture_output(mut self, capture: bool) -> Self {
73 self.capture_output = capture;
74 self
75 }
76
77 pub fn max_output_bytes(mut self, bytes: u64) -> Self {
79 self.max_output_bytes = Some(bytes);
80 self
81 }
82
83 pub fn effective_timeout(&self) -> Duration {
99 self.timeout
100 .unwrap_or(super::executor::DEFAULT_TIMEOUT)
101 .clamp(super::executor::MIN_TIMEOUT, super::executor::MAX_TIMEOUT)
102 }
103}
104
105impl Default for Command {
106 fn default() -> Self {
107 Self::new("")
108 }
109}
110
111#[derive(Debug, Default)]
113pub struct CommandBuilder {
114 command_line: Option<String>,
115 working_dir: Option<PathBuf>,
116 env: HashMap<String, String>,
117 timeout: Option<Duration>,
118 capture_output: bool,
119 max_output_bytes: Option<u64>,
120}
121
122impl CommandBuilder {
123 pub fn new() -> Self {
125 Self {
126 capture_output: true,
127 ..Default::default()
128 }
129 }
130
131 pub fn command_line(mut self, cmd: impl Into<String>) -> Self {
133 self.command_line = Some(cmd.into());
134 self
135 }
136
137 pub fn working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
139 self.working_dir = Some(dir.into());
140 self
141 }
142
143 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
145 self.env.insert(key.into(), value.into());
146 self
147 }
148
149 pub fn timeout(mut self, duration: Duration) -> Self {
151 self.timeout = Some(duration);
152 self
153 }
154
155 pub fn capture_output(mut self, capture: bool) -> Self {
157 self.capture_output = capture;
158 self
159 }
160
161 pub fn build(self) -> Option<Command> {
165 self.command_line.map(|cmd| Command {
166 command_line: cmd,
167 working_dir: self.working_dir,
168 env: self.env,
169 timeout: self.timeout,
170 capture_output: self.capture_output,
171 max_output_bytes: self.max_output_bytes,
172 })
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::execution::{DEFAULT_TIMEOUT, MAX_TIMEOUT, MIN_TIMEOUT};
180
181 #[test]
183 fn an_unset_timeout_is_the_default() {
184 assert_eq!(Command::new("echo hi").effective_timeout(), DEFAULT_TIMEOUT);
185 assert!(
186 DEFAULT_TIMEOUT >= MIN_TIMEOUT && DEFAULT_TIMEOUT <= MAX_TIMEOUT,
187 "the default must itself be a value a caller could have asked for"
188 );
189 }
190
191 #[test]
193 fn a_timeout_within_the_range_is_taken_as_asked() {
194 let asked = Duration::from_secs(45);
195 assert_eq!(
196 Command::new("echo hi").timeout(asked).effective_timeout(),
197 asked
198 );
199 }
200
201 #[test]
208 fn a_timeout_above_the_ceiling_is_clamped() {
209 let absurd = Duration::from_secs(999_999_999);
210 assert_eq!(
211 Command::new("echo hi").timeout(absurd).effective_timeout(),
212 MAX_TIMEOUT
213 );
214 }
215
216 #[test]
222 fn a_zero_timeout_is_raised_to_the_floor() {
223 assert_eq!(
224 Command::new("echo hi")
225 .timeout(Duration::from_secs(0))
226 .effective_timeout(),
227 MIN_TIMEOUT
228 );
229 }
230
231 #[test]
232 fn test_command_new() {
233 let cmd = Command::new("ls -la");
234 assert_eq!(cmd.command_line, "ls -la");
235 assert!(cmd.working_dir.is_none());
236 assert!(cmd.env.is_empty());
237 assert!(cmd.timeout.is_none());
238 assert!(cmd.capture_output);
239 }
240
241 #[test]
242 fn test_command_builder_chain() {
243 let cmd = Command::new("cargo build")
244 .working_dir("/project")
245 .env("RUST_LOG", "debug")
246 .timeout(Duration::from_secs(60))
247 .capture_output(true);
248
249 assert_eq!(cmd.command_line, "cargo build");
250 assert_eq!(cmd.working_dir, Some(PathBuf::from("/project")));
251 assert_eq!(cmd.env.get("RUST_LOG"), Some(&"debug".to_string()));
252 assert_eq!(cmd.timeout, Some(Duration::from_secs(60)));
253 }
254
255 #[test]
256 fn test_command_envs() {
257 let vars = [("KEY1", "val1"), ("KEY2", "val2")];
258 let cmd = Command::new("echo").envs(vars);
259
260 assert_eq!(cmd.env.len(), 2);
261 assert_eq!(cmd.env.get("KEY1"), Some(&"val1".to_string()));
262 assert_eq!(cmd.env.get("KEY2"), Some(&"val2".to_string()));
263 }
264
265 #[test]
266 fn test_command_builder_build() {
267 let cmd = CommandBuilder::new()
268 .command_line("pwd")
269 .working_dir("/tmp")
270 .build();
271
272 assert!(cmd.is_some());
273 let cmd = cmd.unwrap();
274 assert_eq!(cmd.command_line, "pwd");
275 assert_eq!(cmd.working_dir, Some(PathBuf::from("/tmp")));
276 }
277
278 #[test]
279 fn test_command_builder_empty() {
280 let cmd = CommandBuilder::new().build();
281 assert!(cmd.is_none());
282 }
283}