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())
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -128,4 +197,48 @@ mod tests {
|
|||||||
// No type, no online — should not panic.
|
// No type, no online — should not panic.
|
||||||
assert_eq!(parse_power_state_from_dir(dir.path()), PowerState::Unknown);
|
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