use super::Config;
use std::collections::HashMap;
const PREFIX: &str = "CARGO_IMAGE_RUNNER_";
const VAR_PREFIX: &str = "CARGO_IMAGE_RUNNER_VAR_";
pub fn get_profile_name() -> Option<String> {
std::env::var("CARGO_IMAGE_RUNNER_PROFILE").ok().filter(|s| !s.is_empty())
}
pub fn collect_env_variables() -> HashMap<String, String> {
let mut vars = HashMap::new();
for (key, value) in std::env::vars() {
if let Some(name) = key.strip_prefix(VAR_PREFIX) {
if !name.is_empty() {
vars.insert(name.to_string(), value);
}
}
}
vars
}
pub fn get_extra_qemu_args() -> Vec<String> {
match std::env::var(format!("{PREFIX}QEMU_ARGS")) {
Ok(val) if !val.is_empty() => val.split_whitespace().map(String::from).collect(),
_ => Vec::new(),
}
}
pub fn apply_env_overrides(config: &mut Config) {
if let Some(val) = env_str("QEMU_BINARY") {
config.runner.qemu.binary = val;
}
if let Some(val) = env_parse::<u32>("QEMU_MEMORY") {
config.runner.qemu.memory = val;
}
if let Some(val) = env_parse::<u32>("QEMU_CORES") {
config.runner.qemu.cores = val;
}
if let Some(val) = env_str("QEMU_MACHINE") {
config.runner.qemu.machine = val;
}
if let Some(val) = env_str("BOOT_TYPE") {
match val.to_lowercase().as_str() {
"bios" => config.boot.boot_type = super::BootType::Bios,
"uefi" => config.boot.boot_type = super::BootType::Uefi,
"hybrid" => config.boot.boot_type = super::BootType::Hybrid,
_ => {} }
}
if let Some(val) = env_bool("VERBOSE") {
config.verbose = val;
}
if let Some(val) = env_bool("KVM") {
config.runner.qemu.kvm = val;
}
if let Some(val) = env_str("SERIAL_MODE") {
match val.to_lowercase().as_str() {
"mon:stdio" => config.runner.qemu.serial.mode = super::SerialMode::MonStdio,
"stdio" => config.runner.qemu.serial.mode = super::SerialMode::Stdio,
"none" => config.runner.qemu.serial.mode = super::SerialMode::None,
_ => {} }
}
}
pub fn detect_active_overrides() -> Vec<(String, String)> {
let keys = [
"QEMU_BINARY",
"QEMU_MEMORY",
"QEMU_CORES",
"QEMU_MACHINE",
"BOOT_TYPE",
"VERBOSE",
"KVM",
"QEMU_ARGS",
"SERIAL_MODE",
];
let mut active = Vec::new();
for key in keys {
let full = format!("{PREFIX}{key}");
if let Ok(val) = std::env::var(&full) {
if !val.is_empty() {
active.push((full, val));
}
}
}
active
}
fn env_str(suffix: &str) -> Option<String> {
std::env::var(format!("{PREFIX}{suffix}"))
.ok()
.filter(|s| !s.is_empty())
}
fn env_parse<T: std::str::FromStr>(suffix: &str) -> Option<T> {
env_str(suffix).and_then(|s| s.parse().ok())
}
fn env_bool(suffix: &str) -> Option<bool> {
env_str(suffix).map(|s| matches!(s.to_lowercase().as_str(), "1" | "true" | "yes"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn with_env_vars<F: FnOnce()>(vars: &[(&str, &str)], f: F) {
let _guard = ENV_LOCK.lock().unwrap();
let mut old: Vec<(&str, Option<String>)> = Vec::new();
for &(k, v) in vars {
old.push((k, std::env::var(k).ok()));
unsafe { std::env::set_var(k, v) };
}
f();
for (k, prev) in old {
match prev {
Some(v) => unsafe { std::env::set_var(k, v) },
None => unsafe { std::env::remove_var(k) },
}
}
}
fn without_env_vars<F: FnOnce()>(vars: &[&str], f: F) {
let _guard = ENV_LOCK.lock().unwrap();
let mut old: Vec<(&str, Option<String>)> = Vec::new();
for &k in vars {
old.push((k, std::env::var(k).ok()));
unsafe { std::env::remove_var(k) };
}
f();
for (k, prev) in old {
match prev {
Some(v) => unsafe { std::env::set_var(k, v) },
None => unsafe { std::env::remove_var(k) },
}
}
}
#[test]
fn test_get_profile_name_set() {
with_env_vars(&[("CARGO_IMAGE_RUNNER_PROFILE", "debug")], || {
assert_eq!(get_profile_name(), Some("debug".to_string()));
});
}
#[test]
fn test_get_profile_name_unset() {
without_env_vars(&["CARGO_IMAGE_RUNNER_PROFILE"], || {
assert_eq!(get_profile_name(), None);
});
}
#[test]
fn test_get_profile_name_empty() {
with_env_vars(&[("CARGO_IMAGE_RUNNER_PROFILE", "")], || {
assert_eq!(get_profile_name(), None);
});
}
#[test]
fn test_collect_env_variables_multiple() {
with_env_vars(
&[
("CARGO_IMAGE_RUNNER_VAR_TIMEOUT", "10"),
("CARGO_IMAGE_RUNNER_VAR_DEBUG", "1"),
],
|| {
let vars = collect_env_variables();
assert_eq!(vars.get("TIMEOUT").unwrap(), "10");
assert_eq!(vars.get("DEBUG").unwrap(), "1");
},
);
}
#[test]
fn test_collect_env_variables_none() {
without_env_vars(
&[
"CARGO_IMAGE_RUNNER_VAR_TIMEOUT",
"CARGO_IMAGE_RUNNER_VAR_DEBUG",
],
|| {
let vars = collect_env_variables();
assert!(!vars.contains_key("TIMEOUT"));
assert!(!vars.contains_key("DEBUG"));
},
);
}
#[test]
fn test_get_extra_qemu_args_set() {
with_env_vars(
&[("CARGO_IMAGE_RUNNER_QEMU_ARGS", "-s -S -device virtio-net")],
|| {
let args = get_extra_qemu_args();
assert_eq!(args, vec!["-s", "-S", "-device", "virtio-net"]);
},
);
}
#[test]
fn test_get_extra_qemu_args_empty() {
with_env_vars(&[("CARGO_IMAGE_RUNNER_QEMU_ARGS", "")], || {
assert!(get_extra_qemu_args().is_empty());
});
}
#[test]
fn test_get_extra_qemu_args_unset() {
without_env_vars(&["CARGO_IMAGE_RUNNER_QEMU_ARGS"], || {
assert!(get_extra_qemu_args().is_empty());
});
}
#[test]
fn test_apply_env_overrides_qemu_fields() {
with_env_vars(
&[
("CARGO_IMAGE_RUNNER_QEMU_BINARY", "my-qemu"),
("CARGO_IMAGE_RUNNER_QEMU_MEMORY", "4096"),
("CARGO_IMAGE_RUNNER_QEMU_CORES", "4"),
("CARGO_IMAGE_RUNNER_QEMU_MACHINE", "virt"),
],
|| {
let mut config = Config::default();
apply_env_overrides(&mut config);
assert_eq!(config.runner.qemu.binary, "my-qemu");
assert_eq!(config.runner.qemu.memory, 4096);
assert_eq!(config.runner.qemu.cores, 4);
assert_eq!(config.runner.qemu.machine, "virt");
},
);
}
#[test]
fn test_apply_env_overrides_boot_type() {
with_env_vars(&[("CARGO_IMAGE_RUNNER_BOOT_TYPE", "bios")], || {
let mut config = Config::default();
apply_env_overrides(&mut config);
assert_eq!(config.boot.boot_type, super::super::BootType::Bios);
});
}
#[test]
fn test_apply_env_overrides_verbose_and_kvm() {
with_env_vars(
&[
("CARGO_IMAGE_RUNNER_VERBOSE", "yes"),
("CARGO_IMAGE_RUNNER_KVM", "0"),
],
|| {
let mut config = Config::default();
apply_env_overrides(&mut config);
assert!(config.verbose);
assert!(!config.runner.qemu.kvm);
},
);
}
#[test]
fn test_apply_env_overrides_invalid_memory_ignored() {
with_env_vars(&[("CARGO_IMAGE_RUNNER_QEMU_MEMORY", "notanumber")], || {
let mut config = Config::default();
let original_memory = config.runner.qemu.memory;
apply_env_overrides(&mut config);
assert_eq!(config.runner.qemu.memory, original_memory);
});
}
#[test]
fn test_apply_env_overrides_invalid_boot_type_ignored() {
with_env_vars(&[("CARGO_IMAGE_RUNNER_BOOT_TYPE", "invalid")], || {
let mut config = Config::default();
let original = config.boot.boot_type;
apply_env_overrides(&mut config);
assert_eq!(config.boot.boot_type, original);
});
}
#[test]
fn test_apply_env_overrides_serial_mode_stdio() {
with_env_vars(&[("CARGO_IMAGE_RUNNER_SERIAL_MODE", "stdio")], || {
let mut config = Config::default();
apply_env_overrides(&mut config);
assert_eq!(config.runner.qemu.serial.mode, super::super::SerialMode::Stdio);
});
}
#[test]
fn test_apply_env_overrides_serial_mode_none() {
with_env_vars(&[("CARGO_IMAGE_RUNNER_SERIAL_MODE", "none")], || {
let mut config = Config::default();
apply_env_overrides(&mut config);
assert_eq!(config.runner.qemu.serial.mode, super::super::SerialMode::None);
});
}
#[test]
fn test_apply_env_overrides_serial_mode_mon_stdio() {
with_env_vars(&[("CARGO_IMAGE_RUNNER_SERIAL_MODE", "mon:stdio")], || {
let mut config = Config::default();
apply_env_overrides(&mut config);
assert_eq!(config.runner.qemu.serial.mode, super::super::SerialMode::MonStdio);
});
}
#[test]
fn test_apply_env_overrides_serial_mode_invalid_ignored() {
with_env_vars(&[("CARGO_IMAGE_RUNNER_SERIAL_MODE", "invalid")], || {
let mut config = Config::default();
apply_env_overrides(&mut config);
assert_eq!(config.runner.qemu.serial.mode, super::super::SerialMode::MonStdio);
});
}
#[test]
fn test_detect_active_overrides_includes_serial_mode() {
with_env_vars(&[("CARGO_IMAGE_RUNNER_SERIAL_MODE", "stdio")], || {
let overrides = detect_active_overrides();
assert!(overrides.iter().any(|(k, _)| k == "CARGO_IMAGE_RUNNER_SERIAL_MODE"));
});
}
}