diff --git a/crates/core/src/power.rs b/crates/core/src/power.rs index d2fbdba..e032d14 100644 --- a/crates/core/src/power.rs +++ b/crates/core/src/power.rs @@ -64,6 +64,75 @@ fn read_trimmed(path: &Path) -> Option { fs::read_to_string(path).ok().map(|s| s.trim().to_owned()) } +use std::sync::Mutex; + +/// Top-level power-state probe. +/// +/// Resolution order (highest to lowest priority): +/// 1. In-process test override (set via `with_override` from unit tests). +/// 2. `MAGNOTIA_POWER_STATE_OVERRIDE` env var (`ac` | `battery` | `unknown`, +/// case-insensitive). Used by `thread_sweep.rs` integration tests. +/// 3. Linux: `parse_power_state_from_dir("/sys/class/power_supply")`. +/// 4. macOS / Windows / other: `Unknown`. +/// +/// `Unknown` is treated as `OnAc` by callers, which preserves today's +/// pre-clamp behaviour on platforms where the probe doesn't fire. +/// +/// (TTL cache is added in Task 1.4.) +pub fn probe_power_state() -> PowerState { + if let Some(state) = test_get_override() { + return state; + } + if let Some(state) = env_override() { + return state; + } + platform_probe() +} + +fn env_override() -> Option { + let raw = std::env::var("MAGNOTIA_POWER_STATE_OVERRIDE").ok()?; + match raw.trim().to_ascii_lowercase().as_str() { + "ac" => Some(PowerState::OnAc), + "battery" => Some(PowerState::OnBattery), + "unknown" => Some(PowerState::Unknown), + _ => None, + } +} + +#[cfg(target_os = "linux")] +fn platform_probe() -> PowerState { + parse_power_state_from_dir(Path::new("/sys/class/power_supply")) +} + +#[cfg(not(target_os = "linux"))] +fn platform_probe() -> PowerState { + PowerState::Unknown +} + +// In-process override slot. Tests must use `with_override` (below) to +// set it; production code never writes to this. Read on every probe. +static TEST_OVERRIDE: Mutex> = Mutex::new(None); + +fn test_get_override() -> Option { + *TEST_OVERRIDE.lock().expect("power test override mutex poisoned") +} + +/// Run `body` with the in-process override set to `state`. Restores +/// the previous override (always `None` in practice) on return. +/// +/// Holds a dedicated `TEST_LOCK` mutex for the duration of the body, +/// so override-using unit tests run serially with respect to each +/// other even when cargo runs the test binary multi-threaded. +#[cfg(test)] +pub(crate) fn with_override(state: Option, body: impl FnOnce() -> R) -> R { + static TEST_LOCK: Mutex<()> = Mutex::new(()); + let _lock = TEST_LOCK.lock().expect("power TEST_LOCK poisoned"); + *TEST_OVERRIDE.lock().expect("power test override mutex poisoned") = state; + let result = body(); + *TEST_OVERRIDE.lock().expect("power test override mutex poisoned") = None; + result +} + #[cfg(test)] mod tests { use super::*; @@ -128,4 +197,48 @@ mod tests { // No type, no online — should not panic. assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::Unknown); } + + #[test] + fn override_drives_battery() { + with_override(Some(PowerState::OnBattery), || { + assert_eq!(probe_power_state(), PowerState::OnBattery); + }); + } + + #[test] + fn override_drives_ac() { + with_override(Some(PowerState::OnAc), || { + assert_eq!(probe_power_state(), PowerState::OnAc); + }); + } + + #[test] + fn override_drives_unknown() { + with_override(Some(PowerState::Unknown), || { + assert_eq!(probe_power_state(), PowerState::Unknown); + }); + } + + #[test] + fn env_var_override_battery_via_set_var() { + // env-var path tested in isolation under TEST_LOCK so it + // doesn't race with the in-process override tests. + with_override(None, || { + std::env::set_var("MAGNOTIA_POWER_STATE_OVERRIDE", "battery"); + assert_eq!(probe_power_state(), PowerState::OnBattery); + std::env::remove_var("MAGNOTIA_POWER_STATE_OVERRIDE"); + }); + } + + #[test] + fn env_var_override_garbage_falls_through() { + with_override(None, || { + std::env::set_var("MAGNOTIA_POWER_STATE_OVERRIDE", "nonsense"); + // Garbage value falls through to the platform probe. + // We can't assert the platform result so just assert it + // doesn't panic. + let _ = probe_power_state(); + std::env::remove_var("MAGNOTIA_POWER_STATE_OVERRIDE"); + }); + } }