mod common;
use boxlite::BoxliteRuntime;
use boxlite::runtime::id::{BoxID, BoxIDMint};
use boxlite::runtime::options::{BoxOptions, BoxliteOptions};
use boxlite::runtime::types::BoxStatus;
#[tokio::test]
async fn runtime_initialization_creates_empty_list() {
let home = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
assert!(runtime.list_info().await.unwrap().is_empty());
}
#[tokio::test]
async fn create_generates_unique_ids() {
let home = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let box1 = runtime.create(common::alpine_opts(), None).await.unwrap();
let box2 = runtime.create(common::alpine_opts(), None).await.unwrap();
assert_ne!(box1.id(), box2.id());
assert_eq!(box1.id().as_str().len(), BoxIDMint::MINT_LENGTH);
assert_eq!(box2.id().as_str().len(), BoxIDMint::MINT_LENGTH);
box1.stop().await.unwrap();
box2.stop().await.unwrap();
runtime.remove(box1.id().as_str(), false).await.unwrap();
runtime.remove(box2.id().as_str(), false).await.unwrap();
}
#[tokio::test]
async fn create_stores_custom_options() {
let options = BoxOptions {
cpus: Some(4),
memory_mib: Some(1024),
..common::alpine_opts()
};
let home = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let handle = runtime.create(options, None).await.unwrap();
let box_id = handle.id().clone();
let info = runtime.get_info(box_id.as_str()).await.unwrap().unwrap();
assert_eq!(info.cpus, 4);
assert_eq!(info.memory_mib, 1024);
assert!(info.created_at.timestamp() > 0);
handle.stop().await.unwrap();
runtime.remove(box_id.as_str(), false).await.unwrap();
}
#[tokio::test]
async fn list_info_returns_all_boxes() {
let home = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
assert_eq!(runtime.list_info().await.unwrap().len(), 0);
let box1 = runtime.create(common::alpine_opts(), None).await.unwrap();
let box2 = runtime.create(common::alpine_opts(), None).await.unwrap();
let boxes = runtime.list_info().await.unwrap();
assert_eq!(boxes.len(), 2);
let ids: Vec<&str> = boxes.iter().map(|b| b.id.as_str()).collect();
assert!(ids.contains(&box1.id().as_str()));
assert!(ids.contains(&box2.id().as_str()));
box1.stop().await.unwrap();
box2.stop().await.unwrap();
runtime.remove(box1.id().as_str(), false).await.unwrap();
runtime.remove(box2.id().as_str(), false).await.unwrap();
}
#[tokio::test]
async fn list_info_sorted_by_creation_time_newest_first() {
let home = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let box1 = runtime.create(common::alpine_opts(), None).await.unwrap();
let box2 = runtime.create(common::alpine_opts(), None).await.unwrap();
let box3 = runtime.create(common::alpine_opts(), None).await.unwrap();
let boxes = runtime.list_info().await.unwrap();
assert_eq!(boxes.len(), 3);
assert_eq!(boxes[0].id, *box3.id()); assert_eq!(boxes[1].id, *box2.id());
assert_eq!(boxes[2].id, *box1.id());
let box1_id = box1.id().clone();
let box2_id = box2.id().clone();
let box3_id = box3.id().clone();
box1.stop().await.unwrap();
box2.stop().await.unwrap();
box3.stop().await.unwrap();
runtime.remove(box1_id.as_str(), false).await.unwrap();
runtime.remove(box2_id.as_str(), false).await.unwrap();
runtime.remove(box3_id.as_str(), false).await.unwrap();
}
#[tokio::test]
async fn get_info_returns_box_metadata() {
let home = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let handle = runtime
.create(common::alpine_opts_auto(), None)
.await
.unwrap();
let box_id = handle.id().clone();
let info = runtime.get_info(box_id.as_str()).await.unwrap().unwrap();
assert_eq!(info.id, box_id);
assert_eq!(
info.status,
BoxStatus::Configured,
"Expected Configured after create(), got {:?}",
info.status
);
runtime.remove(box_id.as_str(), true).await.unwrap();
}
#[tokio::test]
async fn get_info_returns_none_for_nonexistent() {
let home = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let missing = runtime.get_info("nonexistent-id").await.unwrap();
assert!(missing.is_none());
}
#[tokio::test]
async fn exists_returns_true_for_existing_box() {
let home = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let handle = runtime
.create(common::alpine_opts_auto(), None)
.await
.unwrap();
let box_id = handle.id().clone();
assert!(runtime.exists(box_id.as_str()).await.unwrap());
runtime.remove(box_id.as_str(), true).await.unwrap();
}
#[tokio::test]
async fn exists_returns_false_for_nonexistent() {
let home = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
assert!(!runtime.exists("nonexistent-id").await.unwrap());
}
#[tokio::test]
async fn remove_nonexistent_returns_not_found() {
let home = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let result = runtime.remove("nonexistent-id", false).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("not found"),
"Expected NotFound error, got: {}",
err
);
}
#[tokio::test]
async fn remove_stopped_box_succeeds() {
let home = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let handle = runtime.create(common::alpine_opts(), None).await.unwrap();
let box_id = handle.id().clone();
handle.stop().await.unwrap();
runtime.remove(box_id.as_str(), false).await.unwrap();
assert!(!runtime.exists(box_id.as_str()).await.unwrap());
}
#[tokio::test]
async fn remove_active_without_force_fails() {
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 handle = runtime
.create(common::alpine_opts_auto(), None)
.await
.unwrap();
let box_id = handle.id().clone();
handle.start().await.unwrap();
let info = runtime.get_info(box_id.as_str()).await.unwrap().unwrap();
assert!(info.status.is_active());
let result = runtime.remove(box_id.as_str(), false).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("cannot remove active box"),
"Expected active box error, got: {}",
err
);
assert!(runtime.exists(box_id.as_str()).await.unwrap());
runtime.remove(box_id.as_str(), true).await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn remove_active_with_force_stops_and_removes() {
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 handle = runtime
.create(common::alpine_opts_auto(), None)
.await
.unwrap();
let box_id = handle.id().clone();
handle.start().await.unwrap();
let info = runtime.get_info(box_id.as_str()).await.unwrap().unwrap();
assert!(info.status.is_active());
runtime.remove(box_id.as_str(), true).await.unwrap();
assert!(!runtime.exists(box_id.as_str()).await.unwrap());
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn remove_deletes_box_from_database() {
let home = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let handle = runtime
.create(common::alpine_opts_auto(), None)
.await
.unwrap();
let box_id = handle.id().clone();
assert!(runtime.exists(box_id.as_str()).await.unwrap());
runtime.remove(box_id.as_str(), true).await.unwrap();
assert!(!runtime.exists(box_id.as_str()).await.unwrap());
}
#[tokio::test]
async fn stop_marks_box_as_stopped() {
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 handle = runtime.create(common::alpine_opts(), None).await.unwrap();
let box_id = handle.id().clone();
handle.start().await.unwrap();
handle.stop().await.unwrap();
let info = runtime.get_info(box_id.as_str()).await.unwrap().unwrap();
assert_eq!(info.status, BoxStatus::Stopped);
runtime.remove(box_id.as_str(), false).await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn litebox_info_returns_correct_metadata() {
let home = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let handle = runtime
.create(common::alpine_opts_auto(), None)
.await
.unwrap();
let box_id = handle.id().clone();
let info = runtime
.get_info(box_id.as_str())
.await
.unwrap()
.expect("info should be available");
assert_eq!(info.id, box_id);
assert_eq!(info.status, BoxStatus::Configured);
assert_eq!(info.cpus, 2); assert_eq!(info.memory_mib, 512);
runtime.remove(box_id.as_str(), true).await.unwrap();
}
#[tokio::test]
async fn multiple_runtimes_are_isolated() {
let home1 = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime1 = BoxliteRuntime::new(BoxliteOptions {
home_dir: home1.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let home2 = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime2 = BoxliteRuntime::new(BoxliteOptions {
home_dir: home2.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let box1 = runtime1
.create(common::alpine_opts_auto(), None)
.await
.unwrap();
let box2 = runtime2
.create(common::alpine_opts_auto(), None)
.await
.unwrap();
assert_eq!(runtime1.list_info().await.unwrap().len(), 1);
assert_eq!(runtime2.list_info().await.unwrap().len(), 1);
assert_eq!(runtime1.list_info().await.unwrap()[0].id, *box1.id());
assert_eq!(runtime2.list_info().await.unwrap()[0].id, *box2.id());
runtime1.remove(box1.id().as_str(), true).await.unwrap();
runtime2.remove(box2.id().as_str(), true).await.unwrap();
}
#[tokio::test]
async fn boxes_persist_across_runtime_restart() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let box_id: BoxID;
{
let options = BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
};
let runtime = BoxliteRuntime::new(options).expect("Failed to create runtime");
let litebox = runtime.create(common::alpine_opts(), None).await.unwrap();
box_id = litebox.id().clone();
let boxes = runtime.list_info().await.unwrap();
assert_eq!(boxes.len(), 1);
litebox.start().await.unwrap();
litebox.stop().await.unwrap();
}
{
let options = BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
};
let runtime = BoxliteRuntime::new(options).expect("Failed to create runtime");
let boxes = runtime.list_info().await.unwrap();
assert_eq!(boxes.len(), 1);
let status = &boxes[0].status;
assert_eq!(status, &BoxStatus::Stopped);
runtime.remove(box_id.as_str(), false).await.unwrap();
}
}
#[tokio::test]
async fn multiple_boxes_persist_and_recover_without_lock_errors() {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
let box_ids: Vec<BoxID>;
{
let options = BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
};
let runtime = BoxliteRuntime::new(options).expect("Failed to create runtime");
let litebox1 = runtime.create(common::alpine_opts(), None).await.unwrap();
let litebox2 = runtime.create(common::alpine_opts(), None).await.unwrap();
let litebox3 = runtime.create(common::alpine_opts(), None).await.unwrap();
box_ids = vec![
litebox1.id().clone(),
litebox2.id().clone(),
litebox3.id().clone(),
];
litebox1.start().await.unwrap();
litebox2.start().await.unwrap();
litebox3.start().await.unwrap();
litebox1.stop().await.unwrap();
litebox2.stop().await.unwrap();
litebox3.stop().await.unwrap();
}
{
let options = BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
};
let runtime = BoxliteRuntime::new(options).expect("Failed to create runtime after restart");
let boxes = runtime.list_info().await.unwrap();
assert_eq!(boxes.len(), 3, "All boxes should be recovered");
let recovered_ids: Vec<&BoxID> = boxes.iter().map(|b| &b.id).collect();
for box_id in &box_ids {
assert!(
recovered_ids.contains(&box_id),
"Box {} should be recovered",
box_id
);
}
for info in &boxes {
assert_eq!(
info.status,
BoxStatus::Stopped,
"Recovered box should be stopped"
);
}
for box_id in &box_ids {
runtime.remove(box_id.as_str(), false).await.unwrap();
}
}
}
#[tokio::test]
async fn auto_remove_default_is_true() {
let options = BoxOptions::default();
assert!(
options.auto_remove,
"auto_remove should default to true (like Docker --rm)"
);
}
#[tokio::test]
async fn auto_remove_true_removes_box_on_stop() {
let home = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let handle = runtime
.create(common::alpine_opts_auto(), None)
.await
.unwrap();
let box_id = handle.id().clone();
assert!(runtime.exists(box_id.as_str()).await.unwrap());
handle.stop().await.unwrap();
assert!(
!runtime.exists(box_id.as_str()).await.unwrap(),
"Box should be auto-removed when auto_remove=true"
);
}
#[tokio::test]
async fn auto_remove_false_preserves_box_on_stop() {
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 handle = runtime.create(common::alpine_opts(), None).await.unwrap();
let box_id = handle.id().clone();
handle.start().await.unwrap();
handle.stop().await.unwrap();
assert!(
runtime.exists(box_id.as_str()).await.unwrap(),
"Box should be preserved when auto_remove=false"
);
let info = runtime.get_info(box_id.as_str()).await.unwrap().unwrap();
assert_eq!(info.status, BoxStatus::Stopped);
runtime.remove(box_id.as_str(), false).await.unwrap();
let _ = runtime.shutdown(Some(common::TEST_SHUTDOWN_TIMEOUT)).await;
}
#[tokio::test]
async fn detach_default_is_false() {
let options = BoxOptions::default();
assert!(
!options.detach,
"detach should default to false (box tied to parent lifecycle)"
);
}
#[tokio::test]
async fn detach_option_is_stored_in_box_config() {
let home = boxlite_test_utils::home::PerTestBoxHome::isolated();
let runtime = BoxliteRuntime::new(BoxliteOptions {
home_dir: home.path.clone(),
image_registries: common::test_registries(),
})
.expect("create runtime");
let handle = runtime
.create(
BoxOptions {
detach: true,
..common::alpine_opts()
},
None,
)
.await
.unwrap();
let box_id = handle.id().clone();
assert!(runtime.exists(box_id.as_str()).await.unwrap());
runtime.remove(box_id.as_str(), true).await.unwrap();
}