use std::borrow::Cow;
use std::collections::HashMap;
use serde::Deserialize;
use std::time::Duration;
use crate::detect::Confidence;
use crate::score::carbon::DEFAULT_EMBODIED_CARBON_PER_REQUEST_GCO2;
use crate::score::cloud_energy::config::{CloudEnergyConfig, ServiceCloudConfig};
use crate::score::scaphandre::ScaphandreConfig;
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] pub struct Config {
pub n_plus_one_sql_critical_max: u32,
pub n_plus_one_http_warning_max: u32,
pub io_waste_ratio_max: f64,
pub n_plus_one_threshold: u32,
pub window_duration_ms: u64,
pub slow_query_threshold_ms: u64,
pub slow_query_min_occurrences: u32,
pub max_fanout: u32,
pub chatty_service_min_calls: u32,
pub pool_saturation_concurrent_threshold: u32,
pub serialized_min_sequential: u32,
pub green_enabled: bool,
pub green_default_region: Option<String>,
pub green_service_regions: HashMap<String, String>,
pub green_embodied_carbon_per_request_gco2: f64,
pub green_use_hourly_profiles: bool,
pub green_scaphandre: Option<ScaphandreConfig>,
pub green_cloud_energy: Option<CloudEnergyConfig>,
pub green_per_operation_coefficients: bool,
pub green_include_network_transport: bool,
pub green_network_energy_per_byte_kwh: f64,
pub green_hourly_profiles_file: Option<String>,
pub green_custom_hourly_profiles:
Option<std::sync::Arc<HashMap<String, crate::score::carbon::HourlyProfile>>>,
pub green_calibration_file: Option<String>,
pub green_calibration: Option<crate::calibrate::CalibrationData>,
pub green_electricity_maps: Option<crate::score::electricity_maps::ElectricityMapsConfig>,
pub listen_addr: String,
pub listen_port: u16,
pub listen_port_grpc: u16,
pub json_socket: String,
pub max_active_traces: usize,
pub trace_ttl_ms: u64,
pub sampling_rate: f64,
pub max_events_per_trace: usize,
pub max_payload_size: usize,
pub daemon_environment: DaemonEnvironment,
pub tls_cert_path: Option<String>,
pub tls_key_path: Option<String>,
pub max_retained_findings: usize,
pub daemon_api_enabled: bool,
pub correlation_enabled: bool,
pub correlation_config: crate::detect::correlate_cross::CorrelationConfig,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DaemonEnvironment {
#[default]
Staging,
Production,
}
impl DaemonEnvironment {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Staging => "staging",
Self::Production => "production",
}
}
}
impl Default for Config {
fn default() -> Self {
Self {
n_plus_one_sql_critical_max: 0,
n_plus_one_http_warning_max: 3,
io_waste_ratio_max: 0.30,
n_plus_one_threshold: 5,
window_duration_ms: 500,
slow_query_threshold_ms: 500,
slow_query_min_occurrences: 3,
max_fanout: 20,
chatty_service_min_calls: 15,
pool_saturation_concurrent_threshold: 10,
serialized_min_sequential: 3,
green_enabled: true,
green_default_region: None,
green_service_regions: HashMap::new(),
green_embodied_carbon_per_request_gco2: DEFAULT_EMBODIED_CARBON_PER_REQUEST_GCO2,
green_use_hourly_profiles: true,
green_scaphandre: None,
green_cloud_energy: None,
green_per_operation_coefficients: true,
green_include_network_transport: false,
green_network_energy_per_byte_kwh:
crate::score::carbon::DEFAULT_NETWORK_ENERGY_PER_BYTE_KWH,
green_hourly_profiles_file: None,
green_custom_hourly_profiles: None,
green_calibration_file: None,
green_calibration: None,
green_electricity_maps: None,
listen_addr: "127.0.0.1".to_string(),
listen_port: 4318,
listen_port_grpc: 4317,
json_socket: "/tmp/perf-sentinel.sock".to_string(),
max_active_traces: 10_000,
trace_ttl_ms: 30_000,
sampling_rate: 1.0,
max_events_per_trace: 1_000,
max_payload_size: 1_048_576, daemon_environment: DaemonEnvironment::Staging,
tls_cert_path: None,
tls_key_path: None,
max_retained_findings: 10_000,
daemon_api_enabled: true,
correlation_enabled: false,
correlation_config: crate::detect::correlate_cross::CorrelationConfig::default(),
}
}
}
impl Config {
#[must_use]
pub const fn confidence(&self) -> Confidence {
match self.daemon_environment {
DaemonEnvironment::Staging => Confidence::DaemonStaging,
DaemonEnvironment::Production => Confidence::DaemonProduction,
}
}
#[must_use]
pub fn carbon_context(&self) -> crate::score::carbon::CarbonContext {
crate::score::carbon::CarbonContext {
default_region: self.green_default_region.clone(),
service_regions: self.green_service_regions.clone(),
embodied_per_request_gco2: self.green_embodied_carbon_per_request_gco2,
use_hourly_profiles: self.green_use_hourly_profiles,
energy_snapshot: None,
per_operation_coefficients: self.green_per_operation_coefficients,
include_network_transport: self.green_include_network_transport,
network_energy_per_byte_kwh: self.green_network_energy_per_byte_kwh,
custom_hourly_profiles: self.green_custom_hourly_profiles.clone(),
calibration: self.green_calibration.clone(),
real_time_intensity: None, }
}
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct RawConfig {
thresholds: ThresholdsSection,
detection: DetectionSection,
green: GreenSection,
daemon: DaemonSection,
max_payload_size: Option<usize>,
n_plus_one_threshold: Option<u32>,
listen_addr: Option<String>,
listen_port: Option<u16>,
window_duration_ms: Option<u64>,
trace_ttl_ms: Option<u64>,
max_active_traces: Option<usize>,
max_events_per_trace: Option<usize>,
}
#[derive(Deserialize, Default)]
#[serde(default)]
#[allow(clippy::struct_field_names)] struct ThresholdsSection {
n_plus_one_sql_critical_max: Option<u32>,
n_plus_one_http_warning_max: Option<u32>,
io_waste_ratio_max: Option<f64>,
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct DetectionSection {
window_duration_ms: Option<u64>,
n_plus_one_min_occurrences: Option<u32>,
slow_query_threshold_ms: Option<u64>,
slow_query_min_occurrences: Option<u32>,
max_fanout: Option<u32>,
chatty_service_min_calls: Option<u32>,
pool_saturation_concurrent_threshold: Option<u32>,
serialized_min_sequential: Option<u32>,
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct GreenSection {
enabled: Option<bool>,
default_region: Option<String>,
service_regions: HashMap<String, String>,
embodied_carbon_per_request_gco2: Option<f64>,
use_hourly_profiles: Option<bool>,
scaphandre: ScaphandreSection,
cloud: CloudSection,
per_operation_coefficients: Option<bool>,
include_network_transport: Option<bool>,
network_energy_per_byte_kwh: Option<f64>,
hourly_profiles_file: Option<String>,
calibration_file: Option<String>,
electricity_maps: ElectricityMapsSection,
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct ScaphandreSection {
endpoint: Option<String>,
scrape_interval_secs: Option<u64>,
process_map: HashMap<String, String>,
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct CloudSection {
prometheus_endpoint: Option<String>,
scrape_interval_secs: Option<u64>,
default_provider: Option<String>,
default_instance_type: Option<String>,
cpu_metric: Option<String>,
services: HashMap<String, CloudServiceRaw>,
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct CloudServiceRaw {
provider: Option<String>,
instance_type: Option<String>,
idle_watts: Option<f64>,
max_watts: Option<f64>,
cpu_query: Option<String>,
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct ElectricityMapsSection {
api_key: Option<String>,
endpoint: Option<String>,
poll_interval_secs: Option<u64>,
region_map: HashMap<String, String>,
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct DaemonSection {
listen_address: Option<String>,
listen_port_http: Option<u16>,
listen_port_grpc: Option<u16>,
json_socket: Option<String>,
max_active_traces: Option<usize>,
trace_ttl_ms: Option<u64>,
sampling_rate: Option<f64>,
max_events_per_trace: Option<usize>,
max_payload_size: Option<usize>,
environment: Option<String>,
tls_cert_path: Option<String>,
tls_key_path: Option<String>,
max_retained_findings: Option<usize>,
api_enabled: Option<bool>,
correlation: CorrelationSection,
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct CorrelationSection {
enabled: Option<bool>,
window_minutes: Option<u64>,
lag_threshold_ms: Option<u64>,
min_co_occurrences: Option<u32>,
min_confidence: Option<f64>,
max_tracked_pairs: Option<usize>,
}
const TOML_PATH_STRING_KEYS: &[&str] = &[
"hourly_profiles_file",
"calibration_file",
"json_socket",
"tls_cert_path",
"tls_key_path",
];
fn normalize_toml_path_strings(content: &str) -> Cow<'_, str> {
let mut changed = false;
let mut normalized = String::with_capacity(content.len());
for line in content.split_inclusive('\n') {
let rewritten = normalize_toml_path_line(line);
changed |= matches!(rewritten, Cow::Owned(_));
normalized.push_str(rewritten.as_ref());
}
if changed {
Cow::Owned(normalized)
} else {
Cow::Borrowed(content)
}
}
fn normalize_toml_path_line(line: &str) -> Cow<'_, str> {
let leading_ws = line.len() - line.trim_start_matches([' ', '\t']).len();
let trimmed = &line[leading_ws..];
let Some(eq_idx) = trimmed.find('=') else {
return Cow::Borrowed(line);
};
let key = trimmed[..eq_idx].trim();
if !TOML_PATH_STRING_KEYS.contains(&key) {
return Cow::Borrowed(line);
}
let after_eq = &trimmed[eq_idx + 1..];
let value_ws = after_eq.len() - after_eq.trim_start_matches([' ', '\t']).len();
let value_start = leading_ws + eq_idx + 1 + value_ws;
let value = &line[value_start..];
if !value.starts_with('"') {
return Cow::Borrowed(line);
}
let Some(closing_quote) = find_basic_string_end(value) else {
return Cow::Borrowed(line);
};
let inner = &value[1..closing_quote];
let Cow::Owned(normalized_inner) = escape_toml_path_backslashes(inner) else {
return Cow::Borrowed(line);
};
let mut out =
String::with_capacity(line.len() + normalized_inner.len().saturating_sub(inner.len()));
out.push_str(&line[..value_start]);
out.push('"');
out.push_str(&normalized_inner);
out.push_str(&value[closing_quote..]);
Cow::Owned(out)
}
fn find_basic_string_end(value: &str) -> Option<usize> {
debug_assert!(value.starts_with('"'));
let bytes = value.as_bytes();
let mut run: usize = 0;
let mut idx = 1;
while idx < bytes.len() {
match bytes[idx] {
b'"' if run.is_multiple_of(2) => return Some(idx),
b'\\' => run += 1,
_ => run = 0,
}
idx += 1;
}
None
}
fn escape_toml_path_backslashes(inner: &str) -> Cow<'_, str> {
if !inner.contains('\\') {
return Cow::Borrowed(inner);
}
let bytes = inner.as_bytes();
let mut out = String::with_capacity(inner.len() + 4);
let mut changed = false;
let mut idx = 0;
while idx < bytes.len() {
if bytes[idx] != b'\\' {
idx = copy_until_backslash(inner, bytes, idx, &mut out);
continue;
}
let run_start = idx;
idx = skip_backslash_run(bytes, idx);
let run_len = idx - run_start;
let emit_len = backslash_emit_len(run_start, run_len, bytes.get(idx).copied());
changed |= emit_len != run_len;
for _ in 0..emit_len {
out.push('\\');
}
}
if changed {
Cow::Owned(out)
} else {
Cow::Borrowed(inner)
}
}
fn copy_until_backslash(inner: &str, bytes: &[u8], start: usize, out: &mut String) -> usize {
let mut idx = start;
while idx < bytes.len() && bytes[idx] != b'\\' {
idx += 1;
}
out.push_str(&inner[start..idx]);
idx
}
fn skip_backslash_run(bytes: &[u8], start: usize) -> usize {
let mut idx = start;
while idx < bytes.len() && bytes[idx] == b'\\' {
idx += 1;
}
idx
}
fn backslash_emit_len(run_start: usize, run_len: usize, next_byte: Option<u8>) -> usize {
let raw_unc_prefix = run_start == 0 && run_len == 2 && next_byte != Some(b'\\');
if raw_unc_prefix {
4
} else if run_len == 1 {
2
} else {
run_len
}
}
impl From<RawConfig> for Config {
#[allow(clippy::too_many_lines)] fn from(raw: RawConfig) -> Self {
let defaults = Self::default();
Self {
n_plus_one_sql_critical_max: raw
.thresholds
.n_plus_one_sql_critical_max
.unwrap_or(defaults.n_plus_one_sql_critical_max),
n_plus_one_http_warning_max: raw
.thresholds
.n_plus_one_http_warning_max
.unwrap_or(defaults.n_plus_one_http_warning_max),
io_waste_ratio_max: raw
.thresholds
.io_waste_ratio_max
.unwrap_or(defaults.io_waste_ratio_max),
n_plus_one_threshold: raw
.detection
.n_plus_one_min_occurrences
.or(raw.n_plus_one_threshold)
.unwrap_or(defaults.n_plus_one_threshold),
window_duration_ms: raw
.detection
.window_duration_ms
.or(raw.window_duration_ms)
.unwrap_or(defaults.window_duration_ms),
slow_query_threshold_ms: raw
.detection
.slow_query_threshold_ms
.unwrap_or(defaults.slow_query_threshold_ms),
slow_query_min_occurrences: raw
.detection
.slow_query_min_occurrences
.unwrap_or(defaults.slow_query_min_occurrences),
max_fanout: raw.detection.max_fanout.unwrap_or(defaults.max_fanout),
chatty_service_min_calls: raw
.detection
.chatty_service_min_calls
.unwrap_or(defaults.chatty_service_min_calls),
pool_saturation_concurrent_threshold: raw
.detection
.pool_saturation_concurrent_threshold
.unwrap_or(defaults.pool_saturation_concurrent_threshold),
serialized_min_sequential: raw
.detection
.serialized_min_sequential
.unwrap_or(defaults.serialized_min_sequential),
green_enabled: raw.green.enabled.unwrap_or(defaults.green_enabled),
green_default_region: raw.green.default_region,
green_service_regions: raw
.green
.service_regions
.into_iter()
.map(|(k, v)| (k.to_ascii_lowercase(), v))
.collect(),
green_embodied_carbon_per_request_gco2: raw
.green
.embodied_carbon_per_request_gco2
.unwrap_or(defaults.green_embodied_carbon_per_request_gco2),
green_use_hourly_profiles: raw
.green
.use_hourly_profiles
.unwrap_or(defaults.green_use_hourly_profiles),
green_scaphandre: raw.green.scaphandre.endpoint.as_ref().map(|endpoint| {
ScaphandreConfig {
endpoint: endpoint.clone(),
scrape_interval: Duration::from_secs(
raw.green.scaphandre.scrape_interval_secs.unwrap_or(5),
),
process_map: raw.green.scaphandre.process_map.clone(),
}
}),
green_cloud_energy: convert_cloud_section(&raw.green.cloud),
green_per_operation_coefficients: raw
.green
.per_operation_coefficients
.unwrap_or(defaults.green_per_operation_coefficients),
green_include_network_transport: raw
.green
.include_network_transport
.unwrap_or(defaults.green_include_network_transport),
green_network_energy_per_byte_kwh: raw
.green
.network_energy_per_byte_kwh
.unwrap_or(defaults.green_network_energy_per_byte_kwh),
green_hourly_profiles_file: raw.green.hourly_profiles_file.clone(),
green_custom_hourly_profiles: raw.green.hourly_profiles_file.as_ref().and_then(
|path| {
if has_control_char(path) {
tracing::warn!(
"hourly_profiles_file path contains control characters, skipping"
);
return None;
}
let p = std::path::Path::new(path);
match crate::score::carbon::load_custom_profiles(p) {
Ok(profiles) => Some(std::sync::Arc::new(profiles)),
Err(e) => {
tracing::debug!(
error = %e,
"Custom hourly profiles failed to load"
);
None
}
}
},
),
green_calibration_file: raw.green.calibration_file.clone(),
green_calibration: raw.green.calibration_file.as_ref().and_then(|path| {
if has_control_char(path) {
tracing::warn!("calibration_file path contains control characters, skipping");
return None;
}
match crate::calibrate::load_calibration_file(path) {
Ok(data) => Some(data),
Err(e) => {
tracing::debug!(
error = %e,
"Calibration file failed to load"
);
None
}
}
}),
green_electricity_maps: convert_electricity_maps_section(&raw.green.electricity_maps),
listen_addr: raw
.daemon
.listen_address
.or(raw.listen_addr)
.unwrap_or(defaults.listen_addr),
listen_port: raw
.daemon
.listen_port_http
.or(raw.listen_port)
.unwrap_or(defaults.listen_port),
listen_port_grpc: raw
.daemon
.listen_port_grpc
.unwrap_or(defaults.listen_port_grpc),
json_socket: raw.daemon.json_socket.unwrap_or(defaults.json_socket),
max_active_traces: raw
.daemon
.max_active_traces
.or(raw.max_active_traces)
.unwrap_or(defaults.max_active_traces),
trace_ttl_ms: raw
.daemon
.trace_ttl_ms
.or(raw.trace_ttl_ms)
.unwrap_or(defaults.trace_ttl_ms),
sampling_rate: raw.daemon.sampling_rate.unwrap_or(defaults.sampling_rate),
max_events_per_trace: raw
.daemon
.max_events_per_trace
.or(raw.max_events_per_trace)
.unwrap_or(defaults.max_events_per_trace),
max_payload_size: raw
.daemon
.max_payload_size
.or(raw.max_payload_size)
.unwrap_or(defaults.max_payload_size),
daemon_environment: match raw.daemon.environment.as_deref() {
None => defaults.daemon_environment,
Some(s) => parse_daemon_environment(s).unwrap_or(DaemonEnvironment::Staging),
},
tls_cert_path: raw.daemon.tls_cert_path,
tls_key_path: raw.daemon.tls_key_path,
max_retained_findings: raw
.daemon
.max_retained_findings
.unwrap_or(defaults.max_retained_findings),
daemon_api_enabled: raw
.daemon
.api_enabled
.unwrap_or(defaults.daemon_api_enabled),
correlation_enabled: raw
.daemon
.correlation
.enabled
.unwrap_or(defaults.correlation_enabled),
correlation_config: {
let c = &raw.daemon.correlation;
let d = crate::detect::correlate_cross::CorrelationConfig::default();
crate::detect::correlate_cross::CorrelationConfig {
window_ms: c
.window_minutes
.map_or(d.window_ms, |m| m.saturating_mul(60_000)),
lag_threshold_ms: c.lag_threshold_ms.unwrap_or(d.lag_threshold_ms),
min_co_occurrences: c.min_co_occurrences.unwrap_or(d.min_co_occurrences),
min_confidence: c.min_confidence.unwrap_or(d.min_confidence),
max_tracked_pairs: c.max_tracked_pairs.unwrap_or(d.max_tracked_pairs),
}
},
}
}
}
fn parse_daemon_environment(value: &str) -> Option<DaemonEnvironment> {
match value.trim().to_ascii_lowercase().as_str() {
"staging" => Some(DaemonEnvironment::Staging),
"production" => Some(DaemonEnvironment::Production),
_ => None,
}
}
fn convert_cloud_section(raw: &CloudSection) -> Option<CloudEnergyConfig> {
let endpoint = raw.prometheus_endpoint.as_ref()?;
let mut services = HashMap::with_capacity(raw.services.len());
for (name, svc) in &raw.services {
let config = if svc.idle_watts.is_some() || svc.max_watts.is_some() {
ServiceCloudConfig::ManualWatts {
idle_watts: svc.idle_watts.unwrap_or(0.0),
max_watts: svc.max_watts.unwrap_or(0.0),
cpu_query: svc.cpu_query.clone(),
}
} else {
ServiceCloudConfig::InstanceType {
provider: svc.provider.clone(),
instance_type: svc.instance_type.clone().unwrap_or_default(),
cpu_query: svc.cpu_query.clone(),
}
};
services.insert(name.clone(), config);
}
Some(CloudEnergyConfig {
prometheus_endpoint: endpoint.clone(),
scrape_interval: Duration::from_secs(raw.scrape_interval_secs.unwrap_or(15)),
default_provider: raw.default_provider.clone(),
default_instance_type: raw.default_instance_type.clone(),
cpu_metric: raw.cpu_metric.clone(),
services,
})
}
fn convert_electricity_maps_section(
raw: &ElectricityMapsSection,
) -> Option<crate::score::electricity_maps::ElectricityMapsConfig> {
convert_electricity_maps_section_with_env(raw, || {
std::env::var("PERF_SENTINEL_EMAPS_TOKEN").ok()
})
}
fn convert_electricity_maps_section_with_env(
raw: &ElectricityMapsSection,
env_lookup: impl FnOnce() -> Option<String>,
) -> Option<crate::score::electricity_maps::ElectricityMapsConfig> {
let from_env = env_lookup();
let token = from_env.clone().or_else(|| raw.api_key.clone())?;
if token.is_empty() {
return None;
}
if from_env.is_none() && raw.api_key.is_some() {
tracing::warn!(
"[green.electricity_maps] api_key is set in the config file. \
Prefer the PERF_SENTINEL_EMAPS_TOKEN environment variable \
to avoid committing secrets to version control."
);
}
let poll_secs = raw.poll_interval_secs.unwrap_or(300);
let api_endpoint = raw
.endpoint
.clone()
.unwrap_or_else(|| "https://api.electricitymaps.com/v3".to_string());
Some(crate::score::electricity_maps::ElectricityMapsConfig {
api_endpoint,
auth_token: token,
poll_interval: Duration::from_secs(poll_secs),
region_map: raw
.region_map
.iter()
.map(|(k, v)| (k.to_ascii_lowercase(), v.clone()))
.collect(),
})
}
fn check_range<T: PartialOrd + std::fmt::Display>(
name: &str,
val: &T,
min: &T,
max: &T,
) -> Result<(), String> {
if val < min {
return Err(format!("{name} must be >= {min}, got {val}"));
}
if val > max {
return Err(format!("{name} must be <= {max}, got {val}"));
}
Ok(())
}
fn check_min<T: PartialOrd + std::fmt::Display>(
name: &str,
val: &T,
min: &T,
) -> Result<(), String> {
if val < min {
return Err(format!("{name} must be >= {min}, got {val}"));
}
Ok(())
}
fn warn_outside_comfort_zone<T>(
name: &str,
val: &T,
comfort_lo: &T,
comfort_hi: &T,
note_low: &str,
note_high: &str,
) where
T: PartialOrd + std::fmt::Display,
{
if val < comfort_lo {
tracing::warn!(
field = %name,
value = %val,
recommended_min = %comfort_lo,
"{name} = {val} is below the recommended floor {comfort_lo}; {note_low}"
);
} else if val > comfort_hi {
tracing::warn!(
field = %name,
value = %val,
recommended_max = %comfort_hi,
"{name} = {val} is above the recommended ceiling {comfort_hi}; {note_high}"
);
}
}
fn has_control_char(s: &str) -> bool {
s.bytes().any(|b| b < 0x20 || b == 0x7F)
}
fn validate_http_authority(url: &str, label: &str) -> Result<(), String> {
let after_scheme = url
.strip_prefix("https://")
.or_else(|| url.strip_prefix("http://"))
.unwrap_or(url);
let authority = after_scheme.split('/').next().unwrap_or(after_scheme);
if authority.is_empty() {
return Err(format!("{label} '{url}' has no host"));
}
if authority.contains('@') {
return Err(format!(
"{label} must not contain credentials (userinfo): '{url}'"
));
}
if has_control_char(authority) {
return Err(format!("{label} '{url}' contains control characters"));
}
if authority.starts_with('[') {
if let Some(bracket_end) = authority.find(']') {
let after_bracket = &authority[bracket_end + 1..];
if let Some(port_str) = after_bracket.strip_prefix(':')
&& !port_str.is_empty()
&& port_str.parse::<u16>().is_err()
{
return Err(format!("{label} '{url}' has an invalid port"));
}
}
} else if let Some(port_str) = authority.rsplit(':').next()
&& authority.contains(':')
&& port_str.parse::<u16>().is_err()
{
return Err(format!("{label} '{url}' has an invalid port"));
}
Ok(())
}
impl Config {
pub fn validate(&self) -> Result<(), String> {
self.validate_daemon_limits()?;
self.validate_detection_params()?;
self.validate_rates()?;
self.validate_listen_addr()?;
self.validate_tls()?;
self.validate_green()?;
Ok(())
}
fn validate_tls(&self) -> Result<(), String> {
match (&self.tls_cert_path, &self.tls_key_path) {
(Some(cert), Some(key)) => {
if has_control_char(cert) {
return Err("[daemon] tls_cert_path contains control characters".to_string());
}
if has_control_char(key) {
return Err("[daemon] tls_key_path contains control characters".to_string());
}
if !std::path::Path::new(cert).exists() {
return Err(format!("[daemon] tls_cert_path '{cert}' does not exist"));
}
if !std::path::Path::new(key).exists() {
return Err(format!("[daemon] tls_key_path '{key}' does not exist"));
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = std::fs::metadata(key) {
let mode = meta.permissions().mode();
if mode & 0o077 != 0 {
tracing::warn!(
"TLS key file '{key}' is readable by group/others \
(mode {mode:o}). Consider restricting to owner-only \
(chmod 600)."
);
}
}
}
tracing::info!("TLS enabled for daemon OTLP receivers (cert: {cert})");
Ok(())
}
(None, None) => Ok(()),
(Some(_), None) => {
Err("[daemon] tls_cert_path is set but tls_key_path is missing".to_string())
}
(None, Some(_)) => {
Err("[daemon] tls_key_path is set but tls_cert_path is missing".to_string())
}
}
}
fn validate_green(&self) -> Result<(), String> {
Self::validate_embodied_carbon(self.green_embodied_carbon_per_request_gco2)?;
Self::validate_default_region(self.green_default_region.as_deref())?;
Self::validate_service_regions(&self.green_service_regions)?;
if let Some(cfg) = &self.green_scaphandre {
Self::validate_scaphandre(cfg)?;
}
if let Some(cfg) = &self.green_cloud_energy {
Self::validate_cloud_energy(cfg)?;
}
Self::validate_network_energy(self.green_network_energy_per_byte_kwh)?;
self.validate_hourly_profiles_file()?;
if let Some(cfg) = &self.green_electricity_maps {
Self::validate_electricity_maps(cfg)?;
}
Ok(())
}
fn validate_embodied_carbon(value: f64) -> Result<(), String> {
if !value.is_finite() {
return Err(format!(
"embodied_carbon_per_request_gco2 must be finite, got {value}"
));
}
if value < 0.0 {
return Err(format!(
"embodied_carbon_per_request_gco2 must be >= 0.0, got {value}"
));
}
Ok(())
}
fn validate_default_region(region: Option<&str>) -> Result<(), String> {
let Some(region) = region else {
return Ok(());
};
if crate::score::carbon::is_valid_region_id(region) {
return Ok(());
}
Err(format!(
"[green] default_region '{region}' contains invalid characters; \
expected ASCII alphanumeric + '-' or '_', length 1-64"
))
}
fn validate_service_regions(map: &HashMap<String, String>) -> Result<(), String> {
const MAX_SERVICE_REGIONS: usize = 1024;
if map.len() > MAX_SERVICE_REGIONS {
return Err(format!(
"[green.service_regions] has {} entries; maximum is {MAX_SERVICE_REGIONS}",
map.len()
));
}
for (service, region) in map {
if !crate::score::carbon::is_valid_region_id(service) {
return Err(format!(
"[green.service_regions] invalid service name '{service}'; \
expected ASCII alphanumeric + '-' or '_', length 1-64"
));
}
if !crate::score::carbon::is_valid_region_id(region) {
return Err(format!(
"[green.service_regions] invalid region '{region}' for service '{service}'; \
expected ASCII alphanumeric + '-' or '_', length 1-64"
));
}
}
Ok(())
}
fn validate_network_energy(value: f64) -> Result<(), String> {
if !value.is_finite() || value < 0.0 {
return Err(format!(
"network_energy_per_byte_kwh must be finite and >= 0.0, got {value}"
));
}
Ok(())
}
fn validate_hourly_profiles_file(&self) -> Result<(), String> {
let Some(path) = &self.green_hourly_profiles_file else {
return Ok(());
};
if has_control_char(path) {
return Err("[green] hourly_profiles_file contains control characters".to_string());
}
if self.green_custom_hourly_profiles.is_none() {
return Err(format!(
"[green] hourly_profiles_file '{path}' was configured but \
failed to load. Remove the field to use embedded profiles only."
));
}
Ok(())
}
fn validate_electricity_maps(
cfg: &crate::score::electricity_maps::ElectricityMapsConfig,
) -> Result<(), String> {
if cfg.auth_token.is_empty() {
return Err(
"[green.electricity_maps] api_key or PERF_SENTINEL_EMAPS_TOKEN is required"
.to_string(),
);
}
if has_control_char(&cfg.auth_token) {
return Err(
"[green.electricity_maps] auth token contains control characters".to_string(),
);
}
validate_http_authority(&cfg.api_endpoint, "[green.electricity_maps] endpoint")?;
if cfg.api_endpoint.starts_with("http://") && !cfg.auth_token.is_empty() {
tracing::warn!(
"[green.electricity_maps] auth token will be sent over http:// \
(no TLS). Use https:// for production or set the endpoint to \
a loopback/private address if this is intentional."
);
}
let secs = cfg.poll_interval.as_secs();
check_range(
"[green.electricity_maps] poll_interval_secs",
&secs,
&60,
&86400,
)?;
if cfg.region_map.is_empty() {
return Err(
"[green.electricity_maps] region_map must contain at least one entry".to_string(),
);
}
for (region, zone) in &cfg.region_map {
if zone.is_empty() {
return Err(format!(
"[green.electricity_maps.region_map] zone for '{region}' is empty"
));
}
if has_control_char(zone)
|| zone.contains('&')
|| zone.contains('#')
|| zone.contains('=')
|| zone.contains('?')
|| zone.contains('%')
|| zone.contains(' ')
|| zone.contains('+')
{
return Err(format!(
"[green.electricity_maps.region_map] zone '{zone}' for '{region}' \
contains invalid characters"
));
}
if has_control_char(region) {
return Err(format!(
"[green.electricity_maps.region_map] region key '{region}' \
contains control characters"
));
}
}
Ok(())
}
fn validate_scaphandre(cfg: &ScaphandreConfig) -> Result<(), String> {
if cfg.endpoint.is_empty() {
return Err(
"[green.scaphandre] endpoint is required when the section is present".to_string(),
);
}
if !cfg.endpoint.starts_with("http://") && !cfg.endpoint.starts_with("https://") {
return Err(format!(
"[green.scaphandre] endpoint '{}' must start with 'http://' or 'https://'",
cfg.endpoint
));
}
validate_http_authority(&cfg.endpoint, "[green.scaphandre] endpoint")?;
let secs = cfg.scrape_interval.as_secs();
if !(1..=3600).contains(&secs) {
return Err(format!(
"[green.scaphandre] scrape_interval_secs must be in [1, 3600], got {secs}"
));
}
for (service, exe) in &cfg.process_map {
if service.is_empty() || service.len() > 256 {
return Err(format!(
"[green.scaphandre] process_map service name '{service}' must be 1-256 chars"
));
}
if has_control_char(service) {
return Err(format!(
"[green.scaphandre] process_map service name '{service}' \
contains control characters"
));
}
if exe.is_empty() || exe.len() > 256 {
return Err(format!(
"[green.scaphandre] process_map exe for service '{service}' \
must be 1-256 chars, got '{exe}'"
));
}
if has_control_char(exe) {
return Err(format!(
"[green.scaphandre] process_map exe for service '{service}' \
contains control characters"
));
}
}
Ok(())
}
fn validate_cloud_energy(cfg: &CloudEnergyConfig) -> Result<(), String> {
Self::validate_cloud_endpoint(cfg)?;
Self::validate_cloud_services(cfg)
}
fn validate_cloud_endpoint(cfg: &CloudEnergyConfig) -> Result<(), String> {
if cfg.prometheus_endpoint.is_empty() {
return Err(
"[green.cloud] prometheus_endpoint is required when the section is present"
.to_string(),
);
}
if !cfg.prometheus_endpoint.starts_with("http://")
&& !cfg.prometheus_endpoint.starts_with("https://")
{
return Err(format!(
"[green.cloud] prometheus_endpoint '{}' must start with 'http://' or 'https://'",
cfg.prometheus_endpoint
));
}
validate_http_authority(
&cfg.prometheus_endpoint,
"[green.cloud] prometheus_endpoint",
)?;
let secs = cfg.scrape_interval.as_secs();
if !(1..=3600).contains(&secs) {
return Err(format!(
"[green.cloud] scrape_interval_secs must be in [1, 3600], got {secs}"
));
}
if let Some(ref p) = cfg.default_provider
&& !matches!(p.as_str(), "aws" | "gcp" | "azure")
{
return Err(format!(
"[green.cloud] default_provider must be 'aws', 'gcp', or 'azure', got '{p}'"
));
}
if let Some(ref it) = cfg.default_instance_type
&& !crate::score::cloud_energy::table::is_known_instance_type(it)
{
tracing::warn!(
instance_type = %it,
"[green.cloud] default_instance_type is not in the embedded \
SPECpower table; the provider default watts will be used"
);
}
if let Some(ref m) = cfg.cpu_metric
&& has_control_char(m)
{
return Err("[green.cloud] cpu_metric contains control characters".to_string());
}
Ok(())
}
fn validate_cloud_services(cfg: &CloudEnergyConfig) -> Result<(), String> {
const MAX_CLOUD_SERVICES: usize = 256;
if cfg.services.len() > MAX_CLOUD_SERVICES {
return Err(format!(
"[green.cloud.services] has {} entries; maximum is {MAX_CLOUD_SERVICES}",
cfg.services.len()
));
}
for (service, svc_cfg) in &cfg.services {
Self::validate_cloud_service_name(service)?;
Self::validate_cloud_service_cpu_query(service, svc_cfg)?;
match svc_cfg {
ServiceCloudConfig::ManualWatts {
idle_watts,
max_watts,
..
} => Self::validate_manual_watts(service, *idle_watts, *max_watts)?,
ServiceCloudConfig::InstanceType {
provider,
instance_type,
..
} => Self::validate_instance_type_variant(
service,
provider.as_deref(),
instance_type,
)?,
}
}
Ok(())
}
fn validate_cloud_service_name(service: &str) -> Result<(), String> {
if service.is_empty() || service.len() > 256 {
return Err(format!(
"[green.cloud.services] service name '{service}' must be 1-256 chars"
));
}
if has_control_char(service) {
return Err(format!(
"[green.cloud.services] service name '{service}' contains control characters"
));
}
Ok(())
}
fn validate_cloud_service_cpu_query(
service: &str,
svc_cfg: &ServiceCloudConfig,
) -> Result<(), String> {
let Some(q) = svc_cfg.cpu_query() else {
return Ok(());
};
if has_control_char(q) {
return Err(format!(
"[green.cloud.services.{service}] cpu_query contains control characters"
));
}
Ok(())
}
fn validate_manual_watts(service: &str, idle_watts: f64, max_watts: f64) -> Result<(), String> {
if !idle_watts.is_finite() || idle_watts < 0.0 {
return Err(format!(
"[green.cloud.services.{service}] idle_watts must be finite and >= 0, \
got {idle_watts}"
));
}
if !max_watts.is_finite() || max_watts < 0.0 {
return Err(format!(
"[green.cloud.services.{service}] max_watts must be finite and >= 0, \
got {max_watts}"
));
}
if max_watts < idle_watts {
return Err(format!(
"[green.cloud.services.{service}] max_watts ({max_watts}) must be \
>= idle_watts ({idle_watts})"
));
}
Ok(())
}
fn validate_instance_type_variant(
service: &str,
provider: Option<&str>,
instance_type: &str,
) -> Result<(), String> {
if let Some(p) = provider
&& !matches!(p, "aws" | "gcp" | "azure")
{
return Err(format!(
"[green.cloud.services.{service}] provider must be 'aws', 'gcp', \
or 'azure', got '{p}'"
));
}
if has_control_char(instance_type) {
return Err(format!(
"[green.cloud.services.{service}] instance_type contains control characters"
));
}
if !instance_type.is_empty()
&& !crate::score::cloud_energy::table::is_known_instance_type(instance_type)
{
tracing::warn!(
service = %service,
instance_type = %instance_type,
"[green.cloud.services] instance_type is not in the embedded \
SPECpower table; provider default watts will be used"
);
}
Ok(())
}
fn validate_daemon_limits(&self) -> Result<(), String> {
check_range(
"max_payload_size",
&self.max_payload_size,
&1024,
&(100 * 1024 * 1024),
)?;
check_range("max_active_traces", &self.max_active_traces, &1, &1_000_000)?;
check_range(
"max_events_per_trace",
&self.max_events_per_trace,
&1,
&100_000,
)?;
check_range(
"max_retained_findings",
&self.max_retained_findings,
&0,
&10_000_000,
)?;
check_range("trace_ttl_ms", &self.trace_ttl_ms, &100, &3_600_000)?;
check_range("listen_port_http", &self.listen_port, &1, &65535)?;
check_range("listen_port_grpc", &self.listen_port_grpc, &1, &65535)?;
self.warn_unusual_daemon_limits();
Ok(())
}
fn warn_unusual_daemon_limits(&self) {
warn_outside_comfort_zone(
"max_payload_size",
&self.max_payload_size,
&(256 * 1024),
&(16 * 1024 * 1024),
"tiny payloads may reject legitimate OTLP batches",
"large payloads increase ingest latency and memory pressure",
);
warn_outside_comfort_zone(
"max_active_traces",
&self.max_active_traces,
&1_000,
&100_000,
"aggressive LRU eviction is likely under load",
"memory footprint grows roughly linearly with this cap",
);
warn_outside_comfort_zone(
"max_events_per_trace",
&self.max_events_per_trace,
&100,
&10_000,
"complex traces will be truncated by the per-trace ring buffer",
"very wide ring buffers rarely improve detection quality",
);
if self.max_retained_findings > 0 {
warn_outside_comfort_zone(
"max_retained_findings",
&self.max_retained_findings,
&100,
&100_000,
"old findings will be evicted before /api/findings can serve them",
"the findings store will hold a large in-memory backlog",
);
}
warn_outside_comfort_zone(
"trace_ttl_ms",
&self.trace_ttl_ms,
&1_000,
&600_000,
"TTL below 1s flushes traces before slow spans land",
"TTL above 10min keeps near-dead traces in the active set",
);
}
fn validate_detection_params(&self) -> Result<(), String> {
check_min("n_plus_one_threshold", &self.n_plus_one_threshold, &1)?;
check_min("window_duration_ms", &self.window_duration_ms, &1)?;
check_min("slow_query_threshold_ms", &self.slow_query_threshold_ms, &1)?;
check_min(
"slow_query_min_occurrences",
&self.slow_query_min_occurrences,
&1,
)?;
check_range("max_fanout", &self.max_fanout, &1, &100_000)?;
warn_outside_comfort_zone(
"max_fanout",
&self.max_fanout,
&5,
&1_000,
"very low fanout floods the findings store with noise",
"very high fanout suppresses most fan-out detections",
);
check_min(
"chatty_service_min_calls",
&self.chatty_service_min_calls,
&1,
)?;
check_min(
"pool_saturation_concurrent_threshold",
&self.pool_saturation_concurrent_threshold,
&2,
)?;
check_min(
"serialized_min_sequential",
&self.serialized_min_sequential,
&2,
)?;
Ok(())
}
fn validate_rates(&self) -> Result<(), String> {
if !(0.0..=1.0).contains(&self.sampling_rate) {
return Err(format!(
"sampling_rate must be in [0.0, 1.0], got {}",
self.sampling_rate
));
}
if !(0.0..=1.0).contains(&self.io_waste_ratio_max) {
return Err(format!(
"io_waste_ratio_max must be in [0.0, 1.0], got {}",
self.io_waste_ratio_max
));
}
Ok(())
}
#[allow(clippy::unnecessary_wraps)]
fn validate_listen_addr(&self) -> Result<(), String> {
if self.listen_addr != "127.0.0.1" && self.listen_addr != "::1" {
tracing::warn!(
"Daemon configured to listen on non-loopback address: {}. \
Endpoints have no authentication, use a reverse proxy or \
network policy for security.",
self.listen_addr
);
}
Ok(())
}
}
pub fn load_from_str(content: &str) -> Result<Config, ConfigError> {
let normalized = normalize_toml_path_strings(content);
let raw: RawConfig = match toml::from_str(normalized.as_ref()) {
Ok(raw) => raw,
Err(norm_err) => {
if matches!(normalized, Cow::Owned(_)) {
tracing::debug!(
normalized_error = %norm_err,
"path normalization produced invalid TOML; retrying with original input"
);
toml::from_str(content).map_err(ConfigError::Parse)?
} else {
return Err(ConfigError::Parse(norm_err));
}
}
};
if let Some(env_str) = raw.daemon.environment.as_deref()
&& parse_daemon_environment(env_str).is_none()
{
return Err(ConfigError::Validation(format!(
"[daemon] environment '{env_str}' is invalid; \
expected 'staging' or 'production' (case-insensitive)"
)));
}
let config = Config::from(raw);
config.validate().map_err(ConfigError::Validation)?;
Ok(config)
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ConfigError {
#[error("config parse error: {0}")]
Parse(#[from] toml::de::Error),
#[error("config validation error: {0}")]
Validation(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_has_safe_defaults() {
let config = Config::default();
assert_eq!(config.max_payload_size, 1_048_576);
assert_eq!(config.listen_addr, "127.0.0.1");
assert_eq!(config.n_plus_one_threshold, 5);
assert_eq!(config.window_duration_ms, 500);
assert_eq!(config.trace_ttl_ms, 30_000);
assert_eq!(config.max_active_traces, 10_000);
assert_eq!(config.max_events_per_trace, 1_000);
}
#[test]
fn parse_empty_toml_gives_defaults() {
let config = load_from_str("").unwrap();
assert_eq!(config.max_payload_size, 1_048_576);
}
#[test]
fn parse_partial_toml() {
let config = load_from_str("n_plus_one_threshold = 10").unwrap();
assert_eq!(config.n_plus_one_threshold, 10);
assert_eq!(config.max_payload_size, 1_048_576); }
#[test]
fn parse_window_config() {
let config = load_from_str(
"window_duration_ms = 1000\ntrace_ttl_ms = 60000\nmax_active_traces = 5000",
)
.unwrap();
assert_eq!(config.window_duration_ms, 1000);
assert_eq!(config.trace_ttl_ms, 60_000);
assert_eq!(config.max_active_traces, 5000);
}
#[test]
fn parse_sectioned_format() {
let toml = r#"
[thresholds]
n_plus_one_sql_critical_max = 2
n_plus_one_http_warning_max = 5
io_waste_ratio_max = 0.50
[detection]
window_duration_ms = 1000
n_plus_one_min_occurrences = 10
[green]
enabled = false
[daemon]
listen_address = "0.0.0.0"
listen_port_http = 9418
listen_port_grpc = 9417
json_socket = "/var/run/perf-sentinel.sock"
max_active_traces = 20000
trace_ttl_ms = 60000
sampling_rate = 0.5
max_events_per_trace = 500
max_payload_size = 2097152
"#;
let config = load_from_str(toml).unwrap();
assert_eq!(config.n_plus_one_sql_critical_max, 2);
assert_eq!(config.n_plus_one_http_warning_max, 5);
assert!((config.io_waste_ratio_max - 0.50).abs() < f64::EPSILON);
assert_eq!(config.n_plus_one_threshold, 10);
assert_eq!(config.window_duration_ms, 1000);
assert!(!config.green_enabled);
assert_eq!(config.listen_addr, "0.0.0.0");
assert_eq!(config.listen_port, 9418);
assert_eq!(config.listen_port_grpc, 9417);
assert_eq!(config.json_socket, "/var/run/perf-sentinel.sock");
assert_eq!(config.max_active_traces, 20_000);
assert_eq!(config.trace_ttl_ms, 60_000);
assert!((config.sampling_rate - 0.5).abs() < f64::EPSILON);
assert_eq!(config.max_events_per_trace, 500);
assert_eq!(config.max_payload_size, 2_097_152);
}
#[test]
fn parse_windows_style_json_socket_path_in_basic_string() {
let config = load_from_str(
r#"
[daemon]
json_socket = "C:\temp\perf-sentinel.sock"
"#,
)
.unwrap();
assert_eq!(config.json_socket, r"C:\temp\perf-sentinel.sock");
}
#[test]
fn parse_escaped_windows_style_json_socket_path_stays_stable() {
let config = load_from_str(
r#"
[daemon]
json_socket = "C:\\temp\\perf-sentinel.sock"
"#,
)
.unwrap();
assert_eq!(config.json_socket, r"C:\temp\perf-sentinel.sock");
}
#[test]
fn parse_windows_style_json_socket_path_with_trailing_comment() {
let config = load_from_str(
"[daemon]\n\
json_socket = \"C:\\temp\\sock\" # inline note\n",
)
.unwrap();
assert_eq!(config.json_socket, r"C:\temp\sock");
}
#[test]
fn parse_unc_json_socket_path_preserves_double_leading_backslash() {
let config = load_from_str(
r#"
[daemon]
json_socket = "\\server\share\sock"
"#,
)
.unwrap();
assert_eq!(config.json_socket, r"\\server\share\sock");
}
#[test]
fn parse_pre_escaped_unc_json_socket_path_is_stable() {
let config = load_from_str(
r#"
[daemon]
json_socket = "\\\\server\\share\\sock"
"#,
)
.unwrap();
assert_eq!(config.json_socket, r"\\server\share\sock");
}
#[test]
fn literal_string_windows_path_bypasses_normalization() {
let config = load_from_str(
r"
[daemon]
json_socket = 'C:\temp\sock'
",
)
.unwrap();
assert_eq!(config.json_socket, r"C:\temp\sock");
}
#[test]
fn normalization_applies_to_tls_cert_and_key_paths() {
let err = load_from_str(
r#"
[daemon]
tls_cert_path = "C:\certs\server.crt"
tls_key_path = "C:\certs\server.key"
"#,
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains(r"C:\certs\server.crt") || msg.contains(r"C:\certs\server.key"),
"expected normalized TLS path in error, got: {msg}"
);
}
#[test]
fn normalization_applies_to_all_registered_path_keys() {
for key in TOML_PATH_STRING_KEYS {
let line = format!("{key} = \"C:\\temp\\x\"\n");
let rewritten = normalize_toml_path_strings(&line);
assert!(
matches!(rewritten, Cow::Owned(_)),
"{key}: expected normalization to rewrite bare Windows path"
);
assert!(
rewritten.as_ref().contains(r#""C:\\temp\\x""#),
"{key}: normalized output missing escaped path, got {rewritten}"
);
}
}
#[test]
fn normalization_leaves_toml_escape_sequences_literal_in_path_keys() {
let config = load_from_str(
r#"
[daemon]
json_socket = "C:\new\tmp\sock"
"#,
)
.unwrap();
assert_eq!(config.json_socket, r"C:\new\tmp\sock");
}
#[test]
fn load_from_str_falls_back_when_original_error_is_unrelated_to_path() {
let err = load_from_str(
r#"
[daemon]
json_socket = "C:\temp\sock"
sampling_rate = "not a number"
"#,
)
.unwrap_err();
assert!(
matches!(err, ConfigError::Parse(_)),
"expected ConfigError::Parse, got {err:?}"
);
}
#[test]
fn find_basic_string_end_handles_escaped_inner_quote() {
let value = r#""a\"b""#;
assert_eq!(find_basic_string_end(value), Some(5));
}
#[test]
fn find_basic_string_end_survives_very_long_backslash_run() {
let mut input = String::from("\"");
input.extend(std::iter::repeat_n('\\', 10_000));
input.push('"');
assert_eq!(find_basic_string_end(&input), Some(10_001));
}
#[test]
fn section_overrides_flat_field() {
let toml = r"
n_plus_one_threshold = 7
window_duration_ms = 800
[detection]
n_plus_one_min_occurrences = 12
";
let config = load_from_str(toml).unwrap();
assert_eq!(config.n_plus_one_threshold, 12);
assert_eq!(config.window_duration_ms, 800);
}
#[test]
fn new_fields_have_correct_defaults() {
let config = Config::default();
assert_eq!(config.n_plus_one_sql_critical_max, 0);
assert_eq!(config.n_plus_one_http_warning_max, 3);
assert!((config.io_waste_ratio_max - 0.30).abs() < f64::EPSILON);
assert!(config.green_enabled);
assert_eq!(config.listen_port_grpc, 4317);
assert_eq!(config.json_socket, "/tmp/perf-sentinel.sock");
assert!((config.sampling_rate - 1.0).abs() < f64::EPSILON);
}
#[test]
fn default_config_validates() {
let config = Config::default();
assert!(config.validate().is_ok());
}
#[test]
fn rejects_sampling_rate_above_one() {
let result = load_from_str("[daemon]\nsampling_rate = 5.0");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("sampling_rate"), "got: {err}");
}
#[test]
fn rejects_negative_sampling_rate() {
let result = load_from_str("[daemon]\nsampling_rate = -0.1");
assert!(result.is_err());
}
#[test]
fn rejects_io_waste_ratio_max_above_one() {
let result = load_from_str("[thresholds]\nio_waste_ratio_max = 1.5");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("io_waste_ratio_max"), "got: {err}");
}
#[test]
fn rejects_zero_max_payload_size() {
let result = load_from_str("[daemon]\nmax_payload_size = 0");
assert!(result.is_err());
}
#[test]
fn rejects_zero_n_plus_one_threshold() {
let result = load_from_str("n_plus_one_threshold = 0");
assert!(result.is_err());
}
#[test]
fn rejects_zero_max_active_traces() {
let result = load_from_str("max_active_traces = 0");
assert!(result.is_err());
}
#[test]
fn rejects_zero_max_events_per_trace() {
let result = load_from_str("max_events_per_trace = 0");
assert!(result.is_err());
}
#[test]
fn slow_query_defaults() {
let config = Config::default();
assert_eq!(config.slow_query_threshold_ms, 500);
assert_eq!(config.slow_query_min_occurrences, 3);
assert!(config.green_default_region.is_none());
assert!(config.green_service_regions.is_empty());
assert!(
(config.green_embodied_carbon_per_request_gco2
- DEFAULT_EMBODIED_CARBON_PER_REQUEST_GCO2)
.abs()
< f64::EPSILON
);
}
#[test]
fn parse_slow_query_config() {
let toml = r"
[detection]
slow_query_threshold_ms = 1000
slow_query_min_occurrences = 5
";
let config = load_from_str(toml).unwrap();
assert_eq!(config.slow_query_threshold_ms, 1000);
assert_eq!(config.slow_query_min_occurrences, 5);
}
#[test]
fn parse_green_default_region() {
let toml = r#"
[green]
enabled = true
default_region = "eu-west-3"
"#;
let config = load_from_str(toml).unwrap();
assert_eq!(config.green_default_region.as_deref(), Some("eu-west-3"));
}
#[test]
fn parse_green_service_regions() {
let toml = r#"
[green]
enabled = true
default_region = "eu-west-3"
[green.service_regions]
"order-svc" = "us-east-1"
"chat-svc" = "ap-southeast-1"
"#;
let config = load_from_str(toml).unwrap();
assert_eq!(config.green_service_regions.len(), 2);
assert_eq!(
config
.green_service_regions
.get("order-svc")
.map(String::as_str),
Some("us-east-1")
);
assert_eq!(
config
.green_service_regions
.get("chat-svc")
.map(String::as_str),
Some("ap-southeast-1")
);
}
#[test]
fn parse_green_embodied_carbon_override() {
let toml = r"
[green]
enabled = true
embodied_carbon_per_request_gco2 = 0.005
";
let config = load_from_str(toml).unwrap();
assert!((config.green_embodied_carbon_per_request_gco2 - 0.005).abs() < f64::EPSILON);
}
#[test]
fn rejects_negative_embodied_carbon() {
let result = load_from_str("[green]\nembodied_carbon_per_request_gco2 = -0.001");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("embodied_carbon_per_request_gco2"),
"got: {err}"
);
}
#[test]
fn accepts_zero_embodied_carbon() {
let toml = r"
[green]
embodied_carbon_per_request_gco2 = 0.0
";
let config = load_from_str(toml).unwrap();
assert!((config.green_embodied_carbon_per_request_gco2 - 0.0).abs() < f64::EPSILON);
}
#[test]
fn empty_service_regions_default() {
let toml = r#"
[green]
default_region = "eu-west-3"
"#;
let config = load_from_str(toml).unwrap();
assert!(config.green_service_regions.is_empty());
}
#[test]
fn rejects_invalid_default_region_characters() {
let result = load_from_str("[green]\ndefault_region = \"eu west 3\"");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("default_region"),
"error should mention default_region, got: {err}"
);
}
#[test]
fn rejects_oversized_default_region() {
let long_region = "a".repeat(65);
let toml = format!("[green]\ndefault_region = \"{long_region}\"");
let result = load_from_str(&toml);
assert!(result.is_err());
}
#[test]
fn rejects_default_region_with_newline_escape() {
let result = load_from_str("[green]\ndefault_region = \"eu-west-3\\n\"");
assert!(result.is_err());
assert!(
result.unwrap_err().to_string().contains("default_region"),
"error should mention default_region"
);
}
#[test]
fn rejects_default_region_with_literal_newline() {
let result = load_from_str("[green]\ndefault_region = \"\"\"eu-west-3\n\"\"\"");
assert!(result.is_err());
}
#[test]
fn accepts_known_regions() {
for region in ["eu-west-3", "us-east-1", "fr", "mars-1", "unknown"] {
let toml = format!("[green]\ndefault_region = \"{region}\"");
let config = load_from_str(&toml)
.unwrap_or_else(|e| panic!("region '{region}' should be accepted, got error: {e}"));
assert_eq!(config.green_default_region.as_deref(), Some(region));
}
}
#[test]
fn rejects_invalid_service_regions_service_name() {
let toml = r#"
[green.service_regions]
"bad service" = "us-east-1"
"#;
let result = load_from_str(toml);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("service_regions"),
"error should mention service_regions, got: {err}"
);
}
#[test]
fn rejects_invalid_service_regions_region_value() {
let toml = r#"
[green.service_regions]
"order-svc" = "us east 1"
"#;
let result = load_from_str(toml);
assert!(result.is_err());
}
#[test]
fn rejects_oversized_service_regions_map() {
use std::fmt::Write as _;
let mut toml = String::from("[green.service_regions]\n");
for i in 0..1025 {
let _ = writeln!(toml, "\"svc-{i:04}\" = \"eu-west-3\"");
}
let result = load_from_str(&toml);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("service_regions") && err.contains("1025"),
"error should mention service_regions and the count, got: {err}"
);
}
#[test]
fn accepts_service_regions_at_exactly_the_cap() {
use std::fmt::Write as _;
let mut toml = String::from("[green.service_regions]\n");
for i in 0..1024 {
let _ = writeln!(toml, "\"svc-{i:04}\" = \"eu-west-3\"");
}
let config = load_from_str(&toml).expect("1024 entries should be accepted");
assert_eq!(config.green_service_regions.len(), 1024);
}
#[test]
fn service_regions_keys_are_lowercased_on_load() {
let toml = r#"
[green.service_regions]
"Order-Svc" = "us-east-1"
"CHAT-SVC" = "ap-southeast-1"
"#;
let config = load_from_str(toml).unwrap();
assert_eq!(config.green_service_regions.len(), 2);
assert_eq!(
config
.green_service_regions
.get("order-svc")
.map(String::as_str),
Some("us-east-1")
);
assert_eq!(
config
.green_service_regions
.get("chat-svc")
.map(String::as_str),
Some("ap-southeast-1")
);
assert!(!config.green_service_regions.contains_key("Order-Svc"));
}
#[test]
fn rejects_zero_slow_query_threshold() {
let result = load_from_str("[detection]\nslow_query_threshold_ms = 0");
assert!(result.is_err());
}
#[test]
fn rejects_zero_slow_query_min_occurrences() {
let result = load_from_str("[detection]\nslow_query_min_occurrences = 0");
assert!(result.is_err());
}
#[test]
fn rejects_zero_max_fanout() {
let result = load_from_str("[detection]\nmax_fanout = 0");
assert!(result.is_err());
}
#[test]
fn rejects_max_fanout_over_100k() {
let result = load_from_str("[detection]\nmax_fanout = 100001");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("max_fanout"), "got: {err}");
}
#[test]
fn accepts_max_fanout_at_100k() {
let result = load_from_str("[detection]\nmax_fanout = 100000");
assert!(result.is_ok());
}
#[test]
fn rejects_max_payload_size_over_100mb() {
let result = load_from_str("[daemon]\nmax_payload_size = 104857601");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("max_payload_size"), "got: {err}");
}
#[test]
fn accepts_max_payload_size_at_100mb() {
let result = load_from_str("[daemon]\nmax_payload_size = 104857600");
assert!(result.is_ok());
}
#[test]
fn rejects_max_active_traces_over_1m() {
let result = load_from_str("[daemon]\nmax_active_traces = 1000001");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("max_active_traces"), "got: {err}");
}
#[test]
fn accepts_max_active_traces_at_1m() {
let result = load_from_str("[daemon]\nmax_active_traces = 1000000");
assert!(result.is_ok());
}
#[test]
fn rejects_max_events_per_trace_over_100k() {
let result = load_from_str("[daemon]\nmax_events_per_trace = 100001");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("max_events_per_trace"), "got: {err}");
}
#[test]
fn accepts_max_events_per_trace_at_100k() {
let result = load_from_str("[daemon]\nmax_events_per_trace = 100000");
assert!(result.is_ok());
}
#[test]
fn config_defaults_sit_inside_every_comfort_zone() {
let cfg = Config::default();
assert!(
(256 * 1024..=16 * 1024 * 1024).contains(&cfg.max_payload_size),
"default max_payload_size {} is outside its comfort zone",
cfg.max_payload_size
);
assert!(
(1_000..=100_000).contains(&cfg.max_active_traces),
"default max_active_traces {} is outside its comfort zone",
cfg.max_active_traces
);
assert!(
(100..=10_000).contains(&cfg.max_events_per_trace),
"default max_events_per_trace {} is outside its comfort zone",
cfg.max_events_per_trace
);
assert!(
(100..=100_000).contains(&cfg.max_retained_findings),
"default max_retained_findings {} is outside its comfort zone",
cfg.max_retained_findings
);
assert!(
(1_000..=600_000).contains(&cfg.trace_ttl_ms),
"default trace_ttl_ms {} is outside its comfort zone",
cfg.trace_ttl_ms
);
assert!(
(5..=1_000).contains(&cfg.max_fanout),
"default max_fanout {} is outside its comfort zone",
cfg.max_fanout
);
}
#[test]
fn accepts_max_active_traces_below_comfort_floor_with_warning() {
let result = load_from_str("[daemon]\nmax_active_traces = 500");
assert!(result.is_ok(), "expected parse OK, got {result:?}");
}
#[test]
fn accepts_max_active_traces_above_comfort_ceiling_with_warning() {
let result = load_from_str("[daemon]\nmax_active_traces = 500000");
assert!(result.is_ok(), "expected parse OK, got {result:?}");
}
#[test]
fn accepts_max_events_per_trace_outside_comfort_zone() {
for value in [10, 50_000] {
let result = load_from_str(&format!("[daemon]\nmax_events_per_trace = {value}\n"));
assert!(result.is_ok(), "expected {value} to parse, got {result:?}");
}
}
#[test]
fn accepts_trace_ttl_outside_comfort_but_inside_hard_bounds() {
for value in [200_u64, 1_800_000_u64] {
let result = load_from_str(&format!("[daemon]\ntrace_ttl_ms = {value}\n"));
assert!(result.is_ok(), "expected {value} to parse, got {result:?}");
}
}
#[test]
fn accepts_max_fanout_outside_comfort_but_inside_hard_bounds() {
for value in [2, 5_000] {
let result = load_from_str(&format!("[detection]\nmax_fanout = {value}\n"));
assert!(result.is_ok(), "expected {value} to parse, got {result:?}");
}
}
#[test]
fn accepts_max_payload_size_outside_comfort_but_inside_hard_bounds() {
for value in [64 * 1024_u64, 32 * 1024 * 1024_u64] {
let result = load_from_str(&format!("[daemon]\nmax_payload_size = {value}\n"));
assert!(result.is_ok(), "expected {value} to parse, got {result:?}");
}
}
#[test]
fn accepts_zero_max_retained_findings_disables_store() {
let result = load_from_str("[daemon]\nmax_retained_findings = 0");
assert!(result.is_ok(), "expected 0 to parse, got {result:?}");
}
#[test]
fn rejects_max_retained_findings_above_10m() {
let result = load_from_str("[daemon]\nmax_retained_findings = 10000001");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("max_retained_findings"), "got: {err}");
}
#[test]
fn accepts_max_retained_findings_at_10m_hard_ceiling() {
let result = load_from_str("[daemon]\nmax_retained_findings = 10000000");
assert!(result.is_ok());
}
#[test]
fn accepts_max_retained_findings_outside_comfort_but_inside_hard_bounds() {
for value in [50, 500_000] {
let result = load_from_str(&format!("[daemon]\nmax_retained_findings = {value}\n"));
assert!(result.is_ok(), "expected {value} to parse, got {result:?}");
}
}
#[test]
fn rejects_trace_ttl_below_100() {
let result = load_from_str("[daemon]\ntrace_ttl_ms = 50");
assert!(result.is_err());
}
#[test]
fn rejects_zero_window_duration() {
let result = load_from_str("[detection]\nwindow_duration_ms = 0");
assert!(result.is_err());
}
#[test]
fn green_disabled_parses() {
let config = load_from_str("[green]\nenabled = false").unwrap();
assert!(!config.green_enabled);
}
#[test]
fn rejects_port_zero() {
let result = load_from_str("[daemon]\nlisten_port_http = 0");
assert!(result.is_err());
}
#[test]
fn accepts_port_one() {
let config = load_from_str("[daemon]\nlisten_port_http = 1").unwrap();
assert_eq!(config.listen_port, 1);
}
#[test]
fn accepts_port_65535() {
let config = load_from_str("[daemon]\nlisten_port_http = 65535").unwrap();
assert_eq!(config.listen_port, 65535);
}
#[test]
fn rejects_grpc_port_zero() {
let result = load_from_str("[daemon]\nlisten_port_grpc = 0");
assert!(result.is_err());
}
#[test]
fn rejects_trace_ttl_above_1h() {
let result = load_from_str("[daemon]\ntrace_ttl_ms = 3600001");
assert!(result.is_err());
}
#[test]
fn accepts_trace_ttl_at_1h() {
let config = load_from_str("[daemon]\ntrace_ttl_ms = 3600000").unwrap();
assert_eq!(config.trace_ttl_ms, 3_600_000);
}
#[test]
fn accepts_trace_ttl_at_100ms() {
let config = load_from_str("[daemon]\ntrace_ttl_ms = 100").unwrap();
assert_eq!(config.trace_ttl_ms, 100);
}
#[test]
fn accepts_sampling_rate_zero() {
let config = load_from_str("[daemon]\nsampling_rate = 0.0").unwrap();
assert!((config.sampling_rate - 0.0).abs() < f64::EPSILON);
}
#[test]
fn accepts_sampling_rate_one() {
let config = load_from_str("[daemon]\nsampling_rate = 1.0").unwrap();
assert!((config.sampling_rate - 1.0).abs() < f64::EPSILON);
}
#[test]
fn daemon_environment_defaults_to_staging() {
let config = Config::default();
assert_eq!(config.daemon_environment, DaemonEnvironment::Staging);
assert_eq!(config.confidence(), Confidence::DaemonStaging);
}
#[test]
fn daemon_environment_omitted_uses_default() {
let config = load_from_str("[daemon]\nmax_active_traces = 100").unwrap();
assert_eq!(config.daemon_environment, DaemonEnvironment::Staging);
}
#[test]
fn daemon_environment_staging() {
let config = load_from_str("[daemon]\nenvironment = \"staging\"").unwrap();
assert_eq!(config.daemon_environment, DaemonEnvironment::Staging);
assert_eq!(config.confidence(), Confidence::DaemonStaging);
}
#[test]
fn daemon_environment_production() {
let config = load_from_str("[daemon]\nenvironment = \"production\"").unwrap();
assert_eq!(config.daemon_environment, DaemonEnvironment::Production);
assert_eq!(config.confidence(), Confidence::DaemonProduction);
}
#[test]
fn daemon_environment_case_insensitive() {
let config = load_from_str("[daemon]\nenvironment = \"PRODUCTION\"").unwrap();
assert_eq!(config.daemon_environment, DaemonEnvironment::Production);
let config = load_from_str("[daemon]\nenvironment = \"Staging\"").unwrap();
assert_eq!(config.daemon_environment, DaemonEnvironment::Staging);
}
#[test]
fn daemon_environment_rejects_unknown() {
let result = load_from_str("[daemon]\nenvironment = \"prod\"");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("environment"), "got: {err}");
assert!(err.contains("staging"), "error should mention valid values");
assert!(
err.contains("production"),
"error should mention valid values"
);
}
#[test]
fn daemon_environment_rejects_empty() {
let result = load_from_str("[daemon]\nenvironment = \"\"");
assert!(result.is_err());
}
#[test]
fn daemon_environment_rejects_dev() {
let result = load_from_str("[daemon]\nenvironment = \"dev\"");
assert!(result.is_err());
}
#[test]
fn daemon_environment_as_str() {
assert_eq!(DaemonEnvironment::Staging.as_str(), "staging");
assert_eq!(DaemonEnvironment::Production.as_str(), "production");
}
#[test]
fn green_use_hourly_profiles_defaults_to_true() {
let config = Config::default();
assert!(config.green_use_hourly_profiles);
}
#[test]
fn green_use_hourly_profiles_omitted_uses_default() {
let config = load_from_str("[green]\nenabled = true\n").unwrap();
assert!(config.green_use_hourly_profiles);
}
#[test]
fn green_use_hourly_profiles_explicit_false() {
let config = load_from_str("[green]\nuse_hourly_profiles = false\n").unwrap();
assert!(!config.green_use_hourly_profiles);
}
#[test]
fn green_use_hourly_profiles_explicit_true() {
let config = load_from_str("[green]\nuse_hourly_profiles = true\n").unwrap();
assert!(config.green_use_hourly_profiles);
}
#[test]
fn hourly_profiles_file_absent_by_default() {
let config = Config::default();
assert!(config.green_hourly_profiles_file.is_none());
assert!(config.green_custom_hourly_profiles.is_none());
}
#[test]
fn hourly_profiles_file_control_chars_rejected() {
let config = load_from_str("[green]\nhourly_profiles_file = \"/tmp/profiles\\n.json\"\n");
if let Ok(c) = config {
let err = c.validate().unwrap_err();
assert!(
err.contains("control characters") || err.contains("failed to load"),
"expected control char or load failure error, got: {err}"
);
} else {
}
}
#[test]
fn hourly_profiles_file_nonexistent_path_rejected() {
let result =
load_from_str("[green]\nhourly_profiles_file = \"/nonexistent/profiles.json\"\n");
let err = result.unwrap_err();
assert!(
format!("{err}").contains("failed to load"),
"expected load failure error, got: {err}"
);
}
#[test]
fn hourly_profiles_windows_path_reports_load_failure_not_parse_error() {
let err = load_from_str(
r#"
[green]
hourly_profiles_file = "C:\temp\profiles.json"
"#,
)
.unwrap_err();
assert!(
format!("{err}").contains("failed to load"),
"expected load failure error, got: {err}"
);
}
#[test]
fn scaphandre_absent_by_default() {
let config = Config::default();
assert!(config.green_scaphandre.is_none());
}
#[test]
fn scaphandre_empty_section_parses_to_none() {
let config = load_from_str("[green.scaphandre]\n").unwrap();
assert!(config.green_scaphandre.is_none());
}
#[test]
fn scaphandre_endpoint_only() {
let config =
load_from_str("[green.scaphandre]\nendpoint = \"http://localhost:8080/metrics\"\n")
.unwrap();
let cfg = config.green_scaphandre.unwrap();
assert_eq!(cfg.endpoint, "http://localhost:8080/metrics");
assert_eq!(cfg.scrape_interval.as_secs(), 5);
assert!(cfg.process_map.is_empty());
}
#[test]
fn scaphandre_full_config() {
let toml = r#"
[green.scaphandre]
endpoint = "http://localhost:9090/metrics"
scrape_interval_secs = 10
[green.scaphandre.process_map]
"order-svc" = "java"
"chat-svc" = "dotnet"
"#;
let config = load_from_str(toml).unwrap();
let cfg = config.green_scaphandre.unwrap();
assert_eq!(cfg.endpoint, "http://localhost:9090/metrics");
assert_eq!(cfg.scrape_interval.as_secs(), 10);
assert_eq!(
cfg.process_map.get("order-svc").map(String::as_str),
Some("java")
);
assert_eq!(
cfg.process_map.get("chat-svc").map(String::as_str),
Some("dotnet")
);
}
#[test]
fn scaphandre_accepts_https_endpoint() {
let result =
load_from_str("[green.scaphandre]\nendpoint = \"https://secure:8080/metrics\"\n");
assert!(result.is_ok(), "HTTPS endpoints should be accepted");
}
#[test]
fn scaphandre_rejects_zero_interval() {
let result = load_from_str(
"[green.scaphandre]\nendpoint = \"http://localhost/metrics\"\nscrape_interval_secs = 0\n",
);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("scrape_interval_secs"), "got: {err}");
}
#[test]
fn scaphandre_rejects_huge_interval() {
let result = load_from_str(
"[green.scaphandre]\nendpoint = \"http://localhost/metrics\"\nscrape_interval_secs = 99999\n",
);
assert!(result.is_err());
}
#[test]
fn scaphandre_rejects_empty_exe_in_process_map() {
let toml = r#"
[green.scaphandre]
endpoint = "http://localhost/metrics"
[green.scaphandre.process_map]
"order-svc" = ""
"#;
let result = load_from_str(toml);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("process_map"), "got: {err}");
}
#[test]
fn scaphandre_accepts_interval_at_boundary_1s() {
let config = load_from_str(
"[green.scaphandre]\nendpoint = \"http://localhost/metrics\"\nscrape_interval_secs = 1\n",
)
.unwrap();
assert_eq!(
config
.green_scaphandre
.as_ref()
.unwrap()
.scrape_interval
.as_secs(),
1
);
}
#[test]
fn scaphandre_accepts_interval_at_boundary_3600s() {
let config = load_from_str(
"[green.scaphandre]\nendpoint = \"http://localhost/metrics\"\nscrape_interval_secs = 3600\n",
)
.unwrap();
assert_eq!(
config
.green_scaphandre
.as_ref()
.unwrap()
.scrape_interval
.as_secs(),
3600
);
}
#[test]
fn cloud_section_absent_yields_none() {
let toml = "[green]\nenabled = true\n";
let cfg: Config = toml::from_str::<RawConfig>(toml).unwrap().into();
assert!(cfg.green_cloud_energy.is_none());
}
#[test]
fn cloud_section_endpoint_only_parses_with_defaults() {
let toml = r#"
[green.cloud]
prometheus_endpoint = "http://prom:9090"
"#;
let cfg: Config = toml::from_str::<RawConfig>(toml).unwrap().into();
let cloud = cfg.green_cloud_energy.unwrap();
assert_eq!(cloud.prometheus_endpoint, "http://prom:9090");
assert_eq!(cloud.scrape_interval.as_secs(), 15);
assert!(cloud.default_provider.is_none());
assert!(cloud.services.is_empty());
}
#[test]
fn cloud_section_full_config_with_both_service_types() {
let toml = r#"
[green.cloud]
prometheus_endpoint = "http://prom:9090"
scrape_interval_secs = 30
default_provider = "aws"
[green.cloud.services.svc-a]
provider = "gcp"
instance_type = "n2-standard-8"
[green.cloud.services.svc-b]
idle_watts = 45
max_watts = 120
"#;
let cfg: Config = toml::from_str::<RawConfig>(toml).unwrap().into();
assert!(cfg.validate().is_ok());
let cloud = cfg.green_cloud_energy.as_ref().unwrap();
assert_eq!(cloud.scrape_interval.as_secs(), 30);
assert_eq!(cloud.default_provider.as_deref(), Some("aws"));
assert_eq!(cloud.services.len(), 2);
}
#[test]
fn cloud_accepts_https_endpoint() {
let toml = r#"
[green.cloud]
prometheus_endpoint = "https://prom:9090"
"#;
let cfg: Config = toml::from_str::<RawConfig>(toml).unwrap().into();
assert!(cfg.validate().is_ok(), "HTTPS endpoints should be accepted");
}
#[test]
fn cloud_rejects_credentials_in_endpoint() {
let toml = r#"
[green.cloud]
prometheus_endpoint = "http://user:pass@prom:9090"
"#;
let cfg: Config = toml::from_str::<RawConfig>(toml).unwrap().into();
let err = cfg.validate().unwrap_err();
assert!(err.contains("credentials"), "error: {err}");
}
#[test]
fn cloud_rejects_invalid_scrape_interval() {
let toml = r#"
[green.cloud]
prometheus_endpoint = "http://prom:9090"
scrape_interval_secs = 0
"#;
let cfg: Config = toml::from_str::<RawConfig>(toml).unwrap().into();
let err = cfg.validate().unwrap_err();
assert!(err.contains("scrape_interval"), "error: {err}");
}
#[test]
fn cloud_rejects_invalid_provider() {
let toml = r#"
[green.cloud]
prometheus_endpoint = "http://prom:9090"
default_provider = "alibaba"
"#;
let cfg: Config = toml::from_str::<RawConfig>(toml).unwrap().into();
let err = cfg.validate().unwrap_err();
assert!(err.contains("default_provider"), "error: {err}");
}
#[test]
fn cloud_rejects_max_watts_less_than_idle() {
let toml = r#"
[green.cloud]
prometheus_endpoint = "http://prom:9090"
[green.cloud.services.bad-svc]
idle_watts = 100
max_watts = 50
"#;
let cfg: Config = toml::from_str::<RawConfig>(toml).unwrap().into();
let err = cfg.validate().unwrap_err();
assert!(err.contains("max_watts"), "error: {err}");
}
#[test]
fn cloud_rejects_service_name_with_control_chars() {
let toml = "
[green.cloud]
prometheus_endpoint = \"http://prom:9090\"
[green.cloud.services.\"bad\\nsvc\"]
idle_watts = 10
max_watts = 50
";
let cfg: Config = toml::from_str::<RawConfig>(toml).unwrap().into();
let err = cfg.validate().unwrap_err();
assert!(err.contains("control characters"), "error: {err}");
}
#[test]
fn config_per_operation_coefficients_default_true() {
let cfg = Config::default();
assert!(cfg.green_per_operation_coefficients);
}
#[test]
fn config_include_network_transport_default_false() {
let cfg = Config::default();
assert!(!cfg.green_include_network_transport);
}
#[test]
fn config_network_energy_per_byte_kwh_default() {
let cfg = Config::default();
assert!(
(cfg.green_network_energy_per_byte_kwh
- crate::score::carbon::DEFAULT_NETWORK_ENERGY_PER_BYTE_KWH)
.abs()
< f64::EPSILON
);
}
#[test]
fn config_network_energy_per_byte_kwh_rejects_negative() {
let toml = r"
[green]
network_energy_per_byte_kwh = -0.001
";
let cfg: Config = toml::from_str::<RawConfig>(toml).unwrap().into();
let err = cfg.validate().unwrap_err();
assert!(err.contains("network_energy_per_byte_kwh"), "error: {err}");
}
#[test]
fn config_network_energy_per_byte_kwh_rejects_nan() {
let cfg = Config {
green_network_energy_per_byte_kwh: f64::NAN,
..Config::default()
};
let err = cfg.validate().unwrap_err();
assert!(err.contains("network_energy_per_byte_kwh"), "error: {err}");
}
#[test]
fn config_per_operation_coefficients_from_toml() {
let toml = r"
[green]
per_operation_coefficients = false
";
let cfg: Config = toml::from_str::<RawConfig>(toml).unwrap().into();
assert!(!cfg.green_per_operation_coefficients);
}
#[test]
fn config_include_network_transport_from_toml() {
let toml = r"
[green]
include_network_transport = true
network_energy_per_byte_kwh = 0.00000000008
";
let cfg: Config = toml::from_str::<RawConfig>(toml).unwrap().into();
assert!(cfg.green_include_network_transport);
assert!((cfg.green_network_energy_per_byte_kwh - 0.000_000_000_08).abs() < f64::EPSILON);
}
#[test]
fn validate_http_authority_rejects_empty_host() {
assert!(validate_http_authority("http://", "test").is_err());
}
#[test]
fn validate_http_authority_rejects_credentials() {
let err = validate_http_authority("http://user:pass@host/", "test").unwrap_err();
assert!(err.contains("credentials"));
}
#[test]
fn validate_http_authority_rejects_control_char() {
let err = validate_http_authority("http://bad\thost/", "test").unwrap_err();
assert!(err.contains("control"));
}
#[test]
fn validate_http_authority_rejects_invalid_ipv4_port() {
let err = validate_http_authority("http://host:abc/", "test").unwrap_err();
assert!(err.contains("port"));
}
#[test]
fn validate_http_authority_accepts_bare_ipv6() {
assert!(validate_http_authority("http://[::1]/metrics", "test").is_ok());
}
#[test]
fn validate_http_authority_accepts_ipv6_with_port() {
assert!(validate_http_authority("http://[::1]:8080/metrics", "test").is_ok());
}
#[test]
fn validate_http_authority_rejects_ipv6_with_invalid_port() {
let err = validate_http_authority("http://[::1]:abc/metrics", "test").unwrap_err();
assert!(err.contains("port"));
}
#[test]
fn validate_http_authority_accepts_https_scheme() {
assert!(validate_http_authority("https://host:443/", "test").is_ok());
}
#[test]
fn validate_green_rejects_nonfinite_embodied_carbon() {
let toml = "[green]\nembodied_carbon_per_request_gco2 = nan\n";
let err = load_from_str(toml).unwrap_err();
assert!(format!("{err:?}").contains("finite"));
}
#[test]
fn validate_green_rejects_hourly_profiles_file_that_fails_to_load() {
let toml = r#"
[green]
hourly_profiles_file = "/tmp/does-not-exist-perfsentinel-test.json"
"#;
let err = load_from_str(toml).unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("hourly_profiles_file") || msg.contains("failed to load"));
}
use crate::score::electricity_maps::ElectricityMapsConfig;
#[test]
fn electricity_maps_empty_api_key_returns_none() {
let raw = ElectricityMapsSection {
api_key: Some(String::new()),
endpoint: None,
poll_interval_secs: None,
region_map: HashMap::new(),
};
assert!(convert_electricity_maps_section_with_env(&raw, || None).is_none());
}
#[test]
fn electricity_maps_warn_when_api_key_in_config_file() {
let mut region_map = HashMap::new();
region_map.insert("eu-west-3".to_string(), "FR".to_string());
let raw = ElectricityMapsSection {
api_key: Some("file-token".to_string()),
endpoint: None,
poll_interval_secs: Some(600),
region_map,
};
let cfg = convert_electricity_maps_section_with_env(&raw, || None).expect("should convert");
assert_eq!(cfg.auth_token, "file-token");
assert_eq!(cfg.poll_interval, Duration::from_mins(10));
assert_eq!(cfg.api_endpoint, "https://api.electricitymaps.com/v3");
assert!(cfg.region_map.contains_key("eu-west-3"));
}
#[test]
fn electricity_maps_region_map_keys_lowercased() {
let mut region_map = HashMap::new();
region_map.insert("EU-WEST-3".to_string(), "FR".to_string());
region_map.insert("Us-East-1".to_string(), "US-MIDA-PJM".to_string());
let raw = ElectricityMapsSection {
api_key: Some("tok".to_string()),
endpoint: Some("https://custom.api/v3".to_string()),
poll_interval_secs: Some(120),
region_map,
};
let cfg = convert_electricity_maps_section_with_env(&raw, || None).expect("should convert");
assert!(cfg.region_map.contains_key("eu-west-3"));
assert!(cfg.region_map.contains_key("us-east-1"));
assert_eq!(cfg.api_endpoint, "https://custom.api/v3");
}
#[test]
fn electricity_maps_env_var_takes_precedence_over_config_file() {
let mut region_map = HashMap::new();
region_map.insert("eu-west-3".to_string(), "FR".to_string());
let raw = ElectricityMapsSection {
api_key: Some("from-file".to_string()),
endpoint: None,
poll_interval_secs: None,
region_map,
};
let cfg = convert_electricity_maps_section_with_env(&raw, || Some("from-env".to_string()))
.expect("env-supplied token should produce a valid config");
assert_eq!(cfg.auth_token, "from-env");
}
#[test]
fn validate_electricity_maps_rejects_control_char_in_token() {
let cfg = ElectricityMapsConfig {
api_endpoint: "https://api.electricitymaps.com/v3".to_string(),
auth_token: "tok\x07en".to_string(), poll_interval: Duration::from_mins(5),
region_map: {
let mut m = HashMap::new();
m.insert("eu-west-3".to_string(), "FR".to_string());
m
},
};
let err = Config::validate_electricity_maps(&cfg).unwrap_err();
assert!(err.contains("control"));
}
#[test]
fn validate_electricity_maps_rejects_empty_region_map() {
let cfg = ElectricityMapsConfig {
api_endpoint: "https://api.electricitymaps.com/v3".to_string(),
auth_token: "tok".to_string(),
poll_interval: Duration::from_mins(5),
region_map: HashMap::new(),
};
let err = Config::validate_electricity_maps(&cfg).unwrap_err();
assert!(err.contains("region_map"));
}
#[test]
fn validate_electricity_maps_rejects_empty_zone() {
let mut region_map = HashMap::new();
region_map.insert("eu-west-3".to_string(), String::new());
let cfg = ElectricityMapsConfig {
api_endpoint: "https://api.electricitymaps.com/v3".to_string(),
auth_token: "tok".to_string(),
poll_interval: Duration::from_mins(5),
region_map,
};
let err = Config::validate_electricity_maps(&cfg).unwrap_err();
assert!(err.contains("empty"));
}
#[test]
fn validate_electricity_maps_rejects_invalid_poll_interval() {
let mut region_map = HashMap::new();
region_map.insert("eu-west-3".to_string(), "FR".to_string());
let cfg = ElectricityMapsConfig {
api_endpoint: "https://api.electricitymaps.com/v3".to_string(),
auth_token: "tok".to_string(),
poll_interval: Duration::from_secs(10), region_map,
};
let err = Config::validate_electricity_maps(&cfg).unwrap_err();
assert!(err.contains("poll_interval"));
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn validate_tls_accepts_both_absent() {
let cfg = Config::default();
assert!(cfg.validate_tls().is_ok());
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn validate_tls_rejects_cert_without_key() {
let mut cfg = Config::default();
cfg.tls_cert_path = Some("/tmp/cert.pem".to_string());
let err = cfg.validate_tls().unwrap_err();
assert!(err.contains("tls_key_path is missing"), "{err}");
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn validate_tls_rejects_key_without_cert() {
let mut cfg = Config::default();
cfg.tls_key_path = Some("/tmp/key.pem".to_string());
let err = cfg.validate_tls().unwrap_err();
assert!(err.contains("tls_cert_path is missing"), "{err}");
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn validate_tls_rejects_nonexistent_cert() {
let mut cfg = Config::default();
cfg.tls_cert_path = Some("/nonexistent/cert.pem".to_string());
cfg.tls_key_path = Some("/nonexistent/key.pem".to_string());
let err = cfg.validate_tls().unwrap_err();
assert!(err.contains("does not exist"), "{err}");
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn validate_tls_accepts_existing_files() {
let dir = tempfile::tempdir().unwrap();
let cert = dir.path().join("cert.pem");
let key = dir.path().join("key.pem");
std::fs::write(&cert, b"fake cert").unwrap();
std::fs::write(&key, b"fake key").unwrap();
let mut cfg = Config::default();
cfg.tls_cert_path = Some(cert.to_str().unwrap().to_string());
cfg.tls_key_path = Some(key.to_str().unwrap().to_string());
assert!(cfg.validate_tls().is_ok());
}
#[test]
fn tls_config_fields_round_trip_through_toml() {
let dir = tempfile::tempdir().unwrap();
let cert = dir.path().join("cert.pem");
let key = dir.path().join("key.pem");
std::fs::write(&cert, b"fake cert").unwrap();
std::fs::write(&key, b"fake key").unwrap();
let toml = format!(
"[daemon]\ntls_cert_path = \"{}\"\ntls_key_path = \"{}\"",
cert.display(),
key.display()
);
let cfg = load_from_str(&toml).unwrap();
assert_eq!(cfg.tls_cert_path.as_deref(), Some(cert.to_str().unwrap()));
assert_eq!(cfg.tls_key_path.as_deref(), Some(key.to_str().unwrap()));
}
#[test]
fn tls_config_defaults_to_none() {
let cfg = load_from_str("").unwrap();
assert!(cfg.tls_cert_path.is_none());
assert!(cfg.tls_key_path.is_none());
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn validate_tls_rejects_control_chars_in_cert_path() {
let mut cfg = Config::default();
cfg.tls_cert_path = Some("/tmp/cert\x00.pem".to_string());
cfg.tls_key_path = Some("/tmp/key.pem".to_string());
let err = cfg.validate_tls().unwrap_err();
assert!(err.contains("control characters"), "{err}");
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn validate_tls_rejects_control_chars_in_key_path() {
let mut cfg = Config::default();
cfg.tls_cert_path = Some("/tmp/cert.pem".to_string());
cfg.tls_key_path = Some("/tmp/key\n.pem".to_string());
let err = cfg.validate_tls().unwrap_err();
assert!(err.contains("control characters"), "{err}");
}
}