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:
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user