docker_wrapper/command/
stop.rs

1//! Docker stop command implementation.
2//!
3//! This module provides a comprehensive implementation of the `docker stop` command
4//! with support for all native options and an extensible architecture.
5
6use super::{CommandExecutor, DockerCommand};
7use crate::error::{Error, Result};
8use async_trait::async_trait;
9use std::time::Duration;
10
11/// Docker stop command builder with fluent API
12#[derive(Debug, Clone)]
13pub struct StopCommand {
14    /// Command executor for extensibility
15    pub executor: CommandExecutor,
16    /// Container IDs or names to stop
17    containers: Vec<String>,
18    /// Signal to send to the container
19    signal: Option<String>,
20    /// Seconds to wait before killing the container
21    timeout: Option<u32>,
22}
23
24/// Result of a stop command execution
25#[derive(Debug, Clone, PartialEq)]
26pub struct StopResult {
27    /// Raw stdout from the command
28    pub stdout: String,
29    /// Raw stderr from the command
30    pub stderr: String,
31    /// Container IDs that were stopped
32    pub stopped_containers: Vec<String>,
33}
34
35impl StopCommand {
36    /// Create a new stop command for the specified container(s)
37    ///
38    /// # Examples
39    ///
40    /// ```
41    /// use docker_wrapper::StopCommand;
42    ///
43    /// let cmd = StopCommand::new("my-container");
44    /// ```
45    ///
46    /// ```
47    /// use docker_wrapper::StopCommand;
48    ///
49    /// let cmd = StopCommand::new_multiple(vec!["container1", "container2"]);
50    /// ```
51    pub fn new(container: impl Into<String>) -> Self {
52        Self {
53            executor: CommandExecutor::new(),
54            containers: vec![container.into()],
55            signal: None,
56            timeout: None,
57        }
58    }
59
60    /// Create a new stop command for multiple containers
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// use docker_wrapper::StopCommand;
66    ///
67    /// let cmd = StopCommand::new_multiple(vec!["container1", "container2"]);
68    /// ```
69    pub fn new_multiple<I, S>(containers: I) -> Self
70    where
71        I: IntoIterator<Item = S>,
72        S: Into<String>,
73    {
74        Self {
75            executor: CommandExecutor::new(),
76            containers: containers.into_iter().map(Into::into).collect(),
77            signal: None,
78            timeout: None,
79        }
80    }
81
82    /// Set the signal to send to the container
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// use docker_wrapper::StopCommand;
88    ///
89    /// let cmd = StopCommand::new("my-container")
90    ///     .signal("SIGTERM");
91    /// ```
92    #[must_use]
93    pub fn signal(mut self, signal: impl Into<String>) -> Self {
94        self.signal = Some(signal.into());
95        self
96    }
97
98    /// Set the timeout in seconds to wait before killing the container
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// use docker_wrapper::StopCommand;
104    ///
105    /// let cmd = StopCommand::new("my-container")
106    ///     .timeout(30);
107    /// ```
108    #[must_use]
109    pub fn timeout(mut self, timeout: u32) -> Self {
110        self.timeout = Some(timeout);
111        self
112    }
113
114    /// Set the timeout using a Duration
115    ///
116    /// # Examples
117    ///
118    /// ```
119    /// use docker_wrapper::StopCommand;
120    /// use std::time::Duration;
121    ///
122    /// let cmd = StopCommand::new("my-container")
123    ///     .timeout_duration(Duration::from_secs(30));
124    /// ```
125    #[must_use]
126    #[allow(clippy::cast_possible_truncation)]
127    pub fn timeout_duration(mut self, timeout: Duration) -> Self {
128        self.timeout = Some(timeout.as_secs().min(u64::from(u32::MAX)) as u32);
129        self
130    }
131}
132
133#[async_trait]
134impl DockerCommand for StopCommand {
135    type Output = StopResult;
136
137    fn get_executor(&self) -> &CommandExecutor {
138        &self.executor
139    }
140
141    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
142        &mut self.executor
143    }
144
145    fn build_command_args(&self) -> Vec<String> {
146        let mut args = vec!["stop".to_string()];
147
148        // Add signal option
149        if let Some(signal) = &self.signal {
150            args.push("--signal".to_string());
151            args.push(signal.clone());
152        }
153
154        // Add timeout option
155        if let Some(timeout) = self.timeout {
156            args.push("--timeout".to_string());
157            args.push(timeout.to_string());
158        }
159
160        // Add container names/IDs
161        args.extend(self.containers.clone());
162
163        // Add raw arguments from executor
164        args.extend(self.executor.raw_args.clone());
165
166        args
167    }
168
169    async fn execute(&self) -> Result<Self::Output> {
170        if self.containers.is_empty() {
171            return Err(Error::invalid_config("No containers specified"));
172        }
173
174        let args = self.build_command_args();
175        let output = self.execute_command(args).await?;
176
177        // Parse the output to extract stopped container IDs
178        let stopped_containers = if output.stdout.trim().is_empty() {
179            // If no stdout, assume the containers specified were stopped
180            self.containers.clone()
181        } else {
182            // Parse container IDs from stdout (each line is a container ID)
183            output
184                .stdout
185                .lines()
186                .filter(|line| !line.trim().is_empty())
187                .map(|line| line.trim().to_string())
188                .collect()
189        };
190
191        Ok(StopResult {
192            stdout: output.stdout,
193            stderr: output.stderr,
194            stopped_containers,
195        })
196    }
197}
198
199impl StopCommand {
200    /// Get the command arguments (for testing)
201    #[must_use]
202    pub fn args(&self) -> Vec<String> {
203        self.build_command_args()
204    }
205}
206
207impl StopResult {
208    /// Check if the command was successful
209    #[must_use]
210    pub fn is_success(&self) -> bool {
211        !self.stopped_containers.is_empty()
212    }
213
214    /// Get the number of containers that were stopped
215    #[must_use]
216    pub fn container_count(&self) -> usize {
217        self.stopped_containers.len()
218    }
219
220    /// Get the first stopped container ID (useful for single container operations)
221    #[must_use]
222    pub fn first_container(&self) -> Option<&String> {
223        self.stopped_containers.first()
224    }
225
226    /// Check if a specific container was stopped
227    #[must_use]
228    pub fn contains_container(&self, container: &str) -> bool {
229        self.stopped_containers.iter().any(|c| c == container)
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_stop_command_new() {
239        let cmd = StopCommand::new("test-container");
240        assert_eq!(cmd.containers, vec!["test-container"]);
241        assert!(cmd.signal.is_none());
242        assert!(cmd.timeout.is_none());
243    }
244
245    #[test]
246    fn test_stop_command_new_multiple() {
247        let cmd = StopCommand::new_multiple(vec!["container1", "container2"]);
248        assert_eq!(cmd.containers, vec!["container1", "container2"]);
249    }
250
251    #[test]
252    fn test_stop_command_with_signal() {
253        let cmd = StopCommand::new("test-container").signal("SIGKILL");
254        assert_eq!(cmd.signal, Some("SIGKILL".to_string()));
255    }
256
257    #[test]
258    fn test_stop_command_with_timeout() {
259        let cmd = StopCommand::new("test-container").timeout(30);
260        assert_eq!(cmd.timeout, Some(30));
261    }
262
263    #[test]
264    fn test_stop_command_with_timeout_duration() {
265        let cmd = StopCommand::new("test-container").timeout_duration(Duration::from_secs(45));
266        assert_eq!(cmd.timeout, Some(45));
267    }
268
269    #[test]
270    fn test_stop_command_args_basic() {
271        let cmd = StopCommand::new("test-container");
272        let args = cmd.args();
273        assert_eq!(args, vec!["stop", "test-container"]);
274    }
275
276    #[test]
277    fn test_stop_command_args_with_options() {
278        let cmd = StopCommand::new("test-container")
279            .signal("SIGTERM")
280            .timeout(30);
281        let args = cmd.args();
282        assert_eq!(
283            args,
284            vec![
285                "stop",
286                "--signal",
287                "SIGTERM",
288                "--timeout",
289                "30",
290                "test-container"
291            ]
292        );
293    }
294
295    #[test]
296    fn test_stop_command_args_multiple_containers() {
297        let cmd = StopCommand::new_multiple(vec!["container1", "container2"]).timeout(10);
298        let args = cmd.args();
299        assert_eq!(
300            args,
301            vec!["stop", "--timeout", "10", "container1", "container2"]
302        );
303    }
304
305    #[test]
306    fn test_stop_result_is_success() {
307        let result = StopResult {
308            stdout: "container1\n".to_string(),
309            stderr: String::new(),
310            stopped_containers: vec!["container1".to_string()],
311        };
312        assert!(result.is_success());
313
314        let empty_result = StopResult {
315            stdout: String::new(),
316            stderr: String::new(),
317            stopped_containers: vec![],
318        };
319        assert!(!empty_result.is_success());
320    }
321
322    #[test]
323    fn test_stop_result_container_count() {
324        let result = StopResult {
325            stdout: String::new(),
326            stderr: String::new(),
327            stopped_containers: vec!["container1".to_string(), "container2".to_string()],
328        };
329        assert_eq!(result.container_count(), 2);
330    }
331
332    #[test]
333    fn test_stop_result_first_container() {
334        let result = StopResult {
335            stdout: String::new(),
336            stderr: String::new(),
337            stopped_containers: vec!["container1".to_string(), "container2".to_string()],
338        };
339        assert_eq!(result.first_container(), Some(&"container1".to_string()));
340
341        let empty_result = StopResult {
342            stdout: String::new(),
343            stderr: String::new(),
344            stopped_containers: vec![],
345        };
346        assert_eq!(empty_result.first_container(), None);
347    }
348
349    #[test]
350    fn test_stop_result_contains_container() {
351        let result = StopResult {
352            stdout: String::new(),
353            stderr: String::new(),
354            stopped_containers: vec!["container1".to_string(), "container2".to_string()],
355        };
356        assert!(result.contains_container("container1"));
357        assert!(result.contains_container("container2"));
358        assert!(!result.contains_container("container3"));
359    }
360
361    #[test]
362    fn test_command_name() {
363        let cmd = StopCommand::new("test");
364        let args = cmd.build_command_args();
365        assert_eq!(args[0], "stop");
366    }
367}