//! Integration tests for Volt VM boot //! //! These tests verify that VMs boot correctly and measure boot times. //! Run with: cargo test --test boot_test -- --ignored //! //! Requirements: //! - KVM access (/dev/kvm readable/writable) //! - Built kernel in kernels/vmlinux //! - Built rootfs in images/alpine-rootfs.ext4 use std::io::{BufRead, BufReader}; use std::path::PathBuf; use std::process::{Child, Command, Stdio}; use std::sync::mpsc; use std::thread; use std::time::{Duration, Instant}; /// Get the project root directory fn project_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .to_path_buf() } /// Check if KVM is available fn kvm_available() -> bool { std::path::Path::new("/dev/kvm").exists() && std::fs::metadata("/dev/kvm") .map(|m| !m.permissions().readonly()) .unwrap_or(false) } /// Get path to the Volt binary fn volt-vmm_binary() -> PathBuf { let release = project_root().join("target/release/volt-vmm"); if release.exists() { release } else { project_root().join("target/debug/volt-vmm") } } /// Get path to the test kernel fn test_kernel() -> PathBuf { project_root().join("kernels/vmlinux") } /// Get path to the test rootfs fn test_rootfs() -> PathBuf { let ext4 = project_root().join("images/alpine-rootfs.ext4"); if ext4.exists() { ext4 } else { project_root().join("images/alpine-rootfs.squashfs") } } /// Spawn a VM and return the child process fn spawn_vm(memory_mb: u32, cpus: u32) -> std::io::Result { let binary = volt-vmm_binary(); let kernel = test_kernel(); let rootfs = test_rootfs(); Command::new(&binary) .arg("--kernel") .arg(&kernel) .arg("--rootfs") .arg(&rootfs) .arg("--memory") .arg(memory_mb.to_string()) .arg("--cpus") .arg(cpus.to_string()) .arg("--cmdline") .arg("console=ttyS0 reboot=k panic=1 nomodules quiet") .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() } /// Wait for a specific string in VM output fn wait_for_output( child: &mut Child, pattern: &str, timeout: Duration, ) -> Result { let start = Instant::now(); let stdout = child.stdout.take().ok_or("No stdout")?; let reader = BufReader::new(stdout); let (tx, rx) = mpsc::channel(); let pattern = pattern.to_string(); // Spawn reader thread thread::spawn(move || { for line in reader.lines() { if let Ok(line) = line { if line.contains(&pattern) { let _ = tx.send(Instant::now()); break; } } } }); // Wait for pattern or timeout match rx.recv_timeout(timeout) { Ok(found_time) => Ok(found_time.duration_since(start)), Err(_) => Err(format!("Timeout waiting for '{}'", pattern)), } } // ============================================================================ // Tests // ============================================================================ #[test] #[ignore = "requires KVM and built assets"] fn test_vm_boots() { if !kvm_available() { eprintln!("Skipping: KVM not available"); return; } let binary = volt-vmm_binary(); if !binary.exists() { eprintln!("Skipping: Volt binary not found at {:?}", binary); return; } let kernel = test_kernel(); if !kernel.exists() { eprintln!("Skipping: Kernel not found at {:?}", kernel); return; } let rootfs = test_rootfs(); if !rootfs.exists() { eprintln!("Skipping: Rootfs not found at {:?}", rootfs); return; } println!("Starting VM..."); let mut child = spawn_vm(128, 1).expect("Failed to spawn VM"); // Wait for boot message let result = wait_for_output(&mut child, "Volt microVM booted", Duration::from_secs(30)); // Clean up let _ = child.kill(); match result { Ok(boot_time) => { println!("✓ VM booted successfully in {:?}", boot_time); assert!(boot_time < Duration::from_secs(10), "Boot took too long"); } Err(e) => { panic!("VM boot failed: {}", e); } } } #[test] #[ignore = "requires KVM and built assets"] fn test_boot_time_under_500ms() { if !kvm_available() { eprintln!("Skipping: KVM not available"); return; } let binary = volt-vmm_binary(); let kernel = test_kernel(); let rootfs = test_rootfs(); if !binary.exists() || !kernel.exists() || !rootfs.exists() { eprintln!("Skipping: Required assets not found"); return; } // Run multiple times and average let mut boot_times = Vec::new(); let iterations = 3; for i in 0..iterations { println!("Boot test iteration {}/{}", i + 1, iterations); let mut child = spawn_vm(128, 1).expect("Failed to spawn VM"); // Look for kernel boot message or shell prompt let result = wait_for_output(&mut child, "Booting", Duration::from_secs(5)); let _ = child.kill(); if let Ok(duration) = result { boot_times.push(duration); } } if boot_times.is_empty() { eprintln!("No successful boots recorded"); return; } let avg_boot: Duration = boot_times.iter().sum::() / boot_times.len() as u32; println!("Average boot time: {:?} ({} samples)", avg_boot, boot_times.len()); // Target: <500ms to first kernel output // This is aggressive but achievable with PVH boot if avg_boot < Duration::from_millis(500) { println!("✓ Boot time target met: {:?} < 500ms", avg_boot); } else { println!("⚠ Boot time target missed: {:?} >= 500ms", avg_boot); // Don't fail yet - this is aspirational } } #[test] #[ignore = "requires KVM and built assets"] fn test_multiple_vcpus() { if !kvm_available() { return; } let binary = volt-vmm_binary(); let kernel = test_kernel(); let rootfs = test_rootfs(); if !binary.exists() || !kernel.exists() || !rootfs.exists() { return; } // Test with 2 and 4 vCPUs for cpus in [2, 4] { println!("Testing with {} vCPUs...", cpus); let mut child = spawn_vm(256, cpus).expect("Failed to spawn VM"); let result = wait_for_output( &mut child, "Volt microVM booted", Duration::from_secs(30), ); let _ = child.kill(); assert!(result.is_ok(), "Failed to boot with {} vCPUs", cpus); println!("✓ {} vCPUs: booted in {:?}", cpus, result.unwrap()); } } #[test] #[ignore = "requires KVM and built assets"] fn test_memory_sizes() { if !kvm_available() { return; } let binary = volt-vmm_binary(); let kernel = test_kernel(); let rootfs = test_rootfs(); if !binary.exists() || !kernel.exists() || !rootfs.exists() { return; } // Test various memory sizes for mem_mb in [64, 128, 256, 512] { println!("Testing with {}MB memory...", mem_mb); let mut child = spawn_vm(mem_mb, 1).expect("Failed to spawn VM"); let result = wait_for_output( &mut child, "Volt microVM booted", Duration::from_secs(30), ); let _ = child.kill(); assert!(result.is_ok(), "Failed to boot with {}MB", mem_mb); println!("✓ {}MB: booted in {:?}", mem_mb, result.unwrap()); } } // ============================================================================ // Benchmarks (manual, run with --nocapture) // ============================================================================ #[test] #[ignore = "benchmark - run manually"] fn bench_cold_boot() { if !kvm_available() { return; } println!("\n=== Cold Boot Benchmark ===\n"); let iterations = 10; let mut times = Vec::with_capacity(iterations); for i in 0..iterations { // Clear caches (would need root) // let _ = Command::new("sync").status(); // let _ = std::fs::write("/proc/sys/vm/drop_caches", "3"); let start = Instant::now(); let mut child = spawn_vm(128, 1).expect("Failed to spawn"); let result = wait_for_output( &mut child, "Volt microVM booted", Duration::from_secs(30), ); let _ = child.kill(); if let Ok(_) = result { let elapsed = start.elapsed(); times.push(elapsed); println!(" Run {:2}: {:?}", i + 1, elapsed); } } if times.is_empty() { println!("No successful runs"); return; } times.sort(); let sum: Duration = times.iter().sum(); let avg = sum / times.len() as u32; let min = times.first().unwrap(); let max = times.last().unwrap(); let median = ×[times.len() / 2]; println!("\nResults ({} runs):", times.len()); println!(" Min: {:?}", min); println!(" Max: {:?}", max); println!(" Avg: {:?}", avg); println!(" Median: {:?}", median); }