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