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>,
}
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 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("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)?;
Ok(())
}
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)?;
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 raw: RawConfig = toml::from_str(content).map_err(ConfigError::Parse)?;
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 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 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 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_secs(600));
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_secs(300),
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_secs(300),
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_secs(300),
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}");
}
}