1#[allow(clippy::all)]
29mod generated {
30 include!(concat!(env!("OUT_DIR"), "/generated/settings.rs"));
31}
32pub use generated::*;
33
34#[allow(clippy::all, dead_code)]
36mod meta {
37 include!(concat!(env!("OUT_DIR"), "/generated/settings_meta.rs"));
38}
39
40#[allow(unused_imports)]
41pub use meta::*;
42
43impl Settings {
44 pub fn resolve_mise_bin(&self) -> Option<std::path::PathBuf> {
55 use std::path::PathBuf;
56
57 if !self.general.mise_bin.is_empty() {
59 let p = PathBuf::from(&self.general.mise_bin);
60 if p.is_file() {
61 return Some(p);
62 }
63 warn!(
64 "mise_bin is set to {:?} but the file does not exist",
65 self.general.mise_bin
66 );
67 return None;
68 }
69
70 let home = crate::env::HOME_DIR.as_path();
72 let candidates = [
73 home.join(".local/bin/mise"),
74 home.join(".cargo/bin/mise"),
75 PathBuf::from("/usr/local/bin/mise"),
76 PathBuf::from("/opt/homebrew/bin/mise"),
77 ];
78
79 candidates.into_iter().find(|p| p.is_file())
80 }
81
82 pub fn default_port_bump_attempts(&self) -> u32 {
88 let v = u32::try_from(self.supervisor.port_bump_attempts).unwrap_or_else(|_| {
89 warn!(
90 "supervisor.port_bump_attempts value {} is out of range (0-{}), clamping to 10",
91 self.supervisor.port_bump_attempts,
92 u32::MAX
93 );
94 10
95 });
96 if v == 0 {
97 warn!("supervisor.port_bump_attempts is 0; defaulting to 1");
98 1
99 } else {
100 v
101 }
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use std::time::Duration;
109
110 #[test]
111 fn test_default_settings() {
112 let settings = Settings::default();
113
114 assert_eq!(settings.general.autostop_delay, "1m");
116 assert_eq!(settings.general.interval, "10s");
117 assert_eq!(settings.general.log_level, "info");
118
119 assert_eq!(settings.ipc.connect_attempts, 5);
121 assert_eq!(settings.ipc.request_timeout, "5s");
122 assert_eq!(settings.ipc.rate_limit, 100);
123
124 assert!(!settings.web.auto_start);
126 assert_eq!(settings.web.bind_address, "127.0.0.1");
127 assert_eq!(settings.web.bind_port, 3120);
128 assert_eq!(settings.web.log_lines, 100);
129
130 assert_eq!(settings.tui.refresh_rate, "2s");
132 assert_eq!(settings.tui.stat_history, 60);
133
134 assert_eq!(settings.supervisor.ready_check_interval, "500ms");
136 assert_eq!(settings.supervisor.file_watch_debounce, "1s");
137 }
138
139 #[test]
140 fn test_parse_duration() {
141 assert_eq!(Settings::parse_duration("1s"), Some(Duration::from_secs(1)));
142 assert_eq!(
143 Settings::parse_duration("500ms"),
144 Some(Duration::from_millis(500))
145 );
146 assert_eq!(
147 Settings::parse_duration("1m"),
148 Some(Duration::from_secs(60))
149 );
150 assert_eq!(
151 Settings::parse_duration("2h"),
152 Some(Duration::from_secs(7200))
153 );
154 assert_eq!(Settings::parse_duration("invalid"), None);
155 }
156
157 #[test]
158 fn test_convenience_methods() {
159 let settings = Settings::default();
160
161 assert_eq!(settings.general_autostop_delay(), Duration::from_secs(60));
162 assert_eq!(settings.general_interval(), Duration::from_secs(10));
163 }
164
165 #[test]
166 fn test_load_from_toml_string() {
167 let toml_content = r#"
169[general]
170autostop_delay = "5m"
171interval = "30s"
172log_level = "debug"
173
174[ipc]
175connect_attempts = 10
176request_timeout = "10s"
177
178[web]
179auto_start = true
180bind_port = 8080
181"#;
182
183 let settings: Settings = toml::from_str(toml_content).unwrap();
184
185 assert_eq!(settings.general.autostop_delay, "5m");
187 assert_eq!(settings.general.interval, "30s");
188 assert_eq!(settings.general.log_level, "debug");
189 assert_eq!(settings.ipc.connect_attempts, 10);
190 assert_eq!(settings.ipc.request_timeout, "10s");
191 assert!(settings.web.auto_start);
192 assert_eq!(settings.web.bind_port, 8080);
193 assert_eq!(settings.general.log_file_level, "info");
194 assert_eq!(settings.ipc.rate_limit, 100);
195 assert_eq!(settings.web.bind_address, "127.0.0.1");
196 assert_eq!(settings.tui.refresh_rate, "2s");
197 }
198
199 #[test]
200 fn test_partial_config_uses_defaults() {
201 let toml_content = r#"
203[general]
204log_level = "warn"
205"#;
206
207 let settings: Settings = toml::from_str(toml_content).unwrap();
208
209 assert_eq!(settings.general.log_level, "warn");
211
212 assert_eq!(settings.general.autostop_delay, "1m");
214 assert_eq!(settings.general.interval, "10s");
215 assert_eq!(settings.ipc.connect_attempts, 5);
216 assert!(!settings.web.auto_start);
217 assert_eq!(settings.tui.stat_history, 60);
218 assert_eq!(settings.supervisor.stop_timeout, "5s");
219 }
220
221 #[test]
222 fn test_empty_config_uses_all_defaults() {
223 let settings: Settings = toml::from_str("").unwrap();
225
226 assert_eq!(settings.general.autostop_delay, "1m");
227 assert_eq!(settings.general.interval, "10s");
228 assert_eq!(settings.general.log_level, "info");
229 assert_eq!(settings.ipc.connect_attempts, 5);
230 assert!(!settings.web.auto_start);
231 assert_eq!(settings.tui.refresh_rate, "2s");
232 }
233
234 #[test]
235 fn test_env_override() {
236 use std::sync::{LazyLock, Mutex};
249 static ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
250 let _guard = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
251
252 unsafe {
255 std::env::set_var("PITCHFORK_AUTOSTOP_DELAY", "10m");
256 std::env::set_var("PITCHFORK_INTERVAL", "5s");
257 std::env::set_var("PITCHFORK_IPC_CONNECT_ATTEMPTS", "20");
258 std::env::set_var("PITCHFORK_WEB_AUTO_START", "true");
259 }
260
261 let mut settings = Settings::default();
262 settings.load_from_env();
263
264 assert_eq!(settings.general.autostop_delay, "10m");
266 assert_eq!(settings.general.interval, "5s");
267 assert_eq!(settings.ipc.connect_attempts, 20);
268 assert!(settings.web.auto_start);
269
270 assert_eq!(settings.general.log_level, "info");
272 assert_eq!(settings.ipc.rate_limit, 100);
273
274 unsafe {
277 std::env::remove_var("PITCHFORK_AUTOSTOP_DELAY");
278 std::env::remove_var("PITCHFORK_INTERVAL");
279 std::env::remove_var("PITCHFORK_IPC_CONNECT_ATTEMPTS");
280 std::env::remove_var("PITCHFORK_WEB_AUTO_START");
281 }
282 }
283
284 #[test]
285 fn test_invalid_duration_fallback() {
286 let mut settings = Settings::default();
287
288 settings.general.autostop_delay = "invalid".to_string();
290 settings.general.interval = "not_a_duration".to_string();
291
292 assert_eq!(settings.general_autostop_delay(), Duration::from_secs(60)); assert_eq!(settings.general_interval(), Duration::from_secs(10)); }
296
297 #[test]
298 fn test_duration_methods_all_fields() {
299 let settings = Settings::default();
300
301 assert_eq!(settings.general_autostop_delay(), Duration::from_secs(60));
303 assert_eq!(settings.general_interval(), Duration::from_secs(10));
304 assert_eq!(settings.ipc_connect_min_delay(), Duration::from_millis(100));
305 assert_eq!(settings.ipc_connect_max_delay(), Duration::from_secs(1));
306 assert_eq!(settings.ipc_request_timeout(), Duration::from_secs(5));
307 assert_eq!(settings.ipc_rate_limit_window(), Duration::from_secs(1));
308 assert_eq!(settings.web_sse_poll_interval(), Duration::from_millis(500));
309 assert_eq!(settings.tui_refresh_rate(), Duration::from_secs(2));
310 assert_eq!(settings.tui_tick_rate(), Duration::from_millis(100));
311 assert_eq!(settings.tui_message_duration(), Duration::from_secs(3));
312 assert_eq!(
313 settings.supervisor_ready_check_interval(),
314 Duration::from_millis(500)
315 );
316 assert_eq!(
317 settings.supervisor_file_watch_debounce(),
318 Duration::from_secs(1)
319 );
320 assert_eq!(
321 settings.supervisor_log_flush_interval(),
322 Duration::from_millis(500)
323 );
324 assert_eq!(settings.supervisor_stop_timeout(), Duration::from_secs(5));
325 assert_eq!(
326 settings.supervisor_restart_delay(),
327 Duration::from_millis(100)
328 );
329 assert_eq!(
330 settings.supervisor_cron_check_interval(),
331 Duration::from_secs(10)
332 );
333 assert_eq!(
334 settings.supervisor_http_client_timeout(),
335 Duration::from_secs(5)
336 );
337 }
338
339 #[test]
340 fn test_unknown_fields_ignored() {
341 let toml_content = r#"
346[general]
347log_level = "debug"
348unknown_field = "should be ignored"
349
350[unknown_section]
351foo = "bar"
352"#;
353
354 let result: Result<Settings, _> = toml::from_str(toml_content);
355 let settings = result.expect("unknown fields should be silently ignored by serde");
357 assert_eq!(settings.general.log_level, "debug");
358 assert_eq!(settings.general.autostop_delay, "1m");
361 }
362
363 #[test]
364 fn test_serialize_roundtrip() {
365 let settings = Settings::default();
366
367 let toml_str = toml::to_string_pretty(&settings).unwrap();
369
370 let parsed: Settings = toml::from_str(&toml_str).unwrap();
372
373 assert_eq!(
375 settings.general.autostop_delay,
376 parsed.general.autostop_delay
377 );
378 assert_eq!(settings.general.interval, parsed.general.interval);
379 assert_eq!(settings.ipc.connect_attempts, parsed.ipc.connect_attempts);
380 assert_eq!(settings.web.auto_start, parsed.web.auto_start);
381 assert_eq!(settings.tui.stat_history, parsed.tui.stat_history);
382 }
383
384 #[test]
385 fn test_type_coercion() {
386 let toml_content = r#"
388[ipc]
389connect_attempts = 3
390rate_limit = 50
391
392[web]
393auto_start = true
394bind_port = 9000
395log_lines = 200
396
397[tui]
398stat_history = 120
399"#;
400
401 let settings: Settings = toml::from_str(toml_content).unwrap();
402
403 assert_eq!(settings.ipc.connect_attempts, 3);
404 assert_eq!(settings.ipc.rate_limit, 50);
405 assert!(settings.web.auto_start);
406 assert_eq!(settings.web.bind_port, 9000);
407 assert_eq!(settings.web.log_lines, 200);
408 assert_eq!(settings.tui.stat_history, 120);
409 }
410
411 #[test]
412 fn test_merge_from_non_default_values() {
413 let mut base = Settings::default();
414
415 let mut partial = SettingsPartial::default();
417 partial.general.autostop_delay = Some("5m".to_string());
418 partial.general.log_level = Some("debug".to_string());
419 partial.ipc.connect_attempts = Some(10);
420 partial.web.auto_start = Some(true);
421
422 base.apply_partial(&partial);
424
425 assert_eq!(base.general.autostop_delay, "5m");
427 assert_eq!(base.general.log_level, "debug");
428 assert_eq!(base.ipc.connect_attempts, 10);
429 assert!(base.web.auto_start);
430
431 assert_eq!(base.general.interval, "10s");
433 assert_eq!(base.ipc.rate_limit, 100);
434 }
435
436 #[test]
437 fn test_merge_from_preserves_existing() {
438 let mut base = Settings::default();
439 base.general.autostop_delay = "2m".to_string();
440 base.web.bind_port = 8080;
441
442 let empty_partial = SettingsPartial::default();
444 base.apply_partial(&empty_partial);
445
446 assert_eq!(base.general.autostop_delay, "2m"); assert_eq!(base.web.bind_port, 8080); }
449
450 #[test]
451 fn test_merge_chain() {
452 let mut settings = Settings::default();
454
455 let mut system_partial = SettingsPartial::default();
457 system_partial.general.log_level = Some("warn".to_string());
458 system_partial.web.bind_address = Some("0.0.0.0".to_string());
459 settings.apply_partial(&system_partial);
460
461 let mut user_partial = SettingsPartial::default();
463 user_partial.general.log_level = Some("info".to_string());
464 user_partial.tui.refresh_rate = Some("1s".to_string());
465 settings.apply_partial(&user_partial);
466
467 let mut project_partial = SettingsPartial::default();
469 project_partial.general.log_level = Some("debug".to_string());
470 project_partial.web.auto_start = Some(true);
471 settings.apply_partial(&project_partial);
472
473 assert_eq!(settings.general.log_level, "debug"); assert_eq!(settings.web.bind_address, "0.0.0.0"); assert_eq!(settings.tui.refresh_rate, "1s"); assert!(settings.web.auto_start); let mut s2 = Settings::default();
486 let mut p1 = SettingsPartial::default();
487 p1.general.log_level = Some("warn".to_string());
488 s2.apply_partial(&p1);
489 assert_eq!(s2.general.log_level, "warn");
490
491 let mut p2 = SettingsPartial::default();
493 p2.general.log_level = Some("info".to_string());
494 s2.apply_partial(&p2);
495 assert_eq!(s2.general.log_level, "info"); }
497
498 #[test]
499 fn test_settings_in_pitchfork_toml() {
500 let toml_content = r#"
502[daemons.myapp]
503run = "node server.js"
504
505[settings.general]
506autostop_delay = "5m"
507log_level = "debug"
508
509[settings.web]
510auto_start = true
511bind_port = 8080
512"#;
513
514 let table: toml::Table = toml::from_str(toml_content).unwrap();
516 let settings_table = table.get("settings").unwrap();
517 let partial: SettingsPartial = settings_table.clone().try_into().unwrap();
518
519 let mut settings = Settings::default();
521 settings.apply_partial(&partial);
522
523 assert_eq!(settings.general.autostop_delay, "5m");
524 assert_eq!(settings.general.log_level, "debug");
525 assert!(settings.web.auto_start);
526 assert_eq!(settings.web.bind_port, 8080);
527 assert_eq!(settings.general.interval, "10s");
528 assert_eq!(settings.ipc.connect_attempts, 5);
529
530 assert!(partial.general.interval.is_none());
532 assert!(partial.ipc.connect_attempts.is_none());
533 }
534}