use std::{io, path::Path, process, time::Duration}; use testing_framework_core::adjust_timeout; use tokio::{process::Command, time::timeout}; const COMPOSE_UP_TIMEOUT: Duration = Duration::from_secs(120); /// Errors running docker compose commands. #[derive(Debug, thiserror::Error)] pub enum ComposeCommandError { #[error("{command} exited with status {status}")] Failed { command: String, status: process::ExitStatus, }, #[error("failed to spawn {command}: {source}")] Spawn { command: String, #[source] source: io::Error, }, #[error("{command} timed out after {timeout:?}")] Timeout { command: String, timeout: Duration }, } /// Runs `docker compose up -d` for the generated stack. pub async fn compose_up( compose_path: &Path, project_name: &str, root: &Path, ) -> Result<(), ComposeCommandError> { let mut cmd = Command::new("docker"); cmd.arg("compose") .arg("-f") .arg(compose_path) .arg("-p") .arg(project_name) .arg("up") .arg("-d") .current_dir(root); run_compose_command(cmd, adjust_timeout(COMPOSE_UP_TIMEOUT), "docker compose up").await } /// Runs `docker compose down --volumes` for the generated stack. pub async fn compose_down( compose_path: &Path, project_name: &str, root: &Path, ) -> Result<(), ComposeCommandError> { let mut cmd = Command::new("docker"); cmd.arg("compose") .arg("-f") .arg(compose_path) .arg("-p") .arg(project_name) .arg("down") .arg("--volumes") .current_dir(root); run_compose_command( cmd, adjust_timeout(COMPOSE_UP_TIMEOUT), "docker compose down", ) .await } /// Dump docker compose logs to stderr for debugging failures. pub async fn dump_compose_logs(compose_file: &Path, project: &str, root: &Path) { let mut cmd = Command::new("docker"); cmd.arg("compose") .arg("-f") .arg(compose_file) .arg("-p") .arg(project) .arg("logs") .arg("--no-color") .current_dir(root); match cmd.output().await { Ok(output) => print_logs(&output.stdout, &output.stderr), Err(err) => eprintln!("[compose-runner] failed to collect docker compose logs: {err}"), } } fn print_logs(stdout: &[u8], stderr: &[u8]) { if !stdout.is_empty() { eprintln!( "[compose-runner] docker compose logs:\n{}", String::from_utf8_lossy(stdout) ); } if !stderr.is_empty() { eprintln!( "[compose-runner] docker compose errors:\n{}", String::from_utf8_lossy(stderr) ); } } async fn run_compose_command( mut command: Command, timeout_duration: Duration, description: &str, ) -> Result<(), ComposeCommandError> { let result = timeout(timeout_duration, command.status()).await; match result { Ok(status) => handle_compose_status(status, description), Err(_) => Err(ComposeCommandError::Timeout { command: description.to_owned(), timeout: timeout_duration, }), } } fn handle_compose_status( status: std::io::Result, description: &str, ) -> Result<(), ComposeCommandError> { match status { Ok(code) if code.success() => Ok(()), Ok(code) => Err(ComposeCommandError::Failed { command: description.to_owned(), status: code, }), Err(err) => Err(ComposeCommandError::Spawn { command: description.to_owned(), source: err, }), } }