1use super::{CommandExecutor, DockerCommand};
7use crate::error::{Error, Result};
8use async_trait::async_trait;
9
10#[derive(Debug, Clone)]
12pub struct StartCommand {
13 pub executor: CommandExecutor,
15 containers: Vec<String>,
17 attach: bool,
19 checkpoint: Option<String>,
21 checkpoint_dir: Option<String>,
23 detach_keys: Option<String>,
25 interactive: bool,
27}
28
29#[derive(Debug, Clone, PartialEq)]
31pub struct StartResult {
32 pub stdout: String,
34 pub stderr: String,
36 pub started_containers: Vec<String>,
38}
39
40impl StartCommand {
41 pub fn new(container: impl Into<String>) -> Self {
57 Self {
58 executor: CommandExecutor::new(),
59 containers: vec![container.into()],
60 attach: false,
61 checkpoint: None,
62 checkpoint_dir: None,
63 detach_keys: None,
64 interactive: false,
65 }
66 }
67
68 pub fn new_multiple<I, S>(containers: I) -> Self
78 where
79 I: IntoIterator<Item = S>,
80 S: Into<String>,
81 {
82 Self {
83 executor: CommandExecutor::new(),
84 containers: containers.into_iter().map(Into::into).collect(),
85 attach: false,
86 checkpoint: None,
87 checkpoint_dir: None,
88 detach_keys: None,
89 interactive: false,
90 }
91 }
92
93 #[must_use]
104 pub fn attach(mut self) -> Self {
105 self.attach = true;
106 self
107 }
108
109 #[must_use]
120 pub fn checkpoint(mut self, checkpoint: impl Into<String>) -> Self {
121 self.checkpoint = Some(checkpoint.into());
122 self
123 }
124
125 #[must_use]
136 pub fn checkpoint_dir(mut self, dir: impl Into<String>) -> Self {
137 self.checkpoint_dir = Some(dir.into());
138 self
139 }
140
141 #[must_use]
152 pub fn detach_keys(mut self, keys: impl Into<String>) -> Self {
153 self.detach_keys = Some(keys.into());
154 self
155 }
156
157 #[must_use]
168 pub fn interactive(mut self) -> Self {
169 self.interactive = true;
170 self
171 }
172
173 #[must_use]
184 pub fn ai(self) -> Self {
185 self.attach().interactive()
186 }
187}
188
189#[async_trait]
190impl DockerCommand for StartCommand {
191 type Output = StartResult;
192
193 fn get_executor(&self) -> &CommandExecutor {
194 &self.executor
195 }
196
197 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
198 &mut self.executor
199 }
200
201 fn build_command_args(&self) -> Vec<String> {
202 let mut args = vec!["start".to_string()];
203
204 if self.attach {
206 args.push("--attach".to_string());
207 }
208
209 if let Some(checkpoint) = &self.checkpoint {
211 args.push("--checkpoint".to_string());
212 args.push(checkpoint.clone());
213 }
214
215 if let Some(checkpoint_dir) = &self.checkpoint_dir {
217 args.push("--checkpoint-dir".to_string());
218 args.push(checkpoint_dir.clone());
219 }
220
221 if let Some(detach_keys) = &self.detach_keys {
223 args.push("--detach-keys".to_string());
224 args.push(detach_keys.clone());
225 }
226
227 if self.interactive {
229 args.push("--interactive".to_string());
230 }
231
232 args.extend(self.containers.clone());
234
235 args.extend(self.executor.raw_args.clone());
237
238 args
239 }
240
241 async fn execute(&self) -> Result<Self::Output> {
242 if self.containers.is_empty() {
243 return Err(Error::invalid_config("No containers specified"));
244 }
245
246 let args = self.build_command_args();
247 let output = self.execute_command(args).await?;
248
249 let started_containers = if output.stdout.trim().is_empty() {
251 self.containers.clone()
253 } else {
254 output
256 .stdout
257 .lines()
258 .filter(|line| !line.trim().is_empty())
259 .map(|line| line.trim().to_string())
260 .collect()
261 };
262
263 Ok(StartResult {
264 stdout: output.stdout,
265 stderr: output.stderr,
266 started_containers,
267 })
268 }
269}
270
271impl StartCommand {
272 #[must_use]
274 pub fn args(&self) -> Vec<String> {
275 self.build_command_args()
276 }
277}
278
279impl StartResult {
280 #[must_use]
282 pub fn is_success(&self) -> bool {
283 !self.started_containers.is_empty()
284 }
285
286 #[must_use]
288 pub fn container_count(&self) -> usize {
289 self.started_containers.len()
290 }
291
292 #[must_use]
294 pub fn first_container(&self) -> Option<&String> {
295 self.started_containers.first()
296 }
297
298 #[must_use]
300 pub fn contains_container(&self, container: &str) -> bool {
301 self.started_containers.iter().any(|c| c == container)
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
310 fn test_start_command_new() {
311 let cmd = StartCommand::new("test-container");
312 assert_eq!(cmd.containers, vec!["test-container"]);
313 assert!(!cmd.attach);
314 assert!(cmd.checkpoint.is_none());
315 assert!(cmd.checkpoint_dir.is_none());
316 assert!(cmd.detach_keys.is_none());
317 assert!(!cmd.interactive);
318 }
319
320 #[test]
321 fn test_start_command_new_multiple() {
322 let cmd = StartCommand::new_multiple(vec!["container1", "container2"]);
323 assert_eq!(cmd.containers, vec!["container1", "container2"]);
324 }
325
326 #[test]
327 fn test_start_command_with_attach() {
328 let cmd = StartCommand::new("test-container").attach();
329 assert!(cmd.attach);
330 }
331
332 #[test]
333 fn test_start_command_with_checkpoint() {
334 let cmd = StartCommand::new("test-container").checkpoint("checkpoint1");
335 assert_eq!(cmd.checkpoint, Some("checkpoint1".to_string()));
336 }
337
338 #[test]
339 fn test_start_command_with_checkpoint_dir() {
340 let cmd = StartCommand::new("test-container").checkpoint_dir("/custom/dir");
341 assert_eq!(cmd.checkpoint_dir, Some("/custom/dir".to_string()));
342 }
343
344 #[test]
345 fn test_start_command_with_detach_keys() {
346 let cmd = StartCommand::new("test-container").detach_keys("ctrl-p,ctrl-q");
347 assert_eq!(cmd.detach_keys, Some("ctrl-p,ctrl-q".to_string()));
348 }
349
350 #[test]
351 fn test_start_command_with_interactive() {
352 let cmd = StartCommand::new("test-container").interactive();
353 assert!(cmd.interactive);
354 }
355
356 #[test]
357 fn test_start_command_ai_convenience() {
358 let cmd = StartCommand::new("test-container").ai();
359 assert!(cmd.attach);
360 assert!(cmd.interactive);
361 }
362
363 #[test]
364 fn test_start_command_args_basic() {
365 let cmd = StartCommand::new("test-container");
366 let args = cmd.args();
367 assert_eq!(args, vec!["start", "test-container"]);
368 }
369
370 #[test]
371 fn test_start_command_args_with_options() {
372 let cmd = StartCommand::new("test-container")
373 .attach()
374 .interactive()
375 .checkpoint("checkpoint1");
376 let args = cmd.args();
377 assert_eq!(
378 args,
379 vec![
380 "start",
381 "--attach",
382 "--checkpoint",
383 "checkpoint1",
384 "--interactive",
385 "test-container"
386 ]
387 );
388 }
389
390 #[test]
391 fn test_start_command_args_multiple_containers() {
392 let cmd =
393 StartCommand::new_multiple(vec!["container1", "container2"]).detach_keys("ctrl-c");
394 let args = cmd.args();
395 assert_eq!(
396 args,
397 vec![
398 "start",
399 "--detach-keys",
400 "ctrl-c",
401 "container1",
402 "container2"
403 ]
404 );
405 }
406
407 #[test]
408 fn test_start_result_is_success() {
409 let result = StartResult {
410 stdout: "container1\n".to_string(),
411 stderr: String::new(),
412 started_containers: vec!["container1".to_string()],
413 };
414 assert!(result.is_success());
415
416 let empty_result = StartResult {
417 stdout: String::new(),
418 stderr: String::new(),
419 started_containers: vec![],
420 };
421 assert!(!empty_result.is_success());
422 }
423
424 #[test]
425 fn test_start_result_container_count() {
426 let result = StartResult {
427 stdout: String::new(),
428 stderr: String::new(),
429 started_containers: vec!["container1".to_string(), "container2".to_string()],
430 };
431 assert_eq!(result.container_count(), 2);
432 }
433
434 #[test]
435 fn test_start_result_first_container() {
436 let result = StartResult {
437 stdout: String::new(),
438 stderr: String::new(),
439 started_containers: vec!["container1".to_string(), "container2".to_string()],
440 };
441 assert_eq!(result.first_container(), Some(&"container1".to_string()));
442
443 let empty_result = StartResult {
444 stdout: String::new(),
445 stderr: String::new(),
446 started_containers: vec![],
447 };
448 assert_eq!(empty_result.first_container(), None);
449 }
450
451 #[test]
452 fn test_start_result_contains_container() {
453 let result = StartResult {
454 stdout: String::new(),
455 stderr: String::new(),
456 started_containers: vec!["container1".to_string(), "container2".to_string()],
457 };
458 assert!(result.contains_container("container1"));
459 assert!(result.contains_container("container2"));
460 assert!(!result.contains_container("container3"));
461 }
462
463 #[test]
464 fn test_command_name() {
465 let cmd = StartCommand::new("test");
466 let args = cmd.build_command_args();
467 assert_eq!(args[0], "start");
468 }
469}