diff --git a/Cargo.toml b/Cargo.toml index 023d3b8..e6353e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,14 @@ categories = ["filesystem", "algorithms"] [dependencies] regex = "1" ignore = "0.4" -num_cpus = "1.0" dirs = "4.0.0" -strsim = "0.10.0" \ No newline at end of file +strsim = "0.10.0" +crossbeam-channel = "0.5.15" +rayon = "1.11.0" + +[dev-dependencies] +dirs = "4.0.0" + +[[bench]] +name = "bench_search" +harness = false diff --git a/benches/bench_search.rs b/benches/bench_search.rs new file mode 100644 index 0000000..b4b8af0 --- /dev/null +++ b/benches/bench_search.rs @@ -0,0 +1,330 @@ +use rust_search::{similarity_sort, SearchBuilder}; +use std::fs; +use std::io::Write; +use std::path::PathBuf; +use std::time::{Duration, Instant}; + +const WARMUP_ITERS: usize = 1; +const BENCH_ITERS: usize = 3; + +fn median(times: &mut [Duration]) -> Duration { + times.sort(); + times[times.len() / 2] +} + +/// Create a controlled test directory with many files for benchmarking. +fn create_test_dir(num_dirs: usize, files_per_dir: usize) -> PathBuf { + let dir = std::env::temp_dir().join("rust_search_bench"); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(&dir).unwrap(); + + let extensions = [ + "rs", "txt", "md", "json", "toml", "yaml", "py", "js", "ts", "css", + ]; + + for d in 0..num_dirs { + let subdir = dir.join(format!("dir_{d:04}")); + fs::create_dir_all(&subdir).unwrap(); + for f in 0..files_per_dir { + let ext = extensions[f % extensions.len()]; + let filename = format!("file_{f:04}.{ext}"); + let path = subdir.join(&filename); + let mut file = fs::File::create(&path).unwrap(); + let _ = file.write_all(b"content"); + } + } + + dir +} + +fn run_bench Vec>( + label: &str, + warmup: usize, + iters: usize, + f: F, +) -> (usize, Duration) { + for _ in 0..warmup { + let _ = f(); + } + + let mut times = Vec::with_capacity(iters); + let mut count = 0; + for _ in 0..iters { + let start = Instant::now(); + let results = f(); + times.push(start.elapsed()); + count = results.len(); + } + + let med = median(&mut times); + eprintln!("{label:<28} {count:>8} results, median {med:>12.3?}"); + (count, med) +} + +fn run_sort_bench( + label: &str, + base_results: &[String], + input: &str, + warmup: usize, + iters: usize, +) -> (usize, Duration) { + let count = base_results.len(); + + for _ in 0..warmup { + let mut results = base_results.to_vec(); + similarity_sort(&mut results, input); + } + + let mut times = Vec::with_capacity(iters); + for _ in 0..iters { + let mut results = base_results.to_vec(); + let start = Instant::now(); + similarity_sort(&mut results, input); + times.push(start.elapsed()); + } + + let med = median(&mut times); + eprintln!("{label:<28} {count:>8} items, median {med:>12.3?}"); + (count, med) +} + +fn main() { + let arg = std::env::args().nth(1).unwrap_or_default(); + match arg.as_str() { + "search" => { + let home = dirs::home_dir().unwrap(); + run_bench("search", WARMUP_ITERS, BENCH_ITERS, || { + SearchBuilder::default() + .location(&home) + .ext("rs") + .build() + .collect() + }); + } + "limit" => { + let home = dirs::home_dir().unwrap(); + run_bench("limit", WARMUP_ITERS, BENCH_ITERS, || { + SearchBuilder::default() + .location(&home) + .ext("rs") + .limit(100) + .build() + .collect() + }); + } + "sort" => { + let home = dirs::home_dir().unwrap(); + let base: Vec = SearchBuilder::default() + .location(&home) + .ext("rs") + .build() + .collect(); + run_sort_bench("sort", &base, "main", WARMUP_ITERS, BENCH_ITERS); + } + "all" => { + let home = dirs::home_dir().unwrap(); + + eprintln!("=== Home directory benchmarks ===\n"); + + run_bench("home/ext_only (.rs)", WARMUP_ITERS, BENCH_ITERS, || { + SearchBuilder::default() + .location(&home) + .ext("rs") + .build() + .collect() + }); + run_bench( + "home/ext+limit (.rs, 100)", + WARMUP_ITERS, + BENCH_ITERS, + || { + SearchBuilder::default() + .location(&home) + .ext("rs") + .limit(100) + .build() + .collect() + }, + ); + + let base: Vec = SearchBuilder::default() + .location(&home) + .ext("rs") + .build() + .collect(); + run_sort_bench("home/sort", &base, "main", WARMUP_ITERS, BENCH_ITERS); + + // Controlled benchmarks + eprintln!("\n=== Controlled (100,000 files) ===\n"); + let dir = create_test_dir(500, 200); + + run_bench("ctrl/ext_only (.rs)", WARMUP_ITERS, BENCH_ITERS, || { + SearchBuilder::default() + .location(&dir) + .ext("rs") + .build() + .collect() + }); + run_bench( + "ctrl/ext+input (file_00.rs)", + WARMUP_ITERS, + BENCH_ITERS, + || { + SearchBuilder::default() + .location(&dir) + .search_input("file_00") + .ext("rs") + .build() + .collect() + }, + ); + + let ctrl_base: Vec = SearchBuilder::default() + .location(&dir) + .ext("rs") + .build() + .collect(); + run_sort_bench( + "ctrl/sort", + &ctrl_base, + "file_0042", + WARMUP_ITERS, + BENCH_ITERS, + ); + + let _ = fs::remove_dir_all(&dir); + + eprintln!("\n=== Done ==="); + } + "system" => { + eprintln!("=== Full system benchmarks (searching from /) ==="); + eprintln!("=== {} iters, {} warmup ===\n", BENCH_ITERS, WARMUP_ITERS); + + // 1. Search for .rs files across the entire system + let (rs_count, _) = + run_bench("system/ext_only (.rs)", WARMUP_ITERS, BENCH_ITERS, || { + SearchBuilder::default() + .location("/") + .ext("rs") + .build() + .collect() + }); + + // 2. Search for .txt files (typically many more) + run_bench("system/ext_only (.txt)", WARMUP_ITERS, BENCH_ITERS, || { + SearchBuilder::default() + .location("/") + .ext("txt") + .build() + .collect() + }); + + // 3. Search for .py files + run_bench("system/ext_only (.py)", WARMUP_ITERS, BENCH_ITERS, || { + SearchBuilder::default() + .location("/") + .ext("py") + .build() + .collect() + }); + + // 4. Search with regex pattern + extension + run_bench( + "system/regex+ext (main*.rs)", + WARMUP_ITERS, + BENCH_ITERS, + || { + SearchBuilder::default() + .location("/") + .search_input("main") + .ext("rs") + .build() + .collect() + }, + ); + + // 5. Search with limit + run_bench( + "system/ext+limit (.rs, 1000)", + WARMUP_ITERS, + BENCH_ITERS, + || { + SearchBuilder::default() + .location("/") + .ext("rs") + .limit(1000) + .build() + .collect() + }, + ); + + // 6. Search for all files (no filter) + run_bench("system/no_filter (all)", WARMUP_ITERS, BENCH_ITERS, || { + SearchBuilder::default().location("/").build().collect() + }); + + // 7. Similarity sort on the .rs results + if rs_count > 0 { + let rs_results: Vec = SearchBuilder::default() + .location("/") + .ext("rs") + .build() + .collect(); + run_sort_bench( + "system/sort (.rs results)", + &rs_results, + "main", + WARMUP_ITERS, + BENCH_ITERS, + ); + } + + // 8. Search hidden files + run_bench("system/hidden (.conf)", WARMUP_ITERS, BENCH_ITERS, || { + SearchBuilder::default() + .location("/") + .ext("conf") + .hidden() + .build() + .collect() + }); + + // 9. Strict match + run_bench( + "system/strict (Cargo.toml)", + WARMUP_ITERS, + BENCH_ITERS, + || { + SearchBuilder::default() + .location("/") + .search_input("Cargo") + .ext("toml") + .strict() + .build() + .collect() + }, + ); + + // 10. Case-insensitive search + run_bench( + "system/icase (readme.md)", + WARMUP_ITERS, + BENCH_ITERS, + || { + SearchBuilder::default() + .location("/") + .search_input("readme") + .ext("md") + .ignore_case() + .build() + .collect() + }, + ); + + eprintln!("\n=== Done ==="); + } + _ => { + eprintln!("Usage: bench_search [search|limit|sort|all|system]"); + } + } +} diff --git a/learnings.md b/learnings.md new file mode 100644 index 0000000..e468b4f --- /dev/null +++ b/learnings.md @@ -0,0 +1,154 @@ +# Performance Optimization Learnings + +## Summary + +Achieved significant performance improvements across all benchmarks through 6 iterative +optimization checkpoints. The biggest win was in `similarity_sort` (9x faster for small +datasets, estimated 20-35x faster for large datasets), with meaningful improvements in +search throughput as well. + +## Baseline (Original Code) + +| Benchmark | Result | +|---|---| +| search (home dir, .rs files) | 28 results, **1.290s** | +| search with limit=100 | 28 results, **1.309s** | +| similarity_sort (28 items) | **34.7µs** | + +## Final Results (After All Optimizations) + +| Benchmark | Result | +|---|---| +| search (home dir, .rs files) | 28 results, **1.137s** | +| search with limit=100 | 28 results, **1.148s** | +| similarity_sort (28 items) | **3.79µs** | +| ctrl_search (100K files, ext-only) | 10,000 results, **41.5ms** | +| ctrl_search (100K files, regex) | 5,000 results, **42.4ms** | +| ctrl_sort (10K items) | **506-856µs** | + +## Improvement Summary + +| Benchmark | Before | After | Speedup | +|---|---|---|---| +| search (home dir) | 1.290s | 1.137s | **12% faster** | +| search with limit | 1.309s | 1.148s | **12% faster** | +| similarity_sort (28 items) | 34.7µs | 3.79µs | **9.1x faster** | +| similarity_sort (1K items) | 1.313ms | ~155µs | **8.5x faster** | +| similarity_sort (10K items, est.) | ~17.5ms | ~500µs | **~35x faster** | + +--- + +## What Worked + +### 1. Schwartzian Transform for similarity_sort (Checkpoint 3) — **8.5-9x speedup** +**The single biggest win.** The original code recomputed file name extraction, lowercasing, +and Jaro-Winkler similarity scores during every comparison in the sort. With n=1000, +`sort_by` makes ~10,000 comparisons, each computing 2 scores = 20,000 redundant JW calls. + +The Schwartzian transform precomputes all scores once (O(n)), then sorts by precomputed +float values (O(n log n)). Combined with `sort_unstable_by` for better cache locality, +this produced an immediate 8.5x speedup. + +Also changed `file_name_from_path` to return `&str` instead of `String` to avoid +per-call allocation. + +### 2. AcceptAll matcher with types pre-filter (Checkpoint 6) — measurable +When only a file extension is specified (the most common use case), we set up the +`ignore` crate's TypesBuilder to pre-filter by extension at the walker level. Then our +callback uses `Matcher::AcceptAll` — it doesn't need to check the extension again since +the walker already filtered. This avoids redundant `path.extension()` comparisons on +every entry that reaches our callback. + +### 3. Zero-copy path conversion (Checkpoint 6) — measurable +Replaced `path.to_string_lossy().into_owned()` (always allocates) with +`entry.into_path().into_os_string().into_string()`. For valid UTF-8 paths (99.9% of +cases), this is a zero-copy conversion — the `OsString`'s internal buffer becomes the +`String` directly. + +### 4. crossbeam-channel (Checkpoint 2) — small improvement +Replaced `std::sync::mpsc` with `crossbeam-channel`. The crossbeam implementation has +lower overhead for multi-producer scenarios and better cache behavior. + +### 5. Increased thread count (Checkpoint 2) — small improvement +Changed from `min(12, num_cpus)` to `num_cpus * 2`. For I/O-bound directory traversal, +having more threads than CPUs allows threads to make progress while others wait for I/O. + +### 6. Conditional rayon parallelism (Checkpoint 5) — helps large datasets +Added rayon `par_iter` for computing Jaro-Winkler scores in parallel, but only when +the dataset exceeds 5,000 items. Below that threshold, sequential iteration is faster +due to rayon's thread pool overhead. + +### 7. Removed num_cpus dependency (Checkpoint 4) +Replaced `num_cpus::get()` with `std::thread::available_parallelism()` (stable since +Rust 1.59). Reduces dependency count without changing behavior. + +--- + +## What Did NOT Work (or Had Minimal Impact) + +### 1. Extension-only fast path without types pre-filter (Checkpoint 1) — negligible +Adding a `Matcher::ExtOnly` variant that uses `path.extension() == Some(OsStr::new(ext))` +instead of regex showed no measurable improvement in the home directory benchmark. The +reason: with only 28 matching files across thousands of directories, the bottleneck is +filesystem I/O (directory traversal), not regex matching overhead. The regex is fast and +compiled once. + +### 2. `same_file_system(true)` — removed (behavioral change) +This would prevent traversal into mounted filesystems (network drives, Time Machine), +which could speed up searches on macOS significantly. However, it changes the library's +behavior for users who intentionally search across mount points, so it was reverted. + +### 3. Rayon for small datasets (< 5K items) — **7x SLOWER** +Naive use of `par_iter` for small datasets (28 items) made similarity_sort 7x slower +(3.9µs → 29.5µs) due to rayon's thread pool initialization overhead. Fixed with a +threshold: only use parallel scoring above 5,000 items. + +### 4. Skip empty filter_entry closure — negligible +Skipping `walker.filter_entry()` when no filters exist showed no measurable improvement. +The closure `|dir| [].iter().all(...)` is essentially free. + +--- + +## Benchmark Results by Checkpoint + +| Checkpoint | search (28) | limit (28) | sort (28) | Notes | +|---|---|---|---|---| +| **Baseline** | 1.290s | 1.309s | 34.7µs | Original code | +| **CP1**: Fast ext matcher | 1.308s | 1.278s | 36.5µs | Negligible change | +| **CP2**: crossbeam + types + 2x threads | 1.153s | 1.145s | 35.5µs | ~11% search improvement | +| **CP3**: Schwartzian transform | 1.155s | 1.161s | **3.96µs** | **8.8x sort improvement** | +| **CP4**: Replace num_cpus | 1.155s | 1.146s | 3.88µs | Same perf, fewer deps | +| **CP5**: Conditional rayon | 1.155s | 1.138s | 3.88µs | Helps large datasets | +| **CP6**: AcceptAll + zero-copy | 1.137s | 1.148s | 3.79µs | Final polish | + +--- + +## Key Insights + +1. **Profile before optimizing.** The home directory benchmark is 99%+ I/O-bound. + No amount of CPU optimization in the matching path can significantly improve it. + Creating a controlled benchmark with 100K files revealed the actual matching path + performance. + +2. **Algorithmic improvements beat micro-optimizations.** The Schwartzian transform + (changing the algorithm) gave 8.5x. All the micro-optimizations combined + (crossbeam, zero-copy, etc.) gave ~12%. + +3. **Parallelism has overhead.** Rayon made small sorts 7x slower. Always use + thresholds for parallel algorithms. + +4. **Pre-filtering at the walker level is effective.** Using `ignore`'s TypesBuilder + to filter by extension before our callback reduces the number of entries we process. + +5. **Zero-copy conversions matter in hot paths.** `OsString::into_string()` vs + `to_string_lossy().into_owned()` avoids allocation for valid UTF-8 paths. + +--- + +## Dependencies Changed + +| Dependency | Action | Reason | +|---|---|---| +| `num_cpus` | **Removed** | Replaced with `std::thread::available_parallelism()` | +| `crossbeam-channel` | **Added** | Faster MPSC channel implementation | +| `rayon` | **Added** | Parallel scoring for large similarity_sort datasets | diff --git a/src/search.rs b/src/search.rs index 91d6ade..2dcd8b4 100644 --- a/src/search.rs +++ b/src/search.rs @@ -1,16 +1,27 @@ use std::{ - cmp, + ffi::OsStr, path::Path, sync::{ atomic::{AtomicUsize, Ordering}, - mpsc::{self, Sender}, Arc, }, }; use crate::{filter::FilterType, utils, SearchBuilder}; +use crossbeam_channel::Sender; +use ignore::types::TypesBuilder; use ignore::{WalkBuilder, WalkState}; +/// Matcher strategy for the walk callback. +enum Matcher { + /// The types pre-filter already handles extension matching; accept all entries. + AcceptAll, + /// Simple extension-only check (fallback when types filter setup failed). + ExtOnly(String), + /// Full regex matching on file names. + Regex(regex::Regex), +} + /// A struct that holds the receiver for the search results /// /// Can be iterated on to get the next element in the search results @@ -82,20 +93,62 @@ impl Search { with_hidden: bool, filters: Vec, ) -> Self { - let regex_search_input = - utils::build_regex_search_input(search_input, file_ext, strict, ignore_case); - let mut walker = WalkBuilder::new(search_location); + // Use more threads than CPUs for I/O-bound work: while one thread + // waits for I/O, others can make progress. + let cpus = std::thread::available_parallelism().map_or(8, std::num::NonZero::get); + let thread_count = cpus * 2; + walker .hidden(!with_hidden) .git_ignore(true) .max_depth(depth) - .threads(cmp::min(12, num_cpus::get())); + .threads(thread_count); - // filters getting applied to walker - // only if all filters are true then the walker will return the file - walker.filter_entry(move |dir| filters.iter().all(|f| f.apply(dir))); + // Pre-filter by extension using ignore's type system when possible. + // This avoids calling our callback for non-matching files. + let mut types_filter_active = false; + if let Some(ext) = file_ext { + let mut types = TypesBuilder::new(); + if types.add("custom", &format!("*.{ext}")).is_ok() { + types.select("custom"); + if let Ok(built) = types.build() { + walker.types(built); + types_filter_active = true; + } + } + } + + // Determine the matcher strategy based on search parameters. + let matcher = if search_input.is_none() && !strict && !ignore_case { + if file_ext.is_some() && types_filter_active { + // Types pre-filter handles extension matching; no additional check needed. + Matcher::AcceptAll + } else if let Some(ext) = file_ext { + // Fallback: simple extension comparison. + Matcher::ExtOnly(ext.to_owned()) + } else { + Matcher::Regex(utils::build_regex_search_input( + search_input, + file_ext, + strict, + ignore_case, + )) + } + } else { + Matcher::Regex(utils::build_regex_search_input( + search_input, + file_ext, + strict, + ignore_case, + )) + }; + + // Only apply filter_entry if there are filters to check + if !filters.is_empty() { + walker.filter_entry(move |dir| filters.iter().all(|f| f.apply(dir))); + } if let Some(locations) = more_locations { for location in locations { @@ -103,40 +156,55 @@ impl Search { } } - let (tx, rx) = mpsc::channel::(); - let reg_exp = Arc::new(regex_search_input); + let (tx, rx) = crossbeam_channel::unbounded::(); + let matcher = Arc::new(matcher); let counter = Arc::new(AtomicUsize::new(0)); walker.build_parallel().run(|| { let tx: Sender = tx.clone(); - let reg_exp = Arc::clone(®_exp); + let matcher = Arc::clone(&matcher); let counter = Arc::clone(&counter); Box::new(move |path_entry| { if let Ok(entry) = path_entry { - let path = entry.path(); - if let Some(file_name) = path.file_name() { - // Lossy means that if the file name is not valid UTF-8 - // it will be replaced with �. - // Will return the file name with extension. - let file_name = file_name.to_string_lossy(); - if reg_exp.is_match(&file_name) { - if limit.is_none_or(|l| counter.fetch_add(1, Ordering::Relaxed) < l) - && tx.send(path.display().to_string()).is_ok() - { + // Check match using borrowed path first, then convert to owned + // only if matched (avoids allocation for non-matching entries). + let is_match = match matcher.as_ref() { + Matcher::AcceptAll => entry.file_type().is_some_and(|ft| !ft.is_dir()), + Matcher::ExtOnly(ext) => { + entry.path().extension() == Some(OsStr::new(ext.as_str())) + } + Matcher::Regex(reg_exp) => { + entry.path().file_name().is_some_and(|file_name| { + let file_name = file_name.to_string_lossy(); + reg_exp.is_match(&file_name) + }) + } + }; + if is_match { + if limit.is_none_or(|l| counter.fetch_add(1, Ordering::Relaxed) < l) { + // Use into_path() for zero-copy PathBuf, then try zero-copy + // String conversion (succeeds for valid UTF-8 paths). + let path_string = entry + .into_path() + .into_os_string() + .into_string() + .unwrap_or_else(|os| os.to_string_lossy().into_owned()); + if tx.send(path_string).is_ok() { return WalkState::Continue; } - return WalkState::Quit; } + return WalkState::Quit; } } WalkState::Continue }) }); + // Drop the sender so the receiver knows when all results have been sent + drop(tx); + if let Some(limit) = limit { - // This will take the first `limit` elements from the iterator - // will return all if there are less than `limit` elements Self { rx: Box::new(rx.into_iter().take(limit)), } diff --git a/src/utils.rs b/src/utils.rs index 7a5fd20..58e31b6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,4 @@ +use rayon::prelude::*; use regex::Regex; use std::cmp::Ordering; use std::path::{Path, PathBuf}; @@ -44,12 +45,11 @@ pub fn replace_tilde_with_home_dir(path: impl AsRef) -> PathBuf { path.to_path_buf() } -fn file_name_from_path(path: &str) -> String { +fn file_name_from_path(path: &str) -> &str { Path::new(path) .file_name() .and_then(|f| f.to_str()) .unwrap_or(path) - .to_string() } /// This function can be used to sort the given vector on basis of similarity between the input & the vector @@ -81,14 +81,44 @@ fn file_name_from_path(path: &str) -> String { /// search **with** similarity sort /// `["fly.txt", "flyer.txt", "afly.txt", "bfly.txt",]` pub fn similarity_sort(vector: &mut [String], input: &str) { + const PARALLEL_SORT_THRESHOLD: usize = 5000; let input = input.to_lowercase(); - vector.sort_by(|a, b| { - let a = file_name_from_path(a).to_lowercase(); - let b = file_name_from_path(b).to_lowercase(); - let a = jaro_winkler(a.as_str(), input.as_str()); - let b = jaro_winkler(b.as_str(), input.as_str()); - b.partial_cmp(&a).unwrap_or(Ordering::Equal) - }); + // Schwartzian transform: precompute all scores, then sort by score. + // Use parallel scoring only for large datasets where rayon overhead is worthwhile. + let mut scored: Vec<(usize, f64)> = if vector.len() >= PARALLEL_SORT_THRESHOLD { + vector + .par_iter() + .enumerate() + .map(|(i, path)| { + let name = file_name_from_path(path).to_lowercase(); + (i, jaro_winkler(&name, &input)) + }) + .collect() + } else { + vector + .iter() + .enumerate() + .map(|(i, path)| { + let name = file_name_from_path(path).to_lowercase(); + (i, jaro_winkler(&name, &input)) + }) + .collect() + }; + scored.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal)); + + // Reorder vector in-place according to the sorted indices. + let order: Vec = scored.into_iter().map(|(i, _)| i).collect(); + apply_permutation(vector, order); +} + +fn apply_permutation(v: &mut [T], mut order: Vec) { + for i in 0..v.len() { + while order[i] != i { + let j = order[i]; + v.swap(i, j); + order.swap(i, j); + } + } } #[cfg(test)] diff --git a/tests/filter_tests.rs b/tests/filter_tests.rs index bffbed9..88520ad 100644 --- a/tests/filter_tests.rs +++ b/tests/filter_tests.rs @@ -45,7 +45,7 @@ fn file_size_greater_filter() { // Note: the root directory entry bypasses filter_entry in the ignore crate, // so we only check that no actual file passes the filter. let results: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .file_size_greater(FileSize::Kilobyte(10.0)) .build() .collect(); @@ -64,7 +64,7 @@ fn file_size_greater_filter() { fn file_size_smaller_filter() { // All fixture files are tiny, so size < 10KB should return all files let results: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .file_size_smaller(FileSize::Kilobyte(10.0)) .build() .collect(); @@ -75,7 +75,7 @@ fn file_size_smaller_filter() { fn custom_filter_works() { // Filter to only include files (not directories) let results: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .custom_filter(|dir| dir.metadata().map(|m| m.is_file()).unwrap_or(false)) .build() .collect(); @@ -87,7 +87,7 @@ fn created_after_epoch_finds_files() { // All files were created after UNIX epoch let epoch = SystemTime::UNIX_EPOCH; let results: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .created_after(epoch) .build() .collect(); @@ -102,7 +102,7 @@ fn modified_before_future_finds_files() { // All files were modified before far future let future = SystemTime::now() + Duration::from_secs(3600 * 24 * 365 * 10); let results: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .modified_before(future) .build() .collect(); diff --git a/tests/search_tests.rs b/tests/search_tests.rs index 853ecf1..1cf4119 100644 --- a/tests/search_tests.rs +++ b/tests/search_tests.rs @@ -12,7 +12,7 @@ fn fixtures_path() -> String { #[test] fn basic_search_finds_files() { let results: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .build() .collect(); // Should find at least the known fixture files @@ -27,7 +27,7 @@ fn basic_search_finds_files() { #[test] fn search_ext_filters_by_extension() { let results: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .ext("rs") .build() .collect(); @@ -40,7 +40,7 @@ fn search_ext_filters_by_extension() { #[test] fn search_input_matches_filename() { let results: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .search_input("hello") .build() .collect(); @@ -56,14 +56,14 @@ fn search_input_matches_filename() { fn search_depth_limits_traversal() { // depth(1) means only the fixtures dir itself, not subdir/deep/ let shallow: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .ext("rs") .depth(1) .build() .collect(); let deep: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .ext("rs") .build() .collect(); @@ -88,7 +88,7 @@ fn search_depth_limits_traversal() { #[test] fn search_limit_caps_results() { let results: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .limit(2) .build() .collect(); @@ -102,7 +102,7 @@ fn search_limit_caps_results() { #[test] fn search_strict_matches_exact() { let results: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .search_input("hello") .ext("rs") .strict() @@ -122,7 +122,7 @@ fn search_strict_matches_exact() { #[test] fn search_ignore_case() { let results: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .search_input("HELLO") .ext("rs") .ignore_case() @@ -134,12 +134,12 @@ fn search_ignore_case() { #[test] fn search_hidden_includes_hidden_files() { let without_hidden: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .build() .collect(); let with_hidden: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .hidden() .build() .collect(); @@ -158,7 +158,7 @@ fn search_hidden_includes_hidden_files() { fn search_more_locations() { let subdir = fixtures_dir().join("subdir").display().to_string(); let results: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .more_locations(vec![&subdir]) .ext("rs") .depth(1) @@ -175,7 +175,7 @@ fn search_more_locations() { #[test] fn search_chained_options() { let results: Vec = SearchBuilder::default() - .location(&fixtures_path()) + .location(fixtures_path()) .search_input("nested") .ext("rs") .strict()