diff --git a/core/tests/qemu_differential.rs b/core/tests/qemu_differential.rs index 396c512..ae39110 100644 --- a/core/tests/qemu_differential.rs +++ b/core/tests/qemu_differential.rs @@ -7,8 +7,47 @@ use std::io::{Read, Seek, SeekFrom}; use std::path::Path; +use std::process::Command; use vhd::VhdReader; +/// Resolve a usable `qemu-img`, or `None` (the differential then skips). +/// +/// `PATH` is probed first, so any install location works — including ones a +/// fixed list cannot anticipate: another package manager's prefix, or a +/// hand-built install. The absolute +/// candidates are the fallback for a stripped `PATH`: Homebrew on Apple silicon +/// and Intel, then `/usr/bin`, where the Linux CI runner's `qemu-utils` package +/// lands it. `QEMU_IMG_BIN` overrides both. +/// +/// The hardcoded Homebrew path this replaces resolved only on a macOS-arm64 dev +/// machine, so every qemu differential here skipped silently on the Linux CI +/// runner despite CI installing `qemu-utils` for exactly these tests. +fn qemu_img_bin() -> Option { + if let Ok(explicit) = std::env::var("QEMU_IMG_BIN") { + return usable(&explicit); + } + [ + "qemu-img", + "/opt/homebrew/bin/qemu-img", + "/usr/local/bin/qemu-img", + "/usr/bin/qemu-img", + ] + .into_iter() + .find_map(usable) +} + +/// A candidate counts only if it actually executes: a successful `--version` +/// proves both that the name resolved and that the binary runs on this host, +/// which a bare `Path::exists()` check does not. +fn usable(candidate: &str) -> Option { + Command::new(candidate) + .arg("--version") + .output() + .ok() + .filter(|o| o.status.success()) + .map(|_| candidate.to_string()) +} + // VHD uses CHS geometry, so the virtual size gets rounded from the source. // Strategy: raw → VHD → raw_reference (both via qemu-img), then compare // our reader output against raw_reference. qemu-img is authoritative for @@ -16,10 +55,9 @@ use vhd::VhdReader; #[test] fn reads_match_qemu_raw_convert() { - const QEMU_IMG: &str = "/opt/homebrew/bin/qemu-img"; - if !Path::new(QEMU_IMG).exists() { + let Some(qemu_img) = qemu_img_bin() else { return; - } + }; let tmp = tempfile::tempdir().expect("tempdir"); // 1 MiB source with a deterministic non-trivial pattern. @@ -30,7 +68,7 @@ fn reads_match_qemu_raw_convert() { // raw → VHD (dynamic, qemu default VPC format). let vhd_path = tmp.path().join("test.vhd"); - let ok = std::process::Command::new(QEMU_IMG) + let ok = Command::new(&qemu_img) .args([ "convert", "-O", @@ -45,7 +83,7 @@ fn reads_match_qemu_raw_convert() { // VHD → reference raw (qemu-img resolves CHS rounding authoritatively). let ref_path = tmp.path().join("reference.raw"); - let ok = std::process::Command::new(QEMU_IMG) + let ok = Command::new(&qemu_img) .args([ "convert", "-O", @@ -96,13 +134,15 @@ fn reads_match_qemu_raw_convert() { // ── Corpus differential tests: real qemu-img generated VHDs ────────────── fn corpus_vhd_matches_raw(corpus: &Path) { - const QEMU_IMG: &str = "/opt/homebrew/bin/qemu-img"; - if !Path::new(QEMU_IMG).exists() || !corpus.exists() { + let Some(qemu_img) = qemu_img_bin() else { + return; + }; + if !corpus.exists() { return; } let tmp = tempfile::tempdir().expect("tempdir"); let ref_path = tmp.path().join("reference.raw"); - let ok = std::process::Command::new(QEMU_IMG) + let ok = Command::new(&qemu_img) .args([ "convert", "-O",