From c08cd7cf2992110e49bf5bb88a0707f3cd0e2c42 Mon Sep 17 00:00:00 2001 From: Nikolay Govorov Date: Mon, 13 Jul 2026 00:26:52 +0100 Subject: Build and test CI workflow --- .github/workflows/build.yml | 64 +++++++++++ crates/hule-vmm/tests/qemu_lifecycle.rs | 136 ------------------------ 2 files changed, 64 insertions(+), 136 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 crates/hule-vmm/tests/qemu_lifecycle.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..17a282e --- /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 index d30ac61..0000000 --- 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"); -} -- Gilti