From 59918edcd100d5573418ac0ed4abebdfbcd5e671 Mon Sep 17 00:00:00 2001 From: Parth Jadhav Date: Mon, 2 Mar 2026 01:31:56 +0530 Subject: [PATCH 1/2] feat: fix some bugs, improve some perf, add some tests --- .github/workflows/ci.yml | 27 +--- README.md | 72 +++++---- src/builder.rs | 4 +- src/filter.rs | 14 +- src/search.rs | 43 +++--- src/utils.rs | 81 ++++++++-- tests/filter_tests.rs | 120 +++++++++++++++ tests/fixtures/.hidden_file | 1 + tests/fixtures/Cargo.toml | 2 + tests/fixtures/hello.rs | 1 + tests/fixtures/subdir/deep/deep_file.rs | 1 + tests/fixtures/subdir/nested.rs | 1 + tests/fixtures/world.txt | 1 + tests/search_tests.rs | 187 ++++++++++++++++++++++++ tests/utils_tests.rs | 37 +++++ 15 files changed, 497 insertions(+), 95 deletions(-) create mode 100644 tests/filter_tests.rs create mode 100644 tests/fixtures/.hidden_file create mode 100644 tests/fixtures/Cargo.toml create mode 100644 tests/fixtures/hello.rs create mode 100644 tests/fixtures/subdir/deep/deep_file.rs create mode 100644 tests/fixtures/subdir/nested.rs create mode 100644 tests/fixtures/world.txt create mode 100644 tests/search_tests.rs create mode 100644 tests/utils_tests.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea4cbb3..d9d3613 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,25 +32,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.target }} - profile: minimal - override: true - - - name: Install Rust library source - if: matrix.target == 'x86_64-unknown-linux-gnu' - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.target }} - profile: minimal - override: true - components: rust-src + run: rustup toolchain install stable --target ${{ matrix.target }} --profile minimal - name: Build run: cargo build --verbose --target ${{ matrix.target }} @@ -59,15 +44,13 @@ jobs: run: cargo test --verbose --target ${{ matrix.target }} lint: - name: Formatter - - needs: build-test + name: Lint & Format runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust run: | @@ -79,5 +62,5 @@ jobs: - name: Check formatting run: cargo fmt --all -- --check - - name: Check code for possible improvements + - name: Run clippy run: cargo clippy -- -D warnings diff --git a/README.md b/README.md index be294d9..d585964 100644 --- a/README.md +++ b/README.md @@ -28,52 +28,48 @@ rust_search = "2.0.0" ```rust use rust_search::SearchBuilder; -fn main(){ - let search: Vec = SearchBuilder::default() - .location("~/path/to/directory") - .search_input("what to search") - .more_locations(vec!["/anotherPath/to/search", "/keepAddingIfYouWant/"]) - .limit(1000) // results to return - .ext("extension") - .strict() - .depth(1) - .ignore_case() - .hidden() - .build() - .collect(); - - for path in search { - println!("{}", path); - } +let search: Vec = SearchBuilder::default() + .location("~/path/to/directory") + .search_input("what to search") + .more_locations(vec!["/anotherPath/to/search", "/keepAddingIfYouWant/"]) + .limit(1000) // results to return + .ext("extension") + .strict() + .depth(1) + .ignore_case() + .hidden() + .build() + .collect(); + +for path in search { + println!("{}", path); } ``` - Sort the output by similarity with the input ```rust - use rust_search::{SearchBuilder, similarity_sort}; - fn main() { - let search_input = "fly"; - let mut search: Vec = SearchBuilder::default() - .location("~/Desktop/") - .search_input(search_input) - .depth(1) - .ignore_case() - .build() - .collect(); - - similarity_sort(&mut search, &search_input); - for path in search { - println!("{:?}", path); - } - } - +use rust_search::{SearchBuilder, similarity_sort}; + +let search_input = "fly"; +let mut search: Vec = SearchBuilder::default() + .location("~/Desktop/") + .search_input(search_input) + .depth(1) + .ignore_case() + .build() + .collect(); + +similarity_sort(&mut search, &search_input); +for path in search { + println!("{:?}", path); +} ``` > search **without** similarity sort -`["afly.txt", "bfly.txt", "flyer.txt", "fly.txt"]` +> `["afly.txt", "bfly.txt", "flyer.txt", "fly.txt"]` > search **with** similarity sort -`["fly.txt", "flyer.txt", "afly.txt", "bfly.txt",]` +> `["fly.txt", "flyer.txt", "afly.txt", "bfly.txt",]` - To get all the files with a specific extension in a directory, use: @@ -98,7 +94,7 @@ let files: Vec = SearchBuilder::default() .build() .collect(); ``` -To filter files by date_created, date_modified, file_size and/or custom_filter, use: +To filter files by `date_created`, `date_modified`, `file_size` and/or `custom_filter`, use: ```rust use rust_search::{FileSize, FilterExt, SearchBuilder}; @@ -121,7 +117,7 @@ let search: Vec = SearchBuilder::default() ## ⚙️ Benchmarks -The difference in sample size is due to the fact that fd and glob are different tools and have different use cases. fd is a command line tool that searches for files and directories. glob is a library that can be used to search for files and directories. The benchmark is done on a MacBook Air M2, 16 GB Unified memory. +The difference in sample size is due to the fact that fd and glob are different tools and have different use cases. fd is a command line tool that searches for files and directories. glob is a library that can be used to search for files and directories. The benchmark is done on a `MacBook` Air M2, 16 GB Unified memory. Benchmarks are done using [hyperfine](https://github.com/sharkdp/hyperfine), Benchmarks files are available in the [benchmarks](https://drive.google.com/drive/folders/1ug6ojNixS5jAe6Lh6M0o2d3tku73zQ9w?usp=sharing) drive folder. diff --git a/src/builder.rs b/src/builder.rs index 6351a40..dd14bf3 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -96,7 +96,7 @@ impl SearchBuilder { pub fn ext(mut self, ext: impl Into) -> Self { let ext: String = ext.into(); // Remove the dot if it's there. - self.file_ext = Some(ext.strip_prefix('.').map_or(ext.clone(), str::to_owned)); + self.file_ext = Some(ext.strip_prefix('.').map_or_else(|| ext.clone(), str::to_owned)); self } @@ -201,7 +201,7 @@ impl SearchBuilder { /// use rust_search::SearchBuilder; /// /// let search: Vec = SearchBuilder::default() - /// .with_hidden() + /// .hidden() /// .build() /// .collect(); /// ``` diff --git a/src/filter.rs b/src/filter.rs index 2f8c308..61c811d 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -57,12 +57,11 @@ fn convert(b: f64, pow: u32) -> u64 { (b * 1024_u64.pow(pow) as f64) as u64 } -#[allow(clippy::from_over_into)] -impl Into for FileSize { - fn into(self) -> u64 { +impl From for u64 { + fn from(size: FileSize) -> Self { use self::FileSize::{Byte, Gigabyte, Kilobyte, Megabyte, Terabyte}; - match self { + match size { Byte(b) => b, Kilobyte(b) => convert(b, 1), Megabyte(b) => convert(b, 2), @@ -94,7 +93,12 @@ pub trait FilterExt { fn file_size_greater(self, size: FileSize) -> Self; /// custom filter that exposes the [`DirEntry`] directly /// ```rust - /// builder.custom_filter(|dir| dir.metadata().unwrap().is_file()) + /// use rust_search::{SearchBuilder, FilterExt}; + /// + /// let search: Vec = SearchBuilder::default() + /// .custom_filter(|dir| dir.metadata().unwrap().is_file()) + /// .build() + /// .collect(); /// ``` fn custom_filter(self, f: FilterFn) -> Self; } diff --git a/src/search.rs b/src/search.rs index 89a235c..91d6ade 100644 --- a/src/search.rs +++ b/src/search.rs @@ -1,12 +1,15 @@ use std::{ cmp, path::Path, - sync::mpsc::{self, Sender}, + sync::{ + atomic::{AtomicUsize, Ordering}, + mpsc::{self, Sender}, + Arc, + }, }; use crate::{filter::FilterType, utils, SearchBuilder}; use ignore::{WalkBuilder, WalkState}; -use regex::Regex; /// A struct that holds the receiver for the search results /// @@ -17,9 +20,13 @@ use regex::Regex; /// ## Iterate on the results /// /// ``` -/// use rust_search::Search; +/// use rust_search::SearchBuilder; /// -/// let search = Search::new("src", None, Some(".rs"), Some(1)); +/// let search = SearchBuilder::default() +/// .location("src") +/// .ext("rs") +/// .depth(1) +/// .build(); /// /// for path in search { /// println!("{:?}", path); @@ -29,11 +36,14 @@ use regex::Regex; /// ## Collect results into a vector /// /// ``` -/// use rust_search::Search; +/// use rust_search::SearchBuilder; /// -/// let search = Search::new("src", None, Some(".rs"), Some(1)); -/// -/// let paths_vec: Vec = search.collect(); +/// let paths_vec: Vec = SearchBuilder::default() +/// .location("src") +/// .ext("rs") +/// .depth(1) +/// .build() +/// .collect(); /// ``` pub struct Search { rx: Box>, @@ -94,10 +104,13 @@ impl Search { } let (tx, rx) = mpsc::channel::(); + let reg_exp = Arc::new(regex_search_input); + let counter = Arc::new(AtomicUsize::new(0)); + walker.build_parallel().run(|| { let tx: Sender = tx.clone(); - let reg_exp: Regex = regex_search_input.clone(); - let mut counter = 0; + let reg_exp = Arc::clone(®_exp); + let counter = Arc::clone(&counter); Box::new(move |path_entry| { if let Ok(entry) = path_entry { @@ -106,17 +119,13 @@ impl Search { // 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().to_string(); + let file_name = file_name.to_string_lossy(); if reg_exp.is_match(&file_name) { - // Continue searching if the send was successful - // and there is no limit or the limit has not been reached - if tx.send(path.display().to_string()).is_ok() - && (limit.is_none() || counter < limit.unwrap()) + if limit.is_none_or(|l| counter.fetch_add(1, Ordering::Relaxed) < l) + && tx.send(path.display().to_string()).is_ok() { - counter += 1; return WalkState::Continue; } - return WalkState::Quit; } } diff --git a/src/utils.rs b/src/utils.rs index 3d77a5d..7a5fd20 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,5 @@ use regex::Regex; +use std::cmp::Ordering; use std::path::{Path, PathBuf}; use strsim::jaro_winkler; @@ -14,9 +15,9 @@ pub fn build_regex_search_input( let search_input = search_input.unwrap_or(r"\w+"); let mut formatted_search_input = if strict { - format!(r#"{search_input}\.{file_type}$"#) + format!(r"{search_input}\.{file_type}$") } else { - format!(r#"{search_input}{FUZZY_SEARCH}\.{file_type}$"#) + format!(r"{search_input}{FUZZY_SEARCH}\.{file_type}$") }; if ignore_case { @@ -44,9 +45,11 @@ pub fn replace_tilde_with_home_dir(path: impl AsRef) -> PathBuf { } fn file_name_from_path(path: &str) -> String { - let path = Path::new(path); - let file_name = path.file_name().unwrap().to_str().unwrap(); - file_name.to_string() + 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 @@ -65,7 +68,7 @@ fn file_name_from_path(path: &str) -> String { /// .ignore_case() /// .build() /// .collect(); - +/// /// similarity_sort(&mut search, &search_input); /// for path in search { /// println!("{:?}", path); @@ -77,16 +80,72 @@ fn file_name_from_path(path: &str) -> String { /// /// search **with** similarity sort /// `["fly.txt", "flyer.txt", "afly.txt", "bfly.txt",]` -/// -/// ### Panics -/// Will panic if `partial_cmp` is None pub fn similarity_sort(vector: &mut [String], input: &str) { + let input = input.to_lowercase(); vector.sort_by(|a, b| { - let input = input.to_lowercase(); 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() + b.partial_cmp(&a).unwrap_or(Ordering::Equal) }); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn build_regex_fuzzy_no_ext() { + let re = build_regex_search_input(Some("hello"), None, false, false); + assert!(re.is_match("hello.rs")); + assert!(re.is_match("hello_world.txt")); + } + + #[test] + fn build_regex_strict_with_ext() { + let re = build_regex_search_input(Some("hello"), Some("rs"), true, false); + assert!(re.is_match("hello.rs")); + assert!(!re.is_match("hello_world.rs")); + } + + #[test] + fn build_regex_ignore_case() { + let re = build_regex_search_input(Some("Hello"), None, false, true); + assert!(re.is_match("hello.rs")); + assert!(re.is_match("HELLO.txt")); + } + + #[test] + fn build_regex_defaults() { + let re = build_regex_search_input(None, None, false, false); + // Should match any filename with an extension + assert!(re.is_match("anything.txt")); + } + + #[test] + fn file_name_from_path_normal() { + assert_eq!(file_name_from_path("/some/path/file.txt"), "file.txt"); + } + + #[test] + fn file_name_from_path_no_extension() { + assert_eq!(file_name_from_path("/some/path/file"), "file"); + } + + #[test] + fn replace_tilde_expands() { + let result = replace_tilde_with_home_dir("~/Documents"); + assert!(!result.starts_with("~"), "Tilde should be expanded"); + assert!( + result.to_string_lossy().contains("Documents"), + "Path should still contain Documents" + ); + } + + #[test] + fn replace_tilde_leaves_non_tilde() { + let result = replace_tilde_with_home_dir("/absolute/path"); + assert_eq!(result, PathBuf::from("/absolute/path")); + } +} diff --git a/tests/filter_tests.rs b/tests/filter_tests.rs new file mode 100644 index 0000000..9378ded --- /dev/null +++ b/tests/filter_tests.rs @@ -0,0 +1,120 @@ +use rust_search::{FileSize, FilterExt, SearchBuilder}; +use std::path::PathBuf; +use std::time::{Duration, SystemTime}; + +fn fixtures_path() -> String { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures") + .display() + .to_string() +} + +#[test] +fn file_size_byte_conversion() { + let size: u64 = FileSize::Byte(1024).into(); + assert_eq!(size, 1024); +} + +#[test] +fn file_size_kilobyte_conversion() { + let size: u64 = FileSize::Kilobyte(1.0).into(); + assert_eq!(size, 1024); +} + +#[test] +fn file_size_megabyte_conversion() { + let size: u64 = FileSize::Megabyte(1.0).into(); + assert_eq!(size, 1_048_576); +} + +#[test] +fn file_size_gigabyte_conversion() { + let size: u64 = FileSize::Gigabyte(1.0).into(); + assert_eq!(size, 1_073_741_824); +} + +#[test] +fn file_size_terabyte_conversion() { + let size: u64 = FileSize::Terabyte(1.0).into(); + assert_eq!(size, 1_099_511_627_776); +} + +#[test] +fn file_size_greater_filter() { + // All fixture files are tiny, so filtering > 10KB should exclude them. + // 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()) + .file_size_greater(FileSize::Kilobyte(10.0)) + .build() + .collect(); + let file_results: Vec<&String> = results + .iter() + .filter(|r| std::path::Path::new(r.as_str()).is_file()) + .collect(); + assert!( + file_results.is_empty(), + "No fixture file should be > 10KB: {:?}", + file_results + ); +} + +#[test] +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()) + .file_size_smaller(FileSize::Kilobyte(10.0)) + .build() + .collect(); + assert!( + !results.is_empty(), + "All fixture files should be < 10KB" + ); +} + +#[test] +fn custom_filter_works() { + // Filter to only include files (not directories) + let results: Vec = SearchBuilder::default() + .location(&fixtures_path()) + .custom_filter(|dir| { + dir.metadata() + .map(|m| m.is_file()) + .unwrap_or(false) + }) + .build() + .collect(); + assert!(!results.is_empty(), "Should find files with custom filter"); +} + +#[test] +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()) + .created_after(epoch) + .build() + .collect(); + assert!( + !results.is_empty(), + "All files should be created after epoch" + ); +} + +#[test] +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()) + .modified_before(future) + .build() + .collect(); + assert!( + !results.is_empty(), + "All files should be modified before far future" + ); +} diff --git a/tests/fixtures/.hidden_file b/tests/fixtures/.hidden_file new file mode 100644 index 0000000..136c05e --- /dev/null +++ b/tests/fixtures/.hidden_file @@ -0,0 +1 @@ +hidden diff --git a/tests/fixtures/Cargo.toml b/tests/fixtures/Cargo.toml new file mode 100644 index 0000000..0ba9687 --- /dev/null +++ b/tests/fixtures/Cargo.toml @@ -0,0 +1,2 @@ +[package] +name = "fixture" diff --git a/tests/fixtures/hello.rs b/tests/fixtures/hello.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/tests/fixtures/hello.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/tests/fixtures/subdir/deep/deep_file.rs b/tests/fixtures/subdir/deep/deep_file.rs new file mode 100644 index 0000000..e0bf976 --- /dev/null +++ b/tests/fixtures/subdir/deep/deep_file.rs @@ -0,0 +1 @@ +fn deep() {} diff --git a/tests/fixtures/subdir/nested.rs b/tests/fixtures/subdir/nested.rs new file mode 100644 index 0000000..cf6add8 --- /dev/null +++ b/tests/fixtures/subdir/nested.rs @@ -0,0 +1 @@ +fn nested() {} diff --git a/tests/fixtures/world.txt b/tests/fixtures/world.txt new file mode 100644 index 0000000..3b18e51 --- /dev/null +++ b/tests/fixtures/world.txt @@ -0,0 +1 @@ +hello world diff --git a/tests/search_tests.rs b/tests/search_tests.rs new file mode 100644 index 0000000..853ecf1 --- /dev/null +++ b/tests/search_tests.rs @@ -0,0 +1,187 @@ +use rust_search::SearchBuilder; +use std::path::PathBuf; + +fn fixtures_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures") +} + +fn fixtures_path() -> String { + fixtures_dir().display().to_string() +} + +#[test] +fn basic_search_finds_files() { + let results: Vec = SearchBuilder::default() + .location(&fixtures_path()) + .build() + .collect(); + // Should find at least the known fixture files + assert!( + results.len() >= 4, + "Expected at least 4 results, got {} : {:?}", + results.len(), + results + ); +} + +#[test] +fn search_ext_filters_by_extension() { + let results: Vec = SearchBuilder::default() + .location(&fixtures_path()) + .ext("rs") + .build() + .collect(); + assert!(!results.is_empty(), "Should find .rs files"); + for r in &results { + assert!(r.ends_with(".rs"), "Expected .rs file, got: {}", r); + } +} + +#[test] +fn search_input_matches_filename() { + let results: Vec = SearchBuilder::default() + .location(&fixtures_path()) + .search_input("hello") + .build() + .collect(); + assert!(!results.is_empty(), "Should find hello.rs"); + assert!( + results.iter().any(|r| r.contains("hello")), + "Results should contain 'hello': {:?}", + results + ); +} + +#[test] +fn search_depth_limits_traversal() { + // depth(1) means only the fixtures dir itself, not subdir/deep/ + let shallow: Vec = SearchBuilder::default() + .location(&fixtures_path()) + .ext("rs") + .depth(1) + .build() + .collect(); + + let deep: Vec = SearchBuilder::default() + .location(&fixtures_path()) + .ext("rs") + .build() + .collect(); + + assert!( + deep.len() > shallow.len(), + "Deep search ({}) should find more than shallow search ({})", + deep.len(), + shallow.len() + ); + + // Shallow should not contain deep/deep_file.rs + for r in &shallow { + assert!( + !r.contains("deep_file"), + "Shallow search should not find deep_file: {}", + r + ); + } +} + +#[test] +fn search_limit_caps_results() { + let results: Vec = SearchBuilder::default() + .location(&fixtures_path()) + .limit(2) + .build() + .collect(); + assert!( + results.len() <= 2, + "Expected at most 2 results, got {}", + results.len() + ); +} + +#[test] +fn search_strict_matches_exact() { + let results: Vec = SearchBuilder::default() + .location(&fixtures_path()) + .search_input("hello") + .ext("rs") + .strict() + .build() + .collect(); + assert!(!results.is_empty(), "Should find hello.rs with strict"); + for r in &results { + let fname = PathBuf::from(r) + .file_name() + .unwrap() + .to_string_lossy() + .to_string(); + assert_eq!(fname, "hello.rs", "Strict should match exactly hello.rs"); + } +} + +#[test] +fn search_ignore_case() { + let results: Vec = SearchBuilder::default() + .location(&fixtures_path()) + .search_input("HELLO") + .ext("rs") + .ignore_case() + .build() + .collect(); + assert!(!results.is_empty(), "Case-insensitive should find hello.rs"); +} + +#[test] +fn search_hidden_includes_hidden_files() { + let without_hidden: Vec = SearchBuilder::default() + .location(&fixtures_path()) + .build() + .collect(); + + let with_hidden: Vec = SearchBuilder::default() + .location(&fixtures_path()) + .hidden() + .build() + .collect(); + + let has_hidden = with_hidden.iter().any(|r| r.contains(".hidden_file")); + assert!(has_hidden, "hidden() should include .hidden_file"); + + let default_has_hidden = without_hidden.iter().any(|r| r.contains(".hidden_file")); + assert!( + !default_has_hidden, + "Default search should not include hidden files" + ); +} + +#[test] +fn search_more_locations() { + let subdir = fixtures_dir().join("subdir").display().to_string(); + let results: Vec = SearchBuilder::default() + .location(&fixtures_path()) + .more_locations(vec![&subdir]) + .ext("rs") + .depth(1) + .build() + .collect(); + // Should find hello.rs from fixtures and nested.rs from subdir + assert!( + results.len() >= 2, + "Should find files from multiple locations, got: {:?}", + results + ); +} + +#[test] +fn search_chained_options() { + let results: Vec = SearchBuilder::default() + .location(&fixtures_path()) + .search_input("nested") + .ext("rs") + .strict() + .ignore_case() + .build() + .collect(); + assert!(!results.is_empty(), "Chained options should find nested.rs"); + assert!(results.iter().any(|r| r.contains("nested.rs"))); +} diff --git a/tests/utils_tests.rs b/tests/utils_tests.rs new file mode 100644 index 0000000..e29ac91 --- /dev/null +++ b/tests/utils_tests.rs @@ -0,0 +1,37 @@ +use rust_search::similarity_sort; + +#[test] +fn similarity_sort_basic() { + let mut v = vec![ + "afly.txt".to_string(), + "bfly.txt".to_string(), + "fly.txt".to_string(), + "flyer.txt".to_string(), + ]; + similarity_sort(&mut v, "fly"); + // "fly.txt" should be first (highest similarity) + assert_eq!(v[0], "fly.txt", "fly.txt should be most similar: {:?}", v); +} + +#[test] +fn similarity_sort_empty_vector() { + let mut v: Vec = vec![]; + similarity_sort(&mut v, "anything"); + assert!(v.is_empty()); +} + +#[test] +fn similarity_sort_root_like_paths() { + // Paths like "/" or "" that have no file_name component should not panic + let mut v = vec!["/".to_string(), "foo.txt".to_string()]; + similarity_sort(&mut v, "foo"); + // Should not panic — just ensure it completes + assert_eq!(v.len(), 2); +} + +#[test] +fn similarity_sort_single_element() { + let mut v = vec!["only.txt".to_string()]; + similarity_sort(&mut v, "only"); + assert_eq!(v[0], "only.txt"); +} From ba1daa788cc9e38d118ea2ff79f63d967c0a24ba Mon Sep 17 00:00:00 2001 From: Parth Jadhav Date: Mon, 2 Mar 2026 01:35:16 +0530 Subject: [PATCH 2/2] fix: lint --- src/builder.rs | 5 ++++- tests/filter_tests.rs | 11 ++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index dd14bf3..4fba6b8 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -96,7 +96,10 @@ impl SearchBuilder { pub fn ext(mut self, ext: impl Into) -> Self { let ext: String = ext.into(); // Remove the dot if it's there. - self.file_ext = Some(ext.strip_prefix('.').map_or_else(|| ext.clone(), str::to_owned)); + self.file_ext = Some( + ext.strip_prefix('.') + .map_or_else(|| ext.clone(), str::to_owned), + ); self } diff --git a/tests/filter_tests.rs b/tests/filter_tests.rs index 9378ded..bffbed9 100644 --- a/tests/filter_tests.rs +++ b/tests/filter_tests.rs @@ -68,10 +68,7 @@ fn file_size_smaller_filter() { .file_size_smaller(FileSize::Kilobyte(10.0)) .build() .collect(); - assert!( - !results.is_empty(), - "All fixture files should be < 10KB" - ); + assert!(!results.is_empty(), "All fixture files should be < 10KB"); } #[test] @@ -79,11 +76,7 @@ fn custom_filter_works() { // Filter to only include files (not directories) let results: Vec = SearchBuilder::default() .location(&fixtures_path()) - .custom_filter(|dir| { - dir.metadata() - .map(|m| m.is_file()) - .unwrap_or(false) - }) + .custom_filter(|dir| dir.metadata().map(|m| m.is_file()).unwrap_or(false)) .build() .collect(); assert!(!results.is_empty(), "Should find files with custom filter");