aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'crates/hule-vmm/tests/qemu_lifecycle.rs')
-rw-r--r--crates/hule-vmm/tests/qemu_lifecycle.rs136+136 −0
1 files changed, 136 insertions, 0 deletions
diff --git a/crates/hule-vmm/tests/qemu_lifecycle.rs b/crates/hule-vmm/tests/qemu_lifecycle.rs
new file mode 100644
--- /dev/null
+++ b/crates/hule-vmm/tests/qemu_lifecycle.rs
@@ -0,0 +1,136 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov
+// SPDX-License-Identifier: Apache-2.0
+
+//! Boots a real qemu VM against the repo's alpine fixture and drives it
+//! through lifecycle, QGA exec, and serial-console methods. Needs qemu-system-x86_64 and
+//! `images/alpine/3.24/amd64` on disk; `#[ignore]`d so a normal `cargo
+//! test` run doesn't depend on either.
+
+use std::path::Path;
+use std::time::Duration;
+
+use hule_image::MachineImage;
+use hule_vmm::backend::qemu::QemuHypervisor;
+use hule_vmm::{Hypervisor, MachineId, Settings, State};
+use uuid::Uuid;
+
+#[tokio::test]
+#[ignore = "needs qemu-system-x86_64 and the alpine fixture on disk"]
+async fn pause_unpause_restart_and_kill_track_a_real_qemu() {
+ let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../images/alpine/3.24/amd64");
+ let config = std::fs::read_to_string(fixture.join("config.json"))
+ .expect("read fixture config.json")
+ .replace(
+ r#"{ "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" }"#,
+ r#"{ "type": "qga" }"#,
+ );
+ let image = MachineImage::from_json(config.as_bytes()).expect("valid fixture config.json");
+
+ let scratch = std::env::temp_dir().join(format!("hule-vmm-test-{}", std::process::id()));
+ std::fs::create_dir_all(&scratch).unwrap();
+ for disk in &image.disks {
+ let src = fixture.join(&disk.path);
+ let dest = scratch.join(&disk.path);
+ std::fs::hard_link(&src, &dest)
+ .or_else(|_| std::fs::copy(&src, &dest).map(|_| ()))
+ .expect("stage disk into scratch dir");
+ }
+
+ let hv = QemuHypervisor;
+ let boot = image
+ .machine
+ .boot
+ .iter()
+ .find(|b| hv.supports(b))
+ .expect("fixture declares a boot protocol qemu supports");
+ let settings = Settings {
+ cpu: 1,
+ ram: 512,
+ port_forwards: vec![],
+ };
+
+ let mut machine = hv
+ .create(
+ MachineId(Uuid::now_v7()),
+ None,
+ &image,
+ &scratch,
+ boot,
+ &settings,
+ )
+ .await
+ .expect("create");
+
+ machine.start().await.expect("start");
+
+ let output = machine
+ .exec(&["/bin/echo".into(), "hello from qga".into()])
+ .await
+ .expect("qga exec");
+ assert!(output.status.success());
+ assert_eq!(output.stdout, b"hello from qga\n");
+ assert!(!machine.logs().await.expect("serial logs").is_empty());
+ drop(machine.attach().await.expect("attach serial console"));
+
+ assert_eq!(machine.state().await, State::Running);
+
+ machine.pause().await.expect("pause");
+ assert_eq!(machine.state().await, State::Paused);
+
+ machine.unpause().await.expect("unpause");
+ assert_eq!(machine.state().await, State::Running);
+
+ machine.restart().await.expect("restart");
+ tokio::time::sleep(Duration::from_millis(500)).await;
+ assert_eq!(machine.state().await, State::Running);
+
+ let stats = machine.stats().await.expect("stats");
+ assert!(stats.cpu_time_ns > 0);
+ assert!(stats.memory_bytes > 0);
+
+ let mut new_settings = machine.settings();
+ new_settings.ram = 384;
+ machine
+ .update(new_settings)
+ .await
+ .expect("update (balloon)");
+ assert_eq!(machine.settings().ram, 384);
+
+ machine.kill().await.expect("kill");
+ assert!(matches!(machine.state().await, State::Exited(_)));
+
+ let _ = std::fs::remove_dir_all(&scratch);
+}
+
+/// QMP is internal to the qemu backend, so a black-box image is valid.
+#[tokio::test]
+async fn create_accepts_an_image_without_access() {
+ let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../images/alpine/3.24/amd64");
+ let config = std::fs::read(fixture.join("config.json")).expect("read fixture config.json");
+ let mut image = MachineImage::from_json(&config).expect("valid fixture config.json");
+ image.machine.access.clear();
+
+ let hv = QemuHypervisor;
+ let boot = image
+ .machine
+ .boot
+ .iter()
+ .find(|b| hv.supports(b))
+ .expect("fixture declares a boot protocol qemu supports");
+ let settings = Settings {
+ cpu: 1,
+ ram: 512,
+ port_forwards: vec![],
+ };
+
+ hv.create(
+ MachineId(Uuid::now_v7()),
+ None,
+ &image,
+ &fixture,
+ boot,
+ &settings,
+ )
+ .await
+ .expect("black-box images don't need to declare qemu's QMP channel");
+}