mod common;
use std::path::PathBuf;
use arcbox_vm::config::SnapshotType;
#[cfg(target_os = "linux")]
use arcbox_vm::network::{NetworkAllocation, NetworkManager};
use arcbox_vm::snapshot::SnapshotCatalog;
#[test]
fn snapshot_catalog_persists_across_instances() {
let dir = tempfile::tempdir().unwrap();
let data_dir = dir.path().to_str().unwrap();
let id = {
let catalog = SnapshotCatalog::new(data_dir);
catalog
.register(
"vm-persist",
Some("checkpoint-1".into()),
SnapshotType::Full,
PathBuf::from("/tmp/vmstate"),
None,
None,
None,
None,
)
.unwrap()
.id
};
let catalog2 = SnapshotCatalog::new(data_dir);
let loaded = catalog2.get("vm-persist", &id).unwrap();
assert_eq!(loaded.id, id);
assert_eq!(loaded.name.as_deref(), Some("checkpoint-1"));
assert_eq!(loaded.snapshot_type, SnapshotType::Full);
let list = catalog2.list("vm-persist").unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list[0].id, id);
}
#[test]
#[cfg(target_os = "linux")]
fn tap_lifecycle_via_network_manager() {
if !common::is_root() {
eprintln!("SKIP tap_lifecycle_via_network_manager — requires root");
return;
}
let mgr = NetworkManager::new("10.0.10.0/28", "10.0.10.1", vec![]).unwrap();
let alloc = mgr.allocate("itg-tap-lifecycle").unwrap();
assert!(
common::iface_exists(&alloc.tap_name),
"TAP {} should exist after allocate",
alloc.tap_name
);
mgr.release(&alloc);
assert!(
!common::iface_exists(&alloc.tap_name),
"TAP {} should be gone after release",
alloc.tap_name
);
}
#[test]
#[cfg(target_os = "linux")]
fn network_ip_returns_to_pool_with_tap() {
if !common::is_root() {
eprintln!("SKIP network_ip_returns_to_pool_with_tap — requires root");
return;
}
let mgr = NetworkManager::new("10.0.11.0/28", "10.0.11.1", vec![]).unwrap();
let a1 = mgr.allocate("itg-pool-vm-1").unwrap();
let first_ip = a1.ip_address;
mgr.release(&a1);
let a2 = mgr.allocate("itg-pool-vm-2").unwrap();
assert_eq!(a2.ip_address, first_ip, "released IP should be reused");
mgr.release(&a2);
assert!(
!common::iface_exists(&a2.tap_name),
"TAP {} should be gone after final release",
a2.tap_name
);
}
#[cfg(target_os = "linux")]
struct TapGuard<'a> {
mgr: &'a NetworkManager,
alloc: &'a NetworkAllocation,
}
#[cfg(target_os = "linux")]
impl Drop for TapGuard<'_> {
fn drop(&mut self) {
self.mgr.release(self.alloc);
}
}
#[test]
#[cfg(target_os = "linux")]
fn tap_has_point_to_point_peer_address() {
if !common::is_root() {
eprintln!("SKIP tap_has_point_to_point_peer_address — requires root");
return;
}
let mgr = NetworkManager::new("10.0.12.0/28", "10.0.12.1", vec![]).unwrap();
let alloc = mgr.allocate("itg-ptp-1").unwrap();
let guard = TapGuard {
mgr: &mgr,
alloc: &alloc,
};
let peer = common::get_peer_addr(&alloc.tap_name);
assert_eq!(
peer.as_deref(),
Some(&*alloc.ip_address.to_string()),
"TAP {} should have peer address {}",
alloc.tap_name,
alloc.ip_address
);
let route_dest = format!("{}/32", alloc.ip_address);
assert!(
common::has_route(&route_dest, &alloc.tap_name),
"expected /32 route to {} via {}",
alloc.ip_address,
alloc.tap_name
);
drop(guard);
assert!(
!common::has_route(&route_dest, &alloc.tap_name),
"route to {} should be removed after release",
alloc.ip_address
);
}
#[test]
#[cfg(target_os = "linux")]
fn multiple_taps_are_isolated() {
if !common::is_root() {
eprintln!("SKIP multiple_taps_are_isolated — requires root");
return;
}
let mgr = NetworkManager::new("10.0.13.0/28", "10.0.13.1", vec![]).unwrap();
let a1 = mgr.allocate("itg-iso-1").unwrap();
let _g1 = TapGuard {
mgr: &mgr,
alloc: &a1,
};
let a2 = mgr.allocate("itg-iso-2").unwrap();
let _g2 = TapGuard {
mgr: &mgr,
alloc: &a2,
};
assert!(common::iface_exists(&a1.tap_name));
assert!(common::iface_exists(&a2.tap_name));
assert_ne!(a1.ip_address, a2.ip_address);
assert_eq!(
common::get_peer_addr(&a1.tap_name).as_deref(),
Some(&*a1.ip_address.to_string()),
);
assert_eq!(
common::get_peer_addr(&a2.tap_name).as_deref(),
Some(&*a2.ip_address.to_string()),
);
let out1 = std::process::Command::new("/usr/sbin/ip")
.args(["link", "show", &a1.tap_name])
.output()
.unwrap();
assert!(
!String::from_utf8_lossy(&out1.stdout).contains("master"),
"TAP {} should not be attached to any bridge",
a1.tap_name
);
}