docker_wrapper/command/
wait.rs

1//! Docker wait command implementation.
2//!
3//! This module provides the `docker wait` command for waiting until containers stop.
4
5use super::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8
9/// Docker wait command builder
10///
11/// Block until one or more containers stop, then print their exit codes.
12///
13/// # Example
14///
15/// ```no_run
16/// use docker_wrapper::WaitCommand;
17///
18/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
19/// // Wait for a single container
20/// let result = WaitCommand::new("my-container")
21///     .run()
22///     .await?;
23///
24/// println!("Exit code: {}", result.exit_codes()[0]);
25///
26/// // Wait for multiple containers
27/// let result = WaitCommand::new_multiple(vec!["web", "db"])
28///     .run()
29///     .await?;
30/// # Ok(())
31/// # }
32/// ```
33#[derive(Debug, Clone)]
34pub struct WaitCommand {
35    /// Container names or IDs to wait for
36    containers: Vec<String>,
37    /// Command executor
38    pub executor: CommandExecutor,
39}
40
41impl WaitCommand {
42    /// Create a new wait command for a single container
43    ///
44    /// # Example
45    ///
46    /// ```
47    /// use docker_wrapper::WaitCommand;
48    ///
49    /// let cmd = WaitCommand::new("my-container");
50    /// ```
51    #[must_use]
52    pub fn new(container: impl Into<String>) -> Self {
53        Self {
54            containers: vec![container.into()],
55            executor: CommandExecutor::new(),
56        }
57    }
58
59    /// Create a new wait command for multiple containers
60    ///
61    /// # Example
62    ///
63    /// ```
64    /// use docker_wrapper::WaitCommand;
65    ///
66    /// let cmd = WaitCommand::new_multiple(vec!["web", "db", "cache"]);
67    /// ```
68    #[must_use]
69    pub fn new_multiple(containers: Vec<impl Into<String>>) -> Self {
70        Self {
71            containers: containers.into_iter().map(Into::into).collect(),
72            executor: CommandExecutor::new(),
73        }
74    }
75
76    /// Add another container to wait for
77    #[must_use]
78    pub fn container(mut self, container: impl Into<String>) -> Self {
79        self.containers.push(container.into());
80        self
81    }
82
83    /// Execute the wait command
84    ///
85    /// # Errors
86    /// Returns an error if:
87    /// - The Docker daemon is not running
88    /// - Any of the specified containers don't exist
89    ///
90    /// # Example
91    ///
92    /// ```no_run
93    /// use docker_wrapper::WaitCommand;
94    ///
95    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
96    /// let result = WaitCommand::new("my-container")
97    ///     .run()
98    ///     .await?;
99    ///
100    /// if result.success() {
101    ///     println!("Container exited with code: {}", result.exit_codes()[0]);
102    /// }
103    /// # Ok(())
104    /// # }
105    /// ```
106    pub async fn run(&self) -> Result<WaitResult> {
107        let output = self.execute().await?;
108
109        // Parse exit codes from output
110        let exit_codes = Self::parse_exit_codes(&output.stdout);
111
112        Ok(WaitResult {
113            output,
114            containers: self.containers.clone(),
115            exit_codes,
116        })
117    }
118
119    /// Parse exit codes from command output
120    fn parse_exit_codes(stdout: &str) -> Vec<i32> {
121        stdout
122            .lines()
123            .filter_map(|line| line.trim().parse().ok())
124            .collect()
125    }
126}
127
128#[async_trait]
129impl DockerCommand for WaitCommand {
130    type Output = CommandOutput;
131
132    fn get_executor(&self) -> &CommandExecutor {
133        &self.executor
134    }
135
136    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
137        &mut self.executor
138    }
139
140    fn build_command_args(&self) -> Vec<String> {
141        let mut args = vec!["wait".to_string()];
142        args.extend(self.containers.clone());
143        args.extend(self.executor.raw_args.clone());
144        args
145    }
146
147    async fn execute(&self) -> Result<Self::Output> {
148        if self.containers.is_empty() {
149            return Err(crate::error::Error::invalid_config(
150                "No containers specified for waiting",
151            ));
152        }
153
154        let args = self.build_command_args();
155        let command_name = args[0].clone();
156        let command_args = args[1..].to_vec();
157        self.executor
158            .execute_command(&command_name, command_args)
159            .await
160    }
161}
162
163/// Result from the wait command
164#[derive(Debug, Clone)]
165pub struct WaitResult {
166    /// Raw command output
167    pub output: CommandOutput,
168    /// Containers that were waited for
169    pub containers: Vec<String>,
170    /// Exit codes from the containers
171    pub exit_codes: Vec<i32>,
172}
173
174impl WaitResult {
175    /// Check if the wait was successful
176    #[must_use]
177    pub fn success(&self) -> bool {
178        self.output.success
179    }
180
181    /// Get the waited container names
182    #[must_use]
183    pub fn containers(&self) -> &[String] {
184        &self.containers
185    }
186
187    /// Get the exit codes
188    #[must_use]
189    pub fn exit_codes(&self) -> &[i32] {
190        &self.exit_codes
191    }
192
193    /// Check if all containers exited successfully (exit code 0)
194    #[must_use]
195    pub fn all_successful(&self) -> bool {
196        self.exit_codes.iter().all(|&code| code == 0)
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_wait_single_container() {
206        let cmd = WaitCommand::new("test-container");
207        let args = cmd.build_command_args();
208        assert_eq!(args, vec!["wait", "test-container"]);
209    }
210
211    #[test]
212    fn test_wait_multiple_containers() {
213        let cmd = WaitCommand::new_multiple(vec!["web", "db", "cache"]);
214        let args = cmd.build_command_args();
215        assert_eq!(args, vec!["wait", "web", "db", "cache"]);
216    }
217
218    #[test]
219    fn test_wait_add_container() {
220        let cmd = WaitCommand::new("web").container("db").container("cache");
221        let args = cmd.build_command_args();
222        assert_eq!(args, vec!["wait", "web", "db", "cache"]);
223    }
224
225    #[test]
226    fn test_parse_exit_codes() {
227        let output = "0\n1\n130";
228        let codes = WaitCommand::parse_exit_codes(output);
229        assert_eq!(codes, vec![0, 1, 130]);
230    }
231
232    #[test]
233    fn test_parse_exit_codes_empty() {
234        let codes = WaitCommand::parse_exit_codes("");
235        assert!(codes.is_empty());
236    }
237
238    #[test]
239    fn test_all_successful() {
240        let result = WaitResult {
241            output: CommandOutput {
242                stdout: "0\n0".to_string(),
243                stderr: String::new(),
244                exit_code: 0,
245                success: true,
246            },
247            containers: vec!["web".to_string(), "db".to_string()],
248            exit_codes: vec![0, 0],
249        };
250        assert!(result.all_successful());
251
252        let result_with_failure = WaitResult {
253            output: CommandOutput {
254                stdout: "0\n1".to_string(),
255                stderr: String::new(),
256                exit_code: 0,
257                success: true,
258            },
259            containers: vec!["web".to_string(), "db".to_string()],
260            exit_codes: vec![0, 1],
261        };
262        assert!(!result_with_failure.all_successful());
263    }
264}