#![cfg(feature = "sandbox-it")]
#[allow(dead_code)]
mod support;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use rightsize::backend::{BackendProvider, SandboxBackend};
use rightsize::model::{ContainerSpec, PortBinding};
use rightsize::{Container, MountableFile, Wait};
use rightsize_docker::DockerBackendProvider;
use rightsize_msb::MsbBackendProvider;
macro_rules! require_backend {
() => {
if !support::requested_backend_available() {
support::skip_notice();
return;
}
};
}
fn read_only_mount_enforced() -> bool {
!matches!(
std::env::var("RIGHTSIZE_BACKEND"),
Ok(v) if v.eq_ignore_ascii_case("microsandbox") || v.eq_ignore_ascii_case("msb")
)
}
#[tokio::test]
async fn container_publishes_tcp_port_to_host_loopback() {
require_backend!();
let c = Container::new("python:3.12-alpine")
.with_command(&["python", "-m", "http.server", "8000"])
.with_exposed_ports(&[8000])
.waiting_for(
Wait::for_http("/")
.for_port(8000)
.with_startup_timeout(Duration::from_secs(120)),
);
let guard = c.start().await.expect("container must start");
let url = format!("http://127.0.0.1:{}/", guard.get_mapped_port(8000).unwrap());
let status = support::http_agent()
.get(&url)
.call()
.expect("GET / must succeed")
.status();
assert_eq!(status.as_u16(), 200);
guard.stop().await.unwrap();
}
#[tokio::test]
async fn env_vars_are_visible_to_the_workload() {
require_backend!();
let c = Container::new("alpine:3.19")
.with_env("RZ_PROBE", "hello-rz")
.with_command(&["sh", "-c", "sleep 120"])
.waiting_for(Wait::for_log_message(".*", 0).with_startup_timeout(Duration::from_secs(30)));
let guard = c.start().await.expect("container must start");
let r = guard
.exec(&["sh", "-c", "echo $RZ_PROBE"])
.await
.expect("exec must run");
assert_eq!(r.exit_code, 0);
assert!(r.stdout.contains("hello-rz"), "stdout was: {}", r.stdout);
guard.stop().await.unwrap();
}
#[tokio::test]
async fn exec_returns_real_exit_codes_and_stderr() {
require_backend!();
let c = Container::new("alpine:3.19")
.with_command(&["sleep", "120"])
.waiting_for(Wait::for_log_message(".*", 0).with_startup_timeout(Duration::from_secs(30)));
let guard = c.start().await.expect("container must start");
let r = guard
.exec(&["sh", "-c", "echo oops >&2; exit 7"])
.await
.expect("exec must run");
assert_eq!(r.exit_code, 7);
assert!(r.stderr.contains("oops"), "stderr was: {}", r.stderr);
guard.stop().await.unwrap();
}
#[tokio::test]
async fn logs_capture_workload_stdout_and_for_log_message_waits_on_them() {
require_backend!();
let c = Container::new("alpine:3.19")
.with_command(&["sh", "-c", "echo BOOT-MARKER; sleep 120"])
.waiting_for(Wait::for_log_message(".*BOOT-MARKER.*", 1));
let guard = c.start().await.expect("container must start");
let logs = guard.logs().await.expect("logs must be readable");
assert!(logs.contains("BOOT-MARKER"), "logs were: {logs}");
guard.stop().await.unwrap();
}
#[tokio::test]
async fn stop_terminates_and_frees_the_container() {
require_backend!();
let c = Container::new("alpine:3.19")
.with_command(&[
"sh",
"-c",
"while true; do printf 'HTTP/1.1 200 OK\\r\\nContent-Length: 2\\r\\n\\r\\nok' | nc -l -p 8000; done",
])
.with_exposed_ports(&[8000])
.waiting_for(
Wait::for_http("/").for_port(8000).with_startup_timeout(Duration::from_secs(120)),
);
let guard = c.start().await.expect("container must start");
assert!(guard.is_running());
let host_port = guard.get_mapped_port(8000).unwrap();
assert!(
std::net::TcpStream::connect(("127.0.0.1", host_port)).is_ok(),
"port must be reachable while running"
);
guard.stop().await.expect("stop must succeed");
tokio::time::sleep(Duration::from_millis(500)).await;
assert!(
std::net::TcpStream::connect(("127.0.0.1", host_port)).is_err(),
"port must no longer be reachable after stop() — container/sandbox must be torn down"
);
}
#[tokio::test]
async fn starting_a_container_writes_the_reaping_run_record() {
require_backend!();
let c = Container::new("alpine:3.19")
.with_command(&["sleep", "120"])
.waiting_for(Wait::for_log_message(".*", 0).with_startup_timeout(Duration::from_secs(30)));
let guard = c.start().await.expect("container must start");
let record_path = rightsize::cache_dir::dir()
.join("runs")
.join(format!("{}.json", rightsize::RunId::value()));
let raw = std::fs::read_to_string(&record_path)
.unwrap_or_else(|e| panic!("run record must exist at {record_path:?}: {e}"));
let parsed: serde_json::Value = serde_json::from_str(&raw)
.unwrap_or_else(|e| panic!("run record must be valid JSON: {e}\n{raw}"));
assert_eq!(
parsed.get("pid").and_then(serde_json::Value::as_u64),
Some(u64::from(std::process::id())),
"run record must name this process's own pid: {raw}"
);
guard.stop().await.unwrap();
}
#[tokio::test]
async fn with_copy_file_to_container_round_trips_a_bundled_resource_and_a_host_path() {
require_backend!();
let bundled_bytes = "rightsize-mount-fixture-payload\n";
let host_file =
std::env::temp_dir().join(format!("rightsize-hostpath-{}.txt", std::process::id()));
let host_bytes = "rightsize-host-path-payload\n";
std::fs::write(&host_file, host_bytes).unwrap();
let bundled = MountableFile::for_classpath_resource("tests/fixtures/rightsize-fixture.txt")
.expect("bundled fixture must resolve");
let host_mount = MountableFile::for_host_path(host_file.to_str().unwrap());
let c = Container::new("alpine:3.19")
.with_copy_file_to_container(bundled, "/mnt-a/from-classpath.txt")
.with_copy_file_to_container(host_mount, "/mnt-b/from-host.txt")
.with_command(&["sleep", "120"])
.waiting_for(Wait::for_log_message(".*", 0).with_startup_timeout(Duration::from_secs(30)));
let guard = c.start().await.expect("container must start");
let from_bundled = guard
.exec(&["cat", "/mnt-a/from-classpath.txt"])
.await
.expect("exec must run");
assert_eq!(
from_bundled.exit_code, 0,
"cat bundled mount failed: {}",
from_bundled.stderr
);
assert_eq!(from_bundled.stdout, bundled_bytes);
let from_host = guard
.exec(&["cat", "/mnt-b/from-host.txt"])
.await
.expect("exec must run");
assert_eq!(
from_host.exit_code, 0,
"cat host-path mount failed: {}",
from_host.stderr
);
assert_eq!(from_host.stdout, host_bytes);
guard.stop().await.unwrap();
std::fs::remove_file(&host_file).ok();
}
#[tokio::test]
async fn default_read_only_mount_rejects_an_in_guest_write() {
require_backend!();
let host_file = std::env::temp_dir().join(format!("rightsize-ro-{}.txt", std::process::id()));
std::fs::write(&host_file, "seed\n").unwrap();
let c = Container::new("alpine:3.19")
.with_copy_file_to_container(
MountableFile::for_host_path(host_file.to_str().unwrap()),
"/tmp/ro.txt",
)
.with_command(&["sleep", "120"])
.waiting_for(Wait::for_log_message(".*", 0).with_startup_timeout(Duration::from_secs(30)));
let guard = c.start().await.expect("container must start");
let write = guard
.exec(&["sh", "-c", "echo overwritten > /tmp/ro.txt"])
.await
.expect("exec must run");
if read_only_mount_enforced() {
assert_ne!(
write.exit_code, 0,
"expected read-only mount to reject the write; stderr={}",
write.stderr
);
} else {
assert_eq!(
write.exit_code, 0,
"msb read-only-mount write behavior changed — update read_only_mount_enforced pin"
);
}
guard.stop().await.unwrap();
std::fs::remove_file(&host_file).ok();
}
#[tokio::test]
async fn follow_output_streams_lines_in_order_and_close_halts_delivery() {
require_backend!();
let c = Container::new("alpine:3.19")
.with_command(&[
"sh",
"-c",
"i=1; while [ $i -le 20 ]; do echo LINE-$i; i=$((i+1)); sleep 0.2; done; sleep 120",
])
.waiting_for(Wait::for_log_message("LINE-1", 0));
let guard = c.start().await.expect("container must start");
let received: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let received_clone = received.clone();
let saw_three = Arc::new(tokio::sync::Notify::new());
let saw_three_notify = saw_three.clone();
let saw_three_fired = Arc::new(std::sync::atomic::AtomicBool::new(false));
let saw_three_fired_writer = saw_three_fired.clone();
let follow = guard
.follow_output(move |line| {
let mut lines = received_clone.lock().unwrap();
lines.push(line);
if lines.iter().filter(|l| l.contains("LINE-")).count() >= 3
&& !saw_three_fired_writer.swap(true, std::sync::atomic::Ordering::SeqCst)
{
saw_three_notify.notify_one();
}
})
.await
.expect("follow_output must start");
let waited = tokio::time::timeout(Duration::from_secs(20), saw_three.notified()).await;
assert!(
waited.is_ok() || saw_three_fired.load(std::sync::atomic::Ordering::SeqCst),
"did not observe 3 streamed lines in time; received so far: {:?}",
received.lock().unwrap()
);
let line_numbers: Vec<i32> = received
.lock()
.unwrap()
.iter()
.filter(|l| l.contains("LINE-"))
.filter_map(|l| l.strip_prefix("LINE-").and_then(|n| n.parse().ok()))
.collect();
let mut sorted = line_numbers.clone();
sorted.sort_unstable();
assert_eq!(
line_numbers, sorted,
"streamed lines arrived out of order: {line_numbers:?}"
);
follow.close();
let count_after_close = received.lock().unwrap().len();
tokio::time::sleep(Duration::from_secs(1)).await; assert_eq!(
count_after_close,
received.lock().unwrap().len(),
"follow_output kept delivering after close()"
);
guard.stop().await.unwrap();
}
#[tokio::test]
async fn follow_output_delivers_a_final_unterminated_line_after_the_workload_exits_exactly_once() {
require_backend!();
let c = Container::new("alpine:3.19")
.with_command(&[
"sh",
"-c",
"echo BOOT-MARKER; sleep 2; \
i=1; while [ $i -le 5 ]; do echo LINE-$i; i=$((i+1)); done; \
printf 'LINE-END-NO-NEWLINE'",
])
.waiting_for(Wait::for_log_message(".*BOOT-MARKER.*", 1));
let guard = c.start().await.expect("container must start");
let received: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let received_clone = received.clone();
let saw_final = Arc::new(tokio::sync::Notify::new());
let saw_final_notify = saw_final.clone();
let saw_final_fired = Arc::new(std::sync::atomic::AtomicBool::new(false));
let saw_final_fired_writer = saw_final_fired.clone();
let follow = guard
.follow_output(move |line| {
let mut lines = received_clone.lock().unwrap();
let is_final = line.contains("LINE-END-NO-NEWLINE");
lines.push(line);
if is_final && !saw_final_fired_writer.swap(true, std::sync::atomic::Ordering::SeqCst) {
saw_final_notify.notify_one();
}
})
.await
.expect("follow_output must start");
let waited = tokio::time::timeout(Duration::from_secs(20), saw_final.notified()).await;
assert!(
waited.is_ok() || saw_final_fired.load(std::sync::atomic::Ordering::SeqCst),
"final unterminated line was never delivered; received so far: {:?}",
received.lock().unwrap()
);
tokio::time::sleep(Duration::from_secs(1)).await;
let lines = received.lock().unwrap().clone();
let final_count = lines
.iter()
.filter(|l| l.contains("LINE-END-NO-NEWLINE"))
.count();
assert_eq!(
final_count, 1,
"final unterminated line was delivered more than once: {lines:?}"
);
for n in 1..=5 {
let marker = format!("LINE-{n}");
let count = lines.iter().filter(|l| *l == &marker).count();
assert_eq!(
count, 1,
"LINE-{n} was not delivered exactly once: {lines:?}"
);
}
follow.close();
guard.stop().await.unwrap();
}
const SWEEP_REAP_CEILING: Duration = Duration::from_secs(60);
const SWEEP_POLL_INTERVAL: Duration = Duration::from_millis(500);
fn raw_backend_for(name: &str) -> Box<dyn SandboxBackend> {
if name == "docker" {
DockerBackendProvider
.create()
.expect("docker backend must construct")
} else {
MsbBackendProvider.create().expect(
"msb backend must construct — provisioning must already have \
happened via an earlier test in this binary",
)
}
}
#[tokio::test]
async fn sweep_reaps_a_fabricated_dead_run_at_the_backend_level() {
require_backend!();
let backend_name = rightsize::backends::active_name();
let raw_backend = raw_backend_for(&backend_name);
let cache_dir = std::env::temp_dir().join(format!(
"rz-contract-sweep-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&cache_dir).unwrap();
let host_port = std::net::TcpListener::bind("127.0.0.1:0")
.expect("bind an ephemeral port to reserve one")
.local_addr()
.unwrap()
.port();
let name = format!(
"rz-contractsweep-{:x}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let spec = ContainerSpec {
command: Some(vec![
"sh".to_string(),
"-c".to_string(),
"while true; do printf 'HTTP/1.1 200 OK\\r\\nContent-Length: 2\\r\\n\\r\\nok' | \
nc -l -p 8000; done"
.to_string(),
]),
ports: vec![PortBinding {
host_port,
guest_port: 8000,
}],
..ContainerSpec::new(&name, "alpine:3.19", "contract-sweep-it")
};
let handle = raw_backend
.create(spec)
.await
.expect("create a real sandbox to fabricate as a dead run's leftover");
raw_backend
.start(handle.as_ref())
.await
.expect("start the sandbox this test hand-registers as a dead run's leftover");
let boot_deadline = Instant::now() + Duration::from_secs(120);
let mut booted = false;
while Instant::now() < boot_deadline {
if std::net::TcpStream::connect(("127.0.0.1", host_port)).is_ok() {
booted = true;
break;
}
std::thread::sleep(SWEEP_POLL_INTERVAL);
}
assert!(
booted,
"the fabricated sandbox must actually boot and accept connections on port \
{host_port} before the sweep test proceeds"
);
let runs_dir = cache_dir.join("runs");
std::fs::create_dir_all(&runs_dir).unwrap();
let dead_run_id = "contractdeadrun1";
std::fs::write(
runs_dir.join(format!("{dead_run_id}.json")),
format!(
r#"{{"pid":4294967294,"startedIso":"1999-01-01T00:00:00Z","backend":"{backend_name}"}}"#
),
)
.unwrap();
std::fs::write(
runs_dir.join(format!("{dead_run_id}.sandboxes")),
format!("{name}\n"),
)
.unwrap();
let exe = std::env::current_exe().expect("current_exe");
let status = Command::new(&exe)
.args([
"--exact",
"helper_triggers_a_fresh_backend_resolution_for_the_sweep_contract_test",
"--nocapture",
])
.env("RIGHTSIZE_HELPER_CHILD", "1")
.env("RIGHTSIZE_CACHE_DIR", &cache_dir)
.env("RIGHTSIZE_REAPER", "sweep") .env("RIGHTSIZE_BACKEND", &backend_name)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.status()
.expect("spawn fresh-init child process");
assert!(status.success(), "fresh-init child must exit cleanly");
let reap_deadline = Instant::now() + SWEEP_REAP_CEILING;
let mut gone = false;
while Instant::now() < reap_deadline {
if std::net::TcpStream::connect(("127.0.0.1", host_port)).is_err() {
gone = true;
break;
}
std::thread::sleep(SWEEP_POLL_INTERVAL);
}
assert!(
gone,
"a fresh process's init-time sweep must reap the fabricated dead run's sandbox at \
the backend level (port {host_port} must stop accepting connections) within {}s",
SWEEP_REAP_CEILING.as_secs()
);
assert!(
!runs_dir.join(format!("{dead_run_id}.json")).exists(),
"the sweep must also delete the fabricated dead run's ledger files"
);
raw_backend.remove_by_name(&name);
let _ = std::fs::remove_dir_all(&cache_dir);
}
#[tokio::test]
async fn helper_triggers_a_fresh_backend_resolution_for_the_sweep_contract_test() {
if std::env::var("RIGHTSIZE_HELPER_CHILD").as_deref() != Ok("1") {
return;
}
support::ensure_registered();
let guard = Container::new("alpine:3.19")
.with_command(&["true"])
.start()
.await
.expect("fresh-init helper must boot its own trivial sandbox");
guard.stop().await.expect("stop the helper's own sandbox");
}
#[tokio::test]
async fn diagnostics_report_reflects_a_live_container_in_the_pinned_cross_language_format() {
require_backend!();
let c = Container::new("alpine:3.19")
.with_command(&["sh", "-c", "echo DIAG-CONTRACT-MARKER; sleep 120"])
.waiting_for(Wait::for_log_message(".*DIAG-CONTRACT-MARKER.*", 1));
let guard = c.start().await.expect("container must start");
let name = guard.name().to_string();
let report = rightsize::diagnostics().await;
assert!(
report.contains(&format!("-- {name} (alpine:3.19) --")),
"report must contain this container's pinned header line: {report}"
);
assert!(
report.contains("state: running host: 127.0.0.1 ports:"),
"report must contain the pinned state/host/ports line: {report}"
);
assert!(
report.contains("last 50 log lines:") || report.contains("logs: unavailable ("),
"report must contain either a log tail or a degraded-logs line: {report}"
);
guard.stop().await.unwrap();
let after_stop = rightsize::diagnostics().await;
assert!(
!after_stop.contains(&format!("-- {name} (alpine:3.19) --")),
"a stopped container must be deregistered from the report: {after_stop}"
);
}
#[tokio::test]
async fn capabilities_report_the_pinned_per_backend_values() {
require_backend!();
let backend_name = rightsize::backends::active_name();
let backend = raw_backend_for(&backend_name);
let caps = backend.capabilities();
assert!(caps.checkpoint, "both real backends support checkpointing");
if backend_name == "docker" {
assert!(
!caps.hardware_isolated,
"docker must not report hardware isolation"
);
assert!(
!caps.checkpoint_restarts_workload,
"an image commit leaves the container undisturbed"
);
} else {
assert!(caps.hardware_isolated, "msb must report hardware isolation");
assert!(
caps.checkpoint_restarts_workload,
"the stop/snapshot/start cycle reboots the guest"
);
}
}
#[tokio::test]
async fn require_isolation_gates_start_per_backend_hardware_isolation() {
require_backend!();
let backend_name = rightsize::backends::active_name();
let c = Container::new("alpine:3.19")
.with_command(&["sleep", "30"])
.require_isolation(true);
if backend_name == "docker" {
let err = match c.start().await {
Ok(_) => {
panic!("require_isolation(true) must refuse to start on a non-isolated backend")
}
Err(e) => e,
};
assert!(
matches!(err, rightsize::RightsizeError::IsolationRequired { .. }),
"{err}"
);
let msg = err.to_string();
assert!(msg.contains("docker"), "{msg}");
assert!(msg.contains("RIGHTSIZE_BACKEND=microsandbox"), "{msg}");
} else {
let guard = c
.start()
.await
.expect("require_isolation(true) must start normally on a hardware-isolated backend");
guard.stop().await.unwrap();
}
}
#[tokio::test]
async fn checkpoint_succeeds_on_both_backends_with_a_well_formed_backend_specific_ref() {
require_backend!();
let backend_name = rightsize::backends::active_name();
let c = Container::new("alpine:3.19")
.with_command(&["sleep", "60"])
.waiting_for(Wait::for_log_message(".*", 0).with_startup_timeout(Duration::from_secs(30)));
let guard = c.start().await.expect("container must start");
let cp = guard
.checkpoint()
.await
.expect("checkpoint must succeed on both real backends");
assert_eq!(cp.backend, backend_name);
let prefix = if backend_name == "docker" {
"rightsize/checkpoint:"
} else {
"rz-ckpt-"
};
let tag = cp
.checkpoint_ref
.strip_prefix(prefix)
.unwrap_or_else(|| panic!("expected the {prefix} prefix, got {}", cp.checkpoint_ref));
assert_eq!(tag.len(), 12, "{}", cp.checkpoint_ref);
assert!(
tag.chars().all(|c| c.is_ascii_hexdigit()),
"{}",
cp.checkpoint_ref
);
let backend = raw_backend_for(&backend_name);
let _ = backend.remove_checkpoint(&cp.checkpoint_ref).await;
guard.stop().await.unwrap();
}
#[tokio::test]
async fn checkpoint_restore_round_trips_a_marker_file_written_after_boot() {
require_backend!();
if rightsize::backends::active_name() != "docker" {
eprintln!(
"skipping: this dedicated restore test is docker-only — microsandbox has its own \
equivalent in rightsize-msb/tests/checkpoint_it.rs"
);
return;
}
let original = Container::new("alpine:3.19")
.with_command(&["sleep", "60"])
.waiting_for(Wait::for_log_message(".*", 0).with_startup_timeout(Duration::from_secs(30)));
let original_guard = original.start().await.expect("original must start");
let write = original_guard
.exec(&[
"sh",
"-c",
"echo checkpoint-restore-marker > /tmp/rz-checkpoint-marker.txt",
])
.await
.expect("exec must run");
assert_eq!(write.exit_code, 0, "{}", write.stderr);
let cp = original_guard
.checkpoint()
.await
.expect("checkpoint must succeed");
original_guard
.stop()
.await
.expect("stop the original container");
let restored = Container::from_checkpoint(&cp)
.waiting_for(Wait::for_log_message(".*", 0).with_startup_timeout(Duration::from_secs(30)));
let restored_guard = restored
.start()
.await
.expect("a container restored from the checkpoint image must start");
let read = restored_guard
.exec(&["cat", "/tmp/rz-checkpoint-marker.txt"])
.await
.expect("exec must run in the restored container");
assert_eq!(read.exit_code, 0, "{}", read.stderr);
assert!(
read.stdout.contains("checkpoint-restore-marker"),
"the restored container's filesystem must contain the marker file written before \
checkpointing: stdout was {:?}",
read.stdout
);
restored_guard
.stop()
.await
.expect("stop the restored container");
let backend = raw_backend_for("docker");
let _ = backend.remove_checkpoint(&cp.checkpoint_ref).await;
}
async fn boot_alpine_sleep() -> rightsize::ContainerGuard {
Container::new("alpine:3.19")
.with_command(&["sleep", "120"])
.waiting_for(Wait::for_log_message(".*", 0).with_startup_timeout(Duration::from_secs(30)))
.start()
.await
.expect("container must start")
}
#[tokio::test]
async fn copy_file_to_container_round_trips_a_host_file_into_an_absent_parent() {
require_backend!();
let guard = boot_alpine_sleep().await;
let host_file =
std::env::temp_dir().join(format!("rightsize-copyin-file-{}.txt", std::process::id()));
let payload = "rightsize-runtime-copy-file-payload\n";
std::fs::write(&host_file, payload).unwrap();
guard
.copy_file_to_container(&host_file, "/copyin-file/nested/dst.txt")
.await
.expect("copy in must succeed");
let read = guard
.exec(&["cat", "/copyin-file/nested/dst.txt"])
.await
.expect("exec must run");
assert_eq!(read.exit_code, 0, "{}", read.stderr);
assert_eq!(read.stdout, payload);
std::fs::remove_file(&host_file).ok();
guard.stop().await.unwrap();
}
#[tokio::test]
async fn copy_content_to_container_round_trips_in_memory_bytes() {
require_backend!();
let guard = boot_alpine_sleep().await;
let payload = "rightsize-runtime-copy-content-payload\n";
guard
.copy_content_to_container(payload.as_bytes(), "/copyin-content/nested/dst.txt")
.await
.expect("copy content must succeed");
let read = guard
.exec(&["cat", "/copyin-content/nested/dst.txt"])
.await
.expect("exec must run");
assert_eq!(read.exit_code, 0, "{}", read.stderr);
assert_eq!(read.stdout, payload);
guard.stop().await.unwrap();
}
#[tokio::test]
async fn copy_file_to_container_round_trips_a_directory() {
require_backend!();
let guard = boot_alpine_sleep().await;
let host_dir =
std::env::temp_dir().join(format!("rightsize-copyin-dir-{}", std::process::id()));
std::fs::create_dir_all(host_dir.join("nested")).unwrap();
let payload = "rightsize-runtime-copy-dir-payload\n";
std::fs::write(host_dir.join("nested").join("f.txt"), payload).unwrap();
guard
.copy_file_to_container(&host_dir, "/copyin-dir")
.await
.expect("directory copy in must succeed");
let read = guard
.exec(&["cat", "/copyin-dir/nested/f.txt"])
.await
.expect("exec must run");
assert_eq!(read.exit_code, 0, "{}", read.stderr);
assert_eq!(read.stdout, payload);
std::fs::remove_dir_all(&host_dir).ok();
guard.stop().await.unwrap();
}
#[tokio::test]
async fn copy_file_from_container_round_trips_a_guest_file() {
require_backend!();
let guard = boot_alpine_sleep().await;
let write = guard
.exec(&[
"sh",
"-c",
"echo rightsize-runtime-copy-out-payload > /copyout-src.txt",
])
.await
.expect("exec must run");
assert_eq!(write.exit_code, 0, "{}", write.stderr);
let host_dest = std::env::temp_dir()
.join(format!("rightsize-copyout-{}", std::process::id()))
.join("nested")
.join("f.txt");
guard
.copy_file_from_container("/copyout-src.txt", &host_dest)
.await
.expect("copy out must succeed");
let content = std::fs::read_to_string(&host_dest).expect("host file must exist");
assert_eq!(content, "rightsize-runtime-copy-out-payload\n");
std::fs::remove_dir_all(host_dest.parent().unwrap().parent().unwrap()).ok();
guard.stop().await.unwrap();
}
#[tokio::test]
async fn copy_file_from_container_round_trips_a_guest_directory() {
require_backend!();
let guard = boot_alpine_sleep().await;
let write = guard
.exec(&[
"sh",
"-c",
"mkdir -p /copyout-dir/nested && echo rightsize-runtime-copy-out-dir-payload > \
/copyout-dir/nested/f.txt",
])
.await
.expect("exec must run");
assert_eq!(write.exit_code, 0, "{}", write.stderr);
let host_dest =
std::env::temp_dir().join(format!("rightsize-copyout-dir-{}", std::process::id()));
guard
.copy_file_from_container("/copyout-dir", &host_dest)
.await
.expect("directory copy out must succeed");
let content = std::fs::read_to_string(host_dest.join("nested").join("f.txt"))
.expect("host file must exist");
assert_eq!(content, "rightsize-runtime-copy-out-dir-payload\n");
std::fs::remove_dir_all(&host_dest).ok();
guard.stop().await.unwrap();
}
fn fresh_scratch_cache_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"rz-contract-reuse-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
struct ScratchCacheDirGuard(PathBuf);
impl Drop for ScratchCacheDirGuard {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn contract_reuse_nonce() -> &'static str {
rightsize::RunId::value()
}
fn spawn_reuse_helper(
helper_name: &str,
cache_dir: &Path,
backend_name: &str,
reuse_env: Option<&str>,
) -> std::process::ExitStatus {
let exe = std::env::current_exe().expect("current_exe");
let mut cmd = Command::new(&exe);
cmd.args(["--exact", helper_name, "--nocapture"])
.env("RIGHTSIZE_HELPER_CHILD", "1")
.env("RIGHTSIZE_CACHE_DIR", cache_dir)
.env("RIGHTSIZE_BACKEND", backend_name)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
match reuse_env {
Some(v) => {
cmd.env("RIGHTSIZE_REUSE", v);
}
None => {
cmd.env_remove("RIGHTSIZE_REUSE");
}
}
let output = cmd.output().expect("spawn reuse helper child process");
if !output.status.success() {
eprintln!(
"reuse helper {helper_name} stderr:\n{}",
String::from_utf8_lossy(&output.stderr)
);
}
output.status
}
#[tokio::test]
async fn reuse_double_opt_in_gating_env_disabled_runs_as_an_ordinary_ephemeral_container() {
require_backend!();
let backend_name = rightsize::backends::active_name();
let cache_dir = fresh_scratch_cache_dir("gating");
let _cache_dir_guard = ScratchCacheDirGuard(cache_dir.clone());
let status = spawn_reuse_helper(
"helper_reuse_gating_env_disabled",
&cache_dir,
&backend_name,
None,
);
assert!(status.success(), "gating helper child must exit cleanly");
assert!(
!cache_dir.join("reuse").exists(),
"an env-disabled reuse request must never write a registry entry"
);
}
#[tokio::test]
async fn helper_reuse_gating_env_disabled() {
if std::env::var("RIGHTSIZE_HELPER_CHILD").as_deref() != Ok("1") {
return;
}
support::ensure_registered();
assert!(
std::env::var("RIGHTSIZE_REUSE").is_err(),
"this helper must run with RIGHTSIZE_REUSE unset"
);
let c = Container::new("python:3.12-alpine")
.with_command(&["python", "-m", "http.server", "8000"])
.with_exposed_ports(&[8000])
.reuse(true)
.waiting_for(
Wait::for_http("/")
.for_port(8000)
.with_startup_timeout(Duration::from_secs(120)),
);
let guard = c
.start()
.await
.expect("gating helper must start as an ordinary container despite .reuse(true)");
let name = guard.name().to_string();
assert!(
!name.starts_with("rz-reuse-"),
"env-disabled reuse must produce an ordinary sandbox name, got {name}"
);
let port = guard
.get_mapped_port(8000)
.expect("ordinary container must have a mapped port");
guard
.stop()
.await
.expect("an env-disabled reuse container's stop() must behave like any ordinary stop()");
let deadline = Instant::now() + Duration::from_secs(30);
let mut gone = false;
while Instant::now() < deadline {
if std::net::TcpStream::connect(("127.0.0.1", port)).is_err() {
gone = true;
break;
}
std::thread::sleep(Duration::from_millis(200));
}
assert!(
gone,
"stop() on an env-disabled reuse container must actually tear it down at the backend \
level (port {port} must stop accepting connections)"
);
}
struct ReuseSandboxCleanup {
raw_backend: Box<dyn SandboxBackend>,
name: String,
}
impl Drop for ReuseSandboxCleanup {
fn drop(&mut self) {
self.raw_backend.remove_by_name(&self.name);
}
}
const PINNED_VECTOR_SANDBOX_NAME: &str = "rz-reuse-799aad5a3338";
#[tokio::test]
async fn reuse_pinned_cross_language_hash_vector_produces_the_contract_sandbox_name() {
require_backend!();
let backend_name = rightsize::backends::active_name();
let cache_dir = fresh_scratch_cache_dir("hashvec");
let _cache_dir_guard = ScratchCacheDirGuard(cache_dir.clone());
let status = spawn_reuse_helper(
"helper_reuse_pinned_vector",
&cache_dir,
&backend_name,
Some("true"),
);
assert!(
status.success(),
"hash-vector helper child must exit cleanly"
);
}
#[tokio::test]
async fn helper_reuse_pinned_vector() {
if std::env::var("RIGHTSIZE_HELPER_CHILD").as_deref() != Ok("1") {
return;
}
support::ensure_registered();
let backend_name = rightsize::backends::active_name();
let raw_backend = raw_backend_for(&backend_name);
let c = Container::new("redis:7-alpine")
.with_env("A", "1")
.with_env("B", "2")
.with_exposed_ports(&[6379])
.reuse(true);
let guard = c
.start()
.await
.expect("pinned cross-language vector reuse container must start");
let _cleanup = ReuseSandboxCleanup {
raw_backend,
name: PINNED_VECTOR_SANDBOX_NAME.to_string(),
};
assert_eq!(
guard.name(),
PINNED_VECTOR_SANDBOX_NAME,
"the pinned cross-language vector must produce the pinned sandbox name"
);
guard.stop().await.unwrap();
}
#[tokio::test]
async fn reuse_adopt_reuses_the_same_sandbox_and_mapped_port_across_two_starts() {
require_backend!();
let backend_name = rightsize::backends::active_name();
let cache_dir = fresh_scratch_cache_dir("adopt");
let _cache_dir_guard = ScratchCacheDirGuard(cache_dir.clone());
let status = spawn_reuse_helper(
"helper_reuse_adopt_across_two_starts",
&cache_dir,
&backend_name,
Some("true"),
);
assert!(status.success(), "adopt helper child must exit cleanly");
}
#[tokio::test]
async fn helper_reuse_adopt_across_two_starts() {
if std::env::var("RIGHTSIZE_HELPER_CHILD").as_deref() != Ok("1") {
return;
}
support::ensure_registered();
let backend_name = rightsize::backends::active_name();
let raw_backend = raw_backend_for(&backend_name);
let nonce = contract_reuse_nonce();
let build = move || {
Container::new("redis:7-alpine")
.with_env("RZ_CONTRACT_ADOPT", "1")
.with_env("RZ_TEST_NONCE", nonce)
.with_exposed_ports(&[6379])
.reuse(true)
.waiting_for(Wait::for_log_message(".*Ready to accept connections.*", 1))
};
let first = build()
.start()
.await
.expect("first start() must create+start a fresh reuse sandbox");
let name = first.name().to_string();
let _cleanup = ReuseSandboxCleanup {
raw_backend,
name: name.clone(),
};
assert!(name.starts_with("rz-reuse-"), "{name}");
let first_port = first
.get_mapped_port(6379)
.expect("first sandbox must have a mapped port");
first.stop().await.unwrap();
let second = build()
.start()
.await
.expect("second start() must ADOPT the first sandbox, not re-create one");
assert_eq!(
second.name(),
name,
"adoption must produce the identical sandbox name"
);
assert_eq!(
second.get_mapped_port(6379).unwrap(),
first_port,
"adoption must reuse the SAME mapped port, not allocate a fresh one"
);
let ping = second
.exec(&["redis-cli", "PING"])
.await
.expect("exec against the adopted sandbox must run");
assert_eq!(
ping.stdout.trim(),
"PONG",
"adopted sandbox must actually be alive and serving: {ping:?}"
);
second.stop().await.unwrap();
}
#[tokio::test]
async fn reuse_stop_semantics_leaves_the_sandbox_running_and_never_ledgers_it() {
require_backend!();
let backend_name = rightsize::backends::active_name();
let cache_dir = fresh_scratch_cache_dir("stopsem");
let _cache_dir_guard = ScratchCacheDirGuard(cache_dir.clone());
let status = spawn_reuse_helper(
"helper_reuse_stop_semantics",
&cache_dir,
&backend_name,
Some("true"),
);
assert!(
status.success(),
"stop-semantics helper child must exit cleanly"
);
}
#[tokio::test]
async fn helper_reuse_stop_semantics() {
if std::env::var("RIGHTSIZE_HELPER_CHILD").as_deref() != Ok("1") {
return;
}
support::ensure_registered();
let backend_name = rightsize::backends::active_name();
let raw_backend = raw_backend_for(&backend_name);
let cache_dir = rightsize::cache_dir::dir();
let c = Container::new("redis:7-alpine")
.with_env("RZ_CONTRACT_STOPSEM", "1")
.with_env("RZ_TEST_NONCE", contract_reuse_nonce())
.with_exposed_ports(&[6379])
.reuse(true);
let guard = c
.start()
.await
.expect("stop-semantics reuse container must start");
let name = guard.name().to_string();
let _cleanup = ReuseSandboxCleanup {
raw_backend,
name: name.clone(),
};
let port = guard
.get_mapped_port(6379)
.expect("reuse sandbox must have a mapped port");
guard
.stop()
.await
.expect("stop() on a reuse guard must return Ok even though it leaves the sandbox running");
assert!(
std::net::TcpStream::connect(("127.0.0.1", port)).is_ok(),
"a reuse container's stop() must leave the sandbox genuinely running, not tear it down"
);
let sandboxes_path = cache_dir
.join("runs")
.join(format!("{}.sandboxes", rightsize::RunId::value()));
if let Ok(contents) = std::fs::read_to_string(&sandboxes_path) {
assert!(
!contents.lines().any(|l| l == name),
"a reuse sandbox must never appear in the run's .sandboxes ledger file: {contents:?}"
);
}
}