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 assert_eq!(settings.supervisor.user, "");
138 }
139
140 #[test]
141 fn test_parse_duration() {
142 assert_eq!(Settings::parse_duration("1s"), Some(Duration::from_secs(1)));
143 assert_eq!(
144 Settings::parse_duration("500ms"),
145 Some(Duration::from_millis(500))
146 );
147 assert_eq!(
148 Settings::parse_duration("1m"),
149 Some(Duration::from_secs(60))
150 );
151 assert_eq!(
152 Settings::parse_duration("2h"),
153 Some(Duration::from_secs(7200))
154 );
155 assert_eq!(Settings::parse_duration("invalid"), None);
156 }
157
158 #[test]
159 fn test_convenience_methods() {
160 let settings = Settings::default();
161
162 assert_eq!(settings.general_autostop_delay(), Duration::from_secs(60));
163 assert_eq!(settings.general_interval(), Duration::from_secs(10));
164 }
165
166 #[test]
167 fn test_load_from_toml_string() {
168 let toml_content = r#"
170[general]
171autostop_delay = "5m"
172interval = "30s"
173log_level = "debug"
174
175[ipc]
176connect_attempts = 10
177request_timeout = "10s"
178
179[web]
180auto_start = true
181bind_port = 8080
182"#;
183
184 let settings: Settings = toml::from_str(toml_content).unwrap();
185
186 assert_eq!(settings.general.autostop_delay, "5m");
188 assert_eq!(settings.general.interval, "30s");
189 assert_eq!(settings.general.log_level, "debug");
190 assert_eq!(settings.ipc.connect_attempts, 10);
191 assert_eq!(settings.ipc.request_timeout, "10s");
192 assert!(settings.web.auto_start);
193 assert_eq!(settings.web.bind_port, 8080);
194 assert_eq!(settings.general.log_file_level, "info");
195 assert_eq!(settings.ipc.rate_limit, 100);
196 assert_eq!(settings.web.bind_address, "127.0.0.1");
197 assert_eq!(settings.tui.refresh_rate, "2s");
198 }
199
200 #[test]
201 fn test_partial_config_uses_defaults() {
202 let toml_content = r#"
204[general]
205log_level = "warn"
206"#;
207
208 let settings: Settings = toml::from_str(toml_content).unwrap();
209
210 assert_eq!(settings.general.log_level, "warn");
212
213 assert_eq!(settings.general.autostop_delay, "1m");
215 assert_eq!(settings.general.interval, "10s");
216 assert_eq!(settings.ipc.connect_attempts, 5);
217 assert!(!settings.web.auto_start);
218 assert_eq!(settings.tui.stat_history, 60);
219 assert_eq!(settings.supervisor.stop_timeout, "5s");
220 }
221
222 #[test]
223 fn test_empty_config_uses_all_defaults() {
224 let settings: Settings = toml::from_str("").unwrap();
226
227 assert_eq!(settings.general.autostop_delay, "1m");
228 assert_eq!(settings.general.interval, "10s");
229 assert_eq!(settings.general.log_level, "info");
230 assert_eq!(settings.ipc.connect_attempts, 5);
231 assert!(!settings.web.auto_start);
232 assert_eq!(settings.tui.refresh_rate, "2s");
233 }
234
235 #[test]
236 fn test_env_override() {
237 use std::sync::{LazyLock, Mutex};
250 static ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
251 let _guard = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
252
253 unsafe {
256 std::env::set_var("PITCHFORK_AUTOSTOP_DELAY", "10m");
257 std::env::set_var("PITCHFORK_INTERVAL", "5s");
258 std::env::set_var("PITCHFORK_IPC_CONNECT_ATTEMPTS", "20");
259 std::env::set_var("PITCHFORK_WEB_AUTO_START", "true");
260 }
261
262 let mut settings = Settings::default();
263 settings.load_from_env();
264
265 assert_eq!(settings.general.autostop_delay, "10m");
267 assert_eq!(settings.general.interval, "5s");
268 assert_eq!(settings.ipc.connect_attempts, 20);
269 assert!(settings.web.auto_start);
270
271 assert_eq!(settings.general.log_level, "info");
273 assert_eq!(settings.ipc.rate_limit, 100);
274
275 unsafe {
278 std::env::remove_var("PITCHFORK_AUTOSTOP_DELAY");
279 std::env::remove_var("PITCHFORK_INTERVAL");
280 std::env::remove_var("PITCHFORK_IPC_CONNECT_ATTEMPTS");
281 std::env::remove_var("PITCHFORK_WEB_AUTO_START");
282 }
283 }
284
285 #[test]
286 fn test_invalid_duration_fallback() {
287 let mut settings = Settings::default();
288
289 settings.general.autostop_delay = "invalid".to_string();
291 settings.general.interval = "not_a_duration".to_string();
292
293 assert_eq!(settings.general_autostop_delay(), Duration::from_secs(60)); assert_eq!(settings.general_interval(), Duration::from_secs(10)); }
297
298 #[test]
299 fn test_duration_methods_all_fields() {
300 let settings = Settings::default();
301
302 assert_eq!(settings.general_autostop_delay(), Duration::from_secs(60));
304 assert_eq!(settings.general_interval(), Duration::from_secs(10));
305 assert_eq!(settings.ipc_connect_min_delay(), Duration::from_millis(100));
306 assert_eq!(settings.ipc_connect_max_delay(), Duration::from_secs(1));
307 assert_eq!(settings.ipc_request_timeout(), Duration::from_secs(5));
308 assert_eq!(settings.ipc_rate_limit_window(), Duration::from_secs(1));
309 assert_eq!(settings.web_sse_poll_interval(), Duration::from_millis(500));
310 assert_eq!(settings.tui_refresh_rate(), Duration::from_secs(2));
311 assert_eq!(settings.tui_tick_rate(), Duration::from_millis(100));
312 assert_eq!(settings.tui_message_duration(), Duration::from_secs(3));
313 assert_eq!(
314 settings.supervisor_ready_check_interval(),
315 Duration::from_millis(500)
316 );
317 assert_eq!(
318 settings.supervisor_file_watch_debounce(),
319 Duration::from_secs(1)
320 );
321 assert_eq!(
322 settings.supervisor_log_flush_interval(),
323 Duration::from_millis(500)
324 );
325 assert_eq!(settings.supervisor_stop_timeout(), Duration::from_secs(5));
326 assert_eq!(
327 settings.supervisor_restart_delay(),
328 Duration::from_millis(100)
329 );
330 assert_eq!(
331 settings.supervisor_cron_check_interval(),
332 Duration::from_secs(10)
333 );
334 assert_eq!(
335 settings.supervisor_http_client_timeout(),
336 Duration::from_secs(5)
337 );
338 }
339
340 #[test]
341 fn test_unknown_fields_ignored() {
342 let toml_content = r#"
347[general]
348log_level = "debug"
349unknown_field = "should be ignored"
350
351[unknown_section]
352foo = "bar"
353"#;
354
355 let result: Result<Settings, _> = toml::from_str(toml_content);
356 let settings = result.expect("unknown fields should be silently ignored by serde");
358 assert_eq!(settings.general.log_level, "debug");
359 assert_eq!(settings.general.autostop_delay, "1m");
362 }
363
364 #[test]
365 fn test_serialize_roundtrip() {
366 let settings = Settings::default();
367
368 let toml_str = toml::to_string_pretty(&settings).unwrap();
370
371 let parsed: Settings = toml::from_str(&toml_str).unwrap();
373
374 assert_eq!(
376 settings.general.autostop_delay,
377 parsed.general.autostop_delay
378 );
379 assert_eq!(settings.general.interval, parsed.general.interval);
380 assert_eq!(settings.ipc.connect_attempts, parsed.ipc.connect_attempts);
381 assert_eq!(settings.web.auto_start, parsed.web.auto_start);
382 assert_eq!(settings.tui.stat_history, parsed.tui.stat_history);
383 }
384
385 #[test]
386 fn test_type_coercion() {
387 let toml_content = r#"
389[ipc]
390connect_attempts = 3
391rate_limit = 50
392
393[web]
394auto_start = true
395bind_port = 9000
396log_lines = 200
397
398[tui]
399stat_history = 120
400"#;
401
402 let settings: Settings = toml::from_str(toml_content).unwrap();
403
404 assert_eq!(settings.ipc.connect_attempts, 3);
405 assert_eq!(settings.ipc.rate_limit, 50);
406 assert!(settings.web.auto_start);
407 assert_eq!(settings.web.bind_port, 9000);
408 assert_eq!(settings.web.log_lines, 200);
409 assert_eq!(settings.tui.stat_history, 120);
410 }
411
412 #[test]
413 fn test_merge_from_non_default_values() {
414 let mut base = Settings::default();
415
416 let mut partial = SettingsPartial::default();
418 partial.general.autostop_delay = Some("5m".to_string());
419 partial.general.log_level = Some("debug".to_string());
420 partial.ipc.connect_attempts = Some(10);
421 partial.web.auto_start = Some(true);
422
423 base.apply_partial(&partial);
425
426 assert_eq!(base.general.autostop_delay, "5m");
428 assert_eq!(base.general.log_level, "debug");
429 assert_eq!(base.ipc.connect_attempts, 10);
430 assert!(base.web.auto_start);
431
432 assert_eq!(base.general.interval, "10s");
434 assert_eq!(base.ipc.rate_limit, 100);
435 }
436
437 #[test]
438 fn test_merge_from_preserves_existing() {
439 let mut base = Settings::default();
440 base.general.autostop_delay = "2m".to_string();
441 base.web.bind_port = 8080;
442
443 let empty_partial = SettingsPartial::default();
445 base.apply_partial(&empty_partial);
446
447 assert_eq!(base.general.autostop_delay, "2m"); assert_eq!(base.web.bind_port, 8080); }
450
451 #[test]
452 fn test_merge_chain() {
453 let mut settings = Settings::default();
455
456 let mut system_partial = SettingsPartial::default();
458 system_partial.general.log_level = Some("warn".to_string());
459 system_partial.web.bind_address = Some("0.0.0.0".to_string());
460 settings.apply_partial(&system_partial);
461
462 let mut user_partial = SettingsPartial::default();
464 user_partial.general.log_level = Some("info".to_string());
465 user_partial.tui.refresh_rate = Some("1s".to_string());
466 settings.apply_partial(&user_partial);
467
468 let mut project_partial = SettingsPartial::default();
470 project_partial.general.log_level = Some("debug".to_string());
471 project_partial.web.auto_start = Some(true);
472 settings.apply_partial(&project_partial);
473
474 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();
487 let mut p1 = SettingsPartial::default();
488 p1.general.log_level = Some("warn".to_string());
489 s2.apply_partial(&p1);
490 assert_eq!(s2.general.log_level, "warn");
491
492 let mut p2 = SettingsPartial::default();
494 p2.general.log_level = Some("info".to_string());
495 s2.apply_partial(&p2);
496 assert_eq!(s2.general.log_level, "info"); }
498
499 #[test]
500 fn test_settings_in_pitchfork_toml() {
501 let toml_content = r#"
503[daemons.myapp]
504run = "node server.js"
505
506[settings.general]
507autostop_delay = "5m"
508log_level = "debug"
509
510[settings.web]
511auto_start = true
512bind_port = 8080
513
514[settings.supervisor]
515user = "postgres"
516"#;
517
518 let table: toml::Table = toml::from_str(toml_content).unwrap();
520 let settings_table = table.get("settings").unwrap();
521 let partial: SettingsPartial = settings_table.clone().try_into().unwrap();
522
523 let mut settings = Settings::default();
525 settings.apply_partial(&partial);
526
527 assert_eq!(settings.general.autostop_delay, "5m");
528 assert_eq!(settings.general.log_level, "debug");
529 assert!(settings.web.auto_start);
530 assert_eq!(settings.web.bind_port, 8080);
531 assert_eq!(settings.supervisor.user, "postgres");
532 assert_eq!(settings.general.interval, "10s");
533 assert_eq!(settings.ipc.connect_attempts, 5);
534
535 assert!(partial.general.interval.is_none());
537 assert!(partial.ipc.connect_attempts.is_none());
538 }
539}