mod common;
use boxlite::runtime::options::{BoxliteOptions, CloneOptions, ExportOptions};
use boxlite::runtime::types::BoxStatus;
use boxlite::{BoxCommand, BoxliteRuntime, LiteBox, SnapshotOptions};
use tempfile::TempDir;
use tokio_stream::StreamExt;
async fn exec_stdout(handle: &LiteBox, cmd: BoxCommand) -> String {
let mut execution = handle.exec(cmd).await.expect("exec failed");
let mut stdout = String::new();
if let Some(mut stream) = execution.stdout() {
while let Some(chunk) = stream.next().await {
stdout.push_str(&chunk);
}
}
let result = execution.wait().await.expect("wait failed");
assert_eq!(result.exit_code, 0, "command should exit 0");
stdout
}
async fn create_stopped_box(runtime: &BoxliteRuntime, name: &str) -> LiteBox {
let litebox = runtime
.create(common::alpine_opts(), Some(name.to_string()))
.await
.expect("Failed to create box");
litebox.start().await.expect("Failed to start box");
litebox.stop().await.expect("Failed to stop box");
runtime
.get(name)
.await
.expect("get failed")
.expect("box not found")
}
async fn create_running_box(runtime: &BoxliteRuntime, name: &str) -> LiteBox {
let litebox = runtime
.create(common::alpine_opts(), Some(name.to_string()))
.await
.expect("Failed to create box");
litebox.start().await.expect("Failed to start box");
assert_eq!(litebox.info().status, BoxStatus::Running);
litebox
}
async fn write_file(handle: &LiteBox, path: &str, content: &str) {
let escaped = content.replace('\'', "'\\''");
let shell_cmd = format!("printf '%s' '{escaped}' > {path}");
let cmd = BoxCommand::new("sh").args(["-c", &shell_cmd]);
let mut exec = handle.exec(cmd).await.expect("write_file: exec failed");
let result = exec.wait().await.expect("write_file: wait failed");
assert_eq!(result.exit_code, 0, "write_file({path}): non-zero exit");
}
async fn read_file(handle: &LiteBox, path: &str) -> String {
exec_stdout(handle, BoxCommand::new("cat").args([path])).await
}
async fn write_and_verify(handle: &LiteBox, path: &str, content: &str) {
write_file(handle, path, content).await;
let read = read_file(handle, path).await;
assert_eq!(read, content, "write_and_verify({path}): mismatch");
}
async fn stop_and_refresh(runtime: &BoxliteRuntime, handle: LiteBox, name: &str) -> LiteBox {
handle.stop().await.expect("stop failed");
runtime
.get(name)
.await
.expect("get failed")
.expect("box not found after stop")
}
#[tokio::test]
async fn test_multiple_snapshots_list_order() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "list-order").await;
litebox.start().await.unwrap();
write_file(&litebox, "/root/a.txt", "file-A").await;
let litebox = stop_and_refresh(&runtime, litebox, "list-order").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v1")
.await
.unwrap();
litebox.start().await.unwrap();
write_file(&litebox, "/root/b.txt", "file-B").await;
let litebox = stop_and_refresh(&runtime, litebox, "list-order").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v2")
.await
.unwrap();
litebox.start().await.unwrap();
write_file(&litebox, "/root/c.txt", "file-C").await;
let litebox = stop_and_refresh(&runtime, litebox, "list-order").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v3")
.await
.unwrap();
let snaps = litebox.snapshots().list().await.unwrap();
assert_eq!(snaps.len(), 3);
assert_eq!(snaps[0].name, "v3");
assert_eq!(snaps[1].name, "v2");
assert_eq!(snaps[2].name, "v1");
let v2 = litebox
.snapshots()
.get("v2")
.await
.unwrap()
.expect("v2 should exist");
assert_eq!(v2.name, "v2");
assert_eq!(v2.box_id, litebox.id().to_string());
assert!(!v2.id.is_empty());
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_data_isolation_across_versions() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "data-iso").await;
litebox.start().await.unwrap();
write_and_verify(&litebox, "/root/data.txt", "version1").await;
let litebox = stop_and_refresh(&runtime, litebox, "data-iso").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v1")
.await
.unwrap();
litebox.start().await.unwrap();
write_file(&litebox, "/root/data.txt", "version2").await;
let litebox = stop_and_refresh(&runtime, litebox, "data-iso").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v2")
.await
.unwrap();
litebox.snapshots().restore("v1").await.unwrap();
litebox.start().await.unwrap();
let data = read_file(&litebox, "/root/data.txt").await;
assert_eq!(data, "version1", "v1 restore should show version1");
let litebox = stop_and_refresh(&runtime, litebox, "data-iso").await;
litebox.snapshots().restore("v2").await.unwrap();
litebox.start().await.unwrap();
let data = read_file(&litebox, "/root/data.txt").await;
assert_eq!(data, "version2", "v2 restore should show version2");
litebox.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_restore_discards_post_snapshot_writes() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "discard-writes").await;
litebox.start().await.unwrap();
write_and_verify(&litebox, "/root/state.txt", "before").await;
let litebox = stop_and_refresh(&runtime, litebox, "discard-writes").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "base")
.await
.unwrap();
litebox.start().await.unwrap();
write_file(&litebox, "/root/state.txt", "after").await;
let litebox = stop_and_refresh(&runtime, litebox, "discard-writes").await;
litebox.snapshots().restore("base").await.unwrap();
litebox.start().await.unwrap();
let data = read_file(&litebox, "/root/state.txt").await;
assert_eq!(
data, "before",
"restore should discard post-snapshot writes"
);
litebox.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_multiple_restore_cycles() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "restore-cycles").await;
litebox.start().await.unwrap();
write_and_verify(&litebox, "/root/ver.txt", "data-A").await;
let litebox = stop_and_refresh(&runtime, litebox, "restore-cycles").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v1")
.await
.unwrap();
litebox.start().await.unwrap();
write_file(&litebox, "/root/ver.txt", "data-B").await;
let litebox = stop_and_refresh(&runtime, litebox, "restore-cycles").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v2")
.await
.unwrap();
litebox.snapshots().restore("v1").await.unwrap();
litebox.start().await.unwrap();
assert_eq!(read_file(&litebox, "/root/ver.txt").await, "data-A");
let litebox = stop_and_refresh(&runtime, litebox, "restore-cycles").await;
litebox.snapshots().restore("v2").await.unwrap();
litebox.start().await.unwrap();
assert_eq!(read_file(&litebox, "/root/ver.txt").await, "data-B");
let litebox = stop_and_refresh(&runtime, litebox, "restore-cycles").await;
litebox.snapshots().restore("v1").await.unwrap();
litebox.start().await.unwrap();
assert_eq!(read_file(&litebox, "/root/ver.txt").await, "data-A");
litebox.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_get_returns_correct_metadata() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "meta-test").await;
let box_id = litebox.id().to_string();
litebox
.snapshots()
.create(SnapshotOptions::default(), "meta-test")
.await
.unwrap();
let snap = litebox
.snapshots()
.get("meta-test")
.await
.unwrap()
.expect("snapshot should exist");
assert_eq!(snap.name, "meta-test");
assert_eq!(snap.box_id, box_id);
assert!(!snap.id.is_empty());
assert!(snap.created_at > 0);
let missing = litebox.snapshots().get("nonexistent").await.unwrap();
assert!(missing.is_none());
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_created_at_ordering() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "created-at").await;
let s1 = litebox
.snapshots()
.create(SnapshotOptions::default(), "s1")
.await
.unwrap();
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let s2 = litebox
.snapshots()
.create(SnapshotOptions::default(), "s2")
.await
.unwrap();
assert!(
s2.created_at >= s1.created_at,
"s2 ({}) should be >= s1 ({})",
s2.created_at,
s1.created_at
);
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_remove_success() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "rm-test").await;
let box_id = litebox.id().to_string();
litebox
.snapshots()
.create(SnapshotOptions::default(), "s1")
.await
.unwrap();
litebox
.snapshots()
.create(SnapshotOptions::default(), "s2")
.await
.unwrap();
assert_eq!(litebox.snapshots().list().await.unwrap().len(), 2);
litebox.snapshots().restore("s1").await.unwrap();
let snap_dir = home
.path
.join("boxes")
.join(&box_id)
.join("snapshots")
.join("s2");
assert!(
snap_dir.exists(),
"snapshot dir should exist before removal"
);
litebox.snapshots().remove("s2").await.unwrap();
let snaps = litebox.snapshots().list().await.unwrap();
assert_eq!(snaps.len(), 1);
assert_eq!(snaps[0].name, "s1");
assert!(litebox.snapshots().get("s2").await.unwrap().is_none());
assert!(
!snap_dir.exists(),
"snapshot dir should be deleted after removal"
);
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_remove_then_recreate_same_name() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "reuse-name").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "base")
.await
.unwrap();
litebox
.snapshots()
.create(SnapshotOptions::default(), "tag")
.await
.unwrap();
litebox.snapshots().restore("base").await.unwrap();
litebox.snapshots().remove("tag").await.unwrap();
assert!(litebox.snapshots().get("tag").await.unwrap().is_none());
litebox
.snapshots()
.create(SnapshotOptions::default(), "tag")
.await
.unwrap();
let tag = litebox.snapshots().get("tag").await.unwrap();
assert!(tag.is_some(), "recreated 'tag' should exist");
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_remove_nonexistent_returns_error() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "rm-nonexist").await;
let result = litebox.snapshots().remove("does-not-exist").await;
assert!(
result.is_err(),
"removing nonexistent snapshot should error"
);
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_remove_current_backing_rejected() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "rm-active").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "active")
.await
.unwrap();
let result = litebox.snapshots().remove("active").await;
assert!(
result.is_err(),
"removing snapshot that current disk depends on should fail"
);
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_remove_after_restore_different() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "rm-after-restore").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v1")
.await
.unwrap();
litebox
.snapshots()
.create(SnapshotOptions::default(), "v2")
.await
.unwrap();
litebox.snapshots().restore("v2").await.unwrap();
let result = litebox.snapshots().remove("v1").await;
if result.is_ok() {
let snaps = litebox.snapshots().list().await.unwrap();
assert_eq!(snaps.len(), 1);
assert_eq!(snaps[0].name, "v2");
}
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_chain_remove_oldest_with_newer_depending() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "chain-bug1").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v1")
.await
.unwrap();
litebox
.snapshots()
.create(SnapshotOptions::default(), "v2")
.await
.unwrap();
litebox.snapshots().restore("v2").await.unwrap();
let remove_result = litebox.snapshots().remove("v1").await;
if remove_result.is_ok() {
let litebox = runtime
.get("chain-bug1")
.await
.unwrap()
.expect("box should still exist");
litebox.snapshots().restore("v2").await.unwrap();
let start_result = litebox.start().await;
eprintln!(
"BUG 1 DETECTOR: remove v1 succeeded. start after restore v2: {:?}",
start_result.is_ok()
);
if start_result.is_ok() {
litebox.stop().await.unwrap();
}
} else {
eprintln!("BUG 1 FIXED: remove v1 correctly rejected");
}
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_remove_middle_of_three() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "rm-middle").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v1")
.await
.unwrap();
litebox
.snapshots()
.create(SnapshotOptions::default(), "v2")
.await
.unwrap();
litebox
.snapshots()
.create(SnapshotOptions::default(), "v3")
.await
.unwrap();
litebox.snapshots().restore("v3").await.unwrap();
let result = litebox.snapshots().remove("v1").await;
eprintln!(
"Remove v1 (middle of 3): {}",
if result.is_ok() {
"allowed"
} else {
"rejected"
}
);
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_running_box_with_quiesce() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_running_box(&runtime, "snap-running").await;
write_file(&litebox, "/root/marker.txt", "running-snap").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "live")
.await
.expect("snapshot on running box should succeed");
assert_eq!(litebox.info().status, BoxStatus::Running);
let out = exec_stdout(&litebox, BoxCommand::new("echo").args(["alive"])).await;
assert_eq!(out.trim(), "alive");
litebox.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_restore_rejected_while_running() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "restore-running").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v1")
.await
.unwrap();
litebox.start().await.unwrap();
let result = litebox.snapshots().restore("v1").await;
assert!(result.is_err(), "restore while running should be rejected");
assert_eq!(litebox.info().status, BoxStatus::Running);
litebox.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_remove_while_running() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "rm-while-running").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v1")
.await
.unwrap();
litebox.start().await.unwrap();
litebox
.snapshots()
.create(SnapshotOptions::default(), "v2")
.await
.unwrap();
let result = litebox.snapshots().remove("v1").await;
eprintln!(
"Remove v1 while running: {}",
if result.is_ok() {
"allowed"
} else {
"rejected"
}
);
litebox.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_two_snapshots_while_running() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_running_box(&runtime, "two-snaps-running").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "s1")
.await
.unwrap();
litebox
.snapshots()
.create(SnapshotOptions::default(), "s2")
.await
.unwrap();
assert_eq!(litebox.info().status, BoxStatus::Running);
let out = exec_stdout(&litebox, BoxCommand::new("echo").args(["ok"])).await;
assert_eq!(out.trim(), "ok");
let snaps = litebox.snapshots().list().await.unwrap();
assert_eq!(snaps.len(), 2);
litebox.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_clone_after_snapshot_preserves_snapshot_data() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "snap-then-clone").await;
source.start().await.unwrap();
write_file(&source, "/root/data.txt", "data-v1").await;
let source = stop_and_refresh(&runtime, source, "snap-then-clone").await;
source
.snapshots()
.create(SnapshotOptions::default(), "pre-clone")
.await
.unwrap();
source.start().await.unwrap();
write_file(&source, "/root/data.txt", "data-v2").await;
let source = stop_and_refresh(&runtime, source, "snap-then-clone").await;
let cloned = source
.clone_box(CloneOptions::default(), Some("the-clone".to_string()))
.await
.unwrap();
cloned.start().await.unwrap();
let clone_data = read_file(&cloned, "/root/data.txt").await;
assert_eq!(clone_data, "data-v2", "clone should have latest data");
cloned.stop().await.unwrap();
source.snapshots().restore("pre-clone").await.unwrap();
source.start().await.unwrap();
let src_data = read_file(&source, "/root/data.txt").await;
assert_eq!(src_data, "data-v1", "source should restore to v1");
source.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_then_clone_then_remove_snapshot() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "snap-clone-rm").await;
source.start().await.unwrap();
write_file(&source, "/root/data.txt", "snap-data").await;
let source = stop_and_refresh(&runtime, source, "snap-clone-rm").await;
source
.snapshots()
.create(SnapshotOptions::default(), "base")
.await
.unwrap();
let cloned = source
.clone_box(CloneOptions::default(), Some("dep-clone".to_string()))
.await
.unwrap();
let remove_result = source.snapshots().remove("base").await;
if remove_result.is_ok() {
eprintln!("BUG 2 DETECTOR: remove succeeded. Testing if clone is broken...");
let start_result = cloned.start().await;
eprintln!(
"BUG 2 DETECTOR: clone start after snapshot removal: {:?}",
start_result.is_ok()
);
if start_result.is_ok() {
cloned.stop().await.unwrap();
}
} else {
eprintln!("BUG 2 FIXED: removal correctly rejected");
}
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_clone_then_snapshot_clone() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "clone-then-snap-src").await;
source.start().await.unwrap();
write_file(&source, "/root/data.txt", "src-data").await;
let source = stop_and_refresh(&runtime, source, "clone-then-snap-src").await;
let cloned = source
.clone_box(CloneOptions::default(), Some("snap-clone".to_string()))
.await
.unwrap();
cloned.start().await.unwrap();
write_file(&cloned, "/root/clone.txt", "clone-data").await;
let cloned = stop_and_refresh(&runtime, cloned, "snap-clone").await;
cloned
.snapshots()
.create(SnapshotOptions::default(), "clone-snap")
.await
.unwrap();
cloned.start().await.unwrap();
write_file(&cloned, "/root/clone.txt", "changed").await;
let cloned = stop_and_refresh(&runtime, cloned, "snap-clone").await;
cloned.snapshots().restore("clone-snap").await.unwrap();
cloned.start().await.unwrap();
let data = read_file(&cloned, "/root/clone.txt").await;
assert_eq!(
data, "clone-data",
"restore should bring back clone-snap state"
);
cloned.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_clone_of_clone() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "chain-src").await;
let clone_a = source
.clone_box(CloneOptions::default(), Some("clone-a".to_string()))
.await
.unwrap();
let clone_b = clone_a
.clone_box(CloneOptions::default(), Some("clone-b".to_string()))
.await
.unwrap();
source.start().await.unwrap();
write_file(&source, "/root/id.txt", "source").await;
source.stop().await.unwrap();
clone_a.start().await.unwrap();
write_file(&clone_a, "/root/id.txt", "clone-a").await;
clone_a.stop().await.unwrap();
clone_b.start().await.unwrap();
write_file(&clone_b, "/root/id.txt", "clone-b").await;
let id = read_file(&clone_b, "/root/id.txt").await;
assert_eq!(id, "clone-b");
clone_b.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_clone_source_snapshot_independence() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "snap-indep-src").await;
source.start().await.unwrap();
write_file(&source, "/root/data.txt", "original").await;
let source = stop_and_refresh(&runtime, source, "snap-indep-src").await;
source
.snapshots()
.create(SnapshotOptions::default(), "src-snap")
.await
.unwrap();
let cloned = source
.clone_box(CloneOptions::default(), Some("indep-clone".to_string()))
.await
.unwrap();
source.start().await.unwrap();
write_file(&source, "/root/data.txt", "modified").await;
let source = stop_and_refresh(&runtime, source, "snap-indep-src").await;
source
.snapshots()
.create(SnapshotOptions::default(), "src-snap2")
.await
.unwrap();
cloned.start().await.unwrap();
let clone_data = read_file(&cloned, "/root/data.txt").await;
assert_eq!(
clone_data, "original",
"clone should not be affected by source's new snapshots"
);
cloned.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_batch_clone_produces_correct_count() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "batch-src").await;
let names: Vec<String> = (1..=5).map(|i| format!("c{}", i)).collect();
let clones = source
.clone_boxes(CloneOptions::default(), 5, names)
.await
.unwrap();
assert_eq!(clones.len(), 5);
let mut ids: Vec<String> = clones.iter().map(|c| c.id().to_string()).collect();
ids.sort();
ids.dedup();
assert_eq!(ids.len(), 5, "all clone IDs should be unique");
for (i, c) in clones.iter().enumerate() {
assert_eq!(c.name(), Some(format!("c{}", i + 1).as_str()));
}
for c in &clones {
c.start().await.unwrap();
let out = exec_stdout(c, BoxCommand::new("echo").args(["ok"])).await;
assert_eq!(out.trim(), "ok");
c.stop().await.unwrap();
}
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_batch_clone_names_count_mismatch_errors() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "mismatch-src").await;
let result = source
.clone_boxes(
CloneOptions::default(),
3,
vec!["a".into(), "b".into()], )
.await;
assert!(
result.is_err(),
"mismatched names length should produce error"
);
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_clone_count_zero_returns_empty() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "zero-clone").await;
let clones = source
.clone_boxes(CloneOptions::default(), 0, vec![])
.await
.unwrap();
assert!(clones.is_empty(), "count=0 should return empty vec");
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_clone_data_preserved() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "data-src").await;
source.start().await.unwrap();
write_and_verify(&source, "/root/unique.txt", "clone-test-data").await;
let source = stop_and_refresh(&runtime, source, "data-src").await;
let cloned = source
.clone_box(CloneOptions::default(), Some("data-clone".to_string()))
.await
.unwrap();
cloned.start().await.unwrap();
let data = read_file(&cloned, "/root/unique.txt").await;
assert_eq!(data, "clone-test-data", "clone should preserve source data");
cloned.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_clone_write_isolation_from_source() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "iso-src").await;
let cloned = source
.clone_box(CloneOptions::default(), Some("iso-clone".to_string()))
.await
.unwrap();
source.start().await.unwrap();
cloned.start().await.unwrap();
write_file(&source, "/root/source-only.txt", "from-source").await;
write_file(&cloned, "/root/clone-only.txt", "from-clone").await;
let cmd = BoxCommand::new("test").args(["-f", "/root/clone-only.txt"]);
let mut exec = source.exec(cmd).await.unwrap();
let result = exec.wait().await.unwrap();
assert_ne!(result.exit_code, 0, "source should not see clone's file");
let cmd = BoxCommand::new("test").args(["-f", "/root/source-only.txt"]);
let mut exec = cloned.exec(cmd).await.unwrap();
let result = exec.wait().await.unwrap();
assert_ne!(result.exit_code, 0, "clone should not see source's file");
source.stop().await.unwrap();
cloned.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_clone_without_name() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "unnamed-src").await;
let cloned = source
.clone_box(CloneOptions::default(), None)
.await
.unwrap();
assert!(
cloned.name().is_none(),
"clone without name should have no name"
);
assert_ne!(cloned.id().to_string(), source.id().to_string());
cloned.start().await.unwrap();
cloned.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_clone_with_duplicate_name_errors() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "dup-name-src").await;
source
.clone_box(CloneOptions::default(), Some("dup-clone".to_string()))
.await
.unwrap();
let result = source
.clone_box(CloneOptions::default(), Some("dup-clone".to_string()))
.await;
assert!(result.is_err(), "cloning with duplicate name should fail");
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_multiple_clones_share_base_disk() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "shared-base-src").await;
let names: Vec<String> = (1..=3).map(|i| format!("shared-clone-{}", i)).collect();
let clones = source
.clone_boxes(CloneOptions::default(), 3, names)
.await
.unwrap();
assert_eq!(clones.len(), 3);
for c in &clones {
c.start().await.unwrap();
let out = exec_stdout(c, BoxCommand::new("echo").args(["ok"])).await;
assert_eq!(out.trim(), "ok");
c.stop().await.unwrap();
}
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_export_import_preserves_file_contents() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "export-data-src").await;
source.start().await.unwrap();
write_file(&source, "/root/text.txt", "hello-boxlite-export").await;
let cmd = BoxCommand::new("sh").args([
"-c",
"dd if=/dev/zero of=/root/zeroes.bin bs=1024 count=4 2>/dev/null",
]);
let mut exec = source.exec(cmd).await.unwrap();
let result = exec.wait().await.unwrap();
assert_eq!(result.exit_code, 0);
let source = stop_and_refresh(&runtime, source, "export-data-src").await;
let export_dir = TempDir::new_in("/tmp").unwrap();
let archive = source
.export(ExportOptions::default(), export_dir.path())
.await
.unwrap();
let imported = runtime
.import_box(archive, Some("imported-data".to_string()))
.await
.unwrap();
imported.start().await.unwrap();
let text = read_file(&imported, "/root/text.txt").await;
assert_eq!(text, "hello-boxlite-export");
let size_out = exec_stdout(
&imported,
BoxCommand::new("sh").args(["-c", "wc -c < /root/zeroes.bin"]),
)
.await;
assert_eq!(size_out.trim(), "4096");
imported.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_export_to_directory_uses_box_name() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "my-box").await;
let export_dir = TempDir::new_in("/tmp").unwrap();
let archive = source
.export(ExportOptions::default(), export_dir.path())
.await
.unwrap();
let archive_path = archive.path();
assert_eq!(
archive_path.file_name().unwrap().to_str().unwrap(),
"my-box.boxlite"
);
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_double_import_from_same_archive() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "double-imp-src").await;
let export_dir = TempDir::new_in("/tmp").unwrap();
let archive = source
.export(ExportOptions::default(), export_dir.path())
.await
.unwrap();
let archive_path = archive.path().to_path_buf();
let imported1 = runtime
.import_box(
boxlite::BoxArchive::new(archive_path.clone()),
Some("imp-1".to_string()),
)
.await
.unwrap();
let imported2 = runtime
.import_box(
boxlite::BoxArchive::new(archive_path),
Some("imp-2".to_string()),
)
.await
.unwrap();
assert_ne!(imported1.id().to_string(), imported2.id().to_string());
imported1.start().await.unwrap();
imported1.stop().await.unwrap();
imported2.start().await.unwrap();
imported2.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_imported_box_has_no_snapshots() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "snap-exp-src").await;
source
.snapshots()
.create(SnapshotOptions::default(), "s1")
.await
.unwrap();
source
.snapshots()
.create(SnapshotOptions::default(), "s2")
.await
.unwrap();
let export_dir = TempDir::new_in("/tmp").unwrap();
let archive = source
.export(ExportOptions::default(), export_dir.path())
.await
.unwrap();
let imported = runtime
.import_box(archive, Some("no-snaps".to_string()))
.await
.unwrap();
let snaps = imported.snapshots().list().await.unwrap();
assert!(
snaps.is_empty(),
"imported box should have no snapshots, got {}",
snaps.len()
);
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_export_import_cloned_box() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "exp-clone-src").await;
let cloned = source
.clone_box(CloneOptions::default(), Some("exp-clone".to_string()))
.await
.unwrap();
let export_dir = TempDir::new_in("/tmp").unwrap();
let archive = cloned
.export(ExportOptions::default(), export_dir.path())
.await
.unwrap();
let imported = runtime
.import_box(archive, Some("imp-clone".to_string()))
.await
.unwrap();
imported.start().await.unwrap();
let out = exec_stdout(&imported, BoxCommand::new("echo").args(["from-clone"])).await;
assert_eq!(out.trim(), "from-clone");
imported.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_import_validates_no_backing_references() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "security-src").await;
let export_dir = TempDir::new_in("/tmp").unwrap();
let archive = source
.export(ExportOptions::default(), export_dir.path())
.await
.unwrap();
let imported = runtime
.import_box(archive, Some("secure-imp".to_string()))
.await
.unwrap();
assert_eq!(imported.info().status, BoxStatus::Stopped);
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_export_unnamed_box_uses_default_filename() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = runtime.create(common::alpine_opts(), None).await.unwrap();
litebox.start().await.unwrap();
litebox.stop().await.unwrap();
let export_dir = TempDir::new_in("/tmp").unwrap();
let archive = litebox
.export(ExportOptions::default(), export_dir.path())
.await
.unwrap();
assert_eq!(
archive.path().file_name().unwrap().to_str().unwrap(),
"box.boxlite",
"unnamed box should export as box.boxlite"
);
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_export_import_box_with_custom_options() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "custom-opts").await;
let export_dir = TempDir::new_in("/tmp").unwrap();
let archive = source
.export(ExportOptions::default(), export_dir.path())
.await
.unwrap();
let imported = runtime
.import_box(archive, Some("imp-custom".to_string()))
.await
.unwrap();
let info = imported.info();
assert_eq!(info.status, BoxStatus::Stopped);
assert_eq!(info.name.as_deref(), Some("imp-custom"));
imported.start().await.unwrap();
imported.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_export_archive_has_boxlite_extension() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "ext-test").await;
let export_dir = TempDir::new_in("/tmp").unwrap();
let archive = source
.export(ExportOptions::default(), export_dir.path())
.await
.unwrap();
assert!(archive.path().exists());
assert!(
archive.path().extension().is_some_and(|e| e == "boxlite"),
"archive should have .boxlite extension"
);
assert!(
std::fs::metadata(archive.path()).unwrap().len() > 0,
"archive should be non-empty"
);
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_name_validation_integration() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "name-validate").await;
let bad_names = vec!["", "..", "../evil", "a/b", "a\\b", "a\0b", ".hidden"];
for name in bad_names {
let result = litebox
.snapshots()
.create(SnapshotOptions::default(), name)
.await;
assert!(
result.is_err(),
"snapshot name '{}' should be rejected",
name.escape_default()
);
}
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_duplicate_name_rejected() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "dup-name").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "dup")
.await
.unwrap();
let result = litebox
.snapshots()
.create(SnapshotOptions::default(), "dup")
.await;
assert!(
result.is_err(),
"duplicate snapshot name should be rejected"
);
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_remove_box_with_snapshots_cleans_up() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "rm-with-snaps").await;
let box_id = litebox.id().to_string();
litebox
.snapshots()
.create(SnapshotOptions::default(), "s1")
.await
.unwrap();
litebox
.snapshots()
.create(SnapshotOptions::default(), "s2")
.await
.unwrap();
let box_home = home.path.join("boxes").join(&box_id);
assert!(box_home.exists());
runtime.remove("rm-with-snaps", true).await.unwrap();
assert!(
!box_home.exists(),
"box home should be deleted after removal"
);
let found = runtime.get("rm-with-snaps").await.unwrap();
assert!(found.is_none());
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_remove_source_box_blocked_by_clone() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "blocked-src").await;
let _cloned = source
.clone_box(CloneOptions::default(), Some("blocker-clone".to_string()))
.await
.unwrap();
let result = runtime.remove("blocked-src", false).await;
assert!(
result.is_err(),
"non-force remove with dependent clone should fail"
);
runtime.remove("blocked-src", true).await.unwrap();
assert!(runtime.get("blocked-src").await.unwrap().is_none());
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_on_never_started_box() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = runtime
.create(common::alpine_opts(), Some("never-started".to_string()))
.await
.unwrap();
let result = litebox
.snapshots()
.create(SnapshotOptions::default(), "should-fail")
.await;
assert!(
result.is_err(),
"snapshot on never-started box should fail (no container disk)"
);
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_restore_nonexistent_snapshot() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "restore-missing").await;
let result = litebox.snapshots().restore("does-not-exist").await;
assert!(
result.is_err(),
"restoring nonexistent snapshot should fail"
);
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_clone_box_without_container_disk() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = runtime
.create(common::alpine_opts(), Some("no-disk-clone".to_string()))
.await
.unwrap();
let result = litebox
.clone_box(CloneOptions::default(), Some("should-fail".to_string()))
.await;
assert!(result.is_err(), "clone without container disk should fail");
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_name_max_length() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "max-name").await;
let name_255 = "a".repeat(255);
litebox
.snapshots()
.create(SnapshotOptions::default(), &name_255)
.await
.unwrap();
let name_256 = "a".repeat(256);
let result = litebox
.snapshots()
.create(SnapshotOptions::default(), &name_256)
.await;
assert!(result.is_err(), "256-char name should be rejected");
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_under_write_pressure() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_running_box(&runtime, "snap-stress").await;
write_file(&source, "/root/marker.txt", "snap-stress-marker").await;
let write_script = concat!(
"while true; do ",
"dd if=/dev/urandom of=/root/stress.bin bs=4096 count=1 ",
"seek=$((RANDOM % 256)) conv=notrunc 2>/dev/null; ",
"done"
);
let cmd = BoxCommand::new("sh").args(["-c", write_script]);
let _bg = source.exec(cmd).await.unwrap();
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
source
.snapshots()
.create(SnapshotOptions::default(), "stress-snap")
.await
.expect("snapshot under write pressure should succeed");
assert_eq!(source.info().status, BoxStatus::Running);
let source = stop_and_refresh(&runtime, source, "snap-stress").await;
source.snapshots().restore("stress-snap").await.unwrap();
source.start().await.unwrap();
let marker = read_file(&source, "/root/marker.txt").await;
assert_eq!(marker, "snap-stress-marker");
source.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_clone_under_write_pressure() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_running_box(&runtime, "clone-stress").await;
let write_script = concat!(
"while true; do ",
"dd if=/dev/urandom of=/root/stress.bin bs=4096 count=1 ",
"seek=$((RANDOM % 256)) conv=notrunc 2>/dev/null; ",
"done"
);
let cmd = BoxCommand::new("sh").args(["-c", write_script]);
let _bg = source.exec(cmd).await.unwrap();
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let cloned = source
.clone_box(CloneOptions::default(), Some("stress-clone".to_string()))
.await
.expect("clone under write pressure should succeed");
assert_eq!(source.info().status, BoxStatus::Running);
cloned.start().await.unwrap();
let out = exec_stdout(&cloned, BoxCommand::new("echo").args(["clone-ok"])).await;
assert_eq!(out.trim(), "clone-ok");
cloned.stop().await.unwrap();
source.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_rapid_snapshot_cycle() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "rapid-snap").await;
litebox.start().await.unwrap();
write_file(&litebox, "/root/ver.txt", "v1").await;
let litebox = stop_and_refresh(&runtime, litebox, "rapid-snap").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v1")
.await
.unwrap();
litebox.start().await.unwrap();
write_file(&litebox, "/root/ver.txt", "v2").await;
let litebox = stop_and_refresh(&runtime, litebox, "rapid-snap").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v2")
.await
.unwrap();
litebox.start().await.unwrap();
write_file(&litebox, "/root/ver.txt", "v3").await;
let litebox = stop_and_refresh(&runtime, litebox, "rapid-snap").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v3")
.await
.unwrap();
litebox.start().await.unwrap();
write_file(&litebox, "/root/ver.txt", "v4").await;
let litebox = stop_and_refresh(&runtime, litebox, "rapid-snap").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v4")
.await
.unwrap();
litebox.start().await.unwrap();
write_file(&litebox, "/root/ver.txt", "v5").await;
let litebox = stop_and_refresh(&runtime, litebox, "rapid-snap").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "v5")
.await
.unwrap();
let snaps = litebox.snapshots().list().await.unwrap();
assert_eq!(snaps.len(), 5, "should have 5 snapshots");
litebox.snapshots().restore("v1").await.unwrap();
litebox.start().await.unwrap();
assert_eq!(read_file(&litebox, "/root/ver.txt").await, "v1");
let litebox = stop_and_refresh(&runtime, litebox, "rapid-snap").await;
litebox.snapshots().restore("v3").await.unwrap();
litebox.start().await.unwrap();
assert_eq!(read_file(&litebox, "/root/ver.txt").await, "v3");
let litebox = stop_and_refresh(&runtime, litebox, "rapid-snap").await;
litebox.snapshots().restore("v5").await.unwrap();
litebox.start().await.unwrap();
assert_eq!(read_file(&litebox, "/root/ver.txt").await, "v5");
litebox.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_export_under_write_pressure_with_data_check() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_running_box(&runtime, "exp-stress").await;
write_file(&source, "/root/marker.txt", "export-marker").await;
let write_script = concat!(
"while true; do ",
"dd if=/dev/urandom of=/root/stress.bin bs=4096 count=1 ",
"seek=$((RANDOM % 128)) conv=notrunc 2>/dev/null; ",
"done"
);
let cmd = BoxCommand::new("sh").args(["-c", write_script]);
let _bg = source.exec(cmd).await.unwrap();
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let export_dir = TempDir::new_in("/tmp").unwrap();
let archive = source
.export(ExportOptions::default(), export_dir.path())
.await
.expect("export under write pressure should succeed");
assert_eq!(source.info().status, BoxStatus::Running);
let imported = runtime
.import_box(archive, Some("imp-stress".to_string()))
.await
.unwrap();
imported.start().await.unwrap();
let marker = read_file(&imported, "/root/marker.txt").await;
assert_eq!(marker, "export-marker");
imported.stop().await.unwrap();
source.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_survives_box_restart() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "restart-snap").await;
litebox.start().await.unwrap();
write_file(&litebox, "/root/data.txt", "pre-restart").await;
let litebox = stop_and_refresh(&runtime, litebox, "restart-snap").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "pre-restart")
.await
.unwrap();
litebox.start().await.unwrap();
write_file(&litebox, "/root/data.txt", "post-restart").await;
let litebox = stop_and_refresh(&runtime, litebox, "restart-snap").await;
let snaps = litebox.snapshots().list().await.unwrap();
assert!(snaps.iter().any(|s| s.name == "pre-restart"));
litebox.snapshots().restore("pre-restart").await.unwrap();
litebox.start().await.unwrap();
let data = read_file(&litebox, "/root/data.txt").await;
assert_eq!(data, "pre-restart");
litebox.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_box_info_status_correct_throughout_snapshot_lifecycle() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_running_box(&runtime, "status-track").await;
assert_eq!(litebox.info().status, BoxStatus::Running);
litebox
.snapshots()
.create(SnapshotOptions::default(), "v1")
.await
.unwrap();
assert_eq!(litebox.info().status, BoxStatus::Running);
let litebox = stop_and_refresh(&runtime, litebox, "status-track").await;
assert_eq!(litebox.info().status, BoxStatus::Stopped);
litebox.snapshots().restore("v1").await.unwrap();
assert_eq!(litebox.info().status, BoxStatus::Stopped);
litebox.start().await.unwrap();
assert_eq!(litebox.info().status, BoxStatus::Running);
litebox.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_after_clone_source_modification() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "mod-src").await;
source.start().await.unwrap();
write_file(&source, "/root/data.txt", "data-A").await;
let source = stop_and_refresh(&runtime, source, "mod-src").await;
let cloned = source
.clone_box(CloneOptions::default(), Some("mod-clone".to_string()))
.await
.unwrap();
source.start().await.unwrap();
write_file(&source, "/root/data.txt", "data-B").await;
let source = stop_and_refresh(&runtime, source, "mod-src").await;
source
.snapshots()
.create(SnapshotOptions::default(), "post-clone")
.await
.unwrap();
source.snapshots().restore("post-clone").await.unwrap();
source.start().await.unwrap();
let src_data = read_file(&source, "/root/data.txt").await;
assert_eq!(src_data, "data-B");
source.stop().await.unwrap();
cloned.start().await.unwrap();
let clone_data = read_file(&cloned, "/root/data.txt").await;
assert_eq!(clone_data, "data-A", "clone should be independent");
cloned.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_multiple_boxes_snapshot_independently() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let box_a = create_stopped_box(&runtime, "indep-a").await;
let box_b = create_stopped_box(&runtime, "indep-b").await;
box_a.start().await.unwrap();
write_file(&box_a, "/root/data.txt", "box-a-data").await;
let box_a = stop_and_refresh(&runtime, box_a, "indep-a").await;
box_a
.snapshots()
.create(SnapshotOptions::default(), "a-snap")
.await
.unwrap();
box_b.start().await.unwrap();
write_file(&box_b, "/root/data.txt", "box-b-data").await;
let box_b = stop_and_refresh(&runtime, box_b, "indep-b").await;
box_b
.snapshots()
.create(SnapshotOptions::default(), "b-snap")
.await
.unwrap();
box_a.snapshots().restore("a-snap").await.unwrap();
box_a.start().await.unwrap();
let a_data = read_file(&box_a, "/root/data.txt").await;
assert_eq!(a_data, "box-a-data");
box_a.stop().await.unwrap();
box_b.start().await.unwrap();
let b_data = read_file(&box_b, "/root/data.txt").await;
assert_eq!(b_data, "box-b-data");
box_b.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_clone_and_export_same_box_sequentially() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "clone-export-seq").await;
let cloned = source
.clone_box(CloneOptions::default(), Some("seq-clone".to_string()))
.await
.unwrap();
let export_dir = TempDir::new_in("/tmp").unwrap();
let archive = source
.export(ExportOptions::default(), export_dir.path())
.await
.unwrap();
let imported = runtime
.import_box(archive, Some("seq-import".to_string()))
.await
.unwrap();
cloned.start().await.unwrap();
let out = exec_stdout(&cloned, BoxCommand::new("echo").args(["clone"])).await;
assert_eq!(out.trim(), "clone");
cloned.stop().await.unwrap();
imported.start().await.unwrap();
let out = exec_stdout(&imported, BoxCommand::new("echo").args(["import"])).await;
assert_eq!(out.trim(), "import");
imported.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_preserves_file_permissions() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "perms-snap").await;
litebox.start().await.unwrap();
write_file(&litebox, "/root/script.sh", "#!/bin/sh\necho hi").await;
let cmd = BoxCommand::new("chmod").args(["755", "/root/script.sh"]);
let mut exec = litebox.exec(cmd).await.unwrap();
exec.wait().await.unwrap();
let perms = exec_stdout(
&litebox,
BoxCommand::new("stat").args(["-c", "%a", "/root/script.sh"]),
)
.await;
assert_eq!(perms.trim(), "755");
let litebox = stop_and_refresh(&runtime, litebox, "perms-snap").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "perms")
.await
.unwrap();
litebox.start().await.unwrap();
let cmd = BoxCommand::new("chmod").args(["644", "/root/script.sh"]);
let mut exec = litebox.exec(cmd).await.unwrap();
exec.wait().await.unwrap();
let litebox = stop_and_refresh(&runtime, litebox, "perms-snap").await;
litebox.snapshots().restore("perms").await.unwrap();
litebox.start().await.unwrap();
let perms = exec_stdout(
&litebox,
BoxCommand::new("stat").args(["-c", "%a", "/root/script.sh"]),
)
.await;
assert_eq!(perms.trim(), "755", "permissions should be preserved");
litebox.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_snapshot_preserves_nested_directories() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "nested-snap").await;
litebox.start().await.unwrap();
let cmd = BoxCommand::new("mkdir").args(["-p", "/root/a/b/c/d"]);
let mut exec = litebox.exec(cmd).await.unwrap();
exec.wait().await.unwrap();
write_file(&litebox, "/root/a/b/c/d/deep.txt", "deep-data").await;
write_file(&litebox, "/root/a/top.txt", "top-data").await;
let litebox = stop_and_refresh(&runtime, litebox, "nested-snap").await;
litebox
.snapshots()
.create(SnapshotOptions::default(), "nested")
.await
.unwrap();
litebox.start().await.unwrap();
let cmd = BoxCommand::new("rm").args(["-rf", "/root/a"]);
let mut exec = litebox.exec(cmd).await.unwrap();
exec.wait().await.unwrap();
let litebox = stop_and_refresh(&runtime, litebox, "nested-snap").await;
litebox.snapshots().restore("nested").await.unwrap();
litebox.start().await.unwrap();
let deep = read_file(&litebox, "/root/a/b/c/d/deep.txt").await;
assert_eq!(deep, "deep-data");
let top = read_file(&litebox, "/root/a/top.txt").await;
assert_eq!(top, "top-data");
litebox.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_clone_preserves_multiple_files() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "multi-file-src").await;
source.start().await.unwrap();
for i in 1..=5 {
write_file(
&source,
&format!("/root/file{}.txt", i),
&format!("content-{}", i),
)
.await;
}
let source = stop_and_refresh(&runtime, source, "multi-file-src").await;
let cloned = source
.clone_box(CloneOptions::default(), Some("multi-clone".to_string()))
.await
.unwrap();
cloned.start().await.unwrap();
for i in 1..=5 {
let data = read_file(&cloned, &format!("/root/file{}.txt", i)).await;
assert_eq!(data, format!("content-{}", i));
}
cloned.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_export_import_preserves_symlinks() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "symlink-src").await;
source.start().await.unwrap();
write_file(&source, "/root/target.txt", "symlink-target").await;
let cmd = BoxCommand::new("ln").args(["-s", "/root/target.txt", "/root/link.txt"]);
let mut exec = source.exec(cmd).await.unwrap();
let result = exec.wait().await.unwrap();
assert_eq!(result.exit_code, 0);
let source = stop_and_refresh(&runtime, source, "symlink-src").await;
let export_dir = TempDir::new_in("/tmp").unwrap();
let archive = source
.export(ExportOptions::default(), export_dir.path())
.await
.unwrap();
let imported = runtime
.import_box(archive, Some("symlink-imp".to_string()))
.await
.unwrap();
imported.start().await.unwrap();
let data = read_file(&imported, "/root/link.txt").await;
assert_eq!(data, "symlink-target");
let link_target = exec_stdout(
&imported,
BoxCommand::new("readlink").args(["/root/link.txt"]),
)
.await;
assert_eq!(link_target.trim(), "/root/target.txt");
imported.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_remove_clone_triggers_base_gc_when_last_dependent() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "gc-src").await;
let _cloned = source
.clone_box(CloneOptions::default(), Some("gc-clone".to_string()))
.await
.unwrap();
let bases_dir = home.path.join("bases");
let bases_before: Vec<_> = std::fs::read_dir(&bases_dir)
.unwrap()
.filter_map(|e| e.ok())
.collect();
assert!(
!bases_before.is_empty(),
"bases/ should have files after clone"
);
runtime.remove("gc-clone", true).await.unwrap();
source.start().await.unwrap();
let out = exec_stdout(&source, BoxCommand::new("echo").args(["ok"])).await;
assert_eq!(out.trim(), "ok");
source.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_remove_one_of_two_clones_preserves_base() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "shared-gc-src").await;
let names = vec!["gc-clone-a".to_string(), "gc-clone-b".to_string()];
let clones = source
.clone_boxes(CloneOptions::default(), 2, names)
.await
.unwrap();
assert_eq!(clones.len(), 2);
runtime.remove("gc-clone-a", true).await.unwrap();
let clone_b = runtime
.get("gc-clone-b")
.await
.unwrap()
.expect("clone B should exist");
clone_b.start().await.unwrap();
let out = exec_stdout(&clone_b, BoxCommand::new("echo").args(["b-ok"])).await;
assert_eq!(out.trim(), "b-ok");
clone_b.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_remove_all_clones_cascades_gc() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "cascade-src").await;
let clone1 = source
.clone_box(CloneOptions::default(), Some("cascade-clone".to_string()))
.await
.unwrap();
let _clone2 = clone1
.clone_box(CloneOptions::default(), Some("cascade-clone2".to_string()))
.await
.unwrap();
runtime.remove("cascade-clone2", true).await.unwrap();
let clone1 = runtime
.get("cascade-clone")
.await
.unwrap()
.expect("clone should exist");
clone1.start().await.unwrap();
clone1.stop().await.unwrap();
runtime.remove("cascade-clone", true).await.unwrap();
source.start().await.unwrap();
source.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_box_removal_cleans_all_snapshots() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let litebox = create_stopped_box(&runtime, "clean-snaps").await;
let box_id = litebox.id().to_string();
litebox
.snapshots()
.create(SnapshotOptions::default(), "s1")
.await
.unwrap();
litebox
.snapshots()
.create(SnapshotOptions::default(), "s2")
.await
.unwrap();
litebox
.snapshots()
.create(SnapshotOptions::default(), "s3")
.await
.unwrap();
let snapshots_dir = home.path.join("boxes").join(&box_id).join("snapshots");
assert!(snapshots_dir.exists());
runtime.remove("clean-snaps", true).await.unwrap();
let box_home = home.path.join("boxes").join(&box_id);
assert!(!box_home.exists());
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_archive_file_is_valid_zstd_tar() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "zstd-check").await;
let export_dir = TempDir::new_in("/tmp").unwrap();
let archive = source
.export(ExportOptions::default(), export_dir.path())
.await
.unwrap();
let magic = std::fs::read(archive.path()).unwrap();
assert!(magic.len() >= 4, "archive too small");
assert_eq!(
&magic[0..4],
&[0x28, 0xB5, 0x2F, 0xFD],
"should have zstd magic bytes"
);
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_archive_roundtrip_checksum_integrity() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "checksum-src").await;
source.start().await.unwrap();
write_file(&source, "/root/data.txt", "checksum-test-data").await;
let source = stop_and_refresh(&runtime, source, "checksum-src").await;
let export_dir = TempDir::new_in("/tmp").unwrap();
let archive = source
.export(ExportOptions::default(), export_dir.path())
.await
.unwrap();
let imported = runtime
.import_box(archive, Some("checksum-imp".to_string()))
.await
.unwrap();
imported.start().await.unwrap();
let data = read_file(&imported, "/root/data.txt").await;
assert_eq!(data, "checksum-test-data");
imported.stop().await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn test_export_produces_deterministic_extension() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let source = create_stopped_box(&runtime, "det-ext").await;
let dir1 = TempDir::new_in("/tmp").unwrap();
let archive1 = source
.export(ExportOptions::default(), dir1.path())
.await
.unwrap();
let dir2 = TempDir::new_in("/tmp").unwrap();
let archive2 = source
.export(ExportOptions::default(), dir2.path())
.await
.unwrap();
assert!(archive1.path().extension().is_some_and(|e| e == "boxlite"));
assert!(archive2.path().extension().is_some_and(|e| e == "boxlite"));
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}