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