1pub mod cli;
2pub mod config;
3pub mod parser;
4pub mod printer;
5pub mod scanner;
6
7use anyhow::Result;
8use cli::{Cli, Commands, ConfigFormat, ScanArgs, SortOrder};
9use config::Config;
10use parser::TodoParser;
11use printer::{OutputFormat, PrintOptions, Printer};
12use scanner::{ScanOptions, ScanResult, Scanner};
13use std::path::PathBuf;
14
15pub fn run() -> Result<()> {
17 let cli = Cli::parse_args();
18
19 if cli.global.no_color || std::env::var("NO_COLOR").is_ok() {
21 colored::control::set_override(false);
22 }
23
24 match cli.get_command() {
26 Commands::Scan(args) => cmd_scan(args, &cli.global),
27 Commands::List(args) => cmd_list(args, &cli.global),
28 Commands::Tags(args) => cmd_tags(args, &cli.global),
29 Commands::Init(args) => cmd_init(args),
30 Commands::Stats(args) => cmd_stats(args, &cli.global),
31 }
32}
33
34fn cmd_scan(args: ScanArgs, global: &cli::GlobalOptions) -> Result<()> {
36 let path = args.path.clone().unwrap_or_else(|| PathBuf::from("."));
37 let path = path
38 .canonicalize()
39 .with_context(|| format!("Failed to resolve path: {}", path.display()))?;
40
41 let mut config = load_config(&path, global.config.as_deref())?;
43
44 config.merge_with_cli(
46 args.tags.clone(),
47 args.include.clone(),
48 args.exclude.clone(),
49 args.json,
50 args.flat,
51 global.no_color,
52 );
53
54 let parser = TodoParser::new(&config.tags, args.case_sensitive);
56
57 let scan_options = ScanOptions {
59 include: config.include.clone(),
60 exclude: config.exclude.clone(),
61 max_depth: args.depth,
62 follow_links: args.follow_links,
63 hidden: args.hidden,
64 threads: 0, respect_gitignore: true,
66 };
67
68 let scanner = Scanner::new(parser, scan_options);
70 let mut result = scanner.scan(&path)?;
71
72 sort_results(&mut result, args.sort);
74
75 let print_options = PrintOptions {
77 format: if args.json {
78 OutputFormat::Json
79 } else if args.flat {
80 OutputFormat::Flat
81 } else {
82 OutputFormat::Tree
83 },
84 colored: !global.no_color,
85 show_line_numbers: true,
86 full_paths: false,
87 clickable_links: !global.no_color,
88 base_path: Some(path),
89 show_summary: !args.json,
90 group_by_tag: args.group_by_tag,
91 };
92
93 let printer = Printer::new(print_options);
94 printer.print(&result)?;
95
96 Ok(())
97}
98
99fn cmd_list(args: cli::ListArgs, global: &cli::GlobalOptions) -> Result<()> {
101 let path = args.path.clone().unwrap_or_else(|| PathBuf::from("."));
102 let path = path
103 .canonicalize()
104 .with_context(|| format!("Failed to resolve path: {}", path.display()))?;
105
106 let mut config = load_config(&path, global.config.as_deref())?;
108
109 config.merge_with_cli(
111 args.tags.clone(),
112 args.include.clone(),
113 args.exclude.clone(),
114 args.json,
115 true, global.no_color,
117 );
118
119 let parser = TodoParser::new(&config.tags, args.case_sensitive);
121
122 let scan_options = ScanOptions {
124 include: config.include.clone(),
125 exclude: config.exclude.clone(),
126 ..Default::default()
127 };
128
129 let scanner = Scanner::new(parser, scan_options);
131 let result = scanner.scan(&path)?;
132
133 let result = if let Some(filter_tag) = &args.filter {
135 result.filter_by_tag(filter_tag)
136 } else {
137 result
138 };
139
140 let print_options = PrintOptions {
142 format: if args.json {
143 OutputFormat::Json
144 } else {
145 OutputFormat::Flat
146 },
147 colored: !global.no_color,
148 show_line_numbers: true,
149 full_paths: false,
150 clickable_links: !global.no_color,
151 base_path: Some(path),
152 show_summary: !args.json,
153 group_by_tag: false,
154 };
155
156 let printer = Printer::new(print_options);
157 printer.print(&result)?;
158
159 Ok(())
160}
161
162fn cmd_tags(args: cli::TagsArgs, global: &cli::GlobalOptions) -> Result<()> {
164 let current_dir = std::env::current_dir()?;
165 let mut config = load_config(¤t_dir, global.config.as_deref())?;
166
167 if let Some(new_tag) = &args.add {
169 if !config.tags.iter().any(|t| t.eq_ignore_ascii_case(new_tag)) {
170 config.tags.push(new_tag.to_uppercase());
171 save_config(&config)?;
172 println!("Added tag: {}", new_tag.to_uppercase());
173 } else {
174 println!("Tag already exists: {}", new_tag);
175 }
176 return Ok(());
177 }
178
179 if let Some(remove_tag) = &args.remove {
180 let original_len = config.tags.len();
181 config.tags.retain(|t| !t.eq_ignore_ascii_case(remove_tag));
182 if config.tags.len() < original_len {
183 save_config(&config)?;
184 println!("Removed tag: {}", remove_tag);
185 } else {
186 println!("Tag not found: {}", remove_tag);
187 }
188 return Ok(());
189 }
190
191 if args.reset {
192 config.tags = config::DEFAULT_TAGS.iter().map(|s| s.to_string()).collect();
193 save_config(&config)?;
194 println!("Tags reset to defaults");
195 return Ok(());
196 }
197
198 if args.json {
200 let json = serde_json::json!({
201 "tags": config.tags,
202 "default_tags": config::DEFAULT_TAGS,
203 });
204 println!("{}", serde_json::to_string_pretty(&json)?);
205 } else {
206 use colored::Colorize;
207 println!("{}", "Configured tags:".bold());
208 for tag in &config.tags {
209 if global.no_color {
210 println!(" - {}", tag);
211 } else {
212 let color = parser::Priority::from_tag(tag).to_color();
213 println!(" - {}", tag.color(color));
214 }
215 }
216 }
217
218 Ok(())
219}
220
221fn cmd_init(args: cli::InitArgs) -> Result<()> {
223 let filename = match args.format {
224 ConfigFormat::Json => ".todorc.json",
225 ConfigFormat::Yaml => ".todorc.yaml",
226 };
227
228 let path = PathBuf::from(filename);
229
230 if path.exists() && !args.force {
231 anyhow::bail!(
232 "Config file {} already exists. Use --force to overwrite.",
233 filename
234 );
235 }
236
237 let config = Config::new();
238 config.save(&path)?;
239
240 println!("Created configuration file: {}", filename);
241 println!("\nYou can customize the following settings:");
242 println!(" - tags: List of tags to search for");
243 println!(" - include: File patterns to include");
244 println!(" - exclude: File patterns to exclude");
245 println!(" - json: Default to JSON output");
246 println!(" - flat: Default to flat output");
247
248 Ok(())
249}
250
251fn cmd_stats(args: cli::StatsArgs, global: &cli::GlobalOptions) -> Result<()> {
253 let path = args.path.clone().unwrap_or_else(|| PathBuf::from("."));
254 let path = path
255 .canonicalize()
256 .with_context(|| format!("Failed to resolve path: {}", path.display()))?;
257
258 let config = load_config(&path, global.config.as_deref())?;
260
261 let tags = args.tags.clone().unwrap_or(config.tags.clone());
263
264 let parser = TodoParser::new(&tags, false);
266 let scanner = Scanner::new(parser, ScanOptions::default());
267 let result = scanner.scan(&path)?;
268
269 if args.json {
270 let stats = serde_json::json!({
271 "total_items": result.total_count,
272 "files_with_todos": result.files_with_todos,
273 "files_scanned": result.files_scanned,
274 "tag_counts": result.tag_counts,
275 "items_per_file": if result.files_with_todos > 0 {
276 result.total_count as f64 / result.files_with_todos as f64
277 } else {
278 0.0
279 },
280 });
281 println!("{}", serde_json::to_string_pretty(&stats)?);
282 } else {
283 use colored::Colorize;
284
285 println!("{}", "TODO Statistics".bold().underline());
286 println!();
287 println!(" Total items: {}", result.total_count);
288 println!(" Files with TODOs: {}", result.files_with_todos);
289 println!(" Files scanned: {}", result.files_scanned);
290
291 if result.files_with_todos > 0 {
292 let avg = result.total_count as f64 / result.files_with_todos as f64;
293 println!(" Avg items per file: {:.2}", avg);
294 }
295
296 println!();
297 println!("{}", "By Tag:".bold());
298
299 let mut tags: Vec<_> = result.tag_counts.iter().collect();
300 tags.sort_by(|a, b| b.1.cmp(a.1));
301
302 for (tag, count) in tags {
303 let percentage = if result.total_count > 0 {
304 (*count as f64 / result.total_count as f64) * 100.0
305 } else {
306 0.0
307 };
308
309 let bar_width = 20;
310 let filled = ((percentage / 100.0) * bar_width as f64) as usize;
311 let bar: String = "█".repeat(filled) + &"░".repeat(bar_width - filled);
312
313 if global.no_color {
314 println!(" {:<8} {:>4} ({:>5.1}%) {}", tag, count, percentage, bar);
315 } else {
316 let color = parser::Priority::from_tag(tag).to_color();
317 println!(
318 " {:<8} {:>4} ({:>5.1}%) {}",
319 tag.color(color),
320 count,
321 percentage,
322 bar.dimmed()
323 );
324 }
325 }
326 }
327
328 Ok(())
329}
330
331fn load_config(path: &std::path::Path, config_path: Option<&std::path::Path>) -> Result<Config> {
333 if let Some(config_path) = config_path {
334 return Config::load_from_file(config_path);
335 }
336
337 match Config::load(path)? {
338 Some(config) => Ok(config),
339 None => Ok(Config::new()),
340 }
341}
342
343fn save_config(config: &Config) -> Result<()> {
345 let current_dir = std::env::current_dir()?;
346
347 let config_files = [
349 current_dir.join(".todorc"),
350 current_dir.join(".todorc.json"),
351 current_dir.join(".todorc.yaml"),
352 current_dir.join(".todorc.yml"),
353 ];
354
355 for path in &config_files {
356 if path.exists() {
357 return config.save(path);
358 }
359 }
360
361 let path = current_dir.join(".todorc.json");
363 config.save(&path)
364}
365
366fn sort_results(result: &mut ScanResult, sort: SortOrder) {
368 match sort {
369 SortOrder::File => {
370 }
372
373 SortOrder::Line => {
374 for items in result.files.values_mut() {
376 items.sort_by_key(|item| item.line);
377 }
378 }
379 SortOrder::Priority => {
380 for items in result.files.values_mut() {
382 items.sort_by_key(|item| std::cmp::Reverse(item.priority));
383 }
384 }
385 }
386}
387
388use anyhow::Context;
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use serial_test::serial;
394 use std::fs;
395 use tempfile::TempDir;
396
397 fn create_test_project() -> TempDir {
398 let temp_dir = TempDir::new().unwrap();
399
400 fs::write(
402 temp_dir.path().join("main.rs"),
403 r#"
404fn main() {
405 // TODO: Implement main logic
406 println!("Hello, world!");
407 // FIXME: This is broken
408}
409"#,
410 )
411 .unwrap();
412
413 fs::write(
414 temp_dir.path().join("lib.rs"),
415 r#"
416// NOTE: This is a library module
417pub fn hello() {
418 // TODO(alice): Add documentation
419 // BUG: Memory leak here
420}
421"#,
422 )
423 .unwrap();
424
425 fs::create_dir(temp_dir.path().join("src")).unwrap();
426 fs::write(
427 temp_dir.path().join("src/utils.rs"),
428 r#"
429// HACK: Temporary workaround
430fn temp_fix() {}
431"#,
432 )
433 .unwrap();
434
435 temp_dir
436 }
437
438 #[test]
439 fn test_scan_finds_todos() {
440 let temp_dir = create_test_project();
441
442 let tags: Vec<String> = config::DEFAULT_TAGS.iter().map(|s| s.to_string()).collect();
443 let parser = TodoParser::new(&tags, false);
444 let scanner = Scanner::new(parser, ScanOptions::default());
445
446 let result = scanner.scan(temp_dir.path()).unwrap();
447
448 assert!(result.total_count >= 5);
449 assert!(result.files_with_todos >= 2);
450 }
451
452 #[test]
453 fn test_config_loading() {
454 let temp_dir = TempDir::new().unwrap();
455
456 let config_content = r#"{
458 "tags": ["CUSTOM", "TEST"],
459 "include": ["*.rs"],
460 "exclude": ["target/**"]
461 }"#;
462
463 fs::write(temp_dir.path().join(".todorc.json"), config_content).unwrap();
464
465 let config = load_config(temp_dir.path(), None).unwrap();
466
467 assert_eq!(config.tags, vec!["CUSTOM", "TEST"]);
468 assert_eq!(config.include, vec!["*.rs"]);
469 }
470
471 #[test]
472 fn test_sort_by_priority() {
473 let temp_dir = TempDir::new().unwrap();
474
475 fs::write(
476 temp_dir.path().join("test.rs"),
477 r#"
478// NOTE: Low priority
479// TODO: Medium priority
480// BUG: Critical priority
481// HACK: High priority
482"#,
483 )
484 .unwrap();
485
486 let tags: Vec<String> = config::DEFAULT_TAGS.iter().map(|s| s.to_string()).collect();
487 let parser = TodoParser::new(&tags, false);
488 let scanner = Scanner::new(parser, ScanOptions::default());
489
490 let mut result = scanner.scan(temp_dir.path()).unwrap();
491 sort_results(&mut result, SortOrder::Priority);
492
493 for items in result.files.values() {
495 for window in items.windows(2) {
496 assert!(window[0].priority >= window[1].priority);
497 }
498 }
499 }
500
501 #[test]
502 fn test_sort_by_file() {
503 let temp_dir = TempDir::new().unwrap();
504
505 fs::write(temp_dir.path().join("test.rs"), "// TODO: Test").unwrap();
506
507 let tags: Vec<String> = config::DEFAULT_TAGS.iter().map(|s| s.to_string()).collect();
508 let parser = TodoParser::new(&tags, false);
509 let scanner = Scanner::new(parser, ScanOptions::default());
510
511 let mut result = scanner.scan(temp_dir.path()).unwrap();
512 sort_results(&mut result, SortOrder::File);
514
515 assert!(result.total_count >= 1);
516 }
517
518 #[test]
519 fn test_sort_by_line() {
520 let temp_dir = TempDir::new().unwrap();
521
522 fs::write(
523 temp_dir.path().join("test.rs"),
524 r#"
525// TODO: Line 2
526fn main() {}
527// TODO: Line 4
528// TODO: Line 5
529"#,
530 )
531 .unwrap();
532
533 let tags: Vec<String> = config::DEFAULT_TAGS.iter().map(|s| s.to_string()).collect();
534 let parser = TodoParser::new(&tags, false);
535 let scanner = Scanner::new(parser, ScanOptions::default());
536
537 let mut result = scanner.scan(temp_dir.path()).unwrap();
538 sort_results(&mut result, SortOrder::Line);
539
540 for items in result.files.values() {
542 for window in items.windows(2) {
543 assert!(window[0].line <= window[1].line);
544 }
545 }
546 }
547
548 #[test]
549 fn test_load_config_with_explicit_path() {
550 let temp_dir = TempDir::new().unwrap();
551
552 let config_content = r#"{"tags": ["EXPLICIT"]}"#;
553 let config_path = temp_dir.path().join("custom.json");
554 fs::write(&config_path, config_content).unwrap();
555
556 let config = load_config(temp_dir.path(), Some(&config_path)).unwrap();
557 assert_eq!(config.tags, vec!["EXPLICIT"]);
558 }
559
560 #[test]
561 fn test_load_config_no_file() {
562 let temp_dir = TempDir::new().unwrap();
563
564 let config = load_config(temp_dir.path(), None).unwrap();
565 assert!(!config.tags.is_empty());
567 assert!(config.tags.contains(&"TODO".to_string()));
568 }
569
570 #[test]
571 #[serial]
572 fn test_save_config_creates_new_file() {
573 let temp_dir = TempDir::new().unwrap();
574 let original_dir = std::env::current_dir().unwrap();
575
576 std::env::set_current_dir(temp_dir.path()).unwrap();
578
579 let config = Config::new();
580 let result = save_config(&config);
581
582 std::env::set_current_dir(original_dir).unwrap();
584
585 assert!(result.is_ok());
586 assert!(temp_dir.path().join(".todorc.json").exists());
587 }
588
589 #[test]
590 #[serial]
591 fn test_save_config_updates_existing() {
592 let temp_dir = TempDir::new().unwrap();
593 let original_dir = std::env::current_dir().unwrap();
594
595 let temp_path = temp_dir.path().to_path_buf();
597
598 let existing_path = temp_path.join(".todorc.json");
600 fs::write(&existing_path, r#"{"tags": ["OLD"]}"#).unwrap();
601
602 std::env::set_current_dir(&temp_path).unwrap();
604
605 let mut config = Config::new();
606 config.tags = vec!["NEW".to_string()];
607 let result = save_config(&config);
608
609 std::env::set_current_dir(&original_dir).unwrap();
611
612 assert!(result.is_ok());
613
614 let loaded = Config::load_from_file(&existing_path).unwrap();
616 assert_eq!(loaded.tags, vec!["NEW"]);
617 }
618
619 #[test]
620 fn test_save_config_to_yaml_file() {
621 let temp_dir = TempDir::new().unwrap();
624 let yaml_path = temp_dir.path().join(".todorc.yaml");
625
626 let mut config = Config::new();
627 config.tags = vec!["YAML_TEST".to_string()];
628 config.save(&yaml_path).unwrap();
629
630 let loaded = Config::load_from_file(&yaml_path).unwrap();
632 assert_eq!(loaded.tags, vec!["YAML_TEST"]);
633 }
634
635 #[test]
636 fn test_create_test_project_structure() {
637 let temp_dir = create_test_project();
638
639 assert!(temp_dir.path().join("main.rs").exists());
640 assert!(temp_dir.path().join("lib.rs").exists());
641 assert!(temp_dir.path().join("src/utils.rs").exists());
642 }
643
644 #[test]
645 fn test_cmd_scan_basic() {
646 let temp_dir = create_test_project();
647
648 let args = cli::ScanArgs {
649 path: Some(temp_dir.path().to_path_buf()),
650 tags: None,
651 include: None,
652 exclude: None,
653 json: false,
654 flat: false,
655 depth: 0,
656 follow_links: false,
657 hidden: false,
658 case_sensitive: false,
659 sort: cli::SortOrder::File,
660 group_by_tag: false,
661 };
662
663 let global = cli::GlobalOptions {
664 no_color: true,
665 verbose: false,
666 config: None,
667 };
668
669 let result = cmd_scan(args, &global);
670 assert!(result.is_ok());
671 }
672
673 #[test]
674 fn test_cmd_scan_with_json_output() {
675 let temp_dir = create_test_project();
676
677 let args = cli::ScanArgs {
678 path: Some(temp_dir.path().to_path_buf()),
679 tags: Some(vec!["TODO".to_string()]),
680 include: Some(vec!["*.rs".to_string()]),
681 exclude: None,
682 json: true,
683 flat: false,
684 depth: 0,
685 follow_links: false,
686 hidden: false,
687 case_sensitive: true,
688 sort: cli::SortOrder::Priority,
689 group_by_tag: false,
690 };
691
692 let global = cli::GlobalOptions {
693 no_color: true,
694 verbose: false,
695 config: None,
696 };
697
698 let result = cmd_scan(args, &global);
699 assert!(result.is_ok());
700 }
701
702 #[test]
703 fn test_cmd_scan_with_flat_output() {
704 let temp_dir = create_test_project();
705
706 let args = cli::ScanArgs {
707 path: Some(temp_dir.path().to_path_buf()),
708 tags: None,
709 include: None,
710 exclude: Some(vec!["src/**".to_string()]),
711 json: false,
712 flat: true,
713 depth: 1,
714 follow_links: true,
715 hidden: true,
716 case_sensitive: false,
717 sort: cli::SortOrder::Line,
718 group_by_tag: false,
719 };
720
721 let global = cli::GlobalOptions {
722 no_color: false,
723 verbose: false,
724 config: None,
725 };
726
727 let result = cmd_scan(args, &global);
728 assert!(result.is_ok());
729 }
730
731 #[test]
732 fn test_cmd_scan_group_by_tag() {
733 let temp_dir = create_test_project();
734
735 let args = cli::ScanArgs {
736 path: Some(temp_dir.path().to_path_buf()),
737 tags: None,
738 include: None,
739 exclude: None,
740 json: false,
741 flat: false,
742 depth: 0,
743 follow_links: false,
744 hidden: false,
745 case_sensitive: false,
746 sort: cli::SortOrder::File,
747 group_by_tag: true,
748 };
749
750 let global = cli::GlobalOptions {
751 no_color: true,
752 verbose: false,
753 config: None,
754 };
755
756 let result = cmd_scan(args, &global);
757 assert!(result.is_ok());
758 }
759
760 #[test]
761 fn test_cmd_scan_group_by_tag_with_color() {
762 let temp_dir = create_test_project();
763
764 let args = cli::ScanArgs {
765 path: Some(temp_dir.path().to_path_buf()),
766 tags: None,
767 include: None,
768 exclude: None,
769 json: false,
770 flat: false,
771 depth: 0,
772 follow_links: false,
773 hidden: false,
774 case_sensitive: false,
775 sort: cli::SortOrder::File,
776 group_by_tag: true,
777 };
778
779 let global = cli::GlobalOptions {
780 no_color: false,
781 verbose: false,
782 config: None,
783 };
784
785 let result = cmd_scan(args, &global);
786 assert!(result.is_ok());
787 }
788
789 #[test]
790 fn test_cmd_list_basic() {
791 let temp_dir = create_test_project();
792
793 let args = cli::ListArgs {
794 path: Some(temp_dir.path().to_path_buf()),
795 tags: None,
796 include: None,
797 exclude: None,
798 json: false,
799 filter: None,
800 case_sensitive: false,
801 };
802
803 let global = cli::GlobalOptions {
804 no_color: true,
805 verbose: false,
806 config: None,
807 };
808
809 let result = cmd_list(args, &global);
810 assert!(result.is_ok());
811 }
812
813 #[test]
814 fn test_cmd_list_with_filter() {
815 let temp_dir = create_test_project();
816
817 let args = cli::ListArgs {
818 path: Some(temp_dir.path().to_path_buf()),
819 tags: Some(vec!["TODO".to_string(), "FIXME".to_string()]),
820 include: Some(vec!["*.rs".to_string()]),
821 exclude: Some(vec!["src/**".to_string()]),
822 json: false,
823 filter: Some("TODO".to_string()),
824 case_sensitive: true,
825 };
826
827 let global = cli::GlobalOptions {
828 no_color: true,
829 verbose: false,
830 config: None,
831 };
832
833 let result = cmd_list(args, &global);
834 assert!(result.is_ok());
835 }
836
837 #[test]
838 fn test_cmd_list_with_json_output() {
839 let temp_dir = create_test_project();
840
841 let args = cli::ListArgs {
842 path: Some(temp_dir.path().to_path_buf()),
843 tags: None,
844 include: None,
845 exclude: None,
846 json: true,
847 filter: None,
848 case_sensitive: false,
849 };
850
851 let global = cli::GlobalOptions {
852 no_color: false,
853 verbose: false,
854 config: None,
855 };
856
857 let result = cmd_list(args, &global);
858 assert!(result.is_ok());
859 }
860
861 #[test]
862 #[serial]
863 fn test_cmd_tags_display() {
864 let temp_dir = TempDir::new().unwrap();
865 let original_dir = std::env::current_dir().unwrap();
866
867 fs::write(
869 temp_dir.path().join(".todorc.json"),
870 r#"{"tags": ["TODO", "FIXME"]}"#,
871 )
872 .unwrap();
873
874 std::env::set_current_dir(temp_dir.path()).unwrap();
875
876 let args = cli::TagsArgs {
877 json: false,
878 add: None,
879 remove: None,
880 reset: false,
881 };
882
883 let global = cli::GlobalOptions {
884 no_color: true,
885 verbose: false,
886 config: None,
887 };
888
889 let result = cmd_tags(args, &global);
890
891 std::env::set_current_dir(original_dir).unwrap();
892
893 assert!(result.is_ok());
894 }
895
896 #[test]
897 #[serial]
898 fn test_cmd_tags_display_json() {
899 let temp_dir = TempDir::new().unwrap();
900 let original_dir = std::env::current_dir().unwrap();
901
902 fs::write(
903 temp_dir.path().join(".todorc.json"),
904 r#"{"tags": ["TODO", "FIXME"]}"#,
905 )
906 .unwrap();
907
908 std::env::set_current_dir(temp_dir.path()).unwrap();
909
910 let args = cli::TagsArgs {
911 json: true,
912 add: None,
913 remove: None,
914 reset: false,
915 };
916
917 let global = cli::GlobalOptions {
918 no_color: true,
919 verbose: false,
920 config: None,
921 };
922
923 let result = cmd_tags(args, &global);
924
925 std::env::set_current_dir(original_dir).unwrap();
926
927 assert!(result.is_ok());
928 }
929
930 #[test]
931 #[serial]
932 fn test_cmd_tags_display_with_color() {
933 let temp_dir = TempDir::new().unwrap();
934 let original_dir = std::env::current_dir().unwrap();
935
936 fs::write(
937 temp_dir.path().join(".todorc.json"),
938 r#"{"tags": ["TODO", "FIXME", "BUG"]}"#,
939 )
940 .unwrap();
941
942 std::env::set_current_dir(temp_dir.path()).unwrap();
943
944 let args = cli::TagsArgs {
945 json: false,
946 add: None,
947 remove: None,
948 reset: false,
949 };
950
951 let global = cli::GlobalOptions {
952 no_color: false,
953 verbose: false,
954 config: None,
955 };
956
957 let result = cmd_tags(args, &global);
958
959 std::env::set_current_dir(original_dir).unwrap();
960
961 assert!(result.is_ok());
962 }
963
964 #[test]
965 #[serial]
966 fn test_cmd_tags_add_new() {
967 let temp_dir = TempDir::new().unwrap();
968 let original_dir = std::env::current_dir().unwrap();
969
970 fs::write(
971 temp_dir.path().join(".todorc.json"),
972 r#"{"tags": ["TODO"]}"#,
973 )
974 .unwrap();
975
976 std::env::set_current_dir(temp_dir.path()).unwrap();
977
978 let args = cli::TagsArgs {
979 json: false,
980 add: Some("NEWTAG".to_string()),
981 remove: None,
982 reset: false,
983 };
984
985 let global = cli::GlobalOptions {
986 no_color: true,
987 verbose: false,
988 config: None,
989 };
990
991 let result = cmd_tags(args, &global);
992
993 std::env::set_current_dir(original_dir).unwrap();
994
995 assert!(result.is_ok());
996 }
997
998 #[test]
999 #[serial]
1000 fn test_cmd_tags_add_existing() {
1001 let temp_dir = TempDir::new().unwrap();
1002 let original_dir = std::env::current_dir().unwrap();
1003
1004 fs::write(
1005 temp_dir.path().join(".todorc.json"),
1006 r#"{"tags": ["TODO"]}"#,
1007 )
1008 .unwrap();
1009
1010 std::env::set_current_dir(temp_dir.path()).unwrap();
1011
1012 let args = cli::TagsArgs {
1013 json: false,
1014 add: Some("todo".to_string()), remove: None,
1016 reset: false,
1017 };
1018
1019 let global = cli::GlobalOptions {
1020 no_color: true,
1021 verbose: false,
1022 config: None,
1023 };
1024
1025 let result = cmd_tags(args, &global);
1026
1027 std::env::set_current_dir(original_dir).unwrap();
1028
1029 assert!(result.is_ok());
1030 }
1031
1032 #[test]
1033 #[serial]
1034 fn test_cmd_tags_remove_existing() {
1035 let temp_dir = TempDir::new().unwrap();
1036 let original_dir = std::env::current_dir().unwrap();
1037
1038 fs::write(
1039 temp_dir.path().join(".todorc.json"),
1040 r#"{"tags": ["TODO", "FIXME"]}"#,
1041 )
1042 .unwrap();
1043
1044 std::env::set_current_dir(temp_dir.path()).unwrap();
1045
1046 let args = cli::TagsArgs {
1047 json: false,
1048 add: None,
1049 remove: Some("TODO".to_string()),
1050 reset: false,
1051 };
1052
1053 let global = cli::GlobalOptions {
1054 no_color: true,
1055 verbose: false,
1056 config: None,
1057 };
1058
1059 let result = cmd_tags(args, &global);
1060
1061 std::env::set_current_dir(original_dir).unwrap();
1062
1063 assert!(result.is_ok());
1064 }
1065
1066 #[test]
1067 #[serial]
1068 fn test_cmd_tags_remove_nonexistent() {
1069 let temp_dir = TempDir::new().unwrap();
1070 let original_dir = std::env::current_dir().unwrap();
1071
1072 fs::write(
1073 temp_dir.path().join(".todorc.json"),
1074 r#"{"tags": ["TODO"]}"#,
1075 )
1076 .unwrap();
1077
1078 std::env::set_current_dir(temp_dir.path()).unwrap();
1079
1080 let args = cli::TagsArgs {
1081 json: false,
1082 add: None,
1083 remove: Some("NONEXISTENT".to_string()),
1084 reset: false,
1085 };
1086
1087 let global = cli::GlobalOptions {
1088 no_color: true,
1089 verbose: false,
1090 config: None,
1091 };
1092
1093 let result = cmd_tags(args, &global);
1094
1095 std::env::set_current_dir(original_dir).unwrap();
1096
1097 assert!(result.is_ok());
1098 }
1099
1100 #[test]
1101 #[serial]
1102 fn test_cmd_tags_reset() {
1103 let temp_dir = TempDir::new().unwrap();
1104 let original_dir = std::env::current_dir().unwrap();
1105
1106 fs::write(
1107 temp_dir.path().join(".todorc.json"),
1108 r#"{"tags": ["CUSTOM"]}"#,
1109 )
1110 .unwrap();
1111
1112 std::env::set_current_dir(temp_dir.path()).unwrap();
1113
1114 let args = cli::TagsArgs {
1115 json: false,
1116 add: None,
1117 remove: None,
1118 reset: true,
1119 };
1120
1121 let global = cli::GlobalOptions {
1122 no_color: true,
1123 verbose: false,
1124 config: None,
1125 };
1126
1127 let result = cmd_tags(args, &global);
1128
1129 std::env::set_current_dir(original_dir).unwrap();
1130
1131 assert!(result.is_ok());
1132 }
1133
1134 #[test]
1135 #[serial]
1136 fn test_cmd_init_json() {
1137 let temp_dir = TempDir::new().unwrap();
1138 let original_dir = std::env::current_dir().unwrap();
1139
1140 std::env::set_current_dir(temp_dir.path()).unwrap();
1141
1142 let args = cli::InitArgs {
1143 format: cli::ConfigFormat::Json,
1144 force: false,
1145 };
1146
1147 let result = cmd_init(args);
1148
1149 std::env::set_current_dir(original_dir).unwrap();
1150
1151 assert!(result.is_ok());
1152 assert!(temp_dir.path().join(".todorc.json").exists());
1153 }
1154
1155 #[test]
1156 #[serial]
1157 fn test_cmd_init_yaml() {
1158 let temp_dir = TempDir::new().unwrap();
1159 let original_dir = std::env::current_dir().unwrap();
1160
1161 std::env::set_current_dir(temp_dir.path()).unwrap();
1162
1163 let args = cli::InitArgs {
1164 format: cli::ConfigFormat::Yaml,
1165 force: false,
1166 };
1167
1168 let result = cmd_init(args);
1169
1170 std::env::set_current_dir(original_dir).unwrap();
1171
1172 assert!(result.is_ok());
1173 assert!(temp_dir.path().join(".todorc.yaml").exists());
1174 }
1175
1176 #[test]
1177 #[serial]
1178 fn test_cmd_init_already_exists() {
1179 let temp_dir = TempDir::new().unwrap();
1180 let original_dir = std::env::current_dir().unwrap();
1181
1182 fs::write(temp_dir.path().join(".todorc.json"), "{}").unwrap();
1184
1185 std::env::set_current_dir(temp_dir.path()).unwrap();
1186
1187 let args = cli::InitArgs {
1188 format: cli::ConfigFormat::Json,
1189 force: false,
1190 };
1191
1192 let result = cmd_init(args);
1193
1194 std::env::set_current_dir(original_dir).unwrap();
1195
1196 assert!(result.is_err());
1197 }
1198
1199 #[test]
1200 #[serial]
1201 fn test_cmd_init_force_overwrite() {
1202 let temp_dir = TempDir::new().unwrap();
1203 let original_dir = std::env::current_dir().unwrap();
1204
1205 fs::write(temp_dir.path().join(".todorc.json"), r#"{"tags": ["OLD"]}"#).unwrap();
1207
1208 std::env::set_current_dir(temp_dir.path()).unwrap();
1209
1210 let args = cli::InitArgs {
1211 format: cli::ConfigFormat::Json,
1212 force: true,
1213 };
1214
1215 let result = cmd_init(args);
1216
1217 std::env::set_current_dir(original_dir).unwrap();
1218
1219 assert!(result.is_ok());
1220 }
1221
1222 #[test]
1223 fn test_cmd_stats_basic() {
1224 let temp_dir = create_test_project();
1225
1226 let args = cli::StatsArgs {
1227 path: Some(temp_dir.path().to_path_buf()),
1228 tags: None,
1229 json: false,
1230 };
1231
1232 let global = cli::GlobalOptions {
1233 no_color: true,
1234 verbose: false,
1235 config: None,
1236 };
1237
1238 let result = cmd_stats(args, &global);
1239 assert!(result.is_ok());
1240 }
1241
1242 #[test]
1243 fn test_cmd_stats_with_json() {
1244 let temp_dir = create_test_project();
1245
1246 let args = cli::StatsArgs {
1247 path: Some(temp_dir.path().to_path_buf()),
1248 tags: Some(vec!["TODO".to_string(), "FIXME".to_string()]),
1249 json: true,
1250 };
1251
1252 let global = cli::GlobalOptions {
1253 no_color: true,
1254 verbose: false,
1255 config: None,
1256 };
1257
1258 let result = cmd_stats(args, &global);
1259 assert!(result.is_ok());
1260 }
1261
1262 #[test]
1263 fn test_cmd_stats_with_color() {
1264 let temp_dir = create_test_project();
1265
1266 let args = cli::StatsArgs {
1267 path: Some(temp_dir.path().to_path_buf()),
1268 tags: None,
1269 json: false,
1270 };
1271
1272 let global = cli::GlobalOptions {
1273 no_color: false,
1274 verbose: false,
1275 config: None,
1276 };
1277
1278 let result = cmd_stats(args, &global);
1279 assert!(result.is_ok());
1280 }
1281
1282 #[test]
1283 fn test_cmd_stats_empty_project() {
1284 let temp_dir = TempDir::new().unwrap();
1285
1286 fs::write(temp_dir.path().join("empty.rs"), "fn main() {}").unwrap();
1288
1289 let args = cli::StatsArgs {
1290 path: Some(temp_dir.path().to_path_buf()),
1291 tags: None,
1292 json: false,
1293 };
1294
1295 let global = cli::GlobalOptions {
1296 no_color: true,
1297 verbose: false,
1298 config: None,
1299 };
1300
1301 let result = cmd_stats(args, &global);
1302 assert!(result.is_ok());
1303 }
1304
1305 #[test]
1306 fn test_cmd_stats_empty_project_json() {
1307 let temp_dir = TempDir::new().unwrap();
1308
1309 fs::write(temp_dir.path().join("empty.rs"), "fn main() {}").unwrap();
1311
1312 let args = cli::StatsArgs {
1313 path: Some(temp_dir.path().to_path_buf()),
1314 tags: None,
1315 json: true,
1316 };
1317
1318 let global = cli::GlobalOptions {
1319 no_color: true,
1320 verbose: false,
1321 config: None,
1322 };
1323
1324 let result = cmd_stats(args, &global);
1325 assert!(result.is_ok());
1326 }
1327
1328 #[test]
1329 fn test_cmd_stats_zero_percentage() {
1330 let temp_dir = TempDir::new().unwrap();
1331
1332 let args = cli::StatsArgs {
1336 path: Some(temp_dir.path().to_path_buf()),
1337 tags: Some(vec!["NONEXISTENT".to_string()]),
1338 json: false,
1339 };
1340
1341 let global = cli::GlobalOptions {
1342 no_color: true,
1343 verbose: false,
1344 config: None,
1345 };
1346
1347 let result = cmd_stats(args, &global);
1348 assert!(result.is_ok());
1349 }
1350
1351 #[test]
1352 fn test_cmd_stats_zero_percentage_with_color() {
1353 let temp_dir = TempDir::new().unwrap();
1354
1355 let args = cli::StatsArgs {
1359 path: Some(temp_dir.path().to_path_buf()),
1360 tags: Some(vec!["NONEXISTENT".to_string()]),
1361 json: false,
1362 };
1363
1364 let global = cli::GlobalOptions {
1365 no_color: false,
1366 verbose: false,
1367 config: None,
1368 };
1369
1370 let result = cmd_stats(args, &global);
1371 assert!(result.is_ok());
1372 }
1373
1374 #[test]
1375 fn test_cmd_stats_with_config() {
1376 let temp_dir = create_test_project();
1377
1378 fs::write(
1380 temp_dir.path().join(".todorc.json"),
1381 r#"{"tags": ["TODO", "FIXME", "NOTE"]}"#,
1382 )
1383 .unwrap();
1384
1385 let args = cli::StatsArgs {
1386 path: Some(temp_dir.path().to_path_buf()),
1387 tags: None,
1388 json: false,
1389 };
1390
1391 let global = cli::GlobalOptions {
1392 no_color: true,
1393 verbose: false,
1394 config: None,
1395 };
1396
1397 let result = cmd_stats(args, &global);
1398 assert!(result.is_ok());
1399 }
1400
1401 #[test]
1402 fn test_cmd_scan_with_config_file() {
1403 let temp_dir = create_test_project();
1404
1405 let config_path = temp_dir.path().join("custom-config.json");
1407 fs::write(&config_path, r#"{"tags": ["TODO", "CUSTOM"]}"#).unwrap();
1408
1409 let args = cli::ScanArgs {
1410 path: Some(temp_dir.path().to_path_buf()),
1411 tags: None,
1412 include: None,
1413 exclude: None,
1414 json: false,
1415 flat: false,
1416 depth: 0,
1417 follow_links: false,
1418 hidden: false,
1419 case_sensitive: false,
1420 sort: cli::SortOrder::File,
1421 group_by_tag: false,
1422 };
1423
1424 let global = cli::GlobalOptions {
1425 no_color: true,
1426 verbose: false,
1427 config: Some(config_path),
1428 };
1429
1430 let result = cmd_scan(args, &global);
1431 assert!(result.is_ok());
1432 }
1433
1434 #[test]
1435 fn test_cmd_list_with_config_file() {
1436 let temp_dir = create_test_project();
1437
1438 let config_path = temp_dir.path().join("custom-config.json");
1440 fs::write(&config_path, r#"{"tags": ["TODO"]}"#).unwrap();
1441
1442 let args = cli::ListArgs {
1443 path: Some(temp_dir.path().to_path_buf()),
1444 tags: None,
1445 include: None,
1446 exclude: None,
1447 json: false,
1448 filter: None,
1449 case_sensitive: false,
1450 };
1451
1452 let global = cli::GlobalOptions {
1453 no_color: true,
1454 verbose: false,
1455 config: Some(config_path),
1456 };
1457
1458 let result = cmd_list(args, &global);
1459 assert!(result.is_ok());
1460 }
1461
1462 #[test]
1463 #[serial]
1464 fn test_save_config_yaml_existing() {
1465 let temp_dir = TempDir::new().unwrap();
1466 let original_dir = std::env::current_dir().unwrap();
1467
1468 fs::write(temp_dir.path().join(".todorc.yaml"), "tags:\n - OLD").unwrap();
1470
1471 std::env::set_current_dir(temp_dir.path()).unwrap();
1472
1473 let mut config = Config::new();
1474 config.tags = vec!["UPDATED".to_string()];
1475 let result = save_config(&config);
1476
1477 std::env::set_current_dir(original_dir).unwrap();
1478
1479 assert!(result.is_ok());
1480 }
1481
1482 #[test]
1483 #[serial]
1484 fn test_save_config_yml_existing() {
1485 let temp_dir = TempDir::new().unwrap();
1486 let original_dir = std::env::current_dir().unwrap();
1487
1488 fs::write(temp_dir.path().join(".todorc.yml"), "tags:\n - OLD").unwrap();
1490
1491 std::env::set_current_dir(temp_dir.path()).unwrap();
1492
1493 let mut config = Config::new();
1494 config.tags = vec!["UPDATED".to_string()];
1495 let result = save_config(&config);
1496
1497 std::env::set_current_dir(original_dir).unwrap();
1498
1499 assert!(result.is_ok());
1500 }
1501
1502 #[test]
1503 #[serial]
1504 fn test_save_config_todorc_existing() {
1505 let temp_dir = TempDir::new().unwrap();
1506 let original_dir = std::env::current_dir().unwrap();
1507
1508 fs::write(temp_dir.path().join(".todorc"), r#"{"tags": ["OLD"]}"#).unwrap();
1510
1511 std::env::set_current_dir(temp_dir.path()).unwrap();
1512
1513 let mut config = Config::new();
1514 config.tags = vec!["UPDATED".to_string()];
1515 let result = save_config(&config);
1516
1517 std::env::set_current_dir(original_dir).unwrap();
1518
1519 assert!(result.is_ok());
1520 }
1521}