feat(core): probe_power_state with Linux dispatch + override paths

Adds the public probe entry point with two override mechanisms:
- In-process TEST_OVERRIDE slot serialised by TEST_LOCK mutex for
  unit tests (avoids races with cargo's parallel runner).
- MAGNOTIA_POWER_STATE_OVERRIDE env var for integration tests set
  externally.
Platform dispatch: Linux reads /sys/class/power_supply; all others
return Unknown. Five new override tests pass alongside the six sysfs
parser tests from Task 1.2 (12 total green).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jars
2026-05-09 11:24:50 +01:00
parent 276e3ca6c1
commit 0c761c0be6

View File

@@ -64,6 +64,75 @@ fn read_trimmed(path: &Path) -> Option<String> {
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<PowerState> {
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<Option<PowerState>> = Mutex::new(None);
fn test_get_override() -> Option<PowerState> {
*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<R>(state: Option<PowerState>, 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");
});
}
}