use async_trait::async_trait;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum RuntimeError {
#[error("Runtime not available: {0}")]
NotAvailable(String),
#[error("Failed to start container: {0}")]
StartFailed(String),
#[error("Command execution failed: {0}")]
ExecutionFailed(String),
#[error("Command timed out after {0} seconds")]
Timeout(u64),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
pub type RuntimeResult<T> = std::result::Result<T, RuntimeError>;
#[derive(Debug, Clone)]
pub struct CommandOutput {
pub stdout: String,
pub stderr: String,
pub exit_code: Option<i32>,
}
impl CommandOutput {
pub fn new(stdout: String, stderr: String, exit_code: Option<i32>) -> Self {
Self {
stdout,
stderr,
exit_code,
}
}
pub fn success(&self) -> bool {
self.exit_code == Some(0)
}
pub fn format(&self) -> String {
let mut result = String::new();
if !self.stdout.is_empty() {
result.push_str(&self.stdout);
}
if !self.stderr.is_empty() {
if !result.is_empty() {
result.push_str("\n--- stderr ---\n");
}
result.push_str(&self.stderr);
}
if let Some(code) = self.exit_code {
if code != 0 {
result.push_str(&format!("\n[Exit code: {}]", code));
}
}
result
}
}
#[derive(Debug, Clone, Default)]
pub struct ContainerConfig {
pub workdir: Option<PathBuf>,
pub mounts: Vec<(PathBuf, PathBuf, bool)>,
pub env: Vec<(String, String)>,
pub timeout_secs: u64,
}
impl ContainerConfig {
pub fn new() -> Self {
Self {
timeout_secs: 60,
..Default::default()
}
}
pub fn with_workdir(mut self, workdir: PathBuf) -> Self {
self.workdir = Some(workdir);
self
}
pub fn with_mount(mut self, host: PathBuf, container: PathBuf, readonly: bool) -> Self {
self.mounts.push((host, container, readonly));
self
}
pub fn with_env(mut self, key: &str, value: &str) -> Self {
self.env.push((key.to_string(), value.to_string()));
self
}
pub fn with_timeout(mut self, secs: u64) -> Self {
self.timeout_secs = secs;
self
}
}
#[async_trait]
pub trait ContainerRuntime: Send + Sync {
fn name(&self) -> &str;
async fn is_available(&self) -> bool;
async fn execute(
&self,
command: &str,
config: &ContainerConfig,
) -> RuntimeResult<CommandOutput>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_command_output_success() {
let output = CommandOutput::new("hello".to_string(), "".to_string(), Some(0));
assert!(output.success());
}
#[test]
fn test_command_output_failure() {
let output = CommandOutput::new("".to_string(), "error".to_string(), Some(1));
assert!(!output.success());
}
#[test]
fn test_command_output_format_stdout_only() {
let output = CommandOutput::new("output".to_string(), "".to_string(), Some(0));
assert_eq!(output.format(), "output");
}
#[test]
fn test_command_output_format_with_stderr() {
let output = CommandOutput::new("stdout".to_string(), "stderr".to_string(), Some(0));
let formatted = output.format();
assert!(formatted.contains("stdout"));
assert!(formatted.contains("--- stderr ---"));
assert!(formatted.contains("stderr"));
}
#[test]
fn test_command_output_format_with_exit_code() {
let output = CommandOutput::new("".to_string(), "".to_string(), Some(1));
let formatted = output.format();
assert!(formatted.contains("[Exit code: 1]"));
}
#[test]
fn test_container_config_builder() {
let config = ContainerConfig::new()
.with_workdir(PathBuf::from("/workspace"))
.with_mount(PathBuf::from("/host"), PathBuf::from("/container"), true)
.with_env("FOO", "bar")
.with_timeout(120);
assert_eq!(config.workdir, Some(PathBuf::from("/workspace")));
assert_eq!(config.mounts.len(), 1);
assert_eq!(config.env.len(), 1);
assert_eq!(config.env[0], ("FOO".to_string(), "bar".to_string()));
assert_eq!(config.timeout_secs, 120);
}
#[test]
fn test_container_config_default_timeout() {
let config = ContainerConfig::new();
assert_eq!(config.timeout_secs, 60);
}
#[test]
fn test_runtime_error_display() {
let err = RuntimeError::Timeout(30);
assert_eq!(err.to_string(), "Command timed out after 30 seconds");
}
}