use super::boot::{agent_timeout_error, ensure_earlycon, with_container_network};
use super::types::{DesiredBoot, machine_drift_reason, metadata_image_filename};
use super::*;
use crate::machine::MachineState;
use crate::vm::ensure_sparse_block_image;
use arcbox_constants::cmdline::HV_EARLYCON_DIRECTIVE;
use arcbox_constants::container_network::ContainerNetwork;
#[test]
fn test_lifecycle_state_is_ready() {
assert!(!VmLifecycleState::NotExist.is_ready());
assert!(!VmLifecycleState::Creating.is_ready());
assert!(!VmLifecycleState::Created.is_ready());
assert!(!VmLifecycleState::Starting.is_ready());
assert!(VmLifecycleState::Running.is_ready());
assert!(VmLifecycleState::Idle.is_ready());
assert!(!VmLifecycleState::Stopping.is_ready());
assert!(!VmLifecycleState::Stopped.is_ready());
assert!(!VmLifecycleState::Failed.is_ready());
}
#[test]
fn test_lifecycle_state_needs_start() {
assert!(VmLifecycleState::NotExist.needs_start());
assert!(!VmLifecycleState::Creating.needs_start());
assert!(VmLifecycleState::Created.needs_start());
assert!(!VmLifecycleState::Starting.needs_start());
assert!(!VmLifecycleState::Running.needs_start());
assert!(!VmLifecycleState::Idle.needs_start());
assert!(!VmLifecycleState::Stopping.needs_start());
assert!(VmLifecycleState::Stopped.needs_start());
assert!(VmLifecycleState::Failed.needs_start());
}
#[test]
fn ensure_earlycon_upgrades_bare_on_hv() {
let out = ensure_earlycon(
"console=hvc0 earlycon root=/dev/vda".to_string(),
arcbox_vmm::VmBackend::Hv,
);
assert!(out.contains(HV_EARLYCON_DIRECTIVE));
assert!(!out.split_whitespace().any(|t| t == "earlycon"));
}
#[test]
fn ensure_earlycon_leaves_vz_untouched() {
let cmdline = "console=hvc0 earlycon root=/dev/vda".to_string();
let out = ensure_earlycon(cmdline.clone(), arcbox_vmm::VmBackend::Vz);
assert_eq!(out, cmdline);
assert!(!out.contains("pl011"));
}
#[test]
fn ensure_earlycon_respects_explicit_directive() {
let cmdline = "console=hvc0 earlycon=pl011,0x9000000 root=/dev/vda".to_string();
let out = ensure_earlycon(cmdline.clone(), arcbox_vmm::VmBackend::Hv);
assert_eq!(out, cmdline);
}
#[test]
fn container_network_cmdline_is_authoritative_and_unique() {
let network: ContainerNetwork = "10.64.0.0/16".parse().unwrap();
let cmdline = with_container_network(
"root=/dev/vda arcbox.container_network=172.16.0.0/12 console=hvc0".to_string(),
network,
);
assert!(cmdline.contains("arcbox.container_network=10.64.0.0/16"));
assert!(!cmdline.contains("172.16.0.0/12"));
assert_eq!(
cmdline
.split_whitespace()
.filter(|token| token.starts_with("arcbox.container_network="))
.count(),
1
);
}
#[test]
fn agent_timeout_error_folds_last_error() {
let bare = agent_timeout_error(None).to_string();
assert!(bare.contains("timeout waiting for agent"));
assert!(!bare.contains("last error"));
let folded = agent_timeout_error(Some("guest reported: disk full")).to_string();
assert!(folded.contains("timeout waiting for agent"));
assert!(folded.contains("guest reported: disk full"));
}
#[test]
fn test_default_config() {
let config = VmLifecycleConfig::default();
assert!(config.auto_stop);
assert_eq!(config.max_retries, DEFAULT_MAX_RETRIES);
}
#[test]
fn ensure_sparse_block_image_is_thin_provisioned() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("data").join("docker.img");
let size_bytes = DOCKER_DATA_IMAGE_SIZE_BYTES;
ensure_sparse_block_image(&path, size_bytes).unwrap();
let meta = std::fs::metadata(&path).unwrap();
assert_eq!(
meta.len(),
size_bytes,
"logical size must match the requested virtual size"
);
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let physical_bytes = meta.blocks() * 512;
assert!(
physical_bytes < 1024 * 1024,
"image must be sparse: {physical_bytes} physical bytes allocated \
for an empty {size_bytes}-byte image"
);
}
}
#[test]
fn ensure_sparse_block_image_never_shrinks() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("docker.img");
ensure_sparse_block_image(&path, 8192).unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), 8192);
ensure_sparse_block_image(&path, 4096).unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), 8192);
ensure_sparse_block_image(&path, 16384).unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), 16384);
}
#[test]
fn test_default_vm_config() {
let config = DefaultVmConfig::default();
assert_eq!(config.cpus, arcbox_hypervisor::default_vm_cpu_count());
let expected_mb = arcbox_hypervisor::default_vm_memory_size() / (1024 * 1024);
assert_eq!(config.memory_mb, expected_mb);
assert!(config.memory_mb >= 512);
assert!(config.memory_mb <= 16384);
assert_eq!(config.disk_gb, 50);
}
fn sample_machine(cpus: u32, memory_mb: u64, kernel: &str, cmdline: &str) -> MachineInfo {
MachineInfo {
name: "default".to_string(),
state: MachineState::Created,
vm_id: crate::vm::VmId::new(),
cid: None,
cpus,
memory_mb,
disk_gb: 50,
kernel: Some(kernel.to_string()),
cmdline: Some(cmdline.to_string()),
block_devices: vec![
crate::vm::BlockDeviceConfig {
path: "/rootfs.erofs".to_string(),
read_only: true,
},
crate::vm::BlockDeviceConfig {
path: "/data/docker.img".to_string(),
read_only: false,
},
crate::vm::BlockDeviceConfig {
path: "/data/docker-meta.img".to_string(),
read_only: false,
},
],
distro: None,
distro_version: None,
disk_path: None,
ssh_key_path: None,
ip_address: None,
bridge_ip_address: None,
backend: arcbox_vmm::VmBackend::default(),
nested_virt: true,
created_at: chrono::Utc::now(),
started_at: None,
mounts: Vec::new(),
}
}
#[test]
fn machine_drift_detects_each_overridable_field() {
let want = DefaultVmConfig {
cpus: 4,
memory_mb: 4096,
..DefaultVmConfig::default()
};
let boot = DesiredBoot {
kernel: "/k".to_string(),
cmdline: "console=hvc0 earlycon".to_string(),
rootfs_image: std::path::PathBuf::from("/rootfs.erofs"),
};
let current = sample_machine(want.cpus, want.memory_mb, &boot.kernel, &boot.cmdline);
assert_eq!(machine_drift_reason(¤t, &want, Some(&boot)), None);
let mut m = current.clone();
m.nested_virt = false;
assert_eq!(
machine_drift_reason(&m, &want, Some(&boot)),
Some("nested_virt")
);
let mut m = current.clone();
m.cpus = want.cpus + 1;
assert_eq!(machine_drift_reason(&m, &want, Some(&boot)), Some("cpus"));
let mut m = current.clone();
m.memory_mb = want.memory_mb + 1;
assert_eq!(
machine_drift_reason(&m, &want, Some(&boot)),
Some("memory_mb")
);
let mut m = current.clone();
m.kernel = Some("/other-kernel".to_string());
assert_eq!(machine_drift_reason(&m, &want, Some(&boot)), Some("kernel"));
let mut m = current.clone();
m.cmdline = Some("console=hvc0 earlycon arm64.nosve".to_string());
assert_eq!(
machine_drift_reason(&m, &want, Some(&boot)),
Some("cmdline")
);
let mut m = current.clone();
m.block_devices.pop();
assert_eq!(
machine_drift_reason(&m, &want, Some(&boot)),
Some("block_devices")
);
let mut m = current;
m.block_devices.push(crate::vm::BlockDeviceConfig {
path: "/boot/runtime.erofs".to_string(),
read_only: true,
});
assert_eq!(
machine_drift_reason(&m, &want, Some(&boot)),
Some("block_devices")
);
assert_eq!(machine_drift_reason(&m, &want, None), Some("block_devices"));
}
#[test]
fn metadata_image_filename_pairs_with_data_image() {
assert_eq!(metadata_image_filename("docker.img"), "docker-meta.img");
assert_eq!(
metadata_image_filename("docker-rosetta.img"),
"docker-rosetta-meta.img"
);
}
#[tokio::test]
async fn storage_reservation_blocks_lifecycle_start_and_force_stop() {
let directory = tempfile::tempdir().unwrap();
let (machines, lifecycle) = storage_test_lifecycle(directory.path());
let reservation = machines.reserve_storage().unwrap();
for error in [
lifecycle.ensure_ready().await.unwrap_err(),
lifecycle.force_stop().await.unwrap_err(),
] {
assert!(error.to_string().contains("held for recovery"));
}
assert_eq!(lifecycle.state().await, VmLifecycleState::NotExist);
assert!(machines.get(DEFAULT_MACHINE_NAME).is_none());
lifecycle.shutdown().await.unwrap();
drop(reservation);
lifecycle.force_stop().await.unwrap();
}
#[tokio::test]
async fn storage_resume_rejects_a_foreign_manager_before_boot() {
let directory = tempfile::tempdir().unwrap();
let foreign_directory = tempfile::tempdir().unwrap();
let (machines, lifecycle) = storage_test_lifecycle(directory.path());
let (foreign, _foreign_lifecycle) = storage_test_lifecycle(foreign_directory.path());
let reservation = foreign.reserve_storage().unwrap();
let error = lifecycle
.resume_storage(&reservation, tokio_util::sync::CancellationToken::new())
.await
.unwrap_err();
assert!(
error
.to_string()
.contains("storage recovery requires its stopped System VM")
);
assert_eq!(lifecycle.state().await, VmLifecycleState::NotExist);
assert!(machines.get(DEFAULT_MACHINE_NAME).is_none());
assert!(
foreign
.ensure_storage_available(DEFAULT_MACHINE_NAME)
.is_err()
);
}
pub(super) fn storage_test_lifecycle(
data_dir: &std::path::Path,
) -> (Arc<MachineManager>, VmLifecycleManager) {
let events = EventBus::new();
let machines = Arc::new(MachineManager::new(
Arc::new(crate::vm::VmManager::new(data_dir.join("snapshots"))),
data_dir.to_path_buf(),
crate::vm::HostNetwork::default(),
events.clone(),
));
let lifecycle = VmLifecycleManager::new(
Arc::clone(&machines),
events,
data_dir.to_path_buf(),
VmLifecycleConfig::default(),
)
.unwrap();
(machines, lifecycle)
}
#[tokio::test]
async fn invalid_storage_pair_creates_a_hold_before_any_vm_boot() {
for failure in ["missing metadata", "invalid manifest", "corrupt signature"] {
let directory = tempfile::tempdir().unwrap();
let data = directory.path().join("data/docker.img");
let metadata = directory.path().join("data/docker-meta.img");
arcbox_storage::prepare_pair(&data, &metadata, 4096, 4096).unwrap();
match failure {
"missing metadata" => std::fs::remove_file(&metadata).unwrap(),
"invalid manifest" => {
std::fs::write(arcbox_storage::manifest_path(&data), b"{").unwrap();
}
"corrupt signature" => std::fs::write(&metadata, [0_u8; 4096]).unwrap(),
_ => unreachable!(),
}
let original_data = std::fs::read(&data).unwrap();
let original_metadata = std::fs::read(&metadata).ok();
let events = EventBus::new();
let machines = Arc::new(MachineManager::new(
Arc::new(crate::vm::VmManager::new(
directory.path().join("snapshots"),
)),
directory.path().to_owned(),
crate::vm::HostNetwork::default(),
events.clone(),
));
let lifecycle = VmLifecycleManager::new(
Arc::clone(&machines),
events,
directory.path().to_owned(),
VmLifecycleConfig::default(),
)
.unwrap();
assert!(lifecycle.ensure_ready().await.is_err(), "{failure}");
assert_eq!(lifecycle.state().await, VmLifecycleState::Failed);
assert!(machines.get(DEFAULT_MACHINE_NAME).is_none());
let operation_id = std::fs::read_to_string(machines.storage_hold_path()).unwrap();
uuid::Uuid::parse_str(&operation_id).unwrap();
assert!(machines.storage_is_held().unwrap());
assert!(lifecycle.ensure_ready().await.is_err());
assert_eq!(
std::fs::read_to_string(machines.storage_hold_path()).unwrap(),
operation_id
);
assert_eq!(std::fs::read(&data).unwrap(), original_data);
assert_eq!(std::fs::read(&metadata).ok(), original_metadata);
}
}