flash_watcher/
lib.rs

1use std::path::Path;
2use std::process::{Child, Command};
3
4use anyhow::{Context, Result};
5use colored::Colorize;
6use glob::Pattern;
7use serde::{Deserialize, Serialize};
8
9pub mod bench_results;
10pub mod stats;
11
12/// Configuration file format
13#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
14pub struct Config {
15    pub command: Vec<String>,
16    pub watch: Option<Vec<String>>,
17    pub ext: Option<String>,
18    pub pattern: Option<Vec<String>>,
19    pub ignore: Option<Vec<String>>,
20    pub debounce: Option<u64>,
21    pub initial: Option<bool>,
22    pub clear: Option<bool>,
23    pub restart: Option<bool>,
24    pub stats: Option<bool>,
25    pub stats_interval: Option<u64>,
26}
27
28/// Command line arguments structure
29#[derive(Debug, Clone, PartialEq)]
30pub struct Args {
31    pub command: Vec<String>,
32    pub watch: Vec<String>,
33    pub ext: Option<String>,
34    pub pattern: Vec<String>,
35    pub ignore: Vec<String>,
36    pub debounce: u64,
37    pub initial: bool,
38    pub clear: bool,
39    pub restart: bool,
40    pub stats: bool,
41    pub stats_interval: u64,
42    pub bench: bool,
43    pub config: Option<String>,
44    pub fast: bool,
45}
46
47impl Default for Args {
48    fn default() -> Self {
49        Self {
50            command: vec![],
51            watch: vec![".".to_string()],
52            ext: None,
53            pattern: vec![],
54            ignore: vec![],
55            debounce: 100,
56            initial: false,
57            clear: false,
58            restart: false,
59            stats: false,
60            stats_interval: 10,
61            bench: false,
62            config: None,
63            fast: false,
64        }
65    }
66}
67
68/// Command runner for executing commands when files change
69pub struct CommandRunner {
70    pub command: Vec<String>,
71    pub restart: bool,
72    pub clear: bool,
73    pub current_process: Option<Child>,
74}
75
76impl CommandRunner {
77    pub fn new(command: Vec<String>, restart: bool, clear: bool) -> Self {
78        Self {
79            command,
80            restart,
81            clear,
82            current_process: None,
83        }
84    }
85
86    pub fn run(&mut self) -> Result<()> {
87        // Kill previous process if restart mode is enabled
88        if self.restart {
89            if let Some(ref mut child) = self.current_process {
90                let _ = child.kill();
91                let _ = child.wait();
92            }
93        }
94
95        // Clear console if requested
96        if self.clear {
97            print!("\x1B[2J\x1B[1;1H");
98        }
99
100        // Skip output formatting for faster execution - only show if command fails
101
102        let child = if cfg!(target_os = "windows") {
103            Command::new("cmd").arg("/C").args(&self.command).spawn()
104        } else {
105            Command::new("sh")
106                .arg("-c")
107                .arg(self.command.join(" "))
108                .spawn()
109        }
110        .context("Failed to execute command")?;
111
112        if self.restart {
113            self.current_process = Some(child);
114        } else {
115            let status = child.wait_with_output()?;
116            if !status.status.success() {
117                println!(
118                    "{} {}",
119                    "Command exited with code:".bright_red(),
120                    status.status
121                );
122            }
123        }
124
125        Ok(())
126    }
127
128    /// Dry run for testing - doesn't actually execute commands
129    pub fn dry_run(&mut self) -> Result<()> {
130        if self.restart && self.current_process.is_some() {
131            self.current_process = None;
132        }
133
134        if self.command.is_empty() {
135            anyhow::bail!("Empty command");
136        }
137
138        Ok(())
139    }
140}
141
142/// Load configuration from a YAML file
143pub fn load_config(path: &str) -> Result<Config> {
144    let content =
145        std::fs::read_to_string(path).context(format!("Failed to read config file: {}", path))?;
146
147    serde_yaml::from_str(&content).context(format!("Failed to parse config file: {}", path))
148}
149
150/// Merge configuration file settings with command line arguments
151pub fn merge_config(args: &mut Args, config: Config) {
152    // Only use config values when CLI args are not provided
153    if args.command.is_empty() && !config.command.is_empty() {
154        args.command = config.command;
155    }
156
157    if args.watch.len() == 1 && args.watch[0] == "." {
158        if let Some(watch_dirs) = config.watch {
159            args.watch = watch_dirs;
160        }
161    }
162
163    if args.ext.is_none() {
164        args.ext = config.ext;
165    }
166
167    if args.pattern.is_empty() {
168        if let Some(patterns) = config.pattern {
169            args.pattern = patterns;
170        }
171    }
172
173    if args.ignore.is_empty() {
174        if let Some(ignores) = config.ignore {
175            args.ignore = ignores;
176        }
177    }
178
179    if args.debounce == 100 {
180        if let Some(debounce) = config.debounce {
181            args.debounce = debounce;
182        }
183    }
184
185    if !args.initial {
186        if let Some(initial) = config.initial {
187            args.initial = initial;
188        }
189    }
190
191    if !args.clear {
192        if let Some(clear) = config.clear {
193            args.clear = clear;
194        }
195    }
196
197    if !args.restart {
198        if let Some(restart) = config.restart {
199            args.restart = restart;
200        }
201    }
202
203    if !args.stats {
204        if let Some(stats) = config.stats {
205            args.stats = stats;
206        }
207    }
208
209    if args.stats_interval == 10 {
210        if let Some(interval) = config.stats_interval {
211            args.stats_interval = interval;
212        }
213    }
214}
215
216/// Check if a path should be processed based on filters
217pub fn should_process_path(
218    path: &Path,
219    ext_filter: &Option<String>,
220    include_patterns: &[Pattern],
221    ignore_patterns: &[Pattern],
222) -> bool {
223    // Check ignore patterns first
224    for pattern in ignore_patterns {
225        if pattern.matches_path(path) {
226            return false;
227        }
228    }
229
230    // Check extension filter
231    if let Some(ext_list) = ext_filter {
232        if let Some(extension) = path.extension().and_then(|e| e.to_str()) {
233            let extensions: Vec<&str> = ext_list.split(',').map(|s| s.trim()).collect();
234            if !extensions.contains(&extension) {
235                return false;
236            }
237        } else {
238            // No extension, but we have an extension filter
239            return false;
240        }
241    }
242
243    // Check include patterns
244    if !include_patterns.is_empty() {
245        for pattern in include_patterns {
246            if pattern.matches_path(path) {
247                return true;
248            }
249        }
250        return false;
251    }
252
253    true
254}
255
256/// Check if a directory should be skipped during traversal
257pub fn should_skip_dir(path: &Path, ignore_patterns: &[String]) -> bool {
258    let path_str = path.to_string_lossy();
259
260    // Skip common directories that should be ignored
261    let common_ignores = [".git", "node_modules", "target", ".svn", ".hg"];
262
263    for ignore in &common_ignores {
264        if path_str.contains(ignore) {
265            return true;
266        }
267    }
268
269    // Check user-defined ignore patterns
270    for pattern_str in ignore_patterns {
271        if let Ok(pattern) = glob::Pattern::new(pattern_str) {
272            if pattern.matches_path(path) {
273                return true;
274            }
275        }
276    }
277
278    false
279}
280
281/// Run benchmarks and display results
282pub fn run_benchmarks() -> Result<()> {
283    println!("{}", "Running benchmarks...".bright_green());
284    println!(
285        "{}",
286        "This will compare Flash with other file watchers.".bright_yellow()
287    );
288
289    // Check if benchmarks are available with the benchmarks feature
290    let has_criterion = Command::new("cargo")
291        .args([
292            "bench",
293            "--features",
294            "benchmarks",
295            "--bench",
296            "file_watcher",
297            "--help",
298        ])
299        .output()
300        .map(|output| output.status.success())
301        .unwrap_or(false);
302
303    if has_criterion {
304        // Attempt to run real benchmarks with feature flag
305        println!(
306            "{}",
307            "Running real benchmarks (this may take a few minutes)...".bright_blue()
308        );
309
310        let status = Command::new("cargo")
311            .args([
312                "bench",
313                "--features",
314                "benchmarks",
315                "--bench",
316                "file_watcher",
317            ])
318            .status()
319            .context("Failed to run benchmarks")?;
320
321        if !status.success() {
322            println!(
323                "{}",
324                "Benchmark run failed, showing sample data instead...".bright_yellow()
325            );
326            show_sample_results();
327        }
328    } else {
329        // No criterion benchmarks available, show sample data
330        println!(
331            "{}",
332            "Benchmarks require the 'benchmarks' feature. Showing sample data...".bright_yellow()
333        );
334        println!(
335            "{}",
336            "To run real benchmarks: cargo bench --features benchmarks".bright_blue()
337        );
338        show_sample_results();
339    }
340
341    Ok(())
342}
343
344/// Show sample benchmark results
345pub fn show_sample_results() {
346    use crate::bench_results::BenchResults;
347
348    // Create benchmark results with sample data
349    let results = BenchResults::with_sample_data();
350
351    // Display beautiful benchmark report
352    results.print_report();
353
354    println!(
355        "\n{}",
356        "Note: These are simulated results for demonstration.".bright_yellow()
357    );
358    println!(
359        "{}",
360        "Run 'cargo bench --bench file_watcher' for real benchmarks.".bright_blue()
361    );
362}
363
364/// Compile glob patterns from string patterns
365pub fn compile_patterns(patterns: &[String]) -> Result<Vec<Pattern>> {
366    patterns
367        .iter()
368        .map(|p| Pattern::new(p).context(format!("Invalid pattern: {}", p)))
369        .collect()
370}
371
372/// Validate command line arguments
373pub fn validate_args(args: &Args) -> Result<()> {
374    if args.command.is_empty() {
375        anyhow::bail!("No command specified. Use CLI arguments or a config file.");
376    }
377    Ok(())
378}
379
380/// Format a path for display (show just filename if possible)
381pub fn format_display_path(path: &Path) -> String {
382    path.file_name()
383        .and_then(|n| n.to_str())
384        .unwrap_or_else(|| path.to_str().unwrap_or("unknown path"))
385        .to_string()
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391    use std::io::Write;
392    use tempfile::NamedTempFile;
393
394    fn create_test_config_file(content: &str) -> NamedTempFile {
395        let mut file = NamedTempFile::new().unwrap();
396        write!(file, "{}", content).unwrap();
397        file
398    }
399
400    #[test]
401    fn test_args_default() {
402        let args = Args::default();
403        assert!(args.command.is_empty());
404        assert_eq!(args.watch, vec!["."]);
405        assert_eq!(args.debounce, 100);
406        assert!(!args.initial);
407        assert!(!args.clear);
408        assert!(!args.restart);
409        assert!(!args.stats);
410        assert_eq!(args.stats_interval, 10);
411        assert!(!args.bench);
412    }
413
414    #[test]
415    fn test_command_runner_new() {
416        let command = vec!["echo".to_string(), "hello".to_string()];
417        let runner = CommandRunner::new(command.clone(), true, false);
418
419        assert_eq!(runner.command, command);
420        assert!(runner.restart);
421        assert!(!runner.clear);
422        assert!(runner.current_process.is_none());
423    }
424
425    #[test]
426    fn test_command_runner_dry_run_success() {
427        let mut runner =
428            CommandRunner::new(vec!["echo".to_string(), "test".to_string()], false, false);
429        assert!(runner.dry_run().is_ok());
430    }
431
432    #[test]
433    fn test_command_runner_dry_run_empty_command() {
434        let mut runner = CommandRunner::new(vec![], false, false);
435        assert!(runner.dry_run().is_err());
436    }
437
438    #[test]
439    fn test_command_runner_dry_run_restart_mode() {
440        let mut runner = CommandRunner::new(vec!["echo".to_string()], true, false);
441        // Simulate having a current process
442        runner.current_process = None; // Would be Some(child) in real scenario
443        assert!(runner.dry_run().is_ok());
444        assert!(runner.current_process.is_none());
445    }
446
447    #[test]
448    fn test_load_config_valid() {
449        let config_yaml = r#"
450command: ["npm", "run", "dev"]
451watch:
452  - "src"
453  - "public"
454ext: "js,jsx,ts,tsx"
455pattern:
456  - "src/**/*.{js,jsx,ts,tsx}"
457ignore:
458  - "node_modules"
459  - ".git"
460debounce: 200
461initial: true
462clear: true
463restart: true
464stats: true
465stats_interval: 5
466"#;
467
468        let file = create_test_config_file(config_yaml);
469        let config = load_config(file.path().to_str().unwrap()).unwrap();
470
471        assert_eq!(config.command, vec!["npm", "run", "dev"]);
472        assert_eq!(
473            config.watch,
474            Some(vec!["src".to_string(), "public".to_string()])
475        );
476        assert_eq!(config.ext, Some("js,jsx,ts,tsx".to_string()));
477        assert_eq!(
478            config.pattern,
479            Some(vec!["src/**/*.{js,jsx,ts,tsx}".to_string()])
480        );
481        assert_eq!(
482            config.ignore,
483            Some(vec!["node_modules".to_string(), ".git".to_string()])
484        );
485        assert_eq!(config.debounce, Some(200));
486        assert_eq!(config.initial, Some(true));
487        assert_eq!(config.clear, Some(true));
488        assert_eq!(config.restart, Some(true));
489        assert_eq!(config.stats, Some(true));
490        assert_eq!(config.stats_interval, Some(5));
491    }
492
493    #[test]
494    fn test_load_config_invalid() {
495        let invalid_yaml = r#"
496command: "not-a-list"
497invalid: true
498"#;
499
500        let file = create_test_config_file(invalid_yaml);
501        let result = load_config(file.path().to_str().unwrap());
502        assert!(result.is_err());
503    }
504
505    #[test]
506    fn test_load_config_nonexistent_file() {
507        let result = load_config("nonexistent.yaml");
508        assert!(result.is_err());
509    }
510
511    #[test]
512    fn test_merge_config_empty_args() {
513        let mut args = Args::default();
514        let config = Config {
515            command: vec!["cargo".to_string(), "test".to_string()],
516            watch: Some(vec!["src".to_string(), "tests".to_string()]),
517            ext: Some("rs".to_string()),
518            pattern: Some(vec!["src/**/*.rs".to_string()]),
519            ignore: Some(vec!["target".to_string()]),
520            debounce: Some(200),
521            initial: Some(true),
522            clear: Some(true),
523            restart: Some(true),
524            stats: Some(true),
525            stats_interval: Some(5),
526        };
527
528        merge_config(&mut args, config);
529
530        assert_eq!(args.command, vec!["cargo", "test"]);
531        assert_eq!(args.watch, vec!["src", "tests"]);
532        assert_eq!(args.ext, Some("rs".to_string()));
533        assert_eq!(args.pattern, vec!["src/**/*.rs"]);
534        assert_eq!(args.ignore, vec!["target"]);
535        assert_eq!(args.debounce, 200);
536        assert!(args.initial);
537        assert!(args.clear);
538        assert!(args.restart);
539        assert!(args.stats);
540        assert_eq!(args.stats_interval, 5);
541    }
542
543    #[test]
544    fn test_merge_config_cli_override() {
545        let mut args = Args {
546            command: vec!["echo".to_string(), "hello".to_string()],
547            watch: vec!["src".to_string()],
548            ext: Some("js".to_string()),
549            pattern: vec!["custom-pattern".to_string()],
550            ignore: vec!["custom-ignore".to_string()],
551            debounce: 50,
552            initial: true,
553            clear: true,
554            restart: true,
555            stats: true,
556            stats_interval: 15,
557            bench: false,
558            config: None,
559            fast: false,
560        };
561
562        let config = Config {
563            command: vec!["cargo".to_string(), "test".to_string()],
564            watch: Some(vec!["src".to_string(), "tests".to_string()]),
565            ext: Some("rs".to_string()),
566            pattern: Some(vec!["src/**/*.rs".to_string()]),
567            ignore: Some(vec!["target".to_string()]),
568            debounce: Some(200),
569            initial: Some(false),
570            clear: Some(false),
571            restart: Some(false),
572            stats: Some(false),
573            stats_interval: Some(5),
574        };
575
576        let args_before = args.clone();
577        merge_config(&mut args, config);
578
579        // CLI args should take precedence
580        assert_eq!(args, args_before);
581    }
582
583    #[test]
584    fn test_should_process_path_no_filters() {
585        let path = Path::new("test.txt");
586        let ext_filter = None;
587        let include_patterns = vec![];
588        let ignore_patterns = vec![];
589
590        assert!(should_process_path(
591            path,
592            &ext_filter,
593            &include_patterns,
594            &ignore_patterns
595        ));
596    }
597
598    #[test]
599    fn test_should_process_path_extension_filter_match() {
600        let path = Path::new("test.js");
601        let ext_filter = Some("js,ts".to_string());
602        let include_patterns = vec![];
603        let ignore_patterns = vec![];
604
605        assert!(should_process_path(
606            path,
607            &ext_filter,
608            &include_patterns,
609            &ignore_patterns
610        ));
611    }
612
613    #[test]
614    fn test_should_process_path_extension_filter_no_match() {
615        let path = Path::new("test.py");
616        let ext_filter = Some("js,ts".to_string());
617        let include_patterns = vec![];
618        let ignore_patterns = vec![];
619
620        assert!(!should_process_path(
621            path,
622            &ext_filter,
623            &include_patterns,
624            &ignore_patterns
625        ));
626    }
627
628    #[test]
629    fn test_should_process_path_ignore_pattern() {
630        let path = Path::new("node_modules/test.js");
631        let ext_filter = None;
632        let include_patterns = vec![];
633        let ignore_patterns = vec![Pattern::new("**/node_modules/**").unwrap()];
634
635        assert!(!should_process_path(
636            path,
637            &ext_filter,
638            &include_patterns,
639            &ignore_patterns
640        ));
641    }
642
643    #[test]
644    fn test_should_process_path_include_pattern_match() {
645        let path = Path::new("src/test.js");
646        let ext_filter = None;
647        let include_patterns = vec![Pattern::new("src/**/*.js").unwrap()];
648        let ignore_patterns = vec![];
649
650        assert!(should_process_path(
651            path,
652            &ext_filter,
653            &include_patterns,
654            &ignore_patterns
655        ));
656    }
657
658    #[test]
659    fn test_should_process_path_include_pattern_no_match() {
660        let path = Path::new("docs/test.md");
661        let ext_filter = None;
662        let include_patterns = vec![Pattern::new("src/**/*.js").unwrap()];
663        let ignore_patterns = vec![];
664
665        assert!(!should_process_path(
666            path,
667            &ext_filter,
668            &include_patterns,
669            &ignore_patterns
670        ));
671    }
672
673    #[test]
674    fn test_should_skip_dir_common_ignores() {
675        assert!(should_skip_dir(Path::new(".git"), &[]));
676        assert!(should_skip_dir(Path::new("node_modules"), &[]));
677        assert!(should_skip_dir(Path::new("target"), &[]));
678        assert!(should_skip_dir(Path::new("project/.git/hooks"), &[]));
679        assert!(should_skip_dir(
680            Path::new("project/node_modules/package"),
681            &[]
682        ));
683    }
684
685    #[test]
686    fn test_should_skip_dir_custom_patterns() {
687        let ignore_patterns = vec!["build".to_string(), "dist".to_string()];
688        assert!(should_skip_dir(Path::new("build"), &ignore_patterns));
689        assert!(should_skip_dir(Path::new("dist"), &ignore_patterns));
690        assert!(!should_skip_dir(Path::new("src"), &ignore_patterns));
691    }
692
693    #[test]
694    fn test_should_skip_dir_no_match() {
695        assert!(!should_skip_dir(Path::new("src"), &[]));
696        assert!(!should_skip_dir(Path::new("tests"), &[]));
697        assert!(!should_skip_dir(Path::new("docs"), &[]));
698    }
699
700    #[test]
701    fn test_run_benchmarks() {
702        // This test just ensures the function doesn't panic
703        // In a real scenario, it would check for cargo bench availability
704        let result = run_benchmarks();
705        assert!(result.is_ok());
706    }
707
708    #[test]
709    fn test_show_sample_results() {
710        // This test just ensures the function doesn't panic
711        // It should print sample benchmark results
712        show_sample_results();
713    }
714
715    #[test]
716    fn test_compile_patterns_valid() {
717        let patterns = vec!["*.js".to_string(), "src/**/*.rs".to_string()];
718        let result = compile_patterns(&patterns);
719        assert!(result.is_ok());
720        let compiled = result.unwrap();
721        assert_eq!(compiled.len(), 2);
722    }
723
724    #[test]
725    fn test_compile_patterns_invalid() {
726        let patterns = vec!["[invalid".to_string()];
727        let result = compile_patterns(&patterns);
728        assert!(result.is_err());
729    }
730
731    #[test]
732    fn test_compile_patterns_empty() {
733        let patterns = vec![];
734        let result = compile_patterns(&patterns);
735        assert!(result.is_ok());
736        assert!(result.unwrap().is_empty());
737    }
738
739    #[test]
740    fn test_validate_args_valid() {
741        let args = Args {
742            command: vec!["echo".to_string(), "hello".to_string()],
743            ..Args::default()
744        };
745        assert!(validate_args(&args).is_ok());
746    }
747
748    #[test]
749    fn test_validate_args_empty_command() {
750        let args = Args::default();
751        assert!(validate_args(&args).is_err());
752    }
753
754    #[test]
755    fn test_format_display_path() {
756        assert_eq!(format_display_path(Path::new("test.js")), "test.js");
757        assert_eq!(format_display_path(Path::new("src/test.js")), "test.js");
758        assert_eq!(
759            format_display_path(Path::new("/full/path/to/file.rs")),
760            "file.rs"
761        );
762        assert_eq!(format_display_path(Path::new(".")), ".");
763    }
764
765    #[test]
766    fn test_should_process_path_file_without_extension() {
767        let path = Path::new("Makefile");
768        let ext_filter = Some("js,ts".to_string());
769        let include_patterns = vec![];
770        let ignore_patterns = vec![];
771
772        // File without extension should be rejected when extension filter is present
773        assert!(!should_process_path(
774            path,
775            &ext_filter,
776            &include_patterns,
777            &ignore_patterns
778        ));
779    }
780
781    #[test]
782    fn test_should_process_path_extension_with_spaces() {
783        let path = Path::new("test.js");
784        let ext_filter = Some("js, ts, jsx ".to_string()); // Extensions with spaces
785        let include_patterns = vec![];
786        let ignore_patterns = vec![];
787
788        // Should handle extensions with spaces correctly
789        assert!(should_process_path(
790            path,
791            &ext_filter,
792            &include_patterns,
793            &ignore_patterns
794        ));
795    }
796
797    #[test]
798    fn test_should_skip_dir_invalid_glob_pattern() {
799        // Test with invalid glob pattern that can't be compiled
800        let invalid_patterns = vec!["[invalid".to_string()];
801
802        // Should not skip directories when pattern is invalid
803        assert!(!should_skip_dir(Path::new("some-dir"), &invalid_patterns));
804    }
805
806    #[test]
807    fn test_merge_config_edge_cases() {
808        let mut args = Args {
809            command: vec![],              // Empty command
810            watch: vec![".".to_string()], // Default watch
811            ext: None,
812            pattern: vec![],
813            ignore: vec![],
814            debounce: 100, // Default debounce
815            initial: false,
816            clear: false,
817            restart: false,
818            stats: false,
819            stats_interval: 10, // Default stats interval
820            bench: false,
821            config: None,
822            fast: false,
823        };
824
825        let config = Config {
826            command: vec![], // Empty command in config too
827            watch: None,
828            ext: None,
829            pattern: None,
830            ignore: None,
831            debounce: None,
832            initial: None,
833            clear: None,
834            restart: None,
835            stats: None,
836            stats_interval: None,
837        };
838
839        merge_config(&mut args, config);
840
841        // Args should remain unchanged when config has no values
842        assert!(args.command.is_empty());
843        assert_eq!(args.watch, vec!["."]);
844        assert_eq!(args.debounce, 100);
845        assert_eq!(args.stats_interval, 10);
846    }
847
848    #[test]
849    fn test_config_serialization_roundtrip() {
850        let original_config = Config {
851            command: vec!["cargo".to_string(), "test".to_string()],
852            watch: Some(vec!["src".to_string(), "tests".to_string()]),
853            ext: Some("rs".to_string()),
854            pattern: Some(vec!["**/*.rs".to_string()]),
855            ignore: Some(vec!["target".to_string()]),
856            debounce: Some(200),
857            initial: Some(true),
858            clear: Some(false),
859            restart: Some(true),
860            stats: Some(false),
861            stats_interval: Some(5),
862        };
863
864        // Serialize to YAML
865        let yaml = serde_yaml::to_string(&original_config).unwrap();
866
867        // Deserialize back
868        let deserialized_config: Config = serde_yaml::from_str(&yaml).unwrap();
869
870        // Should be identical
871        assert_eq!(original_config, deserialized_config);
872    }
873
874    #[test]
875    fn test_args_debug_format() {
876        let args = Args {
877            command: vec!["echo".to_string(), "test".to_string()],
878            watch: vec!["src".to_string()],
879            ext: Some("rs".to_string()),
880            pattern: vec!["*.rs".to_string()],
881            ignore: vec!["target".to_string()],
882            debounce: 200,
883            initial: true,
884            clear: false,
885            restart: true,
886            stats: false,
887            stats_interval: 5,
888            bench: false,
889            config: Some("config.yaml".to_string()),
890            fast: false,
891        };
892
893        let debug_str = format!("{:?}", args);
894        assert!(debug_str.contains("command"));
895        assert!(debug_str.contains("echo"));
896        assert!(debug_str.contains("test"));
897    }
898}