use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use async_trait::async_trait;
use super::FakeDriver;
use crate::driver::{VmDriver, VmHandle};
use crate::spec::{
BootSpec, CacheMode, ConsoleSpec, DiskSpec, MacAddr, NicAttachment, NicSpec, VmId, VmSpec,
VsockSpec,
};
mod checks;
pub use checks::*;
#[async_trait]
pub trait ContractHarness: Send + Sync {
fn driver(&self) -> Arc<dyn VmDriver>;
fn spec(&self, id: &VmId) -> VmSpec;
fn runtime_dir(&self) -> PathBuf;
fn dial_port(&self) -> Option<u32>;
fn guest_dial_out_port(&self) -> Option<u32>;
async fn ready(&self, handle: &dyn VmHandle);
}
pub struct FakeHarness {
driver: Arc<FakeDriver>,
root: tempfile::TempDir,
dirs: AtomicUsize,
}
impl FakeHarness {
pub fn new() -> Self {
Self::with_driver(FakeDriver::new())
}
pub fn with_driver(driver: FakeDriver) -> Self {
Self {
driver: Arc::new(driver),
root: tempfile::tempdir().expect("temporary directory for the fake harness"),
dirs: AtomicUsize::new(0),
}
}
pub fn fake(&self) -> &FakeDriver {
&self.driver
}
pub const READY_PORT: u32 = 1025;
}
impl Default for FakeHarness {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ContractHarness for FakeHarness {
fn driver(&self) -> Arc<dyn VmDriver> {
self.driver.clone()
}
fn spec(&self, id: &VmId) -> VmSpec {
VmSpec {
id: id.clone(),
cpus: 1,
memory_mib: 128,
boot: BootSpec::Kernel {
image: self.file("vmlinux"),
cmdline: "console=hvc0".into(),
initrd: None,
},
disks: vec![DiskSpec {
id: "rootfs".into(),
path: self.file("rootfs.ext4"),
read_only: false,
root: true,
cache: CacheMode::Unsafe,
}],
nics: vec![NicSpec {
id: "eth0".into(),
mac: MacAddr::new([0x02, 0xfa, 0xce, 0, 0, 1]),
attachment: NicAttachment::Tap {
name: "tap0".into(),
},
}],
vsock: Some(VsockSpec { guest_cid: 3 }),
shares: vec![],
console: ConsoleSpec::File(self.root.path().join(format!("{id}.console"))),
balloon: true,
entropy: true,
dirty_tracking: true,
isolation: Default::default(),
}
}
fn runtime_dir(&self) -> PathBuf {
let n = self.dirs.fetch_add(1, Ordering::Relaxed);
let dir = self.root.path().join(format!("vm{n}"));
std::fs::create_dir_all(&dir).expect("runtime dir under the harness root");
dir
}
fn dial_port(&self) -> Option<u32> {
Some(1024)
}
fn guest_dial_out_port(&self) -> Option<u32> {
Some(Self::READY_PORT)
}
async fn ready(&self, handle: &dyn VmHandle) {
self.driver
.guest_dial(handle.id(), Self::READY_PORT)
.expect("the fake guest dials out on boot");
}
}
impl FakeHarness {
fn file(&self, name: &str) -> PathBuf {
let path = self.root.path().join(name);
if !path.exists() {
std::fs::write(&path, b"").expect("disk file under the harness root");
}
path
}
}
#[macro_export]
macro_rules! driver_contract {
($mod:ident, $harness:expr) => {
mod $mod {
#[allow(
unused_imports,
reason = "the harness expression may or may not name items of the enclosing module"
)]
use super::*;
$crate::__driver_contract_checks! {
$harness;
boot_reports_running_then_exits_on_kill,
events_deliver_exit_exactly_once,
capabilities_agree_with_accessors,
checkpoint_resume_keeps_running,
checkpoint_hold_quiesces,
restore_yields_a_live_vm,
detach_then_adopt_round_trip,
vsock_dial_reaches_the_guest,
foreign_checkpoint_is_refused,
record_is_stable,
prepare_then_boot_matches_boot,
prepared_listener_is_live_before_boot,
discard_kills_a_prepared_vm,
restore_reattaches_disks,
a_staged_spec_boots,
a_staged_disk_can_be_taken_back_out,
a_staged_checkpoint_restores,
a_dead_vms_area_is_discarded,
}
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __driver_contract_checks {
($harness:expr; $($check:ident),* $(,)?) => {
$(
#[tokio::test]
async fn $check() {
let harness = $harness;
$crate::testkit::contract::$check(&harness).await;
}
)*
};
}