aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat
-rw-r--r--.github/workflows/build.yml64+64 −0
-rw-r--r--crates/hule-vmm/tests/qemu_lifecycle.rs136+0 −136
2 files changed, 64 insertions, 136 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,64 @@
+# SPDX-FileCopyrightText: 2026 Nikolay Govorov
+# SPDX-License-Identifier: Apache-2.0
+
+name: Build
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+
+env:
+ CARGO_TERM_COLOR: always
+
+jobs:
+ lint:
+ name: Static checks
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+
+ - uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
+ with:
+ components: clippy, rustfmt
+ - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
+
+ - name: Check formatting
+ run: cargo fmt --all --check
+
+ - name: Run Clippy
+ run: cargo clippy --workspace --all-targets --all-features --release --locked -- -D warnings
+
+ build:
+ name: Build (${{ matrix.arch }})
+ runs-on: ${{ matrix.runner }}
+ needs: [lint]
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - runner: ubuntu-24.04
+ arch: amd64
+ - runner: ubuntu-24.04-arm
+ arch: arm64
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+
+ - uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
+ - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
+
+ - name: Build
+ run: cargo build --workspace --all-features --release --locked
+
+ - name: Run tests
+ run: cargo test --workspace --all-features --release --locked
+
+ - name: Upload binary
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ with:
+ name: hule-${{ matrix.arch }}
+ path: target/release/hule
diff --git a/crates/hule-vmm/tests/qemu_lifecycle.rs b/crates/hule-vmm/tests/qemu_lifecycle.rs
deleted file mode 100644
--- a/crates/hule-vmm/tests/qemu_lifecycle.rs
+++ /dev/null
@@ -1,136 +0,0 @@
-// 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");
-}