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#[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#[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
68pub 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 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 if self.clear {
97 print!("\x1B[2J\x1B[1;1H");
98 }
99
100 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 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
142pub 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
150pub fn merge_config(args: &mut Args, config: Config) {
152 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
216pub fn should_process_path(
218 path: &Path,
219 ext_filter: &Option<String>,
220 include_patterns: &[Pattern],
221 ignore_patterns: &[Pattern],
222) -> bool {
223 for pattern in ignore_patterns {
225 if pattern.matches_path(path) {
226 return false;
227 }
228 }
229
230 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 return false;
240 }
241 }
242
243 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
256pub fn should_skip_dir(path: &Path, ignore_patterns: &[String]) -> bool {
258 let path_str = path.to_string_lossy();
259
260 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 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
281pub 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 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 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 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
344pub fn show_sample_results() {
346 use crate::bench_results::BenchResults;
347
348 let results = BenchResults::with_sample_data();
350
351 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
364pub 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
372pub 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
380pub 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 runner.current_process = None; 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 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 let result = run_benchmarks();
705 assert!(result.is_ok());
706 }
707
708 #[test]
709 fn test_show_sample_results() {
710 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 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()); let include_patterns = vec![];
786 let ignore_patterns = vec![];
787
788 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 let invalid_patterns = vec!["[invalid".to_string()];
801
802 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![], watch: vec![".".to_string()], ext: None,
812 pattern: vec![],
813 ignore: vec![],
814 debounce: 100, initial: false,
816 clear: false,
817 restart: false,
818 stats: false,
819 stats_interval: 10, bench: false,
821 config: None,
822 fast: false,
823 };
824
825 let config = Config {
826 command: vec![], 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 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 let yaml = serde_yaml::to_string(&original_config).unwrap();
866
867 let deserialized_config: Config = serde_yaml::from_str(&yaml).unwrap();
869
870 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}