use super::{CommandExecutor, DockerCommand};
use crate::error::{Error, Result};
use async_trait::async_trait;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct StopCommand {
pub executor: CommandExecutor,
containers: Vec<String>,
signal: Option<String>,
timeout: Option<u32>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StopResult {
pub stdout: String,
pub stderr: String,
pub stopped_containers: Vec<String>,
}
impl StopCommand {
pub fn new(container: impl Into<String>) -> Self {
Self {
executor: CommandExecutor::new(),
containers: vec![container.into()],
signal: None,
timeout: None,
}
}
pub fn new_multiple<I, S>(containers: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
executor: CommandExecutor::new(),
containers: containers.into_iter().map(Into::into).collect(),
signal: None,
timeout: None,
}
}
#[must_use]
pub fn signal(mut self, signal: impl Into<String>) -> Self {
self.signal = Some(signal.into());
self
}
#[must_use]
pub fn timeout(mut self, timeout: u32) -> Self {
self.timeout = Some(timeout);
self
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn timeout_duration(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout.as_secs().min(u64::from(u32::MAX)) as u32);
self
}
}
#[async_trait]
impl DockerCommand for StopCommand {
type Output = StopResult;
fn get_executor(&self) -> &CommandExecutor {
&self.executor
}
fn get_executor_mut(&mut self) -> &mut CommandExecutor {
&mut self.executor
}
fn build_command_args(&self) -> Vec<String> {
let mut args = vec!["stop".to_string()];
if let Some(signal) = &self.signal {
args.push("--signal".to_string());
args.push(signal.clone());
}
if let Some(timeout) = self.timeout {
args.push("--timeout".to_string());
args.push(timeout.to_string());
}
args.extend(self.containers.clone());
args.extend(self.executor.raw_args.clone());
args
}
async fn execute(&self) -> Result<Self::Output> {
if self.containers.is_empty() {
return Err(Error::invalid_config("No containers specified"));
}
let args = self.build_command_args();
let output = self.execute_command(args).await?;
let stopped_containers = if output.stdout.trim().is_empty() {
self.containers.clone()
} else {
output
.stdout
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| line.trim().to_string())
.collect()
};
Ok(StopResult {
stdout: output.stdout,
stderr: output.stderr,
stopped_containers,
})
}
}
impl StopCommand {
#[must_use]
pub fn args(&self) -> Vec<String> {
self.build_command_args()
}
}
impl StopResult {
#[must_use]
pub fn is_success(&self) -> bool {
!self.stopped_containers.is_empty()
}
#[must_use]
pub fn container_count(&self) -> usize {
self.stopped_containers.len()
}
#[must_use]
pub fn first_container(&self) -> Option<&String> {
self.stopped_containers.first()
}
#[must_use]
pub fn contains_container(&self, container: &str) -> bool {
self.stopped_containers.iter().any(|c| c == container)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stop_command_new() {
let cmd = StopCommand::new("test-container");
assert_eq!(cmd.containers, vec!["test-container"]);
assert!(cmd.signal.is_none());
assert!(cmd.timeout.is_none());
}
#[test]
fn test_stop_command_new_multiple() {
let cmd = StopCommand::new_multiple(vec!["container1", "container2"]);
assert_eq!(cmd.containers, vec!["container1", "container2"]);
}
#[test]
fn test_stop_command_with_signal() {
let cmd = StopCommand::new("test-container").signal("SIGKILL");
assert_eq!(cmd.signal, Some("SIGKILL".to_string()));
}
#[test]
fn test_stop_command_with_timeout() {
let cmd = StopCommand::new("test-container").timeout(30);
assert_eq!(cmd.timeout, Some(30));
}
#[test]
fn test_stop_command_with_timeout_duration() {
let cmd = StopCommand::new("test-container").timeout_duration(Duration::from_secs(45));
assert_eq!(cmd.timeout, Some(45));
}
#[test]
fn test_stop_command_args_basic() {
let cmd = StopCommand::new("test-container");
let args = cmd.args();
assert_eq!(args, vec!["stop", "test-container"]);
}
#[test]
fn test_stop_command_args_with_options() {
let cmd = StopCommand::new("test-container")
.signal("SIGTERM")
.timeout(30);
let args = cmd.args();
assert_eq!(
args,
vec![
"stop",
"--signal",
"SIGTERM",
"--timeout",
"30",
"test-container"
]
);
}
#[test]
fn test_stop_command_args_multiple_containers() {
let cmd = StopCommand::new_multiple(vec!["container1", "container2"]).timeout(10);
let args = cmd.args();
assert_eq!(
args,
vec!["stop", "--timeout", "10", "container1", "container2"]
);
}
#[test]
fn test_stop_result_is_success() {
let result = StopResult {
stdout: "container1\n".to_string(),
stderr: String::new(),
stopped_containers: vec!["container1".to_string()],
};
assert!(result.is_success());
let empty_result = StopResult {
stdout: String::new(),
stderr: String::new(),
stopped_containers: vec![],
};
assert!(!empty_result.is_success());
}
#[test]
fn test_stop_result_container_count() {
let result = StopResult {
stdout: String::new(),
stderr: String::new(),
stopped_containers: vec!["container1".to_string(), "container2".to_string()],
};
assert_eq!(result.container_count(), 2);
}
#[test]
fn test_stop_result_first_container() {
let result = StopResult {
stdout: String::new(),
stderr: String::new(),
stopped_containers: vec!["container1".to_string(), "container2".to_string()],
};
assert_eq!(result.first_container(), Some(&"container1".to_string()));
let empty_result = StopResult {
stdout: String::new(),
stderr: String::new(),
stopped_containers: vec![],
};
assert_eq!(empty_result.first_container(), None);
}
#[test]
fn test_stop_result_contains_container() {
let result = StopResult {
stdout: String::new(),
stderr: String::new(),
stopped_containers: vec!["container1".to_string(), "container2".to_string()],
};
assert!(result.contains_container("container1"));
assert!(result.contains_container("container2"));
assert!(!result.contains_container("container3"));
}
#[test]
fn test_command_name() {
let cmd = StopCommand::new("test");
let args = cmd.build_command_args();
assert_eq!(args[0], "stop");
}
}