use super::*;
use crate::app::CreateWizardState;
#[test]
fn test_shell_escape_safe_strings() {
assert_eq!(shell_escape("hello"), "hello");
assert_eq!(shell_escape("path/to/file.iso"), "path/to/file.iso");
assert_eq!(shell_escape("my-vm_name.qcow2"), "my-vm_name.qcow2");
}
#[test]
fn test_shell_escape_unsafe_strings() {
assert_eq!(shell_escape("hello world"), "'hello world'");
assert_eq!(shell_escape("it's a test"), "'it'\\''s a test'");
assert_eq!(shell_escape("test; echo pwned"), "'test; echo pwned'");
assert_eq!(shell_escape("$(whoami)"), "'$(whoami)'");
assert_eq!(shell_escape("`whoami`"), "'`whoami`'");
assert_eq!(shell_escape("test\"; echo pwned; echo \""), "'test\"; echo pwned; echo \"'");
}
#[test]
fn test_generate_folder_name() {
assert_eq!(CreateWizardState::generate_folder_name("Windows 10"), "windows-10");
assert_eq!(CreateWizardState::generate_folder_name("Debian GNU/Linux"), "debian-gnu-linux");
assert_eq!(CreateWizardState::generate_folder_name("MS-DOS 6.22"), "ms-dos-6-22");
assert_eq!(CreateWizardState::generate_folder_name(" Spaced Out "), "spaced-out");
}
#[test]
fn test_generate_launch_script() {
let config = WizardQemuConfig::default();
let script = generate_launch_script_with_os(
"Test VM",
"test.qcow2",
Some(Path::new("/tmp/test.iso")),
false,
&config,
None,
None,
);
assert!(script.contains("#!/bin/bash"));
assert!(script.contains("Test VM"));
assert!(script.contains("test.qcow2"));
assert!(script.contains("/tmp/test.iso"));
assert!(script.contains("--install"));
assert!(script.contains("--cdrom"));
assert!(script.contains("--recovery"));
}
#[test]
fn test_build_qemu_command_basic() {
let config = WizardQemuConfig {
emulator: "qemu-system-x86_64".to_string(),
memory_mb: 2048,
cpu_cores: 2,
cpu_model: Some("host".to_string()),
machine: Some("q35".to_string()),
vga: "std".to_string(),
audio: vec![],
network_model: "e1000".to_string(),
disk_interface: "ide".to_string(),
enable_kvm: true,
uefi: false,
tpm: false,
rtc_localtime: false,
usb_tablet: true,
display: "gtk".to_string(),
gl_acceleration: false,
network_backend: "user".to_string(),
port_forwards: vec![],
bridge_name: None,
mac_address: None,
extra_args: vec![],
bios_path: None,
};
let cmd = build_qemu_command_with_os(&config, "disk.qcow2", &InstallMedia::None, None, None);
assert!(cmd.contains("qemu-system-x86_64"));
assert!(cmd.contains("-enable-kvm"));
assert!(cmd.contains("-m 2048M"));
assert!(cmd.contains("-smp 2"));
assert!(cmd.contains("-vga std"));
assert!(cmd.contains("-display gtk"));
assert!(cmd.contains("-device e1000"));
assert!(cmd.contains("-usb"));
assert!(cmd.contains("-device usb-tablet"));
}
#[test]
fn test_build_qemu_command_with_cdrom() {
let config = WizardQemuConfig::default();
let cmd = build_qemu_command_with_os(&config, "disk.qcow2", &InstallMedia::Iso(None), None, None);
assert!(cmd.contains("-drive file=\"$ISO\",media=cdrom"));
assert!(cmd.contains("-boot d"));
}
#[test]
fn test_generate_network_args_user_with_portfwd() {
let forwards = vec![
PortForward { protocol: PortProtocol::Tcp, host_port: 2222, guest_port: 22 },
PortForward { protocol: PortProtocol::Tcp, host_port: 8080, guest_port: 80 },
];
let args = generate_network_args("e1000", "user", None, &forwards, None);
assert_eq!(args.len(), 2);
assert!(args[0].contains("hostfwd=tcp::2222-:22"));
assert!(args[0].contains("hostfwd=tcp::8080-:80"));
assert!(args[1].contains("e1000,netdev=net0"));
}
#[test]
fn test_generate_network_args_passt() {
let args = generate_network_args("virtio", "passt", None, &[], None);
assert_eq!(args.len(), 2);
assert!(args[0].contains("-netdev passt,id=net0"));
assert!(args[1].contains("virtio-net-pci,netdev=net0"));
}
#[test]
fn test_generate_network_args_bridge() {
let args = generate_network_args("e1000", "bridge", Some("virbr0"), &[], None);
assert_eq!(args.len(), 2);
assert!(args[0].contains("-netdev bridge,id=net0,br=virbr0"));
}
#[test]
fn test_generate_network_args_with_mac_bridge() {
let args = generate_network_args(
"e1000",
"bridge",
Some("virbr0"),
&[],
Some("52:54:00:de:ad:be"),
);
assert_eq!(args.len(), 2);
assert!(args[1].contains("mac=52:54:00:de:ad:be"), "device line missing mac=: {}", args[1]);
}
#[test]
fn test_generate_network_args_with_mac_user() {
let args = generate_network_args("virtio", "user", None, &[], Some("aa:bb:cc:dd:ee:ff"));
assert!(args[1].contains("virtio-net-pci,netdev=net0,mac=aa:bb:cc:dd:ee:ff"));
}
#[test]
fn test_generate_network_args_invalid_mac_dropped() {
let args = generate_network_args("e1000", "user", None, &[], Some("not-a-mac"));
assert!(!args.iter().any(|a| a.contains("mac=")));
}
#[test]
fn test_generate_network_args_none() {
let args = generate_network_args("none", "user", None, &[], None);
assert!(args.is_empty());
}
#[test]
fn test_build_qemu_command_with_audio() {
let config = WizardQemuConfig {
audio: vec!["intel-hda".to_string(), "hda-duplex".to_string()],
..Default::default()
};
let cmd = build_qemu_command_with_os(&config, "disk.qcow2", &InstallMedia::None, None, None);
assert!(cmd.contains("-audiodev pa,id=audio0"));
assert!(cmd.contains("-device intel-hda"));
assert!(cmd.contains("-device hda-duplex,audiodev=audio0"));
}
#[test]
fn test_build_qemu_command_with_bios() {
let config = WizardQemuConfig {
emulator: "qemu-system-m68k".to_string(),
memory_mb: 32,
cpu_cores: 1,
cpu_model: Some("m68040".to_string()),
machine: Some("q800".to_string()),
vga: "none".to_string(),
audio: vec![],
network_model: "none".to_string(),
disk_interface: "scsi".to_string(),
enable_kvm: false,
gl_acceleration: false,
uefi: false,
tpm: false,
rtc_localtime: false,
usb_tablet: false,
display: "gtk".to_string(),
network_backend: "user".to_string(),
port_forwards: vec![],
bridge_name: None,
mac_address: None,
extra_args: vec![],
bios_path: Some(PathBuf::from("MacROM.bin")),
};
let cmd = build_qemu_command_with_os(&config, "disk.qcow2", &InstallMedia::None, Some("mac-system7"), None);
assert!(cmd.contains("-bios \"$ROM\""), "Should contain -bios \"$ROM\", got:\n{}", cmd);
assert!(cmd.contains("qemu-system-m68k"), "Should contain m68k emulator");
}
#[test]
fn test_build_qemu_command_without_bios() {
let config = WizardQemuConfig::default();
let cmd = build_qemu_command_with_os(&config, "disk.qcow2", &InstallMedia::None, None, None);
assert!(!cmd.contains("-bios"), "Should NOT contain -bios when no bios_path");
}
#[test]
fn test_generate_launch_script_with_rom() {
let config = WizardQemuConfig {
emulator: "qemu-system-m68k".to_string(),
bios_path: Some(PathBuf::from("MacROM.bin")),
..WizardQemuConfig::default()
};
let script = generate_launch_script_with_os(
"Mac System 7",
"mac-system7.qcow2",
None,
false,
&config,
Some("mac-system7"),
None,
);
assert!(script.contains("ROM=\"$VM_DIR/MacROM.bin\""), "Script should contain ROM variable");
assert!(script.contains("-bios \"$ROM\""), "Script should contain -bios \"$ROM\"");
}
#[test]
fn test_build_qemu_command_with_recovery_image() {
let config = WizardQemuConfig::default();
let cmd = build_qemu_command_with_os(&config, "disk.qcow2", &InstallMedia::RecoveryImage(None), None, None);
assert!(cmd.contains("format=dmg"), "Should contain format=dmg for recovery image");
assert!(cmd.contains("snapshot=on"), "Should use snapshot overlay for writability");
assert!(cmd.contains("if=ide,index=2"), "Should attach via IDE/AHCI at index 2");
assert!(!cmd.contains("-boot d"), "Should NOT contain -boot d for recovery images");
assert!(cmd.contains("\"$RECOVERY_IMG\""), "Should reference $RECOVERY_IMG variable");
}
#[test]
fn test_build_qemu_command_with_recovery_image_custom_path() {
let config = WizardQemuConfig::default();
let cmd = build_qemu_command_with_os(&config, "disk.qcow2", &InstallMedia::RecoveryImage(Some("\"$2\"")), None, None);
assert!(cmd.contains("format=dmg"), "Should contain format=dmg");
assert!(cmd.contains("\"$2\""), "Should use custom path expression");
assert!(!cmd.contains("-boot d"), "Should NOT contain -boot d");
}
#[test]
fn test_generate_launch_script_with_recovery_image() {
let config = WizardQemuConfig::default();
let script = generate_launch_script_with_os(
"macOS Tahoe",
"disk.qcow2",
Some(Path::new("/tmp/BaseSystem.dmg")),
true,
&config,
Some("macos-tahoe"),
None,
);
assert!(script.contains("RECOVERY_IMG="), "Should use RECOVERY_IMG variable");
assert!(!script.contains("ISO="), "Should NOT contain ISO variable when recovery image");
assert!(script.contains("/tmp/BaseSystem.dmg"), "Should contain DMG path");
assert!(script.contains("--recovery"), "Should contain --recovery option");
assert!(script.contains("format=dmg"), "Install mode should use format=dmg");
}
#[test]
fn test_generate_launch_script_iso_unchanged() {
let config = WizardQemuConfig::default();
let script = generate_launch_script_with_os(
"Linux VM",
"disk.qcow2",
Some(Path::new("/tmp/linux.iso")),
false,
&config,
None,
None,
);
assert!(script.contains("ISO="), "Should use ISO variable");
assert!(!script.contains("RECOVERY_IMG="), "Should NOT contain RECOVERY_IMG variable");
assert!(script.contains("--cdrom"), "Should contain --cdrom option");
assert!(script.contains("--recovery"), "Should still contain --recovery option for flexibility");
assert!(script.contains("-boot d"), "Install mode should boot from CD-ROM");
}
fn macos_uefi_config() -> WizardQemuConfig {
WizardQemuConfig {
emulator: "qemu-system-x86_64".to_string(),
memory_mb: 8192,
cpu_cores: 4,
cpu_model: Some("Penryn,kvm=on,vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check".to_string()),
machine: Some("q35".to_string()),
vga: "none".to_string(),
audio: vec!["intel-hda".to_string(), "hda-duplex".to_string()],
network_model: "vmxnet3".to_string(),
disk_interface: "ide".to_string(),
enable_kvm: true,
gl_acceleration: false,
uefi: true,
tpm: false,
rtc_localtime: false,
usb_tablet: true,
display: "spice-app".to_string(),
network_backend: "passt".to_string(),
port_forwards: vec![],
bridge_name: None,
mac_address: None,
extra_args: vec!["-device vmware-svga,vgamem_mb=256".to_string()],
bios_path: Some(PathBuf::from("OpenCore.qcow2")),
}
}
fn macos_non_uefi_config() -> WizardQemuConfig {
WizardQemuConfig {
emulator: "qemu-system-x86_64".to_string(),
memory_mb: 2048,
cpu_cores: 2,
cpu_model: Some("Penryn,kvm=on,vendor=GenuineIntel".to_string()),
machine: Some("q35".to_string()),
vga: "none".to_string(),
audio: vec!["intel-hda".to_string(), "hda-duplex".to_string()],
network_model: "vmxnet3".to_string(),
disk_interface: "ide".to_string(),
enable_kvm: true,
gl_acceleration: false,
uefi: false,
tpm: false,
rtc_localtime: false,
usb_tablet: true,
display: "spice-app".to_string(),
network_backend: "passt".to_string(),
port_forwards: vec![],
bridge_name: None,
mac_address: None,
extra_args: vec!["-device vmware-svga,vgamem_mb=256".to_string()],
bios_path: None,
}
}
#[test]
fn test_macos_includes_smc_and_smbios() {
let config = macos_non_uefi_config();
let cmd = build_qemu_command_with_os(&config, "disk.qcow2", &InstallMedia::None, Some("mac-osx-leopard"), None);
assert!(cmd.contains("isa-applesmc,osk="), "Should contain Apple SMC device with quoted value");
assert!(cmd.contains("-smbios type=2"), "Should contain SMBIOS type=2");
}
#[test]
fn test_macos_uefi_uses_ahci() {
let config = macos_uefi_config();
let cmd = build_qemu_command_with_os(&config, "disk.qcow2", &InstallMedia::None, Some("macos-sonoma"), None);
assert!(cmd.contains("ich9-ahci,id=sata"), "Should have explicit AHCI controller");
assert!(cmd.contains("bus=sata."), "Should use sata bus addressing");
assert!(!cmd.contains("if=ide,index=0"), "Should NOT use legacy if=ide,index=0 for macOS UEFI");
}
#[test]
fn test_macos_uefi_with_opencore() {
let config = macos_uefi_config();
let cmd = build_qemu_command_with_os(&config, "disk.qcow2", &InstallMedia::None, Some("macos-sonoma"), None);
assert!(cmd.contains("file=\"$ROM\",format=qcow2,if=none,id=oc"), "Should have OpenCore drive");
assert!(cmd.contains("drive=oc,bus=sata.0"), "OpenCore should be on sata.0");
assert!(cmd.contains("drive=maindisk,bus=sata.1"), "Main disk should be on sata.1");
assert!(!cmd.contains("-bios \"$ROM\""), "Should NOT use -bios for macOS UEFI with OpenCore");
}
#[test]
fn test_macos_recovery_image_qcow2_on_ahci() {
let config = macos_uefi_config();
let cmd = build_qemu_command_with_os(
&config, "disk.qcow2", &InstallMedia::RecoveryImage(None), Some("macos-sonoma"), None
);
assert!(cmd.contains("if=none,id=recovery"), "Recovery should be on AHCI bus");
assert!(!cmd.contains("format=qcow2,if=none,id=recovery"), "Recovery should NOT hardcode format (auto-detect)");
assert!(cmd.contains("bus=sata.2"), "Recovery should be on sata.2 (after OpenCore on sata.0 and disk on sata.1)");
assert!(!cmd.contains("-boot d"), "Should NOT boot from recovery directly (OpenCore handles it)");
}
#[test]
fn test_macos_spice_audio() {
let config = macos_uefi_config();
let cmd = build_qemu_command_with_os(&config, "disk.qcow2", &InstallMedia::None, Some("macos-sonoma"), None);
assert!(cmd.contains("-audiodev spice,id=audio0"), "Should use spice audio backend with spice-app display");
assert!(!cmd.contains("-audiodev pa,id=audio0"), "Should NOT use pa audio with spice-app display");
}
#[test]
fn test_macos_usb_kbd() {
let config = macos_uefi_config();
let cmd = build_qemu_command_with_os(&config, "disk.qcow2", &InstallMedia::None, Some("macos-sonoma"), None);
assert!(cmd.contains("-device usb-kbd"), "Should include USB keyboard for macOS");
assert!(cmd.contains("-device usb-tablet"), "Should also include USB tablet");
}
#[test]
fn test_ppc_macos_no_smc() {
let config = WizardQemuConfig {
emulator: "qemu-system-ppc".to_string(),
machine: Some("mac99".to_string()),
vga: "std".to_string(),
audio: vec!["screamer".to_string()],
network_model: "sungem".to_string(),
disk_interface: "ide".to_string(),
enable_kvm: false,
uefi: false,
usb_tablet: false,
display: "gtk".to_string(),
network_backend: "user".to_string(),
..Default::default()
};
let cmd = build_qemu_command_with_os(&config, "disk.qcow2", &InstallMedia::None, Some("mac-osx-tiger"), None);
assert!(!cmd.contains("applesmc"), "PPC macOS should NOT have Apple SMC");
assert!(!cmd.contains("-smbios type=2"), "PPC macOS should NOT have SMBIOS type=2");
assert!(!cmd.contains("usb-kbd"), "PPC macOS should NOT have USB keyboard");
}
#[test]
fn test_non_macos_unchanged() {
let config = WizardQemuConfig::default();
let cmd = build_qemu_command_with_os(&config, "disk.qcow2", &InstallMedia::None, Some("ubuntu-24-04"), None);
assert!(!cmd.contains("applesmc"), "Linux VM should NOT have Apple SMC");
assert!(!cmd.contains("-smbios type=2"), "Linux VM should NOT have SMBIOS type=2");
assert!(!cmd.contains("usb-kbd"), "Linux VM should NOT have USB keyboard");
assert!(!cmd.contains("ich9-ahci"), "Linux VM should NOT have explicit AHCI controller");
assert!(cmd.contains("-audiodev pa,id=audio0"), "Linux VM should use pa audio backend");
assert!(!cmd.contains("-audiodev spice"), "Linux VM should NOT use spice audio backend");
}
#[test]
fn test_macos_uefi_iso_no_boot_d() {
let config = macos_uefi_config();
let cmd = build_qemu_command_with_os(
&config, "disk.qcow2", &InstallMedia::Iso(None), Some("macos-sonoma"), None
);
assert!(cmd.contains("bus=sata.3"), "ISO should be on sata.3 (after OpenCore.0, disk.1, skipping .2 for recovery)");
assert!(!cmd.contains("-boot d"), "macOS UEFI should NOT use -boot d (OpenCore handles boot)");
}
#[test]
fn test_macos_opencore_bootloader_check_in_script() {
let config = macos_uefi_config();
let script = generate_launch_script_with_os(
"macOS Sonoma",
"disk.qcow2",
None,
false,
&config,
Some("macos-sonoma"),
None,
);
assert!(script.contains("Verify OpenCore bootloader exists"), "Script should verify OpenCore exists");
assert!(script.contains("kholia/OSX-KVM"), "Script should mention OSX-KVM download source");
}
#[test]
fn test_macos_non_uefi_uses_bios() {
let mut config = macos_non_uefi_config();
config.bios_path = Some(PathBuf::from("some-rom.bin"));
let cmd = build_qemu_command_with_os(&config, "disk.qcow2", &InstallMedia::None, Some("mac-osx-leopard"), None);
assert!(cmd.contains("-bios \"$ROM\""), "Non-UEFI macOS with bios_path should use -bios");
assert!(!cmd.contains("ich9-ahci"), "Non-UEFI macOS should NOT use explicit AHCI controller");
}
struct TestVmDir(PathBuf);
impl TestVmDir {
fn new(name: &str) -> Self {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
let mut dir = std::env::temp_dir();
dir.push(format!("vm-curator-test-{}-{}-{}", name, std::process::id(), seq));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
TestVmDir(dir)
}
fn path(&self) -> &Path { &self.0 }
}
impl Drop for TestVmDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn fixture_launch_sh_five_branch_user_net() -> String {
let qemu_block = " qemu-system-x86_64 \\\n \
-enable-kvm \\\n \
-m 2048M \\\n \
-drive file=\"$DISK\",format=qcow2,if=virtio,index=0,media=disk \\\n \
-vga virtio \\\n \
-display sdl \\\n \
-audiodev pa,id=audio0 \\\n \
-device intel-hda \\\n \
-netdev user,id=net0 \\\n \
-device virtio-net-pci,netdev=net0 \\\n \
-usb \\\n \
-device usb-tablet";
format!(
"#!/bin/bash\n\
DISK=\"disk.qcow2\"\n\
case \"$1\" in\n \
--install)\n{qemu}\n ;;\n \
--cdrom)\n{qemu}\n ;;\n \
--recovery)\n{qemu}\n ;;\n \
--floppy)\n{qemu}\n ;;\n \
\"\")\n{qemu}\n ;;\n\
esac\n",
qemu = qemu_block,
)
}
#[test]
fn test_update_network_in_script_inserts_into_all_branches() {
let vm = TestVmDir::new("issue38-bridge");
std::fs::write(vm.path().join("launch.sh"), fixture_launch_sh_five_branch_user_net()).unwrap();
update_network_in_script(
vm.path(),
"virtio",
"bridge",
Some("nm-bridge"),
&[],
Some("52:54:00:12:34:56"),
)
.unwrap();
let updated = std::fs::read_to_string(vm.path().join("launch.sh")).unwrap();
let netdev_count = updated.matches("-netdev bridge,id=net0,br=nm-bridge").count();
assert_eq!(netdev_count, 5, "expected -netdev in all 5 case branches, got {netdev_count}\n---\n{updated}");
let device_count = updated
.matches("-device virtio-net-pci,netdev=net0,mac=52:54:00:12:34:56")
.count();
assert_eq!(device_count, 5, "expected -device with MAC in all 5 case branches, got {device_count}\n---\n{updated}");
assert!(!updated.contains("-netdev user,id=net0"), "old user-mode -netdev not stripped:\n{updated}");
assert_eq!(updated.matches("-usb").count(), 5, "trailing -usb arg must survive in all 5 branches:\n{updated}");
assert_eq!(updated.matches("-device usb-tablet").count(), 5, "trailing -device usb-tablet must survive in all 5 branches:\n{updated}");
assert_eq!(updated.matches("-device intel-hda").count(), 5, "preceding -device intel-hda must survive in all 5 branches:\n{updated}");
}
#[test]
fn test_update_network_in_script_strips_when_model_none() {
let vm = TestVmDir::new("issue38-none");
std::fs::write(vm.path().join("launch.sh"), fixture_launch_sh_five_branch_user_net()).unwrap();
update_network_in_script(vm.path(), "none", "user", None, &[], None).unwrap();
let updated = std::fs::read_to_string(vm.path().join("launch.sh")).unwrap();
assert_eq!(updated.matches("-netdev").count(), 0, "no -netdev lines should remain:\n{updated}");
assert_eq!(updated.matches("virtio-net-pci").count(), 0, "no -device virtio-net-pci lines should remain:\n{updated}");
let lines: Vec<&str> = updated.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.trim() == ";;" && i > 0 {
let prev = lines[i - 1].trim_end();
assert!(
!prev.ends_with('\\'),
"branch terminator `;;` preceded by backslash-continuation at line {i}: {prev:?}\n---\n{updated}",
);
}
}
}
#[test]
fn test_update_network_in_script_originally_no_network_falls_back() {
let vm = TestVmDir::new("issue38-fallback");
let stripped = fixture_launch_sh_five_branch_user_net()
.lines()
.filter(|l| !l.contains("-netdev") && !l.contains("virtio-net-pci"))
.collect::<Vec<_>>()
.join("\n");
std::fs::write(vm.path().join("launch.sh"), stripped).unwrap();
update_network_in_script(
vm.path(),
"virtio",
"bridge",
Some("qemubr0"),
&[],
None,
)
.unwrap();
let updated = std::fs::read_to_string(vm.path().join("launch.sh")).unwrap();
assert!(updated.contains("-netdev bridge,id=net0,br=qemubr0"), "fallback should still inject -netdev:\n{updated}");
assert!(updated.contains("-device virtio-net-pci,netdev=net0"), "fallback should still inject -device:\n{updated}");
}