#![allow(dead_code, unused_imports, unused_variables)]
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Duration, Instant};
fn copy_dir_recursive(src: &Path, dst: &Path, skip_name: Option<&str>) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let name = entry.file_name();
let name_str = name.to_string_lossy();
if let Some(skip) = skip_name {
if name_str == skip {
continue;
}
}
let src_path = entry.path();
let dst_path = dst.join(&name);
if entry.file_type()?.is_dir() {
copy_dir_recursive(&src_path, &dst_path, skip_name)?;
} else {
if let Err(e) = std::fs::copy(&src_path, &dst_path) {
tracing::warn!("sandbox: skipping file {}: {}", src_path.display(), e);
}
}
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct SandboxConfig {
pub image: String,
pub cpus: String,
pub memory: String,
pub timeout: Duration,
pub network: bool,
}
impl Default for SandboxConfig {
fn default() -> Self {
Self {
image: "selfware:latest".to_string(),
cpus: "2".to_string(),
memory: "4g".to_string(),
timeout: Duration::from_secs(3600),
network: false,
}
}
}
#[derive(Debug)]
pub struct Sandbox {
pub container_id: String,
pub container_name: String,
pub config: SandboxConfig,
pub created_at: Instant,
_workspace_tmp: Option<tempfile::TempDir>,
}
#[derive(Debug)]
pub struct SandboxResult {
pub compiled: bool,
pub compile_duration: Duration,
pub tests_passed: usize,
pub tests_total: usize,
pub test_duration: Duration,
pub peak_memory_bytes: u64,
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
}
impl Sandbox {
pub fn create(
name: &str,
repo_root: &Path,
config: SandboxConfig,
) -> Result<Self, SandboxError> {
let container_name = format!("selfware-arena-{}", name);
let workspace_tmp = tempfile::tempdir().map_err(|e| {
SandboxError::IoError(format!("Failed to create workspace temp dir: {}", e))
})?;
let workspace_path = workspace_tmp.path().to_path_buf();
if let Err(e) = copy_dir_recursive(repo_root, &workspace_path, Some("target")) {
return Err(SandboxError::IoError(format!(
"Failed to copy repo to temp workspace: {}",
e
)));
}
let workspace_mount = workspace_path.canonicalize().unwrap_or(workspace_path);
let mut args = vec![
"run".to_string(),
"-d".to_string(),
"--name".to_string(),
container_name.clone(),
format!("--cpus={}", config.cpus),
format!("--memory={}", config.memory),
"-v".to_string(),
format!("{}:/workspace:rw", workspace_mount.display()),
"-v".to_string(),
format!("{}/target:/workspace/target", repo_root.display()),
];
if !config.network {
args.push("--network=none".to_string());
}
args.push(config.image.clone());
args.push("sleep".to_string());
args.push("infinity".to_string());
let output = Command::new("docker")
.args(&args)
.output()
.map_err(|e| SandboxError::DockerFailed(e.to_string()))?;
if !output.status.success() {
return Err(SandboxError::DockerFailed(
String::from_utf8_lossy(&output.stderr).to_string(),
));
}
let container_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(Sandbox {
container_id,
container_name,
config,
created_at: Instant::now(),
_workspace_tmp: Some(workspace_tmp),
})
}
pub fn exec(&self, cmd: &str) -> Result<ExecResult, SandboxError> {
let start = Instant::now();
let output = Command::new("docker")
.args(["exec", &self.container_name, "bash", "-c", cmd])
.output()
.map_err(|e| SandboxError::ExecFailed(e.to_string()))?;
Ok(ExecResult {
success: output.status.success(),
exit_code: output.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
duration: start.elapsed(),
})
}
pub fn apply_patch(&self, patch: &str) -> Result<bool, SandboxError> {
let patch_file = format!("/tmp/mutation-{}.patch", self.container_name);
std::fs::write(&patch_file, patch).map_err(|e| SandboxError::IoError(e.to_string()))?;
let _ = Command::new("docker")
.args([
"cp",
&patch_file,
&format!("{}:/tmp/mutation.patch", self.container_name),
])
.output();
let result = self.exec("cd /workspace && git apply /tmp/mutation.patch")?;
let _ = std::fs::remove_file(&patch_file);
Ok(result.success)
}
pub fn evaluate(&self) -> Result<SandboxResult, SandboxError> {
let compile = self.exec("cd /workspace && cargo build --release 2>&1")?;
if !compile.success {
return Ok(SandboxResult {
compiled: false,
compile_duration: compile.duration,
tests_passed: 0,
tests_total: 0,
test_duration: Duration::ZERO,
peak_memory_bytes: 0,
stdout: compile.stdout,
stderr: compile.stderr,
exit_code: compile.exit_code,
});
}
let test = self.exec("cd /workspace && cargo test --all-features 2>&1")?;
let (passed, total) = parse_test_counts(&test.stdout);
let stats = self.get_stats()?;
Ok(SandboxResult {
compiled: true,
compile_duration: compile.duration,
tests_passed: passed,
tests_total: total,
test_duration: test.duration,
peak_memory_bytes: stats.peak_memory_bytes,
stdout: format!("{}\n---\n{}", compile.stdout, test.stdout),
stderr: format!("{}\n---\n{}", compile.stderr, test.stderr),
exit_code: test.exit_code,
})
}
fn get_stats(&self) -> Result<ContainerStats, SandboxError> {
let output = Command::new("docker")
.args([
"stats",
"--no-stream",
"--format",
"{{.MemUsage}}",
&self.container_name,
])
.output()
.map_err(|e| SandboxError::DockerFailed(e.to_string()))?;
let mem_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
let peak = parse_memory_string(&mem_str);
Ok(ContainerStats {
peak_memory_bytes: peak,
})
}
pub fn destroy(self) -> Result<(), SandboxError> {
let _ = Command::new("docker")
.args(["rm", "-f", &self.container_name])
.output();
Ok(())
}
pub fn is_expired(&self) -> bool {
self.created_at.elapsed() > self.config.timeout
}
}
#[derive(Debug)]
pub struct ExecResult {
pub success: bool,
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub duration: Duration,
}
#[derive(Debug)]
struct ContainerStats {
peak_memory_bytes: u64,
}
fn parse_test_counts(output: &str) -> (usize, usize) {
for line in output.lines().rev() {
if line.contains("test result:") {
let mut passed = 0;
let mut failed = 0;
let mut ignored = 0;
for part in line.split(';') {
let part = part.trim();
if part.contains("passed") {
passed = part
.split_whitespace()
.filter_map(|w| w.parse().ok())
.next()
.unwrap_or(0);
} else if part.contains("failed") {
failed = part
.split_whitespace()
.filter_map(|w| w.parse().ok())
.next()
.unwrap_or(0);
} else if part.contains("ignored") {
ignored = part
.split_whitespace()
.filter_map(|w| w.parse().ok())
.next()
.unwrap_or(0);
}
}
return (passed, passed + failed + ignored);
}
}
(0, 0)
}
fn parse_memory_string(mem: &str) -> u64 {
let usage = mem.split('/').next().unwrap_or("0").trim();
if usage.contains("GiB") {
let n: f64 = usage.replace("GiB", "").trim().parse().unwrap_or(0.0);
(n * 1024.0 * 1024.0 * 1024.0) as u64
} else if usage.contains("MiB") {
let n: f64 = usage.replace("MiB", "").trim().parse().unwrap_or(0.0);
(n * 1024.0 * 1024.0) as u64
} else if usage.contains("KiB") {
let n: f64 = usage.replace("KiB", "").trim().parse().unwrap_or(0.0);
(n * 1024.0) as u64
} else {
0
}
}
#[derive(Debug)]
pub enum SandboxError {
DockerFailed(String),
ExecFailed(String),
IoError(String),
Timeout,
}
impl std::fmt::Display for SandboxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DockerFailed(msg) => write!(f, "Docker operation failed: {}", msg),
Self::ExecFailed(msg) => write!(f, "Container exec failed: {}", msg),
Self::IoError(msg) => write!(f, "IO error: {}", msg),
Self::Timeout => write!(f, "Sandbox evaluation timed out"),
}
}
}
impl std::error::Error for SandboxError {}
#[cfg(test)]
#[path = "../../tests/unit/evolution/sandbox/sandbox_test.rs"]
mod tests;