1use std::path::PathBuf;
15
16use clap::Subcommand;
17use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Default, Serialize, Deserialize)]
20pub struct DolConfig {
21 pub store: Option<String>,
22 pub output: Option<String>,
23 pub metrics_interval: Option<u64>,
24 pub snapshot_interval: Option<u64>,
25 pub host: Option<String>,
26 pub theme: Option<String>,
28
29 pub api_timeout: Option<u64>,
32 pub api_quick_timeout: Option<u64>,
34 pub stats_timeout: Option<u64>,
36 pub events_timeout: Option<u64>,
38 pub webhook_timeout: Option<u64>,
40 pub restart_timeout: Option<u64>,
42
43 pub smtp_host: Option<String>,
46 pub smtp_port: Option<u16>,
48 pub smtp_user: Option<String>,
50 pub smtp_pass: Option<String>,
52
53 pub analysis_high_cpu_percent: Option<f64>,
56 pub analysis_critical_cpu_percent: Option<f64>,
58 pub analysis_memory_pressure_ratio: Option<f64>,
60 pub analysis_critical_memory_ratio: Option<f64>,
62 pub analysis_restart_loop_count: Option<u64>,
64 pub analysis_deployment_error_threshold: Option<u64>,
66 pub analysis_leak_memory_increase_pct: Option<f64>,
68 pub analysis_leak_min_samples: Option<u64>,
70 pub analysis_high_network_bytes: Option<u64>,
72 pub analysis_critical_network_bytes: Option<u64>,
74 pub analysis_high_disk_bytes: Option<u64>,
76 pub analysis_critical_disk_bytes: Option<u64>,
78}
79
80impl DolConfig {
81 #[must_use]
82 pub fn load() -> Self {
83 let paths = config_paths();
84 for path in &paths {
85 if path.exists() {
86 let Ok(content) = std::fs::read_to_string(path) else {
87 continue;
88 };
89 if let Ok(config) = serde_yaml::from_str::<Self>(&content) {
90 return config;
91 }
92 if let Ok(config) = toml::from_str::<Self>(&content) {
93 return config;
94 }
95 }
96 }
97 Self::default()
98 }
99
100 pub fn save(&self) -> anyhow::Result<()> {
101 let path = config_path();
102 if let Some(parent) = path.parent() {
103 std::fs::create_dir_all(parent)?;
104 }
105 let content = serde_yaml::to_string(self)?;
106 std::fs::write(&path, content)?;
107 Ok(())
108 }
109}
110
111#[derive(Debug, Clone, Subcommand)]
112pub enum ConfigAction {
113 Init,
115 Set { key: String, value: String },
117 View,
119}
120
121pub fn execute_config(action: ConfigAction) -> anyhow::Result<()> {
122 match action {
123 ConfigAction::Init => {
124 let path = config_path();
125 if path.exists() {
126 anyhow::bail!("config file already exists at {}", path.display());
127 }
128 let config = DolConfig::default();
129 if let Some(parent) = path.parent() {
130 std::fs::create_dir_all(parent)?;
131 }
132 let content = serde_yaml::to_string(&config)?;
133 std::fs::write(&path, content)?;
134 println!("Created default config at {}", path.display());
135 Ok(())
136 }
137 ConfigAction::Set { key, value } => {
138 let mut config = DolConfig::load();
139 match key.as_str() {
140 "store" => config.store = Some(value.clone()),
141 "output" => config.output = Some(value.clone()),
142 "metrics-interval" | "metrics_interval" => {
143 config.metrics_interval = Some(value.parse().map_err(|_| {
144 anyhow::anyhow!("invalid value for {key}: expected integer")
145 })?);
146 }
147 "snapshot-interval" | "snapshot_interval" => {
148 config.snapshot_interval = Some(value.parse().map_err(|_| {
149 anyhow::anyhow!("invalid value for {key}: expected integer")
150 })?);
151 }
152 "host" => config.host = Some(value.clone()),
153 "theme" => config.theme = Some(value.clone()),
154 "api-timeout" | "api_timeout" => {
155 config.api_timeout = Some(value.parse().map_err(|_| {
156 anyhow::anyhow!("invalid value for {key}: expected integer (seconds)")
157 })?);
158 }
159 "api-quick-timeout" | "api_quick_timeout" => {
160 config.api_quick_timeout = Some(value.parse().map_err(|_| {
161 anyhow::anyhow!("invalid value for {key}: expected integer (seconds)")
162 })?);
163 }
164 "stats-timeout" | "stats_timeout" => {
165 config.stats_timeout = Some(value.parse().map_err(|_| {
166 anyhow::anyhow!("invalid value for {key}: expected integer (seconds)")
167 })?);
168 }
169 "events-timeout" | "events_timeout" => {
170 config.events_timeout = Some(value.parse().map_err(|_| {
171 anyhow::anyhow!("invalid value for {key}: expected integer (seconds)")
172 })?);
173 }
174 "webhook-timeout" | "webhook_timeout" => {
175 config.webhook_timeout = Some(value.parse().map_err(|_| {
176 anyhow::anyhow!("invalid value for {key}: expected integer (seconds)")
177 })?);
178 }
179 "restart-timeout" | "restart_timeout" => {
180 config.restart_timeout = Some(value.parse().map_err(|_| {
181 anyhow::anyhow!("invalid value for {key}: expected integer (seconds)")
182 })?);
183 }
184 "smtp-host" | "smtp_host" => {
185 config.smtp_host = Some(value.clone());
186 }
187 "smtp-port" | "smtp_port" => {
188 config.smtp_port = Some(value.parse().map_err(|_| {
189 anyhow::anyhow!("invalid value for {key}: expected port number")
190 })?);
191 }
192 "smtp-user" | "smtp_user" => {
193 config.smtp_user = Some(value.clone());
194 }
195 "smtp-pass" | "smtp_pass" => {
196 config.smtp_pass = Some(value.clone());
197 }
198 "analysis-high-cpu-percent" | "analysis_high_cpu_percent" => {
200 config.analysis_high_cpu_percent = Some(value.parse().map_err(|_| {
201 anyhow::anyhow!("invalid value for {key}: expected number")
202 })?);
203 }
204 "analysis-critical-cpu-percent" | "analysis_critical_cpu_percent" => {
205 config.analysis_critical_cpu_percent = Some(value.parse().map_err(|_| {
206 anyhow::anyhow!("invalid value for {key}: expected number")
207 })?);
208 }
209 "analysis-memory-pressure-ratio" | "analysis_memory_pressure_ratio" => {
210 config.analysis_memory_pressure_ratio = Some(value.parse().map_err(|_| {
211 anyhow::anyhow!("invalid value for {key}: expected number")
212 })?);
213 }
214 "analysis-critical-memory-ratio" | "analysis_critical_memory_ratio" => {
215 config.analysis_critical_memory_ratio = Some(value.parse().map_err(|_| {
216 anyhow::anyhow!("invalid value for {key}: expected number")
217 })?);
218 }
219 "analysis-restart-loop-count" | "analysis_restart_loop_count" => {
220 config.analysis_restart_loop_count = Some(value.parse().map_err(|_| {
221 anyhow::anyhow!("invalid value for {key}: expected integer")
222 })?);
223 }
224 "analysis-deployment-error-threshold" | "analysis_deployment_error_threshold" => {
225 config.analysis_deployment_error_threshold =
226 Some(value.parse().map_err(|_| {
227 anyhow::anyhow!("invalid value for {key}: expected integer")
228 })?);
229 }
230 "analysis-leak-memory-increase-pct" | "analysis_leak_memory_increase_pct" => {
231 config.analysis_leak_memory_increase_pct =
232 Some(value.parse().map_err(|_| {
233 anyhow::anyhow!("invalid value for {key}: expected number")
234 })?);
235 }
236 "analysis-leak-min-samples" | "analysis_leak_min_samples" => {
237 config.analysis_leak_min_samples = Some(value.parse().map_err(|_| {
238 anyhow::anyhow!("invalid value for {key}: expected integer")
239 })?);
240 }
241 "analysis-high-network-bytes" | "analysis_high_network_bytes" => {
242 config.analysis_high_network_bytes = Some(value.parse().map_err(|_| {
243 anyhow::anyhow!("invalid value for {key}: expected integer")
244 })?);
245 }
246 "analysis-critical-network-bytes" | "analysis_critical_network_bytes" => {
247 config.analysis_critical_network_bytes = Some(value.parse().map_err(|_| {
248 anyhow::anyhow!("invalid value for {key}: expected integer")
249 })?);
250 }
251 "analysis-high-disk-bytes" | "analysis_high_disk_bytes" => {
252 config.analysis_high_disk_bytes = Some(value.parse().map_err(|_| {
253 anyhow::anyhow!("invalid value for {key}: expected integer")
254 })?);
255 }
256 "analysis-critical-disk-bytes" | "analysis_critical_disk_bytes" => {
257 config.analysis_critical_disk_bytes = Some(value.parse().map_err(|_| {
258 anyhow::anyhow!("invalid value for {key}: expected integer")
259 })?);
260 }
261 _ => anyhow::bail!("unknown config key: {key}"),
262 }
263 config.save()?;
264 println!("Set {key} = {value}");
265 Ok(())
266 }
267 ConfigAction::View => {
268 let config = DolConfig::load();
269 let content = serde_yaml::to_string(&config)?;
270 print!("{content}");
271 Ok(())
272 }
273 }
274}
275
276fn config_paths() -> Vec<PathBuf> {
277 let mut paths = Vec::new();
278
279 if let Some(config_dir) = dirs::config_dir() {
280 paths.push(config_dir.join("dol").join("config.yaml"));
281 paths.push(config_dir.join("dol").join("config.toml"));
282 paths.push(config_dir.join("dolrc"));
283 }
284
285 if let Some(home) = dirs::home_dir() {
286 paths.push(home.join(".dolrc"));
287 paths.push(home.join(".dolrc.yaml"));
288 paths.push(home.join(".dolrc.toml"));
289 }
290
291 paths.push(PathBuf::from(".dolrc"));
292 paths.push(PathBuf::from("dol.yaml"));
293 paths.push(PathBuf::from("dol.toml"));
294
295 paths
296}
297
298fn config_path() -> PathBuf {
299 dirs::config_dir().map_or_else(
300 || PathBuf::from(".dolrc"),
301 |config_dir| config_dir.join("dol").join("config.yaml"),
302 )
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use crate::docker::DockerApiConfig;
309
310 #[test]
313 fn default_all_timeout_fields_are_none() {
314 let config = DolConfig::default();
315 assert!(config.api_timeout.is_none());
316 assert!(config.api_quick_timeout.is_none());
317 assert!(config.stats_timeout.is_none());
318 assert!(config.events_timeout.is_none());
319 assert!(config.webhook_timeout.is_none());
320 assert!(config.restart_timeout.is_none());
321 }
322
323 #[test]
324 fn default_non_timeout_fields() {
325 let config = DolConfig::default();
326 assert!(config.store.is_none());
327 assert!(config.output.is_none());
328 assert!(config.metrics_interval.is_none());
329 assert!(config.snapshot_interval.is_none());
330 assert!(config.host.is_none());
331 assert!(config.theme.is_none());
332 }
333
334 #[test]
337 fn yaml_roundtrip_empty() {
338 let config = DolConfig::default();
339 let yaml = serde_yaml::to_string(&config).expect("serialize");
340 let deserialized: DolConfig = serde_yaml::from_str(&yaml).expect("deserialize");
341 assert_eq!(config.store, deserialized.store);
342 assert_eq!(config.api_timeout, deserialized.api_timeout);
343 assert_eq!(config.stats_timeout, deserialized.stats_timeout);
344 }
345
346 #[test]
347 fn yaml_roundtrip_with_timeouts() {
348 let config = DolConfig {
349 api_timeout: Some(60),
350 api_quick_timeout: Some(15),
351 stats_timeout: Some(20),
352 events_timeout: Some(120),
353 webhook_timeout: Some(5),
354 restart_timeout: Some(45),
355 ..DolConfig::default()
356 };
357 let yaml = serde_yaml::to_string(&config).expect("serialize");
358 let deserialized: DolConfig = serde_yaml::from_str(&yaml).expect("deserialize");
359 assert_eq!(deserialized.api_timeout, Some(60));
360 assert_eq!(deserialized.api_quick_timeout, Some(15));
361 assert_eq!(deserialized.stats_timeout, Some(20));
362 assert_eq!(deserialized.events_timeout, Some(120));
363 assert_eq!(deserialized.webhook_timeout, Some(5));
364 assert_eq!(deserialized.restart_timeout, Some(45));
365 }
366
367 #[test]
368 fn yaml_deserialize_from_string() {
369 let yaml = r#"
370api_timeout: 60
371stats_timeout: 15
372events_timeout: 30
373webhook_timeout: 10
374restart_timeout: 45
375"#;
376 let config: DolConfig = serde_yaml::from_str(yaml).expect("deserialize");
377 assert_eq!(config.api_timeout, Some(60));
378 assert_eq!(config.stats_timeout, Some(15));
379 assert_eq!(config.events_timeout, Some(30));
380 assert_eq!(config.webhook_timeout, Some(10));
381 assert_eq!(config.restart_timeout, Some(45));
382 assert!(config.api_quick_timeout.is_none());
384 assert!(config.store.is_none());
386 }
387
388 #[test]
389 fn yaml_deserialize_mixed_fields() {
390 let yaml = r#"
391store: /tmp/dol.db
392output: json
393theme: light
394api_timeout: 90
395"#;
396 let config: DolConfig = serde_yaml::from_str(yaml).expect("deserialize");
397 assert_eq!(config.store.as_deref(), Some("/tmp/dol.db"));
398 assert_eq!(config.output.as_deref(), Some("json"));
399 assert_eq!(config.theme.as_deref(), Some("light"));
400 assert_eq!(config.api_timeout, Some(90));
401 assert!(config.stats_timeout.is_none());
403 assert!(config.events_timeout.is_none());
404 }
405
406 #[test]
409 fn docker_api_config_from_default_config() {
410 let config = DolConfig::default();
411 let api_cfg = DockerApiConfig::from(&config);
412 assert_eq!(api_cfg.call_timeout.as_secs(), 30);
413 assert_eq!(api_cfg.quick_timeout.as_secs(), 10);
414 assert_eq!(api_cfg.max_retries, 2);
415 assert_eq!(api_cfg.retry_base_ms, 200);
416 }
417
418 #[test]
419 fn docker_api_config_from_custom_config() {
420 let config = DolConfig {
421 api_timeout: Some(120),
422 api_quick_timeout: Some(30),
423 ..DolConfig::default()
424 };
425 let api_cfg = DockerApiConfig::from(&config);
426 assert_eq!(api_cfg.call_timeout.as_secs(), 120);
427 assert_eq!(api_cfg.quick_timeout.as_secs(), 30);
428 assert_eq!(api_cfg.max_retries, 2);
430 assert_eq!(api_cfg.retry_base_ms, 200);
431 }
432
433 #[test]
434 fn docker_api_config_mixed_defaults() {
435 let config = DolConfig {
437 api_timeout: Some(45),
438 ..DolConfig::default()
439 };
440 let api_cfg = DockerApiConfig::from(&config);
441 assert_eq!(api_cfg.call_timeout.as_secs(), 45);
442 assert_eq!(api_cfg.quick_timeout.as_secs(), 10);
444 }
445
446 #[test]
449 fn toml_roundtrip_with_timeouts() {
450 let config = DolConfig {
451 api_timeout: Some(60),
452 stats_timeout: Some(15),
453 ..DolConfig::default()
454 };
455 let toml_str = toml::to_string(&config).expect("serialize");
456 let deserialized: DolConfig = toml::from_str(&toml_str).expect("deserialize");
457 assert_eq!(deserialized.api_timeout, Some(60));
458 assert_eq!(deserialized.stats_timeout, Some(15));
459 assert!(deserialized.events_timeout.is_none());
460 }
461
462 #[test]
469 fn config_set_all_timeout_keys_roundtrip_through_yaml() {
470 let cases = [
474 ("api_timeout", 60u64),
475 ("api_quick_timeout", 15u64),
476 ("stats_timeout", 20u64),
477 ("events_timeout", 120u64),
478 ("webhook_timeout", 5u64),
479 ("restart_timeout", 45u64),
480 ];
481
482 for (key, value) in &cases {
483 let yaml = format!("{key}: {value}\n");
484 let config: DolConfig = serde_yaml::from_str(&yaml)
485 .unwrap_or_else(|e| panic!("failed to deserialize {key}: {e}"));
486
487 match *key {
488 "api_timeout" => assert_eq!(config.api_timeout, Some(*value)),
489 "api_quick_timeout" => assert_eq!(config.api_quick_timeout, Some(*value)),
490 "stats_timeout" => assert_eq!(config.stats_timeout, Some(*value)),
491 "events_timeout" => assert_eq!(config.events_timeout, Some(*value)),
492 "webhook_timeout" => assert_eq!(config.webhook_timeout, Some(*value)),
493 "restart_timeout" => assert_eq!(config.restart_timeout, Some(*value)),
494 _ => panic!("unexpected key: {key}"),
495 }
496 }
497 }
498
499 #[test]
500 fn config_set_invalid_value_rejected_by_serde() {
501 let yaml = "api_timeout: not_a_number\n";
503 let result: Result<DolConfig, _> = serde_yaml::from_str(yaml);
504 assert!(
505 result.is_err(),
506 "expected deserialization error for invalid value"
507 );
508 }
509
510 #[test]
511 fn config_set_negative_value_rejected() {
512 let yaml = "stats_timeout: -5\n";
514 let result: Result<DolConfig, _> = serde_yaml::from_str(yaml);
515 assert!(
516 result.is_err(),
517 "expected deserialization error for negative value"
518 );
519 }
520
521 #[test]
524 fn default_smtp_fields_are_none() {
525 let config = DolConfig::default();
526 assert!(config.smtp_host.is_none());
527 assert!(config.smtp_port.is_none());
528 assert!(config.smtp_user.is_none());
529 assert!(config.smtp_pass.is_none());
530 }
531
532 #[test]
533 fn yaml_roundtrip_with_smtp() {
534 let config = DolConfig {
535 smtp_host: Some("smtp.gmail.com".to_owned()),
536 smtp_port: Some(587),
537 smtp_user: Some("user@gmail.com".to_owned()),
538 smtp_pass: Some("app-password".to_owned()),
539 ..DolConfig::default()
540 };
541 let yaml = serde_yaml::to_string(&config).expect("serialize");
542 let deserialized: DolConfig = serde_yaml::from_str(&yaml).expect("deserialize");
543 assert_eq!(deserialized.smtp_host.as_deref(), Some("smtp.gmail.com"));
544 assert_eq!(deserialized.smtp_port, Some(587));
545 assert_eq!(deserialized.smtp_user.as_deref(), Some("user@gmail.com"));
546 assert_eq!(deserialized.smtp_pass.as_deref(), Some("app-password"));
547 }
548
549 #[test]
550 fn yaml_deserialize_smtp_fields() {
551 let yaml = r#"
552smtp_host: smtp.gmail.com
553smtp_port: 587
554smtp_user: user@gmail.com
555smtp_pass: app-password
556"#;
557 let config: DolConfig = serde_yaml::from_str(yaml).expect("deserialize");
558 assert_eq!(config.smtp_host.as_deref(), Some("smtp.gmail.com"));
559 assert_eq!(config.smtp_port, Some(587));
560 assert_eq!(config.smtp_user.as_deref(), Some("user@gmail.com"));
561 assert_eq!(config.smtp_pass.as_deref(), Some("app-password"));
562 }
563
564 #[test]
565 fn yaml_deserialize_smtp_defaults_when_omitted() {
566 let yaml = "api_timeout: 30\n";
567 let config: DolConfig = serde_yaml::from_str(yaml).expect("deserialize");
568 assert!(config.smtp_host.is_none());
569 assert!(config.smtp_port.is_none());
570 assert!(config.smtp_user.is_none());
571 assert!(config.smtp_pass.is_none());
572 }
573
574 #[test]
575 fn config_set_all_smtp_keys_roundtrip_through_yaml() {
576 let cases = [
580 ("smtp_host", "smtp.gmail.com"),
581 ("smtp_port", "587"),
582 ("smtp_user", "user@gmail.com"),
583 ("smtp_pass", "app-password"),
584 ];
585
586 for &(key, value) in &cases {
587 let yaml = format!("{key}: {value}\n");
588 let config: DolConfig = serde_yaml::from_str(&yaml)
589 .unwrap_or_else(|e| panic!("failed to deserialize {key}: {e}"));
590
591 match key {
592 "smtp_host" => assert_eq!(config.smtp_host.as_deref(), Some(value)),
593 "smtp_port" => {
594 let expected: u16 = value.parse().unwrap();
595 assert_eq!(config.smtp_port, Some(expected));
596 }
597 "smtp_user" => assert_eq!(config.smtp_user.as_deref(), Some(value)),
598 "smtp_pass" => assert_eq!(config.smtp_pass.as_deref(), Some(value)),
599 _ => panic!("unexpected key: {key}"),
600 }
601 }
602 }
603
604 #[test]
605 fn toml_roundtrip_with_smtp() {
606 let config = DolConfig {
607 smtp_host: Some("mail.example.com".to_owned()),
608 smtp_port: Some(465),
609 ..DolConfig::default()
610 };
611 let toml_str = toml::to_string(&config).expect("serialize");
612 let deserialized: DolConfig = toml::from_str(&toml_str).expect("deserialize");
613 assert_eq!(deserialized.smtp_host.as_deref(), Some("mail.example.com"));
614 assert_eq!(deserialized.smtp_port, Some(465));
615 assert!(deserialized.smtp_user.is_none());
616 assert!(deserialized.smtp_pass.is_none());
617 }
618
619 #[test]
622 fn default_analysis_threshold_fields_are_none() {
623 let config = DolConfig::default();
624 assert!(config.analysis_high_cpu_percent.is_none());
625 assert!(config.analysis_critical_cpu_percent.is_none());
626 assert!(config.analysis_memory_pressure_ratio.is_none());
627 assert!(config.analysis_critical_memory_ratio.is_none());
628 assert!(config.analysis_restart_loop_count.is_none());
629 assert!(config.analysis_deployment_error_threshold.is_none());
630 assert!(config.analysis_leak_memory_increase_pct.is_none());
631 assert!(config.analysis_leak_min_samples.is_none());
632 assert!(config.analysis_high_network_bytes.is_none());
633 assert!(config.analysis_critical_network_bytes.is_none());
634 assert!(config.analysis_high_disk_bytes.is_none());
635 assert!(config.analysis_critical_disk_bytes.is_none());
636 }
637
638 #[test]
639 fn yaml_roundtrip_with_analysis_thresholds() {
640 let config = DolConfig {
641 analysis_high_cpu_percent: Some(85.0),
642 analysis_critical_cpu_percent: Some(98.0),
643 analysis_memory_pressure_ratio: Some(0.90),
644 analysis_critical_memory_ratio: Some(0.99),
645 analysis_restart_loop_count: Some(5),
646 analysis_deployment_error_threshold: Some(10),
647 analysis_leak_memory_increase_pct: Some(30.0),
648 analysis_leak_min_samples: Some(5),
649 analysis_high_network_bytes: Some(2_097_152),
650 analysis_critical_network_bytes: Some(20_971_520),
651 analysis_high_disk_bytes: Some(20_971_520),
652 analysis_critical_disk_bytes: Some(209_715_200),
653 ..DolConfig::default()
654 };
655 let yaml = serde_yaml::to_string(&config).expect("serialize");
656 let deserialized: DolConfig = serde_yaml::from_str(&yaml).expect("deserialize");
657 assert_eq!(deserialized.analysis_high_cpu_percent, Some(85.0));
658 assert_eq!(deserialized.analysis_critical_cpu_percent, Some(98.0));
659 assert_eq!(deserialized.analysis_memory_pressure_ratio, Some(0.90));
660 assert_eq!(deserialized.analysis_critical_memory_ratio, Some(0.99));
661 assert_eq!(deserialized.analysis_restart_loop_count, Some(5));
662 assert_eq!(deserialized.analysis_deployment_error_threshold, Some(10));
663 assert_eq!(deserialized.analysis_leak_memory_increase_pct, Some(30.0));
664 assert_eq!(deserialized.analysis_leak_min_samples, Some(5));
665 assert_eq!(deserialized.analysis_high_network_bytes, Some(2_097_152));
666 assert_eq!(
667 deserialized.analysis_critical_network_bytes,
668 Some(20_971_520)
669 );
670 assert_eq!(deserialized.analysis_high_disk_bytes, Some(20_971_520));
671 assert_eq!(deserialized.analysis_critical_disk_bytes, Some(209_715_200));
672 }
673
674 #[test]
675 fn yaml_deserialize_analysis_defaults_when_omitted() {
676 let yaml = "api_timeout: 30\n";
677 let config: DolConfig = serde_yaml::from_str(yaml).expect("deserialize");
678 assert!(config.analysis_high_cpu_percent.is_none());
679 assert!(config.analysis_critical_cpu_percent.is_none());
680 assert!(config.analysis_restart_loop_count.is_none());
681 }
682
683 #[test]
684 fn config_set_all_analysis_keys_roundtrip_through_yaml() {
685 let cases = [
686 ("analysis_high_cpu_percent", "85.0"),
687 ("analysis_critical_cpu_percent", "98.0"),
688 ("analysis_memory_pressure_ratio", "0.90"),
689 ("analysis_critical_memory_ratio", "0.99"),
690 ("analysis_restart_loop_count", "5"),
691 ("analysis_deployment_error_threshold", "10"),
692 ("analysis_leak_memory_increase_pct", "30.0"),
693 ("analysis_leak_min_samples", "5"),
694 ("analysis_high_network_bytes", "2097152"),
695 ("analysis_critical_network_bytes", "20971520"),
696 ("analysis_high_disk_bytes", "20971520"),
697 ("analysis_critical_disk_bytes", "209715200"),
698 ];
699
700 for &(key, value) in &cases {
701 let yaml = format!("{key}: {value}\n");
702 let config: DolConfig = serde_yaml::from_str(&yaml)
703 .unwrap_or_else(|e| panic!("failed to deserialize {key}: {e}"));
704
705 match key {
706 "analysis_high_cpu_percent" => {
707 assert_eq!(config.analysis_high_cpu_percent, Some(85.0));
708 }
709 "analysis_critical_cpu_percent" => {
710 assert_eq!(config.analysis_critical_cpu_percent, Some(98.0));
711 }
712 "analysis_memory_pressure_ratio" => {
713 assert_eq!(config.analysis_memory_pressure_ratio, Some(0.90));
714 }
715 "analysis_critical_memory_ratio" => {
716 assert_eq!(config.analysis_critical_memory_ratio, Some(0.99));
717 }
718 "analysis_restart_loop_count" => {
719 assert_eq!(config.analysis_restart_loop_count, Some(5));
720 }
721 "analysis_deployment_error_threshold" => {
722 assert_eq!(config.analysis_deployment_error_threshold, Some(10));
723 }
724 "analysis_leak_memory_increase_pct" => {
725 assert_eq!(config.analysis_leak_memory_increase_pct, Some(30.0));
726 }
727 "analysis_leak_min_samples" => {
728 assert_eq!(config.analysis_leak_min_samples, Some(5));
729 }
730 "analysis_high_network_bytes" => {
731 assert_eq!(config.analysis_high_network_bytes, Some(2_097_152));
732 }
733 "analysis_critical_network_bytes" => {
734 assert_eq!(config.analysis_critical_network_bytes, Some(20_971_520));
735 }
736 "analysis_high_disk_bytes" => {
737 assert_eq!(config.analysis_high_disk_bytes, Some(20_971_520));
738 }
739 "analysis_critical_disk_bytes" => {
740 assert_eq!(config.analysis_critical_disk_bytes, Some(209_715_200));
741 }
742 _ => panic!("unexpected key: {key}"),
743 }
744 }
745 }
746
747 #[test]
748 fn toml_roundtrip_with_analysis_thresholds() {
749 let config = DolConfig {
750 analysis_high_cpu_percent: Some(90.0),
751 analysis_restart_loop_count: Some(7),
752 ..DolConfig::default()
753 };
754 let toml_str = toml::to_string(&config).expect("serialize");
755 let deserialized: DolConfig = toml::from_str(&toml_str).expect("deserialize");
756 assert_eq!(deserialized.analysis_high_cpu_percent, Some(90.0));
757 assert_eq!(deserialized.analysis_restart_loop_count, Some(7));
758 assert!(deserialized.analysis_critical_cpu_percent.is_none());
759 }
760
761 #[test]
762 fn config_set_smtp_invalid_port_rejected() {
763 let yaml = "smtp_port: not_a_number\n";
765 let result: Result<DolConfig, _> = serde_yaml::from_str(yaml);
766 assert!(
767 result.is_err(),
768 "expected deserialization error for invalid port"
769 );
770 }
771}