mod common;
#[cfg(target_os = "linux")]
#[path = "integration/rootfs.rs"]
mod rootfs;
use arcbox_computer_runtime::config::SnapshotType;
use arcbox_computer_runtime::snapshot::{SnapshotCatalog, SnapshotDraft};
#[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);
let pending = catalog.begin("vm-persist").unwrap();
std::fs::write(pending.dir().join("vmstate"), b"vmstate").unwrap();
pending
.commit(SnapshotDraft {
labels: std::collections::HashMap::new(),
name: Some("checkpoint-1".into()),
snapshot_type: SnapshotType::Full,
parent_id: None,
kernel_path: None,
rootfs_path: None,
net_invariant: false,
geometry: None,
format: "firecracker/v1".into(),
})
.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 block_device_numbers_round_trip_through_mknod() {
use std::os::unix::fs::{FileTypeExt as _, MetadataExt as _};
use arcbox_computer_runtime::snapshot_cow::{device_major_minor, mknod_blkdev};
if !common::is_root() {
eprintln!("SKIP block_device_numbers_round_trip_through_mknod — requires root");
return;
}
let _serial = common::LOOP_DEVICE_TEST.blocking_lock();
let dir = tempfile::tempdir().unwrap();
let backing = dir.path().join("backing.img");
std::fs::File::create(&backing)
.unwrap()
.set_len(4 * 1024 * 1024)
.unwrap();
let attached = std::process::Command::new("losetup")
.args(["-f", "--show"])
.arg(&backing)
.output();
let Ok(output) = attached else {
eprintln!("SKIP block_device_numbers_round_trip_through_mknod — no losetup");
return;
};
assert!(
output.status.success(),
"losetup: {}",
String::from_utf8_lossy(&output.stderr)
);
let loop_dev = String::from_utf8_lossy(&output.stdout).trim().to_string();
let result = (|| -> Result<(), String> {
let (major, minor) = device_major_minor(&loop_dev).map_err(|e| e.to_string())?;
let node = dir.path().join("rootfs.node");
mknod_blkdev(&node, major, minor).map_err(|e| e.to_string())?;
let node_meta = std::fs::metadata(&node).map_err(|e| e.to_string())?;
let dev_meta = std::fs::metadata(&loop_dev).map_err(|e| e.to_string())?;
if !node_meta.file_type().is_block_device() {
return Err("mknod did not create a block device".into());
}
if node_meta.rdev() != dev_meta.rdev() {
return Err(format!(
"node rdev {:#x} != loop rdev {:#x}",
node_meta.rdev(),
dev_meta.rdev()
));
}
if node_meta.mode() & 0o777 != 0o600 {
return Err(format!(
"node mode {:o}, want 600",
node_meta.mode() & 0o777
));
}
Ok(())
})();
let _ = std::process::Command::new("losetup")
.args(["-d", &loop_dev])
.status();
result.unwrap();
}
#[cfg(target_os = "linux")]
mod block_tools {
use arcbox_computer_runtime::snapshot_cow::{
BlockTools, BusyboxBlockTools, UtilLinuxBlockTools,
};
use super::common;
const IMAGE_BYTES: u64 = 4 * 1024 * 1024;
const REQUIRE: &str = "ARCBOX_REQUIRE_BLOCK_TOOLS";
fn skipped(test: &str, reason: &str) {
assert!(
std::env::var_os(REQUIRE).is_none(),
"{test}: {reason}, but {REQUIRE} says this host has both userlands"
);
eprintln!("SKIP {test} — {reason}");
}
#[test]
fn busybox_attach_report_and_detach() {
const TEST: &str = "busybox_attach_report_and_detach";
if !common::is_root() {
skipped(TEST, "requires root");
return;
}
let busybox = std::env::var("BUSYBOX").unwrap_or_else(|_| "/bin/busybox".into());
if !std::path::Path::new(&busybox).exists() {
skipped(TEST, &format!("no busybox at {busybox}"));
return;
}
attach_report_and_detach(&BusyboxBlockTools::new(&busybox));
}
#[test]
fn util_linux_attach_report_and_detach() {
const TEST: &str = "util_linux_attach_report_and_detach";
if !common::is_root() {
skipped(TEST, "requires root");
return;
}
let tools = match UtilLinuxBlockTools::discover() {
Ok(tools) => tools,
Err(e) => {
skipped(TEST, &e.to_string());
return;
}
};
attach_report_and_detach(&tools);
}
fn attach_report_and_detach(tools: &dyn BlockTools) {
let _serial = common::LOOP_DEVICE_TEST.blocking_lock();
let dir = tempfile::tempdir().unwrap();
let backing = dir.path().join("backing.img");
std::fs::File::create(&backing)
.unwrap()
.set_len(IMAGE_BYTES)
.unwrap();
let device = tools.attach_loop(&backing, false).unwrap();
let sysfs_backing_file = {
let index = device
.strip_prefix("/dev/loop")
.filter(|index| !index.is_empty() && index.bytes().all(|b| b.is_ascii_digit()));
index.map(|index| format!("/sys/block/loop{index}/loop/backing_file"))
};
let result = (|| -> Result<(), String> {
let sysfs_backing_file = sysfs_backing_file
.as_deref()
.ok_or_else(|| format!("attach returned {device}, not /dev/loopN"))?;
let reported = std::fs::read_to_string(sysfs_backing_file)
.map_err(|e| format!("sysfs backing_file for {device}: {e}"))?;
let expected = std::fs::canonicalize(&backing).map_err(|e| e.to_string())?;
if reported.trim() != expected.to_string_lossy() {
return Err(format!(
"{device} backs {:?}, expected {}",
reported.trim(),
expected.display()
));
}
let sectors = tools.device_sectors(&device).map_err(|e| e.to_string())?;
if sectors != IMAGE_BYTES / 512 {
return Err(format!(
"{device} has {sectors} sectors, expected {}",
IMAGE_BYTES / 512
));
}
Ok(())
})();
let detached = tools.detach_loop(&device);
result.unwrap();
detached.unwrap();
let sysfs_backing_file = sysfs_backing_file.unwrap();
let released_by = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::path::Path::new(&sysfs_backing_file).exists() {
assert!(
std::time::Instant::now() < released_by,
"{device} still backs {} five seconds after detach",
std::fs::read_to_string(&sysfs_backing_file)
.unwrap_or_default()
.trim()
);
std::thread::sleep(std::time::Duration::from_millis(20));
}
assert!(
tools.detach_loop(&device).is_err(),
"detaching the released {device} again should fail"
);
}
}