1use super::CodeTool;
11use super::ToolError;
12use ignore::WalkBuilder;
13use ignore::WalkState;
14use regex_lite::Regex;
16use std::path::Path;
17use std::path::PathBuf;
18use std::sync::Arc;
19use std::sync::Mutex;
20use std::sync::atomic::AtomicBool;
21use std::sync::atomic::AtomicUsize;
22use std::sync::atomic::Ordering;
23use std::time::Duration;
24use std::time::SystemTime;
25use wildmatch::WildMatch;
26
27#[derive(Debug, Clone, Default)]
29pub struct FdFind {
30 pub default_max_depth: Option<usize>,
32 pub default_threads: Option<usize>,
34}
35
36#[derive(Debug, Clone)]
38pub struct FdQuery {
39 pub base_dir: PathBuf,
41 pub glob_patterns: Vec<String>,
43 pub regex_pattern: Option<String>,
45 pub file_type: FileTypeFilter,
47 pub size_filter: Option<SizeFilter>,
49 pub max_depth: Option<usize>,
51 pub include_hidden: bool,
53 pub follow_links: bool,
55 pub case_sensitive: bool,
57 pub max_results: usize,
59 pub timeout: Option<Duration>,
61 pub threads: Option<usize>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum FileTypeFilter {
68 All,
70 FilesOnly,
72 DirectoriesOnly,
74 ExecutableOnly,
76 SymlinksOnly,
78 EmptyOnly,
80}
81
82#[derive(Debug, Clone)]
84pub struct SizeFilter {
85 pub min_size: Option<u64>,
86 pub max_size: Option<u64>,
87}
88
89#[derive(Debug, Clone)]
91pub struct FdResult {
92 pub path: PathBuf,
94 pub file_type: FdFileType,
96 pub size: Option<u64>,
98 pub modified: Option<SystemTime>,
100 pub executable: bool,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum FdFileType {
107 File,
108 Directory,
109 Symlink,
110 Other,
111}
112
113#[derive(Debug)]
115struct SearchState {
116 results: Arc<Mutex<Vec<FdResult>>>,
118 cancelled: Arc<AtomicBool>,
120 processed: Arc<AtomicUsize>,
122 max_results: usize,
124 start_time: SystemTime,
126 timeout: Option<Duration>,
128}
129
130#[derive(Debug)]
132struct CompiledFilters {
133 glob_matchers: Vec<WildMatch>,
135 regex: Option<Regex>,
137 size_filter: Option<SizeFilter>,
139 file_type: FileTypeFilter,
141}
142
143impl Default for FdQuery {
144 fn default() -> Self {
145 Self {
146 base_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
147 glob_patterns: Vec::new(),
148 regex_pattern: None,
149 file_type: FileTypeFilter::All,
150 size_filter: None,
151 max_depth: None,
152 include_hidden: false,
153 follow_links: false,
154 case_sensitive: true,
155 max_results: 0, timeout: Some(Duration::from_secs(30)), threads: None, }
159 }
160}
161
162impl FdFind {
163 pub const fn new() -> Self {
165 Self {
166 default_max_depth: Some(32), default_threads: None, }
169 }
170
171 pub const fn with_defaults(max_depth: Option<usize>, threads: Option<usize>) -> Self {
173 Self {
174 default_max_depth: max_depth,
175 default_threads: threads,
176 }
177 }
178
179 pub fn find_files_by_extension<P: AsRef<Path>>(
181 &self,
182 base_dir: P,
183 extensions: &[&str],
184 ) -> Result<Vec<FdResult>, ToolError> {
185 let patterns: Vec<String> = extensions
186 .iter()
187 .map(|ext| format!("**/*.{}", ext.trim_start_matches('.')))
188 .collect();
189
190 let query = FdQuery {
191 base_dir: base_dir.as_ref().to_path_buf(),
192 glob_patterns: patterns,
193 file_type: FileTypeFilter::FilesOnly,
194 ..Default::default()
195 };
196
197 self.search(query)
198 }
199
200 pub fn find_directories_by_name<P: AsRef<Path>>(
202 &self,
203 base_dir: P,
204 name_pattern: &str,
205 ) -> Result<Vec<FdResult>, ToolError> {
206 let query = FdQuery {
207 base_dir: base_dir.as_ref().to_path_buf(),
208 glob_patterns: vec![format!("**/{}", name_pattern)],
209 file_type: FileTypeFilter::DirectoriesOnly,
210 ..Default::default()
211 };
212
213 self.search(query)
214 }
215
216 pub fn find_modified_since<P: AsRef<Path>>(
218 &self,
219 base_dir: P,
220 since: SystemTime,
221 ) -> Result<Vec<FdResult>, ToolError> {
222 let query = FdQuery {
223 base_dir: base_dir.as_ref().to_path_buf(),
224 file_type: FileTypeFilter::FilesOnly,
225 ..Default::default()
226 };
227
228 let results = self.search(query)?;
229 Ok(results
230 .into_iter()
231 .filter(|r| r.modified.map(|modified| modified > since).unwrap_or(false))
232 .collect())
233 }
234
235 pub fn find_by_content_type<P: AsRef<Path>>(
237 &self,
238 base_dir: P,
239 content_type: ContentType,
240 ) -> Result<Vec<FdResult>, ToolError> {
241 let extensions = match content_type {
242 ContentType::Source => {
243 vec![
244 "rs", "py", "js", "ts", "jsx", "tsx", "go", "java", "c", "cpp", "cc", "cxx",
245 "h", "hpp", "cs", "php", "rb", "scala", "kt", "swift", "zig", "odin",
246 ]
247 }
248 ContentType::Config => {
249 vec![
250 "toml", "yaml", "yml", "json", "xml", "ini", "conf", "config", "env",
251 ]
252 }
253 ContentType::Documentation => {
254 vec!["md", "rst", "txt", "adoc", "org", "tex", "html"]
255 }
256 ContentType::Build => {
257 vec![
258 "Makefile",
259 "makefile",
260 "Dockerfile",
261 "dockerfile",
262 "BUILD",
263 "build",
264 "cmake",
265 "meson",
266 "ninja",
267 "gradle",
268 "pom",
269 ]
270 }
271 };
272
273 self.find_files_by_extension(base_dir, &extensions)
274 }
275
276 fn compile_filters(&self, query: &FdQuery) -> Result<CompiledFilters, ToolError> {
278 let mut glob_matchers = Vec::new();
280 for pattern in &query.glob_patterns {
281 let matcher = if query.case_sensitive {
282 WildMatch::new(pattern)
283 } else {
284 WildMatch::new(&pattern.to_lowercase())
285 };
286 glob_matchers.push(matcher);
287 }
288
289 let regex = if let Some(ref pattern) = query.regex_pattern {
291 Some(
292 regex_lite::RegexBuilder::new(pattern)
293 .case_insensitive(!query.case_sensitive)
294 .build()
295 .map_err(|e| ToolError::InvalidQuery(format!("Invalid regex: {}", e)))?,
296 )
297 } else {
298 None
299 };
300
301 Ok(CompiledFilters {
302 glob_matchers,
303 regex,
304 size_filter: query.size_filter.clone(),
305 file_type: query.file_type.clone(),
306 })
307 }
308
309 fn matches_filters(
311 &self,
312 path: &Path,
313 metadata: &std::fs::Metadata,
314 filters: &CompiledFilters,
315 case_sensitive: bool,
316 ) -> bool {
317 match filters.file_type {
319 FileTypeFilter::FilesOnly => {
320 if !metadata.is_file() {
321 return false;
322 }
323 }
324 FileTypeFilter::DirectoriesOnly => {
325 if !metadata.is_dir() {
326 return false;
327 }
328 }
329 FileTypeFilter::SymlinksOnly => {
330 if !metadata.file_type().is_symlink() {
331 return false;
332 }
333 }
334 FileTypeFilter::ExecutableOnly => {
335 #[cfg(unix)]
336 {
337 use std::os::unix::fs::PermissionsExt;
338 if metadata.permissions().mode() & 0o111 == 0 {
339 return false;
340 }
341 }
342 #[cfg(not(unix))]
343 {
344 if let Some(ext) = path.extension() {
346 let ext_str = ext.to_string_lossy().to_lowercase();
347 if !["exe", "bat", "cmd", "com"].contains(&ext_str.as_str()) {
348 return false;
349 }
350 } else {
351 return false;
352 }
353 }
354 }
355 FileTypeFilter::EmptyOnly => {
356 if metadata.is_file() && metadata.len() > 0 {
357 return false;
358 }
359 if metadata.is_dir() {
360 if let Ok(mut entries) = std::fs::read_dir(path)
362 && entries.next().is_some()
363 {
364 return false;
365 }
366 }
367 }
368 FileTypeFilter::All => {} }
370
371 if let Some(ref size_filter) = filters.size_filter {
373 let file_size = metadata.len();
374 if let Some(min_size) = size_filter.min_size
375 && file_size < min_size
376 {
377 return false;
378 }
379 if let Some(max_size) = size_filter.max_size
380 && file_size > max_size
381 {
382 return false;
383 }
384 }
385
386 if !filters.glob_matchers.is_empty() {
388 let path_str = path.to_string_lossy();
389 let test_str = if case_sensitive {
390 path_str.as_ref()
391 } else {
392 &path_str.to_lowercase()
393 };
394
395 let matches_glob = filters
396 .glob_matchers
397 .iter()
398 .any(|matcher| matcher.matches(test_str));
399
400 if !matches_glob {
401 return false;
402 }
403 }
404
405 if let Some(ref regex) = filters.regex {
407 let path_str = path.to_string_lossy();
408 if !regex.is_match(&path_str) {
409 return false;
410 }
411 }
412
413 true
414 }
415
416 fn create_result(&self, path: PathBuf, metadata: std::fs::Metadata) -> FdResult {
418 let file_type = if metadata.is_file() {
419 FdFileType::File
420 } else if metadata.is_dir() {
421 FdFileType::Directory
422 } else if metadata.file_type().is_symlink() {
423 FdFileType::Symlink
424 } else {
425 FdFileType::Other
426 };
427
428 let size = if metadata.is_file() {
429 Some(metadata.len())
430 } else {
431 None
432 };
433
434 let modified = metadata.modified().ok();
435
436 let executable = {
437 #[cfg(unix)]
438 {
439 use std::os::unix::fs::PermissionsExt;
440 metadata.permissions().mode() & 0o111 != 0
441 }
442 #[cfg(not(unix))]
443 {
444 if let Some(ext) = path.extension() {
445 let ext_str = ext.to_string_lossy().to_lowercase();
446 ["exe", "bat", "cmd", "com"].contains(&ext_str.as_str())
447 } else {
448 false
449 }
450 }
451 };
452
453 FdResult {
454 path,
455 file_type,
456 size,
457 modified,
458 executable,
459 }
460 }
461
462 fn search_internal(&self, mut query: FdQuery) -> Result<Vec<FdResult>, ToolError> {
464 if query.max_depth.is_none() {
466 query.max_depth = self.default_max_depth;
467 }
468 if query.threads.is_none() {
469 query.threads = self.default_threads;
470 }
471
472 if !query.base_dir.exists() {
474 return Err(ToolError::InvalidQuery(format!(
475 "Base directory does not exist: {}",
476 query.base_dir.display()
477 )));
478 }
479
480 let filters = self.compile_filters(&query)?;
482
483 let search_state = SearchState {
485 results: Arc::new(Mutex::new(Vec::new())),
486 cancelled: Arc::new(AtomicBool::new(false)),
487 processed: Arc::new(AtomicUsize::new(0)),
488 max_results: query.max_results,
489 start_time: SystemTime::now(),
490 timeout: query.timeout,
491 };
492
493 let mut builder = WalkBuilder::new(&query.base_dir);
495 builder
496 .hidden(!query.include_hidden)
497 .follow_links(query.follow_links)
498 .git_ignore(true) .git_exclude(true)
500 .git_global(true);
501
502 if let Some(max_depth) = query.max_depth {
503 builder.max_depth(Some(max_depth));
504 }
505
506 let thread_count = query.threads.unwrap_or_else(|| {
508 std::thread::available_parallelism()
509 .map(|n| n.get())
510 .unwrap_or(4)
511 });
512 builder.threads(thread_count);
513
514 let state_clone = search_state.clone();
516 let filters = Arc::new(filters);
517 let case_sensitive = query.case_sensitive;
518
519 builder.build_parallel().run(|| {
521 let state = state_clone.clone();
522 let filters = filters.clone();
523 let fd_find = self.clone();
524
525 Box::new(move |result| {
526 if state.cancelled.load(Ordering::Relaxed) {
528 return WalkState::Quit;
529 }
530
531 if let Some(timeout) = state.timeout
533 && state.start_time.elapsed().unwrap_or(Duration::ZERO) > timeout
534 {
535 state.cancelled.store(true, Ordering::Relaxed);
536 return WalkState::Quit;
537 }
538
539 match result {
540 Ok(entry) => {
541 let path = entry.path();
542
543 let metadata = match entry.metadata() {
545 Ok(meta) => meta,
546 Err(_) => return WalkState::Continue,
547 };
548
549 if fd_find.matches_filters(path, &metadata, &filters, case_sensitive) {
551 let result = fd_find.create_result(path.to_path_buf(), metadata);
552
553 {
555 let mut results = state.results.lock().unwrap();
556 results.push(result);
557
558 if state.max_results > 0 && results.len() >= state.max_results {
560 state.cancelled.store(true, Ordering::Relaxed);
561 return WalkState::Quit;
562 }
563 }
564 }
565
566 state.processed.fetch_add(1, Ordering::Relaxed);
568 WalkState::Continue
569 }
570 Err(_) => WalkState::Continue, }
572 })
573 });
574
575 let mut results = search_state.results.lock().unwrap().clone();
578
579 if query.max_results > 0 && results.len() > query.max_results {
581 results.truncate(query.max_results);
582 }
583
584 Ok(results)
585 }
586}
587
588#[derive(Debug, Clone, PartialEq, Eq)]
590pub enum ContentType {
591 Source,
593 Config,
595 Documentation,
597 Build,
599}
600
601impl Clone for SearchState {
603 fn clone(&self) -> Self {
604 Self {
605 results: self.results.clone(),
606 cancelled: self.cancelled.clone(),
607 processed: self.processed.clone(),
608 max_results: self.max_results,
609 start_time: self.start_time,
610 timeout: self.timeout,
611 }
612 }
613}
614
615impl CodeTool for FdFind {
617 type Query = FdQuery;
618 type Output = Vec<FdResult>;
619
620 fn search(&self, query: Self::Query) -> Result<Self::Output, ToolError> {
621 self.search_internal(query)
622 }
623}
624
625impl FdFind {
627 pub fn search_paths(&self, query: FdQuery) -> Result<Vec<String>, ToolError> {
629 let results = self.search(query)?;
630 Ok(results
631 .into_iter()
632 .map(|r| r.path.to_string_lossy().to_string())
633 .collect())
634 }
635}
636
637impl FdQuery {
639 pub fn new<P: AsRef<Path>>(base_dir: P) -> Self {
641 Self {
642 base_dir: base_dir.as_ref().to_path_buf(),
643 ..Default::default()
644 }
645 }
646
647 pub fn glob(mut self, pattern: &str) -> Self {
649 self.glob_patterns.push(pattern.to_string());
650 self
651 }
652
653 pub fn globs(mut self, patterns: &[&str]) -> Self {
655 self.glob_patterns
656 .extend(patterns.iter().map(|s| (*s).to_string()));
657 self
658 }
659
660 pub fn regex(mut self, pattern: &str) -> Self {
662 self.regex_pattern = Some(pattern.to_string());
663 self
664 }
665
666 pub const fn file_type(mut self, file_type: FileTypeFilter) -> Self {
668 self.file_type = file_type;
669 self
670 }
671
672 pub const fn size_range(mut self, min: Option<u64>, max: Option<u64>) -> Self {
674 self.size_filter = Some(SizeFilter {
675 min_size: min,
676 max_size: max,
677 });
678 self
679 }
680
681 pub const fn max_depth(mut self, depth: usize) -> Self {
683 self.max_depth = Some(depth);
684 self
685 }
686
687 pub const fn include_hidden(mut self, include: bool) -> Self {
689 self.include_hidden = include;
690 self
691 }
692
693 pub const fn follow_links(mut self, follow: bool) -> Self {
695 self.follow_links = follow;
696 self
697 }
698
699 pub const fn case_sensitive(mut self, sensitive: bool) -> Self {
701 self.case_sensitive = sensitive;
702 self
703 }
704
705 pub const fn max_results(mut self, max: usize) -> Self {
707 self.max_results = max;
708 self
709 }
710
711 pub const fn timeout(mut self, timeout: Duration) -> Self {
713 self.timeout = Some(timeout);
714 self
715 }
716
717 pub const fn threads(mut self, threads: usize) -> Self {
719 self.threads = Some(threads);
720 self
721 }
722}
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727 use std::fs;
728 use tempfile::TempDir;
729
730 fn create_test_structure() -> TempDir {
731 let temp_dir = TempDir::new().unwrap();
732 let base = temp_dir.path();
733
734 fs::create_dir_all(base.join("src")).unwrap();
736 fs::create_dir_all(base.join("target/debug")).unwrap();
737 fs::create_dir_all(base.join(".git")).unwrap();
738 fs::create_dir_all(base.join("docs")).unwrap();
739
740 fs::write(base.join("src/main.rs"), "fn main() {}").unwrap();
742 fs::write(base.join("src/lib.rs"), "pub mod test;").unwrap();
743 fs::write(base.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
744 fs::write(base.join("README.md"), "# Test Project").unwrap();
745 fs::write(base.join(".gitignore"), "target/").unwrap();
746 fs::write(base.join("target/debug/test"), "binary").unwrap();
747 fs::write(base.join("docs/guide.md"), "# Guide").unwrap();
748
749 fs::write(base.join(".hidden"), "hidden content").unwrap();
751
752 temp_dir
753 }
754
755 #[test]
756 fn test_basic_search() {
757 let temp_dir = create_test_structure();
758 let fd_find = FdFind::new();
759
760 let query = FdQuery::new(temp_dir.path()).file_type(FileTypeFilter::FilesOnly);
761
762 let results = fd_find.search(query).unwrap();
763 assert!(!results.is_empty());
764
765 let paths: Vec<_> = results.iter().map(|r| &r.path).collect();
767 assert!(paths.iter().any(|p| p.file_name().unwrap() == "main.rs"));
768 assert!(paths.iter().any(|p| p.file_name().unwrap() == "Cargo.toml"));
769 }
770
771 #[test]
772 fn test_glob_pattern_search() {
773 let temp_dir = create_test_structure();
774 let fd_find = FdFind::new();
775
776 let query = FdQuery::new(temp_dir.path())
777 .globs(&["*.rs", "*.toml"])
778 .file_type(FileTypeFilter::FilesOnly);
779
780 let results = fd_find.search(query).unwrap();
781
782 for result in &results {
783 let filename = result.path.file_name().unwrap().to_string_lossy();
784 assert!(filename.ends_with(".rs") || filename.ends_with(".toml"));
785 }
786 }
787
788 #[test]
789 fn test_find_files_by_extension() {
790 let temp_dir = create_test_structure();
791 let fd_find = FdFind::new();
792
793 let results = fd_find
794 .find_files_by_extension(temp_dir.path(), &["rs", "md"])
795 .unwrap();
796
797 assert!(!results.is_empty());
798 for result in &results {
799 let ext = result.path.extension().unwrap().to_string_lossy();
800 assert!(ext == "rs" || ext == "md");
801 }
802 }
803
804 #[test]
805 fn test_find_directories_by_name() {
806 let temp_dir = create_test_structure();
807 let fd_find = FdFind::new();
808
809 let results = fd_find
810 .find_directories_by_name(temp_dir.path(), "src")
811 .unwrap();
812
813 assert_eq!(results.len(), 1);
814 assert_eq!(results[0].file_type, FdFileType::Directory);
815 assert_eq!(results[0].path.file_name().unwrap(), "src");
816 }
817
818 #[test]
819 fn test_regex_search() {
820 let temp_dir = create_test_structure();
821 let fd_find = FdFind::new();
822
823 let query = FdQuery::new(temp_dir.path())
824 .regex(r".*\.(rs|toml)$")
825 .file_type(FileTypeFilter::FilesOnly);
826
827 let results = fd_find.search(query).unwrap();
828
829 for result in &results {
830 let filename = result.path.to_string_lossy();
831 assert!(filename.ends_with(".rs") || filename.ends_with(".toml"));
832 }
833 }
834
835 #[test]
836 fn test_max_results_limit() {
837 let temp_dir = create_test_structure();
838 let fd_find = FdFind::new();
839
840 let query = FdQuery::new(temp_dir.path())
841 .file_type(FileTypeFilter::All)
842 .max_results(3);
843
844 let results = fd_find.search(query).unwrap();
845 assert!(results.len() <= 3);
846 }
847
848 #[test]
849 fn test_hidden_files() {
850 let temp_dir = create_test_structure();
851 let fd_find = FdFind::new();
852
853 let query_no_hidden = FdQuery::new(temp_dir.path())
855 .file_type(FileTypeFilter::FilesOnly)
856 .include_hidden(false);
857
858 let results_no_hidden = fd_find.search(query_no_hidden).unwrap();
859 let hidden_found = results_no_hidden
860 .iter()
861 .any(|r| r.path.file_name().unwrap() == ".hidden");
862 assert!(!hidden_found);
863
864 let query_with_hidden = FdQuery::new(temp_dir.path())
866 .file_type(FileTypeFilter::FilesOnly)
867 .include_hidden(true);
868
869 let results_with_hidden = fd_find.search(query_with_hidden).unwrap();
870 let hidden_found = results_with_hidden
871 .iter()
872 .any(|r| r.path.file_name().unwrap() == ".hidden");
873 assert!(hidden_found);
874 }
875
876 #[test]
877 fn test_depth_limiting() {
878 let temp_dir = create_test_structure();
879 let fd_find = FdFind::new();
880
881 let query = FdQuery::new(temp_dir.path())
882 .file_type(FileTypeFilter::All)
883 .max_depth(1); let results = fd_find.search(query).unwrap();
886
887 let deep_file_found = results
889 .iter()
890 .any(|r| r.path.file_name().unwrap() == "main.rs");
891 assert!(!deep_file_found);
892 }
893
894 #[test]
895 fn test_size_filtering() {
896 let temp_dir = create_test_structure();
897 let fd_find = FdFind::new();
898
899 let large_content = "x".repeat(1024); fs::write(temp_dir.path().join("large.txt"), &large_content).unwrap();
902
903 let query = FdQuery::new(temp_dir.path())
904 .file_type(FileTypeFilter::FilesOnly)
905 .size_range(Some(500), Some(2000)); let results = fd_find.search(query).unwrap();
908
909 let large_found = results
911 .iter()
912 .any(|r| r.path.file_name().unwrap() == "large.txt");
913 assert!(large_found);
914
915 let large_result = results
917 .iter()
918 .find(|r| r.path.file_name().unwrap() == "large.txt")
919 .unwrap();
920 assert!(large_result.size.unwrap() >= 500);
921 }
922
923 #[test]
924 fn test_content_type_search() {
925 let temp_dir = create_test_structure();
926 let fd_find = FdFind::new();
927
928 let results = fd_find
929 .find_by_content_type(temp_dir.path(), ContentType::Source)
930 .unwrap();
931
932 let rust_found = results.iter().any(|r| r.path.extension().unwrap() == "rs");
934 assert!(rust_found);
935 }
936
937 #[test]
938 fn test_case_sensitivity() {
939 let temp_dir = create_test_structure();
940
941 fs::write(temp_dir.path().join("Test.TXT"), "content").unwrap();
943 fs::write(temp_dir.path().join("test.txt"), "content").unwrap();
944
945 let fd_find = FdFind::new();
946
947 let query_sensitive = FdQuery::new(temp_dir.path())
949 .glob("*.TXT")
950 .case_sensitive(true);
951
952 let results_sensitive = fd_find.search(query_sensitive).unwrap();
953 assert_eq!(results_sensitive.len(), 1);
954 assert_eq!(results_sensitive[0].path.file_name().unwrap(), "Test.TXT");
955
956 let query_insensitive = FdQuery::new(temp_dir.path())
958 .glob("*.txt")
959 .case_sensitive(false);
960
961 let results_insensitive = fd_find.search(query_insensitive).unwrap();
962 assert_eq!(results_insensitive.len(), 2);
963 }
964
965 #[test]
966 fn test_builder_pattern() {
967 let temp_dir = create_test_structure();
968 let fd_find = FdFind::new();
969
970 let query = FdQuery::new(temp_dir.path())
971 .globs(&["*.rs", "*.toml"])
972 .file_type(FileTypeFilter::FilesOnly)
973 .max_depth(5)
974 .include_hidden(false)
975 .case_sensitive(true)
976 .max_results(10)
977 .timeout(Duration::from_secs(5));
978
979 let results = fd_find.search(query).unwrap();
980 assert!(!results.is_empty());
981 }
982
983 #[test]
984 fn test_error_handling() {
985 let fd_find = FdFind::new();
986
987 let query = FdQuery::new("/non/existent/path");
989 let result = fd_find.search(query);
990 assert!(result.is_err());
991
992 let temp_dir = create_test_structure();
994 let query = FdQuery::new(temp_dir.path()).regex("[invalid regex");
995 let result = fd_find.search(query);
996 assert!(result.is_err());
997 }
998}