1use crate::EnvVarManager;
2use ahash::AHashMap as HashMap;
3use color_eyre::Result;
4use notify::{RecommendedWatcher, RecursiveMode};
5use notify_debouncer_mini::{DebounceEventResult, DebouncedEvent, Debouncer, new_debouncer};
6use serde::Deserialize;
7use std::path::{Path, PathBuf};
8use std::sync::mpsc::{Receiver, Sender, channel};
9use std::sync::{Arc, Mutex};
10use std::time::Duration;
11use std::{fs, thread};
12
13#[derive(Debug, Clone)]
14pub enum SyncMode {
15 WatchOnly,
17 FileToSystem,
19 SystemToFile,
21 Bidirectional,
23}
24
25#[derive(Debug, Clone)]
26pub struct WatchConfig {
27 pub paths: Vec<PathBuf>,
29 pub mode: SyncMode,
31 pub auto_reload: bool,
33 pub debounce_duration: Duration,
35 pub patterns: Vec<String>,
37 pub log_changes: bool,
39 pub conflict_strategy: ConflictStrategy,
41}
42
43#[derive(Debug, Clone)]
44pub enum ConflictStrategy {
45 UseLatest,
47 PreferFile,
49 PreferSystem,
51 AskUser,
53}
54
55impl Default for WatchConfig {
56 fn default() -> Self {
57 Self {
58 paths: vec![PathBuf::from(".")],
59 mode: SyncMode::FileToSystem,
60 auto_reload: true,
61 debounce_duration: Duration::from_millis(300),
62 patterns: vec![
63 "*.env".to_string(),
64 ".env.*".to_string(),
65 "*.yaml".to_string(),
66 "*.yml".to_string(),
67 "*.toml".to_string(),
68 ],
69 log_changes: true,
70 conflict_strategy: ConflictStrategy::UseLatest,
71 }
72 }
73}
74
75pub struct EnvWatcher {
76 config: WatchConfig,
77 debouncer: Option<Debouncer<RecommendedWatcher>>,
78 stop_signal: Option<Sender<()>>,
79 manager: Arc<Mutex<EnvVarManager>>,
80 change_log: Arc<Mutex<Vec<ChangeEvent>>>,
81 variable_filter: Option<Vec<String>>,
82 output_file: Option<PathBuf>,
83}
84
85#[derive(Debug, Clone, serde::Serialize, Deserialize)]
86pub struct ChangeEvent {
87 pub timestamp: chrono::DateTime<chrono::Utc>,
88 pub path: PathBuf,
89 pub change_type: ChangeType,
90 pub details: String,
91}
92
93#[derive(Debug, Clone, serde::Serialize, Deserialize)]
94pub enum ChangeType {
95 FileCreated,
96 FileModified,
97 FileDeleted,
98 VariableAdded(String),
99 VariableModified(String),
100 VariableDeleted(String),
101}
102
103impl EnvWatcher {
104 #[must_use]
105 pub fn new(config: WatchConfig, manager: EnvVarManager) -> Self {
106 Self {
107 config,
108 debouncer: None,
109 stop_signal: None,
110 manager: Arc::new(Mutex::new(manager)),
111 change_log: Arc::new(Mutex::new(Vec::new())),
112 variable_filter: None,
113 output_file: None,
114 }
115 }
116
117 pub fn start(&mut self) -> Result<()> {
126 let (tx, rx) = channel();
127 let (stop_tx, stop_rx) = channel();
128
129 let tx_clone = tx;
131 let log_changes = self.config.log_changes;
132
133 let mut debouncer = new_debouncer(
135 self.config.debounce_duration,
136 move |result: DebounceEventResult| match result {
137 Ok(events) => {
138 for event in events {
139 if log_changes {
140 println!("đ File system event detected: {}", event.path.display());
141 }
142 if let Err(e) = tx_clone.send(event) {
143 eprintln!("Failed to send event: {e:?}");
144 }
145 }
146 }
147 Err(errors) => {
148 eprintln!("Watch error: {errors:?}");
149 }
150 },
151 )?;
152
153 let watcher = debouncer.watcher();
155
156 for path in &self.config.paths {
158 if path.exists() {
159 if path.is_file() {
160 if let Some(parent) = path.parent() {
162 watcher.watch(parent, RecursiveMode::NonRecursive)?;
163 if self.config.log_changes {
164 println!("đ Watching file: {} (via parent directory)", path.display());
165 }
166 }
167 } else {
168 watcher.watch(path, RecursiveMode::Recursive)?;
169 if self.config.log_changes {
170 println!("đ Watching directory: {}", path.display());
171 }
172 }
173 } else {
174 eprintln!("â ī¸ Path does not exist: {}", path.display());
175 }
176 }
177
178 self.debouncer = Some(debouncer);
180 self.stop_signal = Some(stop_tx);
181
182 let config = self.config.clone();
184 let manager = Arc::clone(&self.manager);
185 let change_log = Arc::clone(&self.change_log);
186 let variable_filter = self.variable_filter.clone();
187 let output_file = self.output_file.clone();
188
189 thread::spawn(move || {
190 Self::handle_events(
191 &rx,
192 &stop_rx,
193 &config,
194 &manager,
195 &change_log,
196 variable_filter.as_ref(),
197 output_file.as_ref(),
198 );
199 });
200
201 if matches!(self.config.mode, SyncMode::SystemToFile | SyncMode::Bidirectional) {
202 self.start_system_monitor();
203 }
204
205 Ok(())
206 }
207
208 pub fn stop(&mut self) -> Result<()> {
215 if let Some(stop_signal) = self.stop_signal.take() {
217 let _ = stop_signal.send(());
218 }
219
220 self.debouncer = None;
222
223 if self.config.log_changes {
224 println!("đ Stopped watching");
225 }
226
227 Ok(())
228 }
229
230 fn handle_events(
231 rx: &Receiver<DebouncedEvent>,
232 stop_rx: &Receiver<()>,
233 config: &WatchConfig,
234 manager: &Arc<Mutex<EnvVarManager>>,
235 change_log: &Arc<Mutex<Vec<ChangeEvent>>>,
236 variable_filter: Option<&Vec<String>>,
237 output_file: Option<&PathBuf>,
238 ) {
239 loop {
240 if stop_rx.try_recv().is_ok() {
242 break;
243 }
244
245 match rx.recv_timeout(Duration::from_millis(100)) {
247 Ok(event) => {
248 if config.log_changes {
249 println!("đ Processing event for: {}", event.path.display());
250 }
251
252 let path = event.path.clone();
253
254 if let Some(output) = output_file {
256 if path == *output && matches!(config.mode, SyncMode::Bidirectional) {
257 if config.log_changes {
258 println!("âī¸ Skipping output file to avoid loop");
259 }
260 continue;
261 }
262 }
263
264 if !Self::matches_patterns(&path, &config.patterns) {
266 if config.log_changes {
267 println!("âī¸ File doesn't match patterns: {}", path.display());
268 }
269 continue;
270 }
271
272 let change_type = if path.exists() {
274 if config.log_changes {
275 println!("âī¸ Modified: {}", path.display());
276 }
277 ChangeType::FileModified
278 } else {
279 if config.log_changes {
280 println!("đī¸ Deleted: {}", path.display());
281 }
282 ChangeType::FileDeleted
283 };
284
285 match config.mode {
287 SyncMode::WatchOnly => {
288 Self::log_change(
289 change_log,
290 path,
291 change_type,
292 "File changed (watch only mode)".to_string(),
293 );
294 }
295 SyncMode::FileToSystem | SyncMode::Bidirectional => {
296 if matches!(change_type, ChangeType::FileModified | ChangeType::FileCreated) {
297 if let Err(e) = Self::handle_file_change(
298 &path,
299 change_type,
300 config,
301 manager,
302 change_log,
303 variable_filter,
304 ) {
305 eprintln!("Error handling file change: {e}");
306 }
307 }
308 }
309 SyncMode::SystemToFile => {
310 if config.log_changes {
312 println!("âšī¸ Ignoring file change in system-to-file mode");
313 }
314 }
315 }
316 }
317 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
318 }
320 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
321 break;
323 }
324 }
325 }
326 }
327
328 fn handle_file_change(
329 path: &Path,
330 _change_type: ChangeType,
331 config: &WatchConfig,
332 manager: &Arc<Mutex<EnvVarManager>>,
333 change_log: &Arc<Mutex<Vec<ChangeEvent>>>,
334 variable_filter: Option<&Vec<String>>,
335 ) -> Result<()> {
336 if !config.auto_reload {
337 return Ok(());
338 }
339
340 thread::sleep(Duration::from_millis(50));
342
343 let mut manager = manager.lock().unwrap();
345
346 let before_vars: HashMap<String, String> = manager
348 .list()
349 .into_iter()
350 .filter(|v| {
351 variable_filter
352 .as_ref()
353 .is_none_or(|filter| filter.iter().any(|f| v.name.contains(f)))
354 })
355 .map(|v| (v.name.clone(), v.value.clone()))
356 .collect();
357
358 let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("");
360
361 let load_result = match extension {
362 "env" => Self::load_env_file(path, &mut manager, variable_filter),
363 "yaml" | "yml" => Self::load_yaml_file(path, &mut manager, variable_filter),
364 "json" => Self::load_json_file(path, &mut manager, variable_filter),
365 _ => {
366 Self::load_env_file(path, &mut manager, variable_filter)
368 }
369 };
370
371 if let Err(e) = load_result {
372 eprintln!("Failed to load file: {e}");
373 return Err(e);
374 }
375
376 let after_vars = manager.list();
378 let mut changes_made = false;
379
380 for var in after_vars {
381 if let Some(filter) = variable_filter {
383 if !filter.iter().any(|f| var.name.contains(f)) {
384 continue;
385 }
386 }
387
388 if let Some(old_value) = before_vars.get(&var.name) {
389 if old_value != &var.value {
390 Self::log_change(
391 change_log,
392 path.to_path_buf(),
393 ChangeType::VariableModified(var.name.clone()),
394 format!("Changed {} from '{}' to '{}'", var.name, old_value, var.value),
395 );
396
397 if config.log_changes {
398 println!(" đ {} changed from '{}' to '{}'", var.name, old_value, var.value);
399 }
400 changes_made = true;
401 }
402 } else {
403 Self::log_change(
404 change_log,
405 path.to_path_buf(),
406 ChangeType::VariableAdded(var.name.clone()),
407 format!("Added {} = '{}'", var.name, var.value),
408 );
409
410 if config.log_changes {
411 println!(" â {} = '{}'", var.name, var.value);
412 }
413 changes_made = true;
414 }
415 }
416
417 for (name, _) in before_vars {
419 if manager.get(&name).is_none() {
420 Self::log_change(
421 change_log,
422 path.to_path_buf(),
423 ChangeType::VariableDeleted(name.clone()),
424 format!("Deleted {name}"),
425 );
426
427 if config.log_changes {
428 println!(" â {name} deleted");
429 }
430 changes_made = true;
431 }
432 }
433
434 if !changes_made && config.log_changes {
435 println!(" âšī¸ No changes detected");
436 }
437
438 Ok(())
439 }
440
441 fn load_env_file(path: &Path, manager: &mut EnvVarManager, variable_filter: Option<&Vec<String>>) -> Result<()> {
442 let content = fs::read_to_string(path)?;
443
444 for line in content.lines() {
445 let line = line.trim();
446 if line.is_empty() || line.starts_with('#') {
447 continue;
448 }
449
450 if let Some((key, value)) = line.split_once('=') {
451 let key = key.trim();
452 let value = value.trim().trim_matches('"').trim_matches('\'');
453
454 if let Some(filter) = variable_filter {
456 if !filter.iter().any(|f| key.contains(f)) {
457 continue;
458 }
459 }
460
461 manager.set(key, value, true)?;
462 }
463 }
464
465 Ok(())
466 }
467
468 fn load_yaml_file(path: &Path, manager: &mut EnvVarManager, variable_filter: Option<&Vec<String>>) -> Result<()> {
469 let content = fs::read_to_string(path)?;
470 let yaml: serde_yaml::Value = serde_yaml::from_str(&content)?;
471
472 if let serde_yaml::Value::Mapping(map) = yaml {
473 for (key, value) in map {
474 if let (Some(key_str), Some(value_str)) = (key.as_str(), value.as_str()) {
475 if let Some(filter) = variable_filter {
477 if !filter.iter().any(|f| key_str.contains(f)) {
478 continue;
479 }
480 }
481
482 manager.set(key_str, value_str, true)?;
483 }
484 }
485 }
486
487 Ok(())
488 }
489
490 fn load_json_file(path: &Path, manager: &mut EnvVarManager, variable_filter: Option<&Vec<String>>) -> Result<()> {
491 let content = fs::read_to_string(path)?;
492 let json: serde_json::Value = serde_json::from_str(&content)?;
493
494 if let serde_json::Value::Object(map) = json {
495 for (key, value) in map {
496 if let serde_json::Value::String(value_str) = value {
497 if let Some(filter) = variable_filter {
499 if !filter.iter().any(|f| key.contains(f)) {
500 continue;
501 }
502 }
503
504 manager.set(&key, &value_str, true)?;
505 }
506 }
507 }
508
509 Ok(())
510 }
511
512 fn start_system_monitor(&mut self) {
513 let manager = Arc::clone(&self.manager);
514 let config = self.config.clone();
515 let _change_log = Arc::clone(&self.change_log);
516 let variable_filter = self.variable_filter.clone();
517 let output_file = self.output_file.clone();
518
519 thread::spawn(move || {
520 let mut last_snapshot = HashMap::new();
521
522 loop {
523 thread::sleep(Duration::from_secs(1));
524
525 manager.lock().unwrap().load_all().ok();
526
527 let current_snapshot: HashMap<String, String> = manager
528 .lock()
529 .unwrap()
530 .list()
531 .iter()
532 .filter(|v| {
533 variable_filter
534 .as_ref()
535 .is_none_or(|filter| filter.iter().any(|f| v.name.contains(f)))
536 })
537 .map(|v| (v.name.clone(), v.value.clone()))
538 .collect();
539
540 if matches!(config.mode, SyncMode::SystemToFile | SyncMode::Bidirectional) {
542 if let Some(ref output) = output_file {
543 let mut changed = false;
544
545 for (name, value) in ¤t_snapshot {
546 if last_snapshot.get(name) != Some(value) {
547 changed = true;
548 if config.log_changes {
549 println!("đ System change detected: {name} changed");
550 }
551 }
552 }
553
554 for name in last_snapshot.keys() {
556 if !current_snapshot.contains_key(name) {
557 changed = true;
558 if config.log_changes {
559 println!("â System change detected: {name} deleted");
560 }
561 }
562 }
563
564 if changed {
565 let mut content = String::new();
567 #[allow(clippy::format_push_string)]
568 for (name, value) in ¤t_snapshot {
569 content.push_str(&format!("{name}={value}\n"));
570 }
571
572 if let Err(e) = fs::write(output, &content) {
573 eprintln!("Failed to write to output file: {e}");
574 } else if config.log_changes {
575 println!("đž Updated output file");
576 }
577 }
578 }
579 }
580
581 last_snapshot = current_snapshot;
582 }
583 });
584 }
585
586 fn matches_patterns(path: &Path, patterns: &[String]) -> bool {
587 let file_name = match path.file_name() {
588 Some(name) => name.to_string_lossy(),
589 None => return false,
590 };
591
592 patterns.iter().any(|pattern| {
593 if pattern.contains('*') {
594 let regex_pattern = pattern.replace('.', r"\.").replace('*', ".*");
595 if let Ok(re) = regex::Regex::new(&format!("^{regex_pattern}$")) {
596 return re.is_match(&file_name);
597 }
598 }
599 &file_name == pattern
600 })
601 }
602
603 fn log_change(change_log: &Arc<Mutex<Vec<ChangeEvent>>>, path: PathBuf, change_type: ChangeType, details: String) {
604 let event = ChangeEvent {
605 timestamp: chrono::Utc::now(),
606 path,
607 change_type,
608 details,
609 };
610
611 let mut log = change_log.lock().expect("Failed to lock change log");
612 log.push(event);
613
614 if log.len() > 1000 {
616 log.drain(0..100);
617 }
618 }
619
620 #[must_use]
626 pub fn get_change_log(&self) -> Vec<ChangeEvent> {
627 self.change_log.lock().expect("Failed to lock change log").clone()
628 }
629
630 pub fn export_change_log(&self, path: &Path) -> Result<()> {
638 let log = self.get_change_log();
639 let json = serde_json::to_string_pretty(&log)?;
640 fs::write(path, json)?;
641 Ok(())
642 }
643
644 pub fn set_variable_filter(&mut self, vars: Vec<String>) {
645 self.variable_filter = Some(vars);
646 }
647
648 pub fn set_output_file(&mut self, path: PathBuf) {
650 self.output_file = Some(path);
651 }
652}
653
654#[cfg(test)]
657mod tests {
658 use super::*;
659 use std::fs;
660 use std::time::Duration;
661 use tempfile::TempDir;
662
663 fn create_test_manager() -> EnvVarManager {
664 let mut manager = EnvVarManager::new();
665 manager.set("TEST_VAR", "initial_value", false).unwrap();
666 manager.set("ANOTHER_VAR", "another_value", false).unwrap();
667 manager
668 }
669
670 fn create_test_config(temp_dir: &Path) -> WatchConfig {
671 WatchConfig {
672 paths: vec![temp_dir.to_path_buf()],
673 mode: SyncMode::FileToSystem,
674 auto_reload: true,
675 debounce_duration: Duration::from_millis(100),
676 patterns: vec!["*.env".to_string(), "*.json".to_string(), "*.yaml".to_string()],
677 log_changes: false,
678 conflict_strategy: ConflictStrategy::UseLatest,
679 }
680 }
681
682 fn wait_for_debounce() {
683 thread::sleep(Duration::from_millis(200));
684 }
685
686 #[test]
687 fn test_env_watcher_creation() {
688 let config = WatchConfig::default();
689 let manager = create_test_manager();
690 let watcher = EnvWatcher::new(config, manager);
691
692 assert!(watcher.debouncer.is_none());
693 assert!(watcher.stop_signal.is_none());
694 assert!(watcher.variable_filter.is_none());
695 assert!(watcher.output_file.is_none());
696 }
697
698 #[test]
699 fn test_watch_config_default() {
700 let config = WatchConfig::default();
701
702 assert_eq!(config.paths, vec![PathBuf::from(".")]);
703 assert!(matches!(config.mode, SyncMode::FileToSystem));
704 assert!(config.auto_reload);
705 assert_eq!(config.debounce_duration, Duration::from_millis(300));
706 assert_eq!(config.patterns.len(), 5);
707 assert!(config.log_changes);
708 assert!(matches!(config.conflict_strategy, ConflictStrategy::UseLatest));
709 }
710
711 #[test]
712 fn test_variable_filter() {
713 let config = WatchConfig::default();
714 let manager = create_test_manager();
715 let mut watcher = EnvWatcher::new(config, manager);
716
717 assert!(watcher.variable_filter.is_none());
718
719 watcher.set_variable_filter(vec!["TEST".to_string(), "API".to_string()]);
720 assert!(watcher.variable_filter.is_some());
721 assert_eq!(watcher.variable_filter.as_ref().unwrap().len(), 2);
722 }
723
724 #[test]
725 fn test_output_file() {
726 let config = WatchConfig::default();
727 let manager = create_test_manager();
728 let mut watcher = EnvWatcher::new(config, manager);
729
730 assert!(watcher.output_file.is_none());
731
732 let output_path = PathBuf::from("output.env");
733 watcher.set_output_file(output_path.clone());
734 assert_eq!(watcher.output_file, Some(output_path));
735 }
736
737 #[test]
738 fn test_change_log() {
739 let config = WatchConfig::default();
740 let manager = create_test_manager();
741 let watcher = EnvWatcher::new(config, manager);
742
743 let log = watcher.get_change_log();
744 assert!(log.is_empty());
745
746 let change_event = ChangeEvent {
748 timestamp: chrono::Utc::now(),
749 path: PathBuf::from("test.env"),
750 change_type: ChangeType::FileModified,
751 details: "Test change".to_string(),
752 };
753
754 watcher.change_log.lock().unwrap().push(change_event);
755
756 let log = watcher.get_change_log();
757 assert_eq!(log.len(), 1);
758 assert_eq!(log[0].details, "Test change");
759 }
760
761 #[test]
762 fn test_export_change_log() {
763 let temp_dir = TempDir::new().unwrap();
764 let log_file = temp_dir.path().join("changes.json");
765
766 let config = WatchConfig::default();
767 let manager = create_test_manager();
768 let watcher = EnvWatcher::new(config, manager);
769
770 let mut log = watcher.change_log.lock().unwrap();
772 log.push(ChangeEvent {
773 timestamp: chrono::Utc::now(),
774 path: PathBuf::from("test1.env"),
775 change_type: ChangeType::FileCreated,
776 details: "Created file".to_string(),
777 });
778 log.push(ChangeEvent {
779 timestamp: chrono::Utc::now(),
780 path: PathBuf::from("test2.env"),
781 change_type: ChangeType::VariableAdded("NEW_VAR".to_string()),
782 details: "Added NEW_VAR".to_string(),
783 });
784 drop(log);
785
786 watcher.export_change_log(&log_file).unwrap();
788
789 assert!(log_file.exists());
791 let content = fs::read_to_string(&log_file).unwrap();
792 let parsed: Vec<ChangeEvent> = serde_json::from_str(&content).unwrap();
793 assert_eq!(parsed.len(), 2);
794 }
795
796 #[test]
797 fn test_matches_patterns() {
798 let patterns = vec!["*.env".to_string(), "*.yaml".to_string(), "config.json".to_string()];
799
800 assert!(EnvWatcher::matches_patterns(&PathBuf::from("test.env"), &patterns));
801 assert!(EnvWatcher::matches_patterns(&PathBuf::from("app.yaml"), &patterns));
802 assert!(EnvWatcher::matches_patterns(&PathBuf::from("config.json"), &patterns));
803 assert!(!EnvWatcher::matches_patterns(&PathBuf::from("test.txt"), &patterns));
804 assert!(!EnvWatcher::matches_patterns(&PathBuf::from("README.md"), &patterns));
805 }
806
807 #[test]
808 fn test_load_env_file() {
809 let temp_dir = TempDir::new().unwrap();
810 let env_file = temp_dir.path().join("test.env");
811
812 let content = r#"
814# Comment line
815TEST_VAR=test_value
816ANOTHER_VAR=another_value
817QUOTED_VAR="quoted value"
818SINGLE_QUOTED='single quoted'
819 "#;
820 fs::write(&env_file, content).unwrap();
821
822 let mut manager = EnvVarManager::new();
823 EnvWatcher::load_env_file(&env_file, &mut manager, None).unwrap();
824
825 assert_eq!(manager.get("TEST_VAR").unwrap().value, "test_value");
826 assert_eq!(manager.get("ANOTHER_VAR").unwrap().value, "another_value");
827 assert_eq!(manager.get("QUOTED_VAR").unwrap().value, "quoted value");
828 assert_eq!(manager.get("SINGLE_QUOTED").unwrap().value, "single quoted");
829 }
830
831 #[test]
832 fn test_load_env_file_with_filter() {
833 let temp_dir = TempDir::new().unwrap();
834 let env_file = temp_dir.path().join("test.env");
835
836 let content = r"
837TEST_VAR=test_value
838API_KEY=secret_key
839DATABASE_URL=postgres://localhost
840API_SECRET=another_secret
841 ";
842 fs::write(&env_file, content).unwrap();
843
844 let mut manager = EnvVarManager::new();
845 let filter = vec!["API".to_string()];
846 EnvWatcher::load_env_file(&env_file, &mut manager, Some(&filter)).unwrap();
847
848 assert!(manager.get("API_KEY").is_some());
849 assert!(manager.get("API_SECRET").is_some());
850 assert!(manager.get("TEST_VAR").is_none());
851 assert!(manager.get("DATABASE_URL").is_none());
852 }
853
854 #[test]
855 fn test_load_json_file() {
856 let temp_dir = TempDir::new().unwrap();
857 let json_file = temp_dir.path().join("config.json");
858
859 let content = r#"{
860 "TEST_VAR": "json_value",
861 "NUMBER_VAR": "42",
862 "BOOL_VAR": "true"
863 }"#;
864 fs::write(&json_file, content).unwrap();
865
866 let mut manager = EnvVarManager::new();
867 EnvWatcher::load_json_file(&json_file, &mut manager, None).unwrap();
868
869 assert_eq!(manager.get("TEST_VAR").unwrap().value, "json_value");
870 assert_eq!(manager.get("NUMBER_VAR").unwrap().value, "42");
871 assert_eq!(manager.get("BOOL_VAR").unwrap().value, "true");
872 }
873
874 #[test]
875 fn test_load_yaml_file() {
876 let temp_dir = TempDir::new().unwrap();
877 let yaml_file = temp_dir.path().join("config.yaml");
878
879 let content = r#"
880TEST_VAR: yaml_value
881NESTED_VAR: nested_value
882QUOTED: "quoted yaml"
883 "#;
884 fs::write(&yaml_file, content).unwrap();
885
886 let mut manager = EnvVarManager::new();
887 EnvWatcher::load_yaml_file(&yaml_file, &mut manager, None).unwrap();
888
889 assert_eq!(manager.get("TEST_VAR").unwrap().value, "yaml_value");
890 assert_eq!(manager.get("NESTED_VAR").unwrap().value, "nested_value");
891 assert_eq!(manager.get("QUOTED").unwrap().value, "quoted yaml");
892 }
893
894 #[test]
895 fn test_start_and_stop() {
896 let temp_dir = TempDir::new().unwrap();
897 let config = create_test_config(temp_dir.path());
898 let manager = create_test_manager();
899 let mut watcher = EnvWatcher::new(config, manager);
900
901 watcher.start().unwrap();
903 assert!(watcher.debouncer.is_some());
904 assert!(watcher.stop_signal.is_some());
905
906 watcher.stop().unwrap();
908 assert!(watcher.debouncer.is_none());
909 assert!(watcher.stop_signal.is_none());
910 }
911
912 #[test]
913 fn test_file_watching_integration() {
914 let temp_dir = TempDir::new().unwrap();
915 let env_file = temp_dir.path().join("test.env");
916
917 fs::write(&env_file, "INITIAL=value1").unwrap();
919
920 let config = WatchConfig {
921 paths: vec![env_file.clone()],
922 mode: SyncMode::FileToSystem,
923 auto_reload: true,
924 debounce_duration: Duration::from_millis(50),
925 patterns: vec!["*.env".to_string()],
926 log_changes: false,
927 conflict_strategy: ConflictStrategy::UseLatest,
928 };
929
930 let manager = EnvVarManager::new();
931 let mut watcher = EnvWatcher::new(config, manager);
932
933 watcher.start().unwrap();
935
936 wait_for_debounce();
938
939 fs::write(&env_file, "INITIAL=value2\nNEW_VAR=new_value").unwrap();
941
942 thread::sleep(Duration::from_millis(300));
944
945 let log = watcher.get_change_log();
947 assert!(!log.is_empty());
948
949 watcher.stop().unwrap();
951 }
952
953 #[test]
954 fn test_sync_mode_watch_only() {
955 let temp_dir = TempDir::new().unwrap();
956 let config = WatchConfig {
957 paths: vec![temp_dir.path().to_path_buf()],
958 mode: SyncMode::WatchOnly,
959 auto_reload: true,
960 debounce_duration: Duration::from_millis(50),
961 patterns: vec!["*.env".to_string()],
962 log_changes: false,
963 conflict_strategy: ConflictStrategy::UseLatest,
964 };
965
966 let manager = create_test_manager();
967 let watcher = EnvWatcher::new(config, manager);
968
969 let log = watcher.get_change_log();
971 assert!(log.is_empty());
972 }
973
974 #[test]
975 fn test_system_to_file_mode() {
976 let temp_dir = TempDir::new().unwrap();
977 let output_file = temp_dir.path().join("output.env");
978
979 let config = WatchConfig {
980 paths: vec![temp_dir.path().to_path_buf()],
981 mode: SyncMode::SystemToFile,
982 auto_reload: true,
983 debounce_duration: Duration::from_millis(50),
984 patterns: vec!["*.env".to_string()],
985 log_changes: false,
986 conflict_strategy: ConflictStrategy::UseLatest,
987 };
988
989 let manager = create_test_manager();
990 let mut watcher = EnvWatcher::new(config, manager);
991 watcher.set_output_file(output_file.clone());
992
993 watcher.start().unwrap();
995
996 thread::sleep(Duration::from_millis(1500));
998
999 assert!(output_file.exists());
1001
1002 watcher.stop().unwrap();
1004 }
1005
1006 #[test]
1007 fn test_change_log_limit() {
1008 let config = WatchConfig::default();
1009 let manager = create_test_manager();
1010 let watcher = EnvWatcher::new(config, manager);
1011
1012 for i in 0..1100 {
1014 EnvWatcher::log_change(
1015 &watcher.change_log,
1016 PathBuf::from(format!("test{i}.env")),
1017 ChangeType::FileModified,
1018 format!("Change {i}"),
1019 );
1020 }
1021
1022 let current_log = watcher.get_change_log();
1024 assert_eq!(current_log.len(), 1000);
1025 assert_eq!(current_log[0].details, "Change 100");
1026 }
1027
1028 #[test]
1029 fn test_handle_file_change_no_auto_reload() {
1030 let temp_dir = TempDir::new().unwrap();
1031 let env_file = temp_dir.path().join("test.env");
1032 fs::write(&env_file, "TEST=value").unwrap();
1033
1034 let config = WatchConfig {
1035 paths: vec![env_file.clone()],
1036 mode: SyncMode::FileToSystem,
1037 auto_reload: false, debounce_duration: Duration::from_millis(50),
1039 patterns: vec!["*.env".to_string()],
1040 log_changes: false,
1041 conflict_strategy: ConflictStrategy::UseLatest,
1042 };
1043
1044 let manager = EnvVarManager::new();
1045 let manager_arc = Arc::new(Mutex::new(manager));
1046 let change_log = Arc::new(Mutex::new(Vec::new()));
1047
1048 let result = EnvWatcher::handle_file_change(
1050 &env_file,
1051 ChangeType::FileModified,
1052 &config,
1053 &manager_arc,
1054 &change_log,
1055 None,
1056 );
1057
1058 assert!(result.is_ok());
1059 assert!(manager_arc.lock().unwrap().get("TEST").is_none());
1060 }
1061
1062 #[test]
1063 fn test_bidirectional_sync() {
1064 let temp_dir = TempDir::new().unwrap();
1065 let sync_file = temp_dir.path().join("sync.env");
1066
1067 let config = WatchConfig {
1068 paths: vec![sync_file.clone()],
1069 mode: SyncMode::Bidirectional,
1070 auto_reload: true,
1071 debounce_duration: Duration::from_millis(50),
1072 patterns: vec!["*.env".to_string()],
1073 log_changes: false,
1074 conflict_strategy: ConflictStrategy::UseLatest,
1075 };
1076
1077 let manager = create_test_manager();
1078 let mut watcher = EnvWatcher::new(config, manager);
1079 watcher.set_output_file(sync_file.clone());
1080
1081 watcher.start().unwrap();
1083
1084 fs::write(&sync_file, "BIDIRECTIONAL=test").unwrap();
1086
1087 wait_for_debounce();
1089 thread::sleep(Duration::from_millis(200));
1090
1091 watcher.stop().unwrap();
1093 }
1094
1095 #[test]
1096 fn test_conflict_strategy() {
1097 let strategies = vec![
1098 ConflictStrategy::UseLatest,
1099 ConflictStrategy::PreferFile,
1100 ConflictStrategy::PreferSystem,
1101 ConflictStrategy::AskUser,
1102 ];
1103
1104 #[allow(clippy::field_reassign_with_default)]
1105 for strategy in strategies {
1106 let mut config = WatchConfig::default();
1107 config.conflict_strategy = strategy.clone();
1108
1109 #[allow(clippy::assertions_on_constants)]
1110 match strategy {
1111 ConflictStrategy::UseLatest
1112 | ConflictStrategy::PreferFile
1113 | ConflictStrategy::PreferSystem
1114 | ConflictStrategy::AskUser => assert!(true),
1115 }
1116 }
1117 }
1118}