use super::*;
use crate::fuzzy_matcher::util::{assert_order, wrap_matches};
fn wrap_fuzzy_match(choice: &str, pattern: &str) -> Option<String> {
let (_score, indices) = fuzzy_indices(choice, pattern)?;
Some(wrap_matches(choice, &indices))
}
#[test]
fn test_no_match() {
assert_eq!(None, fuzzy_match("abc", "abx"));
assert_eq!(None, fuzzy_match("abc", "d"));
assert_eq!(None, fuzzy_match("", "a"));
}
#[test]
fn test_has_match() {
assert!(fuzzy_match("axbycz", "abc").is_some());
assert!(fuzzy_match("axbycz", "xyz").is_some());
assert!(fuzzy_match("abc", "abc").is_some());
}
#[test]
fn test_exact_match_is_max() {
let matcher = FzyMatcher::default().ignore_case();
let score = matcher.fuzzy_match("abc", "abc").unwrap();
assert!(score > 1_000_000);
}
#[test]
fn test_match_indices() {
assert_eq!("[a]x[b]y[c]z", &wrap_fuzzy_match("axbycz", "abc").unwrap());
assert_eq!("a[x]b[y]c[z]", &wrap_fuzzy_match("axbycz", "xyz").unwrap());
}
#[test]
fn test_consecutive_bonus() {
let matcher = FzyMatcher::default().ignore_case();
let consecutive = matcher.fuzzy_match("foobar", "foo").unwrap();
let scattered = matcher.fuzzy_match("fxoxo", "foo").unwrap();
assert!(
consecutive > scattered,
"consecutive={consecutive} > scattered={scattered}"
);
}
#[test]
fn test_word_boundary_bonus() {
let matcher = FzyMatcher::default().ignore_case();
let boundary = matcher.fuzzy_match("foo_bar_baz", "fbb").unwrap();
let inner = matcher.fuzzy_match("fooobarbaz", "fbb").unwrap();
assert!(boundary > inner, "boundary={boundary} > inner={inner}");
}
#[test]
fn test_path_separator_bonus() {
let matcher = FzyMatcher::default().ignore_case();
let path = matcher.fuzzy_match("src/lib/foo.rs", "foo").unwrap();
let no_path = matcher.fuzzy_match("srcxlibxfoo.rs", "foo").unwrap();
assert!(path > no_path, "path={path} > no_path={no_path}");
}
#[test]
fn test_camel_case_bonus() {
let matcher = FzyMatcher::default().ignore_case();
let camel = matcher.fuzzy_match("FooBarBaz", "fbb").unwrap();
let no_camel = matcher.fuzzy_match("foobarbaz", "fbb").unwrap();
assert!(camel > no_camel, "camel={camel} > no_camel={no_camel}");
}
#[test]
fn test_shorter_match_preferred() {
let matcher = FzyMatcher::default().ignore_case();
let short = matcher.fuzzy_match("ab", "ab").unwrap();
let long = matcher.fuzzy_match("axxxxxxb", "ab").unwrap();
assert!(short > long, "short={short} > long={long}");
}
#[test]
fn test_match_quality_ordering() {
let matcher = FzyMatcher::default();
assert_order(&matcher, "monad", &["monad", "Monad", "mONAD"]);
assert_order(&matcher, "ab", &["ab", "aoo_boo", "acb"]);
assert_order(&matcher, "ma", &["map", "many", "maximum"]);
}
#[test]
fn test_unicode_match() {
let matcher = FzyMatcher::default().ignore_case();
let result = matcher.fuzzy_indices("Hello, 世界", "H世");
assert!(result.is_some());
let (_, indices) = result.unwrap();
assert_eq!(indices.as_slice(), &[0, 7]);
}
#[test]
fn test_smart_case() {
let matcher = FzyMatcher::default().smart_case();
assert!(matcher.fuzzy_match("FooBar", "foobar").is_some());
assert!(matcher.fuzzy_match("foobar", "FooBar").is_none());
assert!(matcher.fuzzy_match("FooBar", "FooBar").is_some());
}
#[test]
fn test_respect_case() {
let matcher = FzyMatcher::default().respect_case();
assert!(matcher.fuzzy_match("abc", "ABC").is_none());
assert!(matcher.fuzzy_match("ABC", "ABC").is_some());
}
#[test]
fn test_long_haystack() {
let matcher = FzyMatcher::default().ignore_case();
let long = "a".repeat(MATCH_MAX_LEN + 1);
assert_eq!(None, matcher.fuzzy_match(&long, "a"));
}
#[test]
fn test_typo_no_typos_behaves_like_default() {
let strict = FzyMatcher::default().ignore_case();
let typo0 = FzyMatcher::default().ignore_case().max_typos(Some(0));
assert!(strict.fuzzy_match("axbycz", "abc").is_some());
assert!(typo0.fuzzy_match("axbycz", "abc").is_some());
assert!(strict.fuzzy_match("abc", "abx").is_none());
assert!(typo0.fuzzy_match("abc", "abx").is_none());
}
#[test]
fn test_typo_substitution_single() {
let matcher = FzyMatcher::default().ignore_case().max_typos(Some(1));
assert!(matcher.fuzzy_match("abc", "abx").is_some(), "substitution: 'x' for 'c'");
}
#[test]
fn test_typo_substitution_returns_none_when_too_many_typos() {
let matcher = FzyMatcher::default().ignore_case().max_typos(Some(1));
assert!(
matcher.fuzzy_match("abc", "ayx").is_none(),
"2 typos needed but only 1 allowed"
);
let matcher2 = FzyMatcher::default().ignore_case().max_typos(Some(2));
assert!(matcher2.fuzzy_match("abc", "ayx").is_some(), "2 typos allowed");
}
#[test]
fn test_typo_needle_deletion() {
let matcher = FzyMatcher::default().ignore_case().max_typos(Some(1));
assert!(matcher.fuzzy_match("abd", "abcd").is_some(), "needle deletion of 'c'");
let strict = FzyMatcher::default().ignore_case();
assert!(strict.fuzzy_match("abd", "abcd").is_none());
}
#[test]
fn test_typo_exact_match_scores_higher_than_typo_match() {
let matcher = FzyMatcher::default().ignore_case().max_typos(Some(1));
let exact = matcher.fuzzy_match("abc", "abc").unwrap();
let typo = matcher.fuzzy_match("axc", "abc").unwrap();
assert!(exact > typo, "exact ({exact}) > typo ({typo})");
}
#[test]
fn test_typo_subsequence_beats_typo() {
let matcher = FzyMatcher::default().ignore_case().max_typos(Some(1));
let subseq = matcher.fuzzy_match("axbycz", "abc").unwrap();
let typo = matcher.fuzzy_match("abx", "abc").unwrap();
assert!(subseq > typo, "subsequence ({subseq}) > typo ({typo})");
}
#[test]
fn test_typo_indices_substitution() {
let matcher = FzyMatcher::default().ignore_case().max_typos(Some(1));
let result = matcher.fuzzy_indices("abx", "abc");
assert!(result.is_some());
let (_, indices) = result.unwrap();
assert_eq!(indices.as_slice(), &[0, 1, 2]);
}
#[test]
fn test_typo_indices_needle_deletion() {
let matcher = FzyMatcher::default().ignore_case().max_typos(Some(1));
let result = matcher.fuzzy_indices("abd", "abcd");
assert!(result.is_some());
let (_, indices) = result.unwrap();
assert_eq!(indices.as_slice(), &[0, 1, 2]);
}
#[test]
fn test_typo_max_typos_none_is_zero_overhead() {
let default = FzyMatcher::default().ignore_case();
let explicit_none = FzyMatcher::default().ignore_case().max_typos(None);
let choices = ["foobar", "axbycz", "src/lib/foo.rs", "FooBarBaz"];
let pattern = "foo";
for choice in &choices {
assert_eq!(
default.fuzzy_match(choice, pattern),
explicit_none.fuzzy_match(choice, pattern),
"max_typos(None) should match default for '{choice}'"
);
}
}
#[test]
fn test_typo_realistic_filename() {
let matcher = FzyMatcher::default().ignore_case().max_typos(Some(1));
let result = matcher.fuzzy_match("controller", "controllr");
assert!(
result.is_some(),
"should match 'controller' with needle 'controllr' (1 typo)"
);
}
#[test]
fn test_typo_two_typos() {
let matcher = FzyMatcher::default().ignore_case().max_typos(Some(2));
assert!(matcher.fuzzy_match("abc", "xyz").is_none());
assert!(matcher.fuzzy_match("abc", "axz").is_some());
}
#[test]
fn test_typo_empty_pattern() {
let matcher = FzyMatcher::default().ignore_case().max_typos(Some(1));
assert_eq!(None, matcher.fuzzy_match("abc", ""));
}
#[test]
fn test_typo_pattern_longer_than_haystack() {
let matcher = FzyMatcher::default().ignore_case().max_typos(Some(1));
assert!(matcher.fuzzy_match("ab", "abc").is_some(), "delete 'c' from needle");
assert!(matcher.fuzzy_match("a", "abc").is_none());
let matcher2 = FzyMatcher::default().ignore_case().max_typos(Some(2));
assert!(matcher2.fuzzy_match("a", "abc").is_some());
}
#[test]
fn test_uppercase_after_separator_bonuses() {
let m = FzyMatcher::default().respect_case();
assert!(m.fuzzy_match("foo/Bar", "B").is_some());
assert!(m.fuzzy_match("foo-Bar", "B").is_some());
assert!(m.fuzzy_match("foo.Bar", "B").is_some());
assert!(m.fuzzy_match("fooBar", "B").is_some());
assert!(m.fuzzy_match("ABc", "B").is_some());
}
#[test]
fn test_use_cache_setter_is_chainable() {
let m = FzyMatcher::default().ignore_case().use_cache(true);
assert!(m.fuzzy_match("foobar", "fb").is_some());
}
#[test]
fn test_exact_length_match_returns_all_indices() {
let m = FzyMatcher::default().ignore_case();
let (_score, indices) = m.fuzzy_indices("abc", "abc").unwrap();
assert_eq!(indices, vec![0, 1, 2]);
}
#[test]
fn test_case_sensitive_subsequence_scoring() {
let m = FzyMatcher::default().respect_case();
let (score, indices) = m.fuzzy_indices("aXbYc", "abc").unwrap();
assert_eq!(indices, vec![0, 2, 4]);
assert!(score > 0);
assert!(m.fuzzy_match("aXBYc", "abc").is_none());
assert!(m.fuzzy_match("aXbYc", "abc").is_some());
}
#[test]
fn test_dp_cell_match_at_first_haystack_char_for_later_needle() {
let m = FzyMatcher::default().ignore_case();
let (_score, indices) = m.fuzzy_indices("bab", "ab").unwrap();
assert_eq!(indices, vec![1, 2]);
}
#[test]
fn test_case_sensitive_typo_substitution() {
let m = FzyMatcher::default().respect_case().max_typos(Some(1));
assert!(m.fuzzy_match("abXd", "abcd").is_some());
let (_score, indices) = m.fuzzy_indices("abXd", "abcd").unwrap();
assert_eq!(indices.len(), 4);
let strict = FzyMatcher::default().respect_case();
assert!(strict.fuzzy_match("abCd", "abcd").is_none());
assert!(m.fuzzy_match("abCd", "abcd").is_some());
}
#[test]
fn test_typo_indices_zero_allowed_falls_back_to_none() {
let m = FzyMatcher::default().ignore_case().max_typos(Some(0));
assert!(m.fuzzy_indices("abc", "abx").is_none());
assert!(m.fuzzy_indices("axbxc", "abc").is_some());
}
#[test]
fn test_typo_indices_pattern_too_long_for_haystack() {
let m = FzyMatcher::default().ignore_case().max_typos(Some(1));
assert!(m.fuzzy_indices("ab", "abcd").is_none());
assert!(m.fuzzy_indices("abc", "abcd").is_some());
}
#[test]
fn test_typo_use_cache_disabled_in_slow_path() {
let m = FzyMatcher::default().ignore_case().max_typos(Some(1)).use_cache(false);
assert!(m.fuzzy_match("abx", "abc").is_some()); assert!(m.fuzzy_indices("abx", "abc").is_some()); assert!(m.fuzzy_match("abx", "abc").is_some()); }
#[test]
fn test_typo_match_at_first_haystack_char_for_later_needle() {
let m = FzyMatcher::default().ignore_case().max_typos(Some(1));
assert!(m.fuzzy_match("bc", "ab").is_none());
assert!(m.fuzzy_indices("bc", "ab").is_none());
}
#[test]
fn test_typo_substitution_at_start_and_end() {
let m = FzyMatcher::default().ignore_case().max_typos(Some(1));
assert!(m.fuzzy_match("Xbc", "abc").is_some());
let (_s, idx) = m.fuzzy_indices("Xbc", "abc").unwrap();
assert_eq!(idx, vec![1, 2]);
assert!(m.fuzzy_match("abX", "abc").is_some());
let (_s, idx) = m.fuzzy_indices("abX", "abc").unwrap();
assert_eq!(idx, vec![0, 1, 2]);
}
#[test]
fn test_typo_sparse_alignment_with_min_predecessors() {
let m = FzyMatcher::default().ignore_case().max_typos(Some(2));
assert!(m.fuzzy_match("aXYd", "abcd").is_some());
let (_s, idx) = m.fuzzy_indices("aXYd", "abcd").unwrap();
assert_eq!(idx.len(), 4);
assert_eq!(idx, vec![0, 1, 2, 3]);
}
#[test]
fn test_typo_no_alignment_returns_none_from_dp() {
let m = FzyMatcher::default().ignore_case().max_typos(Some(1));
assert!(m.fuzzy_match("abc", "xyz").is_none());
assert!(m.fuzzy_indices("abc", "xyz").is_none());
}
#[test]
fn test_typo_needle_deletion_indices_and_gaps() {
let m = FzyMatcher::default().ignore_case().max_typos(Some(2));
let (_s, idx) = m.fuzzy_indices("abd", "abcde").unwrap();
assert!(idx.len() <= 3 && !idx.is_empty());
assert!(idx.windows(2).all(|w| w[0] < w[1]), "indices strictly increasing");
}
#[test]
fn test_typo_pattern_too_long_even_with_typos() {
let m = FzyMatcher::default().ignore_case().max_typos(Some(1));
assert!(m.fuzzy_match("ab", "abcde").is_none());
assert!(m.fuzzy_indices("ab", "abcde").is_none());
}
#[test]
fn test_score_only_match_at_first_char_for_later_needle() {
let m = FzyMatcher::default().ignore_case();
assert!(m.fuzzy_match("aXa", "aa").is_some());
}
#[test]
fn test_single_char_pattern_indices() {
let m = FzyMatcher::default().ignore_case();
let (_s, idx) = m.fuzzy_indices("help", "l").unwrap();
assert_eq!(idx, vec![2]); }
#[test]
fn test_fzy_score_pattern_longer_than_choice_returns_none() {
assert_eq!(fzy_score(&['a', 'b', 'c'], &['a'], false, None), None);
assert_eq!(fzy_score(&[], &['a'], false, None), None); }
#[test]
fn test_can_match_with_typos_length_guard() {
let pat = ['a', 'b', 'c', 'd'];
assert!(!can_match_with_typos(&['a'], &pat, &pat, false, 1));
assert!(can_match_with_typos(&['a', 'b', 'c'], &pat, &pat, false, 1));
}
#[test]
fn test_typo_choice_exceeds_match_max_len() {
let m = FzyMatcher::default().ignore_case().max_typos(Some(1));
let huge = "a".repeat(2000); assert!(m.fuzzy_match(&huge, "az").is_none());
assert!(m.fuzzy_indices(&huge, "az").is_none());
}
#[test]
fn test_typo_indices_internal_gap_backtrace() {
let m = FzyMatcher::default().ignore_case().max_typos(Some(1));
let (_score, idx) = m.fuzzy_indices("aXbc", "abd").expect("typo match with gap");
assert_eq!(idx, vec![0, 2, 3]);
}
#[test]
fn test_indices_scattered_match_backtrace_scans_gaps() {
let m = FzyMatcher::default().ignore_case();
let (_score, idx) = m.fuzzy_indices("a_b_c_d", "abcd").expect("scattered match");
assert_eq!(idx, vec![0, 2, 4, 6]);
let (_score, idx) = m.fuzzy_indices("xaybzc", "abc").expect("scattered match");
assert_eq!(idx, vec![1, 3, 5]);
}
#[test]
fn test_typo_fast_path_subsequence_too_long_falls_through() {
let m = FzyMatcher::default().ignore_case().max_typos(Some(1));
let huge = "a".repeat(2000);
assert!(m.fuzzy_indices(&huge, "aa").is_none());
assert!(m.fuzzy_match(&huge, "aa").is_none());
}
#[test]
fn test_indices_repeated_char_backtrace_disambiguates() {
let m = FzyMatcher::default().ignore_case();
let (_s, idx) = m.fuzzy_indices("aabaa", "aba").expect("should match");
assert_eq!(idx.len(), 3);
assert!(idx.windows(2).all(|w| w[0] < w[1]));
let (_s, idx) = m.fuzzy_indices("banana", "aaa").expect("should match");
assert_eq!(idx, vec![1, 3, 5]);
}
#[test]
fn test_indices_nonoptimal_match_cells_in_backtrace() {
let m = FzyMatcher::default().ignore_case();
for (choice, pattern) in [
("aXaXa", "aaa"),
("aabaab", "aba"),
("a.a.a", "aa"),
("baba", "ba"),
("xaxaxa", "aaa"),
] {
let r = m.fuzzy_indices(choice, pattern);
assert!(r.is_some(), "{choice:?}/{pattern:?} should match");
let (_s, idx) = r.unwrap();
assert_eq!(idx.len(), pattern.chars().count());
assert!(idx.windows(2).all(|w| w[0] < w[1]), "indices increasing");
}
}
#[test]
fn test_typo_indices_multiple_gaps_and_deletions() {
let m = FzyMatcher::default().ignore_case().max_typos(Some(2));
for (choice, pattern) in [("aXbYcd", "abce"), ("a_b_c", "axc"), ("foobar", "fxxr")] {
let _ = m.fuzzy_indices(choice, pattern);
}
let (_s, idx) = m.fuzzy_indices("aXbYZ", "abc").expect("typo match");
assert!(!idx.is_empty() && idx.windows(2).all(|w| w[0] < w[1]));
}
#[test]
fn test_internal_to_skim_score_sentinels() {
assert_eq!(internal_to_skim_score(SCORE_MAX), ScoreType::MAX / 2);
assert_eq!(internal_to_skim_score(SCORE_MIN), ScoreType::MIN / 2);
assert_eq!(internal_to_skim_score(100), 100 * SCORE_TO_SKIM);
}
#[test]
fn test_typo_empty_pattern_returns_none() {
let m = FzyMatcher::default().ignore_case().max_typos(Some(1));
assert_eq!(m.fuzzy_match("abc", ""), None);
assert_eq!(m.fuzzy_indices("abc", ""), None);
}
#[test]
fn test_fzy_score_backtrace_falls_back_at_column_zero() {
let mut pos = Vec::new();
let score = fzy_score(&['z', 'a'], &['a', 'b', 'c'], false, Some(&mut pos));
assert!(score.is_some(), "fzy_score always returns a (possibly poor) score");
let mut pos2 = Vec::new();
let _ = fzy_score(&['c', 'a'], &['a', 'b', 'c'], false, Some(&mut pos2));
}
#[test]
fn test_fzy_score_typos_full_backtrace_gaps_direct() {
let needle: Vec<char> = "ace".chars().collect();
let haystack: Vec<char> = "aXbYc".chars().collect();
let lower_needle = needle.clone();
let lower_haystack = haystack.clone();
let match_bonus = precompute_bonus(&haystack);
let mut bufs = TypoDpBuffers::default();
let mut positions = Vec::new();
let _ = fzy_score_typos_full(
&needle,
&haystack,
&lower_needle,
&lower_haystack,
&match_bonus,
false,
2,
&mut positions,
&mut bufs,
);
assert!(positions.windows(2).all(|w| w[0] < w[1]));
}
#[test]
fn test_fzy_score_typos_full_column_zero_fallback_direct() {
let cases: &[(&str, &str, usize)] = &[
("ax", "abc", 1),
("za", "abc", 1),
("xb", "abc", 1),
("ac", "aXbYc", 2),
("zc", "abc", 2),
];
for &(needle_s, hay_s, t) in cases {
let needle: Vec<char> = needle_s.chars().collect();
let haystack: Vec<char> = hay_s.chars().collect();
let nl = needle.clone();
let hl = haystack.clone();
let mb = precompute_bonus(&haystack);
let mut bufs = TypoDpBuffers::default();
let mut positions = Vec::new();
let _ = fzy_score_typos_full(&needle, &haystack, &nl, &hl, &mb, false, t, &mut positions, &mut bufs);
assert!(
positions.windows(2).all(|w| w[0] < w[1]),
"positions must be increasing for {needle_s:?}/{hay_s:?}"
);
}
}
#[test]
fn test_fzy_score_backtrace_nonconsecutive_match() {
for (needle, hay) in [
(vec!['a', 'c'], vec!['a', 'b', 'c']),
(vec!['a', 'd'], vec!['a', 'b', 'c', 'd']),
(vec!['a', 'z'], vec!['a', 'b', 'z']),
] {
let mut pos = Vec::new();
let s = fzy_score(&needle, &hay, false, Some(&mut pos));
assert!(s.is_some());
assert!(pos.windows(2).all(|w| w[0] < w[1]));
}
}