impemented mock testing
This commit is contained in:
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -530,6 +530,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sysinfo",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"toml",
|
||||
"tracing",
|
||||
@@ -595,6 +596,12 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "filedescriptor"
|
||||
version = "0.8.3"
|
||||
@@ -1705,6 +1712,19 @@ dependencies = [
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.1",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.4.3"
|
||||
|
||||
@@ -31,3 +31,6 @@ num_cpus = "1.17"
|
||||
toml = "1.0.3"
|
||||
regex = "1.12.3"
|
||||
which = "8.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
@@ -141,6 +141,13 @@ ryzenadj = "ryzenadj"
|
||||
|
||||
# env health verification
|
||||
|
||||
[benchmarking]
|
||||
idle_duration_s = 10
|
||||
stress_duration_min_s = 15
|
||||
stress_duration_max_s = 45
|
||||
cool_down_s = 5
|
||||
power_steps_watts = [15.0, 20.0, 25.0, 30.0, 35.0]
|
||||
|
||||
[[preflight_checks]]
|
||||
name = "MSR Write Access"
|
||||
check_cmd = "grep -q 'msr.allow_writes=on' /proc/cmdline"
|
||||
|
||||
@@ -4,41 +4,57 @@ use anyhow::Result;
|
||||
pub struct I8kmonConfig {
|
||||
pub t_ambient: f32,
|
||||
pub t_max_fan: f32,
|
||||
pub thermal_resistance_kw: f32,
|
||||
}
|
||||
|
||||
pub struct I8kmonTranslator;
|
||||
|
||||
impl I8kmonTranslator {
|
||||
pub fn generate_conf(config: &I8kmonConfig) -> String {
|
||||
// Higher resistance means we need to start fans sooner.
|
||||
// If R_theta is 2.5 K/W, it's quite high for a laptop.
|
||||
// We'll scale the 'low' threshold based on R_theta.
|
||||
let aggression_factor = (config.thermal_resistance_kw / 1.5).clamp(0.8, 1.5);
|
||||
|
||||
let t_off = config.t_ambient + 5.0;
|
||||
let t_low_on = config.t_ambient + 12.0;
|
||||
let t_low_off = config.t_ambient + 10.0;
|
||||
let t_low_on = config.t_ambient + (10.0 / aggression_factor);
|
||||
let t_low_off = t_low_on - 2.0;
|
||||
|
||||
let t_high_on = config.t_max_fan;
|
||||
let t_high_off = config.t_max_fan - 5.0;
|
||||
let t_low_trigger = (config.t_max_fan - 15.0).max(t_low_on + 2.0);
|
||||
let t_high_off = t_high_on - 5.0;
|
||||
|
||||
let t_mid_on = (t_low_on + t_high_on) / 2.0;
|
||||
let t_mid_off = t_mid_on - 3.0;
|
||||
|
||||
format!(
|
||||
r#"# Generated by ember-tune Optimizer
|
||||
# Grounded in physical thermal resistance
|
||||
# Grounded in physical thermal resistance (Rθ = {r_theta:.3} K/W)
|
||||
|
||||
set config(gen_shadow) 1
|
||||
set config(i8k_ignore_dmi) 1
|
||||
|
||||
# Fan states: {{state_low state_high temp_on temp_off}}
|
||||
# 0: Off
|
||||
set config(0) {{0 0 {t_low_on:.0} {t_off:.0}}}
|
||||
set config(1) {{1 1 {t_low_trigger:.0} {t_low_off:.0}}}
|
||||
set config(2) {{2 2 {t_high_on:.0} {t_high_off:.0}}}
|
||||
# 1: Low
|
||||
set config(1) {{1 1 {t_mid_on:.0} {t_low_off:.0}}}
|
||||
# 2: High
|
||||
set config(2) {{2 2 {t_high_on:.0} {t_mid_off:.0}}}
|
||||
|
||||
# Speed thresholds (approximate for XPS 9380)
|
||||
# Hysteresis reference (internal use)
|
||||
# High Off Threshold: {t_high_off:.0}
|
||||
|
||||
# Speed thresholds
|
||||
set config(speed_low) 2500
|
||||
set config(speed_high) 4500
|
||||
"#,
|
||||
r_theta = config.thermal_resistance_kw,
|
||||
t_low_on = t_low_on,
|
||||
t_off = t_off,
|
||||
t_low_trigger = t_low_trigger,
|
||||
t_mid_on = t_mid_on,
|
||||
t_low_off = t_low_off,
|
||||
t_high_on = t_high_on,
|
||||
t_high_off = t_high_off
|
||||
t_mid_off = t_mid_off
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
8
src/lib.rs
Normal file
8
src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
pub mod mediator;
|
||||
pub mod sal;
|
||||
pub mod load;
|
||||
pub mod orchestrator;
|
||||
pub mod ui;
|
||||
pub mod engine;
|
||||
pub mod cli;
|
||||
pub mod sys;
|
||||
34
src/main.rs
34
src/main.rs
@@ -1,12 +1,4 @@
|
||||
mod mediator;
|
||||
mod sal;
|
||||
mod load;
|
||||
mod orchestrator;
|
||||
mod ui;
|
||||
mod engine;
|
||||
mod cli;
|
||||
|
||||
use miette::{Result, IntoDiagnostic, Diagnostic, Report, Context};
|
||||
use miette::{Result, IntoDiagnostic, Diagnostic, Report};
|
||||
use thiserror::Error;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
@@ -25,16 +17,16 @@ use crossterm::{
|
||||
};
|
||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
|
||||
use cli::Cli;
|
||||
use mediator::{TelemetryState, UiCommand, BenchmarkPhase};
|
||||
use sal::traits::{AuditError, PlatformSal};
|
||||
use sal::mock::MockSal;
|
||||
use sal::heuristic::engine::HeuristicEngine;
|
||||
use sal::heuristic::discovery::SystemFactSheet;
|
||||
use load::{StressNg};
|
||||
use orchestrator::BenchmarkOrchestrator;
|
||||
use ui::dashboard::{draw_dashboard, DashboardState};
|
||||
use engine::OptimizationResult;
|
||||
use ember_tune_rs::cli::Cli;
|
||||
use ember_tune_rs::mediator::{TelemetryState, UiCommand, BenchmarkPhase};
|
||||
use ember_tune_rs::sal::traits::{AuditError, PlatformSal};
|
||||
use ember_tune_rs::sal::mock::MockSal;
|
||||
use ember_tune_rs::sal::heuristic::engine::HeuristicEngine;
|
||||
use ember_tune_rs::sal::heuristic::discovery::SystemFactSheet;
|
||||
use ember_tune_rs::load::{StressNg};
|
||||
use ember_tune_rs::orchestrator::BenchmarkOrchestrator;
|
||||
use ember_tune_rs::ui::dashboard::{draw_dashboard, DashboardState};
|
||||
use ember_tune_rs::engine::OptimizationResult;
|
||||
use owo_colors::OwoColorize;
|
||||
|
||||
#[derive(Error, Diagnostic, Debug)]
|
||||
@@ -109,11 +101,13 @@ fn main() -> Result<()> {
|
||||
|
||||
info!("ember-tune starting with args: {:?}", args);
|
||||
|
||||
let ctx = ember_tune_rs::sal::traits::EnvironmentCtx::production();
|
||||
|
||||
// 2. Platform Detection & Audit
|
||||
let (sal_box, facts): (Box<dyn PlatformSal>, SystemFactSheet) = if args.mock {
|
||||
(Box::new(MockSal::new()), SystemFactSheet::default())
|
||||
} else {
|
||||
HeuristicEngine::detect_and_build()?
|
||||
HeuristicEngine::detect_and_build(ctx)?
|
||||
};
|
||||
let sal: Arc<dyn PlatformSal> = sal_box.into();
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::sal::traits::{PlatformSal, AuditStep, SafetyStatus};
|
||||
use crate::sal::traits::{PlatformSal, SafetyStatus};
|
||||
use crate::sal::heuristic::discovery::SystemFactSheet;
|
||||
use crate::load::Workload;
|
||||
use crate::mediator::{TelemetryState, UiCommand, BenchmarkPhase};
|
||||
@@ -94,6 +94,8 @@ impl BenchmarkOrchestrator {
|
||||
}
|
||||
|
||||
fn execute_benchmark(&mut self) -> Result<OptimizationResult> {
|
||||
let bench_cfg = self.facts.bench_config.clone().context("Benchmarking config missing in facts")?;
|
||||
|
||||
// Phase 1: Audit & Baseline
|
||||
self.phase = BenchmarkPhase::Auditing;
|
||||
for step in self.sal.audit() {
|
||||
@@ -107,13 +109,13 @@ impl BenchmarkOrchestrator {
|
||||
|
||||
// Baseline (Idle Calibration)
|
||||
self.phase = BenchmarkPhase::IdleCalibration;
|
||||
self.log("Phase 1: Recording Idle Baseline (10s)...")?;
|
||||
self.log(&format!("Phase 1: Recording Idle Baseline ({}s)...", bench_cfg.idle_duration_s))?;
|
||||
self.sal.set_fan_mode("auto")?; // Use auto for idle
|
||||
|
||||
let mut idle_temps = Vec::new();
|
||||
let start = Instant::now();
|
||||
let mut tick = 0;
|
||||
while start.elapsed() < Duration::from_secs(10) {
|
||||
while start.elapsed() < Duration::from_secs(bench_cfg.idle_duration_s) {
|
||||
self.check_abort()?;
|
||||
self.send_telemetry(tick)?;
|
||||
idle_temps.push(self.sal.get_temp().unwrap_or(0.0));
|
||||
@@ -128,19 +130,19 @@ impl BenchmarkOrchestrator {
|
||||
self.log("Phase 2: Starting Synthetic Stress Matrix.")?;
|
||||
self.sal.set_fan_mode("max")?; // Lock fans for consistent resistance
|
||||
|
||||
let power_steps = [15.0, 20.0, 25.0, 30.0, 35.0];
|
||||
for &pl in &power_steps {
|
||||
let steps = bench_cfg.power_steps_watts.clone();
|
||||
for &pl in &steps {
|
||||
self.log(&format!("Testing PL1 = {:.0}W...", pl))?;
|
||||
self.sal.set_sustained_power_limit(pl)?;
|
||||
self.sal.set_burst_power_limit(pl + 5.0)?;
|
||||
|
||||
self.workload.start(num_cpus::get(), 100)?;
|
||||
|
||||
// Wait for equilibrium: Hybrid approach (15s min, 45s max)
|
||||
// Wait for equilibrium
|
||||
let step_start = Instant::now();
|
||||
let mut step_temps = VecDeque::with_capacity(30); // Last 15s @ 500ms
|
||||
let mut step_temps = VecDeque::with_capacity(30);
|
||||
|
||||
while step_start.elapsed() < Duration::from_secs(45) {
|
||||
while step_start.elapsed() < Duration::from_secs(bench_cfg.stress_duration_max_s) {
|
||||
self.check_abort()?;
|
||||
|
||||
let t = self.sal.get_temp().unwrap_or(0.0);
|
||||
@@ -151,7 +153,7 @@ impl BenchmarkOrchestrator {
|
||||
tick += 1;
|
||||
|
||||
// Check for stability: Range < 0.5C over last 5s (10 ticks)
|
||||
if step_start.elapsed() > Duration::from_secs(15) && step_temps.len() == 10 {
|
||||
if step_start.elapsed() > Duration::from_secs(bench_cfg.stress_duration_min_s) && step_temps.len() == 10 {
|
||||
let min = step_temps.iter().fold(f32::MAX, |a, &b| a.min(b));
|
||||
let max = step_temps.iter().fold(f32::MIN, |a, &b| a.max(b));
|
||||
if (max - min) < 0.5 {
|
||||
@@ -179,8 +181,8 @@ impl BenchmarkOrchestrator {
|
||||
});
|
||||
|
||||
self.workload.stop()?;
|
||||
self.log(" Step complete. Cooling down for 5s...")?;
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
self.log(&format!(" Step complete. Cooling down for {}s...", bench_cfg.cool_down_s))?;
|
||||
thread::sleep(Duration::from_secs(bench_cfg.cool_down_s));
|
||||
}
|
||||
|
||||
// Phase 4: Physical Modeling
|
||||
@@ -216,6 +218,7 @@ impl BenchmarkOrchestrator {
|
||||
let i8k_config = crate::engine::formatters::i8kmon::I8kmonConfig {
|
||||
t_ambient: self.profile.ambient_temp,
|
||||
t_max_fan: res.max_temp_c - 5.0,
|
||||
thermal_resistance_kw: res.thermal_resistance_kw,
|
||||
};
|
||||
crate::engine::formatters::i8kmon::I8kmonTranslator::save(i8k_path, &i8k_config)?;
|
||||
self.log(&format!("✓ Saved '{}'.", i8k_path.display()))?;
|
||||
@@ -229,6 +232,7 @@ impl BenchmarkOrchestrator {
|
||||
let abort = self.emergency_abort.clone();
|
||||
let reason_store = self.emergency_reason.clone();
|
||||
let sal = self.sal.clone();
|
||||
let tx = self.telemetry_tx.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
while !abort.load(Ordering::SeqCst) {
|
||||
@@ -239,7 +243,30 @@ impl BenchmarkOrchestrator {
|
||||
abort.store(true, Ordering::SeqCst);
|
||||
break;
|
||||
}
|
||||
Ok(SafetyStatus::Warning(_msg)) | Ok(SafetyStatus::Critical(_msg)) => {}
|
||||
Ok(SafetyStatus::Warning(msg)) | Ok(SafetyStatus::Critical(msg)) => {
|
||||
let state = TelemetryState {
|
||||
cpu_model: String::new(),
|
||||
total_ram_gb: 0,
|
||||
tick: 0,
|
||||
cpu_temp: 0.0,
|
||||
power_w: 0.0,
|
||||
current_freq: 0.0,
|
||||
fans: Vec::new(),
|
||||
governor: String::new(),
|
||||
pl1_limit: 0.0,
|
||||
pl2_limit: 0.0,
|
||||
fan_tier: String::new(),
|
||||
phase: BenchmarkPhase::StressTesting,
|
||||
history_watts: Vec::new(),
|
||||
history_temp: Vec::new(),
|
||||
history_mhz: Vec::new(),
|
||||
log_event: Some(format!("WATCHDOG: {}", msg)),
|
||||
metadata: std::collections::HashMap::new(),
|
||||
is_emergency: false,
|
||||
emergency_reason: None,
|
||||
};
|
||||
let _ = tx.send(state);
|
||||
}
|
||||
Ok(SafetyStatus::Nominal) => {}
|
||||
Err(e) => {
|
||||
*reason_store.lock().unwrap() = Some(format!("Watchdog Sensor Failure: {}", e));
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use super::traits::{PreflightAuditor, EnvironmentGuard, SensorBus, ActuatorBus, HardwareWatchdog, AuditError, AuditStep, SafetyStatus};
|
||||
use super::traits::{PreflightAuditor, EnvironmentGuard, SensorBus, ActuatorBus, HardwareWatchdog, AuditError, AuditStep, SafetyStatus, EnvironmentCtx};
|
||||
use anyhow::{Result, Context, anyhow};
|
||||
use std::fs;
|
||||
use std::path::{PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::sync::Mutex;
|
||||
use tracing::{debug};
|
||||
use crate::sal::heuristic::discovery::SystemFactSheet;
|
||||
|
||||
pub struct DellXps9380Sal {
|
||||
ctx: EnvironmentCtx,
|
||||
fact_sheet: SystemFactSheet,
|
||||
temp_path: PathBuf,
|
||||
pwr_path: PathBuf,
|
||||
@@ -25,15 +25,18 @@ pub struct DellXps9380Sal {
|
||||
}
|
||||
|
||||
impl DellXps9380Sal {
|
||||
pub fn init(facts: SystemFactSheet) -> Result<Self> {
|
||||
pub fn init(ctx: EnvironmentCtx, facts: SystemFactSheet) -> Result<Self> {
|
||||
let temp_path = facts.temp_path.clone().context("Dell SAL requires temperature sensor")?;
|
||||
let pwr_base = facts.rapl_paths.first().cloned().context("Dell SAL requires RAPL interface")?;
|
||||
let fan_paths = facts.fan_paths.clone();
|
||||
|
||||
let freq_path = PathBuf::from("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq");
|
||||
let freq_path = ctx.sysfs_base.join("sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq");
|
||||
let msr_path = ctx.sysfs_base.join("dev/cpu/0/msr");
|
||||
|
||||
let msr_file = fs::OpenOptions::new().read(true).write(true).open("/dev/cpu/0/msr")
|
||||
.context("Failed to open /dev/cpu/0/msr. Is the 'msr' module loaded?")?;
|
||||
let msr_file = fs::OpenOptions::new().read(true).write(true).open(&msr_path)
|
||||
.with_context(|| format!("Failed to open {:?}. Is the 'msr' module loaded?", msr_path))?;
|
||||
|
||||
let initial_energy = fs::read_to_string(pwr_base.join("energy_uj")).unwrap_or_default().trim().parse().unwrap_or(0);
|
||||
|
||||
Ok(Self {
|
||||
temp_path,
|
||||
@@ -47,8 +50,9 @@ impl DellXps9380Sal {
|
||||
last_fans: Mutex::new(Vec::new()),
|
||||
suppressed_services: Mutex::new(Vec::new()),
|
||||
msr_file: Mutex::new(msr_file),
|
||||
last_energy: Mutex::new((0, Instant::now())),
|
||||
last_energy: Mutex::new((initial_energy, Instant::now())),
|
||||
fact_sheet: facts,
|
||||
ctx,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -78,16 +82,17 @@ impl PreflightAuditor for DellXps9380Sal {
|
||||
|
||||
let modules = ["dell_smm_hwmon", "msr", "intel_rapl_msr"];
|
||||
for mod_name in modules {
|
||||
let path = format!("/sys/module/{}", mod_name);
|
||||
let path = self.ctx.sysfs_base.join(format!("sys/module/{}", mod_name));
|
||||
steps.push(AuditStep {
|
||||
description: format!("Kernel Module: {}", mod_name),
|
||||
outcome: if PathBuf::from(path).exists() { Ok(()) } else {
|
||||
outcome: if path.exists() { Ok(()) } else {
|
||||
Err(AuditError::ToolMissing(format!("Module '{}' not loaded.", mod_name)))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let cmdline = fs::read_to_string("/proc/cmdline").unwrap_or_default();
|
||||
let cmdline_path = self.ctx.sysfs_base.join("proc/cmdline");
|
||||
let cmdline = fs::read_to_string(cmdline_path).unwrap_or_default();
|
||||
let params = [
|
||||
("dell_smm_hwmon.ignore_dmi=1", "dell_smm_hwmon.ignore_dmi=1"),
|
||||
("dell_smm_hwmon.restricted=0", "dell_smm_hwmon.restricted=0"),
|
||||
@@ -100,7 +105,8 @@ impl PreflightAuditor for DellXps9380Sal {
|
||||
});
|
||||
}
|
||||
|
||||
let ac_status = fs::read_to_string("/sys/class/power_supply/AC/online").unwrap_or_else(|_| "0".to_string());
|
||||
let ac_status_path = self.ctx.sysfs_base.join("sys/class/power_supply/AC/online");
|
||||
let ac_status = fs::read_to_string(ac_status_path).unwrap_or_else(|_| "0".to_string());
|
||||
steps.push(AuditStep {
|
||||
description: "AC Power Connection".to_string(),
|
||||
outcome: if ac_status.trim() == "1" { Ok(()) } else {
|
||||
@@ -123,9 +129,9 @@ impl EnvironmentGuard for DellXps9380Sal {
|
||||
let services = ["tlp", "thermald", "i8kmon"];
|
||||
let mut suppressed = self.suppressed_services.lock().unwrap();
|
||||
for s in services {
|
||||
if Command::new("systemctl").args(["is-active", "--quiet", s]).status()?.success() {
|
||||
if self.ctx.runner.run("systemctl", &["is-active", "--quiet", s]).is_ok() {
|
||||
debug!("Suppressing service: {}", s);
|
||||
Command::new("systemctl").args(["stop", s]).status()?;
|
||||
self.ctx.runner.run("systemctl", &["stop", s])?;
|
||||
suppressed.push(s.to_string());
|
||||
}
|
||||
}
|
||||
@@ -135,7 +141,7 @@ impl EnvironmentGuard for DellXps9380Sal {
|
||||
fn restore(&self) -> Result<()> {
|
||||
let mut suppressed = self.suppressed_services.lock().unwrap();
|
||||
for s in suppressed.drain(..) {
|
||||
let _ = Command::new("systemctl").args(["start", &s]).status();
|
||||
let _ = self.ctx.runner.run("systemctl", &["start", &s]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -156,6 +162,7 @@ impl SensorBus for DellXps9380Sal {
|
||||
}
|
||||
|
||||
fn get_power_w(&self) -> Result<f32> {
|
||||
if self.pwr_path.to_string_lossy().contains("energy_uj") {
|
||||
let mut last = self.last_energy.lock().unwrap();
|
||||
let e2 = fs::read_to_string(&self.pwr_path)?.trim().parse::<u64>()?;
|
||||
let t2 = Instant::now();
|
||||
@@ -165,6 +172,10 @@ impl SensorBus for DellXps9380Sal {
|
||||
*last = (e2, t2);
|
||||
if delta_t < 0.01 { return Ok(0.0); }
|
||||
Ok((delta_e as f32 / 1_000_000.0) / delta_t)
|
||||
} else {
|
||||
let s = fs::read_to_string(&self.pwr_path)?;
|
||||
Ok(s.trim().parse::<f32>()? / 1000000.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_fan_rpms(&self) -> Result<Vec<u32>> {
|
||||
@@ -194,10 +205,11 @@ impl ActuatorBus for DellXps9380Sal {
|
||||
fn set_fan_mode(&self, mode: &str) -> Result<()> {
|
||||
let tool_path = self.fact_sheet.paths.tools.get("dell_fan_ctrl")
|
||||
.ok_or_else(|| anyhow!("Dell fan control tool not found in PATH"))?;
|
||||
let tool_str = tool_path.to_string_lossy();
|
||||
|
||||
match mode {
|
||||
"max" | "Manual" => { Command::new(tool_path).arg("0").status()?; }
|
||||
"auto" | "Auto" => { Command::new(tool_path).arg("1").status()?; }
|
||||
"max" | "Manual" => { self.ctx.runner.run(&tool_str, &["0"])?; }
|
||||
"auto" | "Auto" => { self.ctx.runner.run(&tool_str, &["1"])?; }
|
||||
_ => { debug!("Unknown fan mode: {}", mode); }
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::path::Path;
|
||||
use std::path::{Path};
|
||||
use std::fs;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::process::Command;
|
||||
use tracing::{debug, warn};
|
||||
use std::sync::Mutex;
|
||||
use tracing::{debug};
|
||||
|
||||
use crate::sal::traits::{SensorBus, ActuatorBus, EnvironmentGuard, HardwareWatchdog, PreflightAuditor, AuditStep, AuditError, SafetyStatus};
|
||||
use crate::sal::traits::{SensorBus, ActuatorBus, EnvironmentGuard, HardwareWatchdog, PreflightAuditor, AuditStep, AuditError, SafetyStatus, EnvironmentCtx};
|
||||
use crate::sal::heuristic::discovery::SystemFactSheet;
|
||||
use crate::sal::heuristic::schema::HardwareDb;
|
||||
|
||||
pub struct GenericLinuxSal {
|
||||
ctx: EnvironmentCtx,
|
||||
fact_sheet: SystemFactSheet,
|
||||
db: HardwareDb,
|
||||
suppressed_services: Mutex<Vec<String>>,
|
||||
@@ -20,14 +20,21 @@ pub struct GenericLinuxSal {
|
||||
}
|
||||
|
||||
impl GenericLinuxSal {
|
||||
pub fn new(facts: SystemFactSheet, db: HardwareDb) -> Self {
|
||||
pub fn new(ctx: EnvironmentCtx, facts: SystemFactSheet, db: HardwareDb) -> Self {
|
||||
let initial_energy = if let Some(pwr_base) = facts.rapl_paths.first() {
|
||||
fs::read_to_string(pwr_base.join("energy_uj")).unwrap_or_default().trim().parse().unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Self {
|
||||
db,
|
||||
suppressed_services: Mutex::new(Vec::new()),
|
||||
last_valid_temp: Mutex::new((0.0, Instant::now())),
|
||||
current_pl1: Mutex::new(15.0),
|
||||
last_energy: Mutex::new((0, Instant::now())),
|
||||
last_energy: Mutex::new((initial_energy, Instant::now())),
|
||||
fact_sheet: facts,
|
||||
ctx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +42,6 @@ impl GenericLinuxSal {
|
||||
self.fact_sheet.vendor.to_lowercase().contains("dell")
|
||||
}
|
||||
|
||||
/// Read sysfs safely. We removed the thread-per-read timeout logic
|
||||
/// as it was inefficient. sysfs reads are generally fast enough.
|
||||
fn read_sysfs(&self, path: &Path) -> Result<String> {
|
||||
fs::read_to_string(path).map(|s| s.trim().to_string()).map_err(|e| anyhow!(e))
|
||||
}
|
||||
@@ -46,11 +51,11 @@ impl PreflightAuditor for GenericLinuxSal {
|
||||
fn audit(&self) -> Box<dyn Iterator<Item = AuditStep> + '_> {
|
||||
let mut steps = Vec::new();
|
||||
for check in &self.db.preflight_checks {
|
||||
let status = Command::new("sh").arg("-c").arg(&check.check_cmd).status();
|
||||
let status = self.ctx.runner.run("sh", &["-c", &check.check_cmd]);
|
||||
steps.push(AuditStep {
|
||||
description: check.name.clone(),
|
||||
outcome: match status {
|
||||
Ok(s) if s.success() => Ok(()),
|
||||
Ok(_) => Ok(()),
|
||||
_ => Err(AuditError::KernelIncompatible(check.fail_help.clone())),
|
||||
}
|
||||
});
|
||||
@@ -106,11 +111,12 @@ impl SensorBus for GenericLinuxSal {
|
||||
}
|
||||
|
||||
fn get_freq_mhz(&self) -> Result<f32> {
|
||||
let path = Path::new("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq");
|
||||
let path = self.ctx.sysfs_base.join("sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq");
|
||||
if path.exists() {
|
||||
Ok(self.read_sysfs(path)?.parse::<f32>()? / 1000.0)
|
||||
Ok(self.read_sysfs(&path)?.parse::<f32>()? / 1000.0)
|
||||
} else {
|
||||
let cpuinfo = fs::read_to_string("/proc/cpuinfo")?;
|
||||
let cpuinfo_path = self.ctx.sysfs_base.join("proc/cpuinfo");
|
||||
let cpuinfo = fs::read_to_string(cpuinfo_path)?;
|
||||
for line in cpuinfo.lines() {
|
||||
if line.starts_with("cpu MHz") {
|
||||
if let Some((_, mhz)) = line.split_once(':') {
|
||||
@@ -133,7 +139,7 @@ impl ActuatorBus for GenericLinuxSal {
|
||||
};
|
||||
if let Some(cmd_str) = cmd {
|
||||
let parts: Vec<&str> = cmd_str.split_whitespace().collect();
|
||||
Command::new(parts[0]).args(&parts[1..]).status()?;
|
||||
self.ctx.runner.run(parts[0], &parts[1..])?;
|
||||
Ok(())
|
||||
} else { Err(anyhow!("Dell fan command missing")) }
|
||||
} else { Ok(()) }
|
||||
@@ -159,7 +165,8 @@ impl EnvironmentGuard for GenericLinuxSal {
|
||||
for conflict_id in &self.fact_sheet.active_conflicts {
|
||||
if let Some(conflict) = self.db.conflicts.iter().find(|c| &c.id == conflict_id) {
|
||||
for service in &conflict.services {
|
||||
if Command::new("systemctl").arg("stop").arg(service).status()?.success() {
|
||||
if self.ctx.runner.run("systemctl", &["is-active", "--quiet", service]).is_ok() {
|
||||
self.ctx.runner.run("systemctl", &["stop", service])?;
|
||||
suppressed.push(service.clone());
|
||||
}
|
||||
}
|
||||
@@ -171,7 +178,7 @@ impl EnvironmentGuard for GenericLinuxSal {
|
||||
fn restore(&self) -> Result<()> {
|
||||
let mut suppressed = self.suppressed_services.lock().unwrap();
|
||||
for service in suppressed.drain(..) {
|
||||
let _ = Command::new("systemctl").arg("start").arg(service).status();
|
||||
let _ = self.ctx.runner.run("systemctl", &["start", &service]);
|
||||
}
|
||||
if self.is_dell() { let _ = self.set_fan_mode("auto"); }
|
||||
Ok(())
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::time::{Duration};
|
||||
use std::thread;
|
||||
use std::sync::mpsc;
|
||||
use std::collections::HashMap;
|
||||
use crate::sal::heuristic::schema::{SensorDiscovery, ActuatorDiscovery, Conflict, Discovery};
|
||||
use crate::sal::heuristic::schema::{SensorDiscovery, ActuatorDiscovery, Conflict, Discovery, Benchmarking};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
/// Registry of dynamically discovered paths for configs and tools.
|
||||
@@ -25,19 +25,22 @@ pub struct SystemFactSheet {
|
||||
pub rapl_paths: Vec<PathBuf>,
|
||||
pub active_conflicts: Vec<String>,
|
||||
pub paths: PathRegistry,
|
||||
pub bench_config: Option<Benchmarking>,
|
||||
}
|
||||
|
||||
/// Probes the system for hardware sensors, actuators, service conflicts, and paths.
|
||||
pub fn discover_facts(
|
||||
base_path: &Path,
|
||||
discovery: &Discovery,
|
||||
conflicts: &[Conflict]
|
||||
conflicts: &[Conflict],
|
||||
bench_config: Benchmarking,
|
||||
) -> SystemFactSheet {
|
||||
let (vendor, model) = read_dmi_info();
|
||||
let (vendor, model) = read_dmi_info(base_path);
|
||||
|
||||
debug!("DMI Identity: Vendor='{}', Model='{}'", vendor, model);
|
||||
|
||||
let (temp_path, fan_paths) = discover_hwmon(&discovery.sensors);
|
||||
let rapl_paths = discover_rapl(&discovery.actuators);
|
||||
let (temp_path, fan_paths) = discover_hwmon(base_path, &discovery.sensors);
|
||||
let rapl_paths = discover_rapl(base_path, &discovery.actuators);
|
||||
|
||||
let mut active_conflicts = Vec::new();
|
||||
for conflict in conflicts {
|
||||
@@ -50,7 +53,7 @@ pub fn discover_facts(
|
||||
}
|
||||
}
|
||||
|
||||
let paths = discover_paths(discovery);
|
||||
let paths = discover_paths(base_path, discovery);
|
||||
|
||||
SystemFactSheet {
|
||||
vendor,
|
||||
@@ -60,10 +63,11 @@ pub fn discover_facts(
|
||||
rapl_paths,
|
||||
active_conflicts,
|
||||
paths,
|
||||
bench_config: Some(bench_config),
|
||||
}
|
||||
}
|
||||
|
||||
fn discover_paths(discovery: &Discovery) -> PathRegistry {
|
||||
fn discover_paths(base_path: &Path, discovery: &Discovery) -> PathRegistry {
|
||||
let mut registry = PathRegistry::default();
|
||||
|
||||
// 1. Discover Tools via PATH
|
||||
@@ -77,7 +81,12 @@ fn discover_paths(discovery: &Discovery) -> PathRegistry {
|
||||
// 2. Discover Configs via existence check
|
||||
for (id, candidates) in &discovery.configs {
|
||||
for candidate in candidates {
|
||||
let path = PathBuf::from(candidate);
|
||||
let path = if candidate.starts_with('/') {
|
||||
base_path.join(&candidate[1..])
|
||||
} else {
|
||||
base_path.join(candidate)
|
||||
};
|
||||
|
||||
if path.exists() {
|
||||
debug!("Discovered config: {} -> {:?}", id, path);
|
||||
registry.configs.insert(id.clone(), path);
|
||||
@@ -96,24 +105,24 @@ fn discover_paths(discovery: &Discovery) -> PathRegistry {
|
||||
}
|
||||
|
||||
/// Reads DMI information from sysfs with a safety timeout.
|
||||
fn read_dmi_info() -> (String, String) {
|
||||
let vendor = read_sysfs_with_timeout(Path::new("/sys/class/dmi/id/sys_vendor"), Duration::from_millis(100))
|
||||
fn read_dmi_info(base_path: &Path) -> (String, String) {
|
||||
let vendor = read_sysfs_with_timeout(&base_path.join("sys/class/dmi/id/sys_vendor"), Duration::from_millis(100))
|
||||
.unwrap_or_else(|| "Unknown".to_string());
|
||||
let model = read_sysfs_with_timeout(Path::new("/sys/class/dmi/id/product_name"), Duration::from_millis(100))
|
||||
let model = read_sysfs_with_timeout(&base_path.join("sys/class/dmi/id/product_name"), Duration::from_millis(100))
|
||||
.unwrap_or_else(|| "Unknown".to_string());
|
||||
(vendor, model)
|
||||
}
|
||||
|
||||
/// Discovers hwmon sensors by matching labels and prioritizing drivers.
|
||||
fn discover_hwmon(cfg: &SensorDiscovery) -> (Option<PathBuf>, Vec<PathBuf>) {
|
||||
fn discover_hwmon(base_path: &Path, cfg: &SensorDiscovery) -> (Option<PathBuf>, Vec<PathBuf>) {
|
||||
let mut temp_candidates = Vec::new();
|
||||
let mut fan_candidates = Vec::new();
|
||||
|
||||
let hwmon_base = Path::new("/sys/class/hwmon");
|
||||
let entries = match fs::read_dir(hwmon_base) {
|
||||
let hwmon_base = base_path.join("sys/class/hwmon");
|
||||
let entries = match fs::read_dir(&hwmon_base) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
warn!("Could not read /sys/class/hwmon: {}", e);
|
||||
warn!("Could not read {:?}: {}", hwmon_base, e);
|
||||
return (None, Vec::new());
|
||||
}
|
||||
};
|
||||
@@ -170,11 +179,11 @@ fn discover_hwmon(cfg: &SensorDiscovery) -> (Option<PathBuf>, Vec<PathBuf>) {
|
||||
}
|
||||
|
||||
/// Discovers RAPL powercap paths.
|
||||
fn discover_rapl(cfg: &ActuatorDiscovery) -> Vec<PathBuf> {
|
||||
fn discover_rapl(base_path: &Path, cfg: &ActuatorDiscovery) -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
let powercap_base = Path::new("/sys/class/powercap");
|
||||
let powercap_base = base_path.join("sys/class/powercap");
|
||||
|
||||
let entries = match fs::read_dir(powercap_base) {
|
||||
let entries = match fs::read_dir(&powercap_base) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::fs;
|
||||
use regex::Regex;
|
||||
use tracing::{info, debug};
|
||||
|
||||
use crate::sal::traits::PlatformSal;
|
||||
use crate::sal::traits::{PlatformSal, EnvironmentCtx};
|
||||
use crate::sal::dell_xps_9380::DellXps9380Sal;
|
||||
use crate::sal::generic_linux::GenericLinuxSal;
|
||||
use crate::sal::heuristic::schema::HardwareDb;
|
||||
@@ -13,7 +13,7 @@ pub struct HeuristicEngine;
|
||||
|
||||
impl HeuristicEngine {
|
||||
/// Loads the hardware database, probes the system, and builds the appropriate SAL.
|
||||
pub fn detect_and_build() -> Result<(Box<dyn PlatformSal>, SystemFactSheet)> {
|
||||
pub fn detect_and_build(ctx: EnvironmentCtx) -> Result<(Box<dyn PlatformSal>, SystemFactSheet)> {
|
||||
// 1. Load Hardware DB
|
||||
let db_path = "assets/hardware_db.toml";
|
||||
let db_content = fs::read_to_string(db_path)
|
||||
@@ -24,7 +24,7 @@ impl HeuristicEngine {
|
||||
.context("Failed to parse hardware_db.toml")?;
|
||||
|
||||
// 2. Discover Facts
|
||||
let facts = discover_facts(&db.discovery, &db.conflicts);
|
||||
let facts = discover_facts(&ctx.sysfs_base, &db.discovery, &db.conflicts, db.benchmarking.clone());
|
||||
info!("System Identity: {} {}", facts.vendor, facts.model);
|
||||
|
||||
// 3. Routing Logic
|
||||
@@ -32,7 +32,7 @@ impl HeuristicEngine {
|
||||
// --- Special Case: Dell XPS 13 9380 ---
|
||||
if is_match(&facts.vendor, "(?i)Dell.*") && is_match(&facts.model, "(?i)XPS.*13.*9380.*") {
|
||||
info!("Specialized SAL Match Found: Dell XPS 13 9380");
|
||||
let sal = DellXps9380Sal::init(facts.clone()).map_err(|e| miette::miette!(e))?;
|
||||
let sal = DellXps9380Sal::init(ctx, facts.clone()).map_err(|e| miette::miette!(e))?;
|
||||
return Ok((Box::new(sal), facts));
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ impl HeuristicEngine {
|
||||
return Err(miette::miette!("No RAPL power interface discovered. Generic fallback impossible."));
|
||||
}
|
||||
|
||||
Ok((Box::new(GenericLinuxSal::new(facts.clone(), db)), facts))
|
||||
Ok((Box::new(GenericLinuxSal::new(ctx, facts.clone(), db)), facts))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ pub struct HardwareDb {
|
||||
pub ecosystems: HashMap<String, Ecosystem>,
|
||||
pub quirks: Vec<Quirk>,
|
||||
pub discovery: Discovery,
|
||||
pub benchmarking: Benchmarking,
|
||||
pub preflight_checks: Vec<PreflightCheck>,
|
||||
}
|
||||
|
||||
@@ -72,6 +73,15 @@ pub struct Discovery {
|
||||
pub tools: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Benchmarking {
|
||||
pub idle_duration_s: u64,
|
||||
pub stress_duration_min_s: u64,
|
||||
pub stress_duration_max_s: u64,
|
||||
pub cool_down_s: u64,
|
||||
pub power_steps_watts: Vec<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct SensorDiscovery {
|
||||
pub temp_labels: Vec<String>,
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use super::traits::{PreflightAuditor, EnvironmentGuard, SensorBus, ActuatorBus, HardwareWatchdog, AuditStep, PlatformSal, SafetyStatus};
|
||||
use super::traits::{PreflightAuditor, EnvironmentGuard, SensorBus, ActuatorBus, HardwareWatchdog, AuditStep, SafetyStatus};
|
||||
use anyhow::Result;
|
||||
|
||||
pub struct MockSal;
|
||||
pub struct MockSal {
|
||||
pub temperature_sequence: std::sync::atomic::AtomicUsize,
|
||||
}
|
||||
|
||||
impl MockSal {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
Self {
|
||||
temperature_sequence: std::sync::atomic::AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +40,9 @@ impl EnvironmentGuard for MockSal {
|
||||
|
||||
impl SensorBus for MockSal {
|
||||
fn get_temp(&self) -> Result<f32> {
|
||||
Ok(42.0)
|
||||
// Support dynamic sequence for Step 5
|
||||
let seq = self.temperature_sequence.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
Ok(40.0 + (seq as f32 * 0.5).min(50.0)) // Heats up from 40 to 90
|
||||
}
|
||||
fn get_power_w(&self) -> Result<f32> {
|
||||
Ok(15.0)
|
||||
|
||||
@@ -2,6 +2,24 @@ use anyhow::Result;
|
||||
use thiserror::Error;
|
||||
use miette::Diagnostic;
|
||||
use std::sync::Arc;
|
||||
use std::path::PathBuf;
|
||||
use crate::sys::SyscallRunner;
|
||||
|
||||
/// Context holding OS abstractions (filesystem base and syscall runner).
|
||||
#[derive(Clone)]
|
||||
pub struct EnvironmentCtx {
|
||||
pub sysfs_base: PathBuf,
|
||||
pub runner: Arc<dyn SyscallRunner>,
|
||||
}
|
||||
|
||||
impl EnvironmentCtx {
|
||||
pub fn production() -> Self {
|
||||
Self {
|
||||
sysfs_base: PathBuf::from("/"),
|
||||
runner: Arc::new(crate::sys::RealSyscallRunner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Diagnostic, Debug, Clone)]
|
||||
pub enum AuditError {
|
||||
|
||||
56
src/sys/cmd.rs
Normal file
56
src/sys/cmd.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::process::Command;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Trait for executing system commands. Allows mocking for tests.
|
||||
pub trait SyscallRunner: Send + Sync {
|
||||
fn run(&self, cmd: &str, args: &[&str]) -> Result<String>;
|
||||
}
|
||||
|
||||
/// The real implementation that executes actual OS commands.
|
||||
pub struct RealSyscallRunner;
|
||||
|
||||
impl SyscallRunner for RealSyscallRunner {
|
||||
fn run(&self, cmd: &str, args: &[&str]) -> Result<String> {
|
||||
let output = Command::new(cmd)
|
||||
.args(args)
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
} else {
|
||||
let err = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
Err(anyhow!("Command failed: {} {:?} -> {}", cmd, args, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A mocked implementation for isolated unit and E2E testing.
|
||||
pub struct MockSyscallRunner {
|
||||
/// Maps "cmd arg1 arg2" to stdout response.
|
||||
responses: Mutex<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
impl MockSyscallRunner {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
responses: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_response(&self, full_cmd: &str, response: &str) {
|
||||
self.responses.lock().unwrap().insert(full_cmd.to_string(), response.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
impl SyscallRunner for MockSyscallRunner {
|
||||
fn run(&self, cmd: &str, args: &[&str]) -> Result<String> {
|
||||
let full_cmd = format!("{} {}", cmd, args.join(" ")).trim().to_string();
|
||||
let responses = self.responses.lock().unwrap();
|
||||
|
||||
responses.get(&full_cmd)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("No mocked response for command: '{}'", full_cmd))
|
||||
}
|
||||
}
|
||||
3
src/sys/mod.rs
Normal file
3
src/sys/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod cmd;
|
||||
|
||||
pub use cmd::{SyscallRunner, RealSyscallRunner, MockSyscallRunner};
|
||||
55
tests/common/fakesys.rs
Normal file
55
tests/common/fakesys.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
|
||||
pub struct FakeSysBuilder {
|
||||
temp_dir: TempDir,
|
||||
}
|
||||
|
||||
impl FakeSysBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
temp_dir: TempDir::new().expect("Failed to create temporary directory"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn base_path(&self) -> PathBuf {
|
||||
self.temp_dir.path().to_path_buf()
|
||||
}
|
||||
|
||||
pub fn add_dmi(&self, vendor: &str, product: &str) -> &Self {
|
||||
let dmi_path = self.base_path().join("sys/class/dmi/id");
|
||||
fs::create_dir_all(&dmi_path).expect("Failed to create DMI directory");
|
||||
|
||||
fs::write(dmi_path.join("sys_vendor"), vendor).expect("Failed to write sys_vendor");
|
||||
fs::write(dmi_path.join("product_name"), product).expect("Failed to write product_name");
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_hwmon(&self, name: &str, temp_label: &str, temp_input: &str) -> &Self {
|
||||
let hwmon_path = self.base_path().join("sys/class/hwmon/hwmon0");
|
||||
fs::create_dir_all(&hwmon_path).expect("Failed to create hwmon directory");
|
||||
|
||||
fs::write(hwmon_path.join("name"), name).expect("Failed to write hwmon name");
|
||||
fs::write(hwmon_path.join("temp1_label"), temp_label).expect("Failed to write temp label");
|
||||
fs::write(hwmon_path.join("temp1_input"), temp_input).expect("Failed to write temp input");
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_rapl(&self, name: &str, energy_uj: &str, pl1_uw: &str) -> &Self {
|
||||
let rapl_path = self.base_path().join("sys/class/powercap/intel-rapl:0");
|
||||
fs::create_dir_all(&rapl_path).expect("Failed to create RAPL directory");
|
||||
|
||||
fs::write(rapl_path.join("name"), name).expect("Failed to write RAPL name");
|
||||
fs::write(rapl_path.join("energy_uj"), energy_uj).expect("Failed to write energy_uj");
|
||||
fs::write(rapl_path.join("constraint_0_power_limit_uw"), pl1_uw).expect("Failed to write pl1_uw");
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_proc_cmdline(&self, cmdline: &str) -> &Self {
|
||||
let proc_path = self.base_path().join("proc");
|
||||
fs::create_dir_all(&proc_path).expect("Failed to create proc directory");
|
||||
fs::write(proc_path.join("cmdline"), cmdline).expect("Failed to write cmdline");
|
||||
self
|
||||
}
|
||||
}
|
||||
1
tests/common/mod.rs
Normal file
1
tests/common/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod fakesys;
|
||||
35
tests/config_merge_test.rs
Normal file
35
tests/config_merge_test.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
#[path = "../src/engine/formatters/throttled.rs"]
|
||||
mod throttled;
|
||||
|
||||
use throttled::{ThrottledTranslator, ThrottledConfig};
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn test_throttled_formatter_non_destructive() {
|
||||
let fixture_path = "tests/fixtures/throttled.conf";
|
||||
let existing_content = fs::read_to_string(fixture_path).expect("Failed to read fixture");
|
||||
|
||||
let config = ThrottledConfig {
|
||||
pl1_limit: 25.0,
|
||||
pl2_limit: 35.0,
|
||||
trip_temp: 90.0,
|
||||
};
|
||||
|
||||
let merged = ThrottledTranslator::merge_conf(&existing_content, &config);
|
||||
|
||||
// Assert updates
|
||||
assert!(merged.contains("PL1_Tdp_W: 25"));
|
||||
assert!(merged.contains("PL2_Tdp_W: 35"));
|
||||
assert!(merged.contains("Trip_Temp_C: 90"));
|
||||
|
||||
// Assert preservation
|
||||
assert!(merged.contains("[UNDERVOLT]"));
|
||||
assert!(merged.contains("CORE: -100"));
|
||||
assert!(merged.contains("GPU: -50"));
|
||||
assert!(merged.contains("# Important: Preserving undervolt offsets is critical!"));
|
||||
assert!(merged.contains("Update_Interval_ms: 3000"));
|
||||
|
||||
// Check that we didn't lose the [GENERAL] section
|
||||
assert!(merged.contains("[GENERAL]"));
|
||||
assert!(merged.contains("# This is a complex test fixture"));
|
||||
}
|
||||
45
tests/heuristic_discovery_test.rs
Normal file
45
tests/heuristic_discovery_test.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use ember_tune_rs::sal::heuristic::discovery::discover_facts;
|
||||
use ember_tune_rs::sal::heuristic::schema::{Discovery, SensorDiscovery, ActuatorDiscovery, Benchmarking};
|
||||
use crate::common::fakesys::FakeSysBuilder;
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
fn test_heuristic_discovery_with_fakesys() {
|
||||
let fake = FakeSysBuilder::new();
|
||||
fake.add_dmi("Dell Inc.", "XPS 13 9380")
|
||||
.add_hwmon("dell_smm", "Package id 0", "45000")
|
||||
.add_rapl("intel-rapl:0", "123456", "15000000")
|
||||
.add_proc_cmdline("quiet msr.allow_writes=on");
|
||||
|
||||
let discovery = Discovery {
|
||||
sensors: SensorDiscovery {
|
||||
temp_labels: vec!["Package id 0".to_string()],
|
||||
fan_labels: vec![],
|
||||
hwmon_priority: vec!["dell_smm".to_string()],
|
||||
},
|
||||
actuators: ActuatorDiscovery {
|
||||
rapl_paths: vec!["intel-rapl:0".to_string()],
|
||||
amd_energy_paths: vec![],
|
||||
governor_files: vec![],
|
||||
},
|
||||
configs: std::collections::HashMap::new(),
|
||||
tools: std::collections::HashMap::new(),
|
||||
};
|
||||
|
||||
let benchmarking = Benchmarking {
|
||||
idle_duration_s: 1,
|
||||
stress_duration_min_s: 1,
|
||||
stress_duration_max_s: 2,
|
||||
cool_down_s: 1,
|
||||
power_steps_watts: vec![10.0, 15.0],
|
||||
};
|
||||
|
||||
let facts = discover_facts(&fake.base_path(), &discovery, &[], benchmarking);
|
||||
|
||||
assert_eq!(facts.vendor, "Dell Inc.");
|
||||
assert_eq!(facts.model, "XPS 13 9380");
|
||||
assert!(facts.temp_path.is_some());
|
||||
assert!(facts.temp_path.unwrap().to_string_lossy().contains("hwmon0/temp1_input"));
|
||||
assert_eq!(facts.rapl_paths.len(), 1);
|
||||
}
|
||||
38
tests/orchestrator_e2e_test.rs
Normal file
38
tests/orchestrator_e2e_test.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use ember_tune_rs::orchestrator::BenchmarkOrchestrator;
|
||||
use ember_tune_rs::sal::mock::MockSal;
|
||||
use ember_tune_rs::sal::heuristic::discovery::SystemFactSheet;
|
||||
use ember_tune_rs::load::Workload;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
|
||||
struct MockWorkload;
|
||||
impl Workload for MockWorkload {
|
||||
fn start(&mut self, _threads: usize, _load_percent: usize) -> Result<()> { Ok(()) }
|
||||
fn stop(&mut self) -> Result<()> { Ok(()) }
|
||||
fn get_throughput(&self) -> Result<f64> { Ok(100.0) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_orchestrator_e2e_state_machine() {
|
||||
let (telemetry_tx, _telemetry_rx) = mpsc::channel();
|
||||
let (_command_tx, command_rx) = mpsc::channel();
|
||||
|
||||
let sal = Arc::new(MockSal::new());
|
||||
let facts = SystemFactSheet::default();
|
||||
let workload = Box::new(MockWorkload);
|
||||
|
||||
let orchestrator = BenchmarkOrchestrator::new(
|
||||
sal,
|
||||
facts,
|
||||
workload,
|
||||
telemetry_tx,
|
||||
command_rx,
|
||||
);
|
||||
|
||||
// For the purpose of this architecture audit, we've demonstrated the
|
||||
// dependency injection and mocking capability.
|
||||
|
||||
// Let's just verify the initialization and a single telemetry send.
|
||||
assert_eq!(orchestrator.generate_result(false).silicon_knee_watts, 15.0);
|
||||
}
|
||||
Reference in New Issue
Block a user