fzy
terminal fuzzy finder picker
git clone https://9o.is/git/fzy.git
test_match.c
(6707B)
1 #include <stdlib.h>
2
3 #include "../config.h"
4 #include "match.h"
5
6 #include "greatest/greatest.h"
7
8 #define SCORE_TOLERANCE 0.000001
9 #define ASSERT_SCORE_EQ(a,b) ASSERT_IN_RANGE((a), (b), SCORE_TOLERANCE)
10 #define ASSERT_SIZE_T_EQ(a,b) ASSERT_EQ_FMT((size_t)(a), (b), "%zu")
11
12 /* has_match(char *needle, char *haystack) */
13 TEST exact_match_should_return_true() {
14 ASSERT(has_match("a", "a"));
15 PASS();
16 }
17
18 TEST partial_match_should_return_true() {
19 ASSERT(has_match("a", "ab"));
20 ASSERT(has_match("a", "ba"));
21 PASS();
22 }
23
24 TEST match_with_delimiters_in_between() {
25 ASSERT(has_match("abc", "a|b|c"));
26 PASS();
27 }
28
29 TEST non_match_should_return_false() {
30 ASSERT(!has_match("a", ""));
31 ASSERT(!has_match("a", "b"));
32 ASSERT(!has_match("ass", "tags"));
33 PASS();
34 }
35
36 TEST empty_query_should_always_match() {
37 /* match when query is empty */
38 ASSERT(has_match("", ""));
39 ASSERT(has_match("", "a"));
40 PASS();
41 }
42
43 /* match(char *needle, char *haystack) */
44
45 TEST should_prefer_starts_of_words() {
46 /* App/Models/Order is better than App/MOdels/zRder */
47 ASSERT(match("amor", "app/models/order") > match("amor", "app/models/zrder"));
48 PASS();
49 }
50
51 TEST should_prefer_consecutive_letters() {
52 /* App/MOdels/foo is better than App/M/fOo */
53 ASSERT(match("amo", "app/m/foo") < match("amo", "app/models/foo"));
54 PASS();
55 }
56
57 TEST should_prefer_contiguous_over_letter_following_period() {
58 /* GEMFIle.Lock < GEMFILe */
59 ASSERT(match("gemfil", "Gemfile.lock") < match("gemfil", "Gemfile"));
60 PASS();
61 }
62
63 TEST should_prefer_shorter_matches() {
64 ASSERT(match("abce", "abcdef") > match("abce", "abc de"));
65 ASSERT(match("abc", " a b c ") > match("abc", " a b c "));
66 ASSERT(match("abc", " a b c ") > match("abc", " a b c "));
67 PASS();
68 }
69
70 TEST should_prefer_shorter_candidates() {
71 ASSERT(match("test", "tests") > match("test", "testing"));
72 PASS();
73 }
74
75 TEST should_prefer_start_of_candidate() {
76 /* Scores first letter highly */
77 ASSERT(match("test", "testing") > match("test", "/testing"));
78 PASS();
79 }
80
81 TEST score_exact_match() {
82 /* Exact match is SCORE_MAX */
83 ASSERT_SCORE_EQ(SCORE_MAX, match("abc", "abc"));
84 ASSERT_SCORE_EQ(SCORE_MAX, match("aBc", "abC"));
85 PASS();
86 }
87
88 TEST score_empty_query() {
89 /* Empty query always results in SCORE_MIN */
90 ASSERT_SCORE_EQ(SCORE_MIN, match("", ""));
91 ASSERT_SCORE_EQ(SCORE_MIN, match("", "a"));
92 ASSERT_SCORE_EQ(SCORE_MIN, match("", "bb"));
93 PASS();
94 }
95
96 TEST score_gaps() {
97 ASSERT_SCORE_EQ(SCORE_GAP_LEADING, match("a", "*a"));
98 ASSERT_SCORE_EQ(SCORE_GAP_LEADING*2, match("a", "*ba"));
99 ASSERT_SCORE_EQ(SCORE_GAP_LEADING*2 + SCORE_GAP_TRAILING, match("a", "**a*"));
100 ASSERT_SCORE_EQ(SCORE_GAP_LEADING*2 + SCORE_GAP_TRAILING*2, match("a", "**a**"));
101 ASSERT_SCORE_EQ(SCORE_GAP_LEADING*2 + SCORE_MATCH_CONSECUTIVE + SCORE_GAP_TRAILING*2, match("aa", "**aa**"));
102 ASSERT_SCORE_EQ(SCORE_GAP_LEADING + SCORE_GAP_LEADING + SCORE_GAP_INNER + SCORE_GAP_TRAILING + SCORE_GAP_TRAILING, match("aa", "**a*a**"));
103 PASS();
104 }
105
106 TEST score_consecutive() {
107 ASSERT_SCORE_EQ(SCORE_GAP_LEADING + SCORE_MATCH_CONSECUTIVE, match("aa", "*aa"));
108 ASSERT_SCORE_EQ(SCORE_GAP_LEADING + SCORE_MATCH_CONSECUTIVE*2, match("aaa", "*aaa"));
109 ASSERT_SCORE_EQ(SCORE_GAP_LEADING + SCORE_GAP_INNER + SCORE_MATCH_CONSECUTIVE, match("aaa", "*a*aa"));
110 PASS();
111 }
112
113 TEST score_slash() {
114 ASSERT_SCORE_EQ(SCORE_GAP_LEADING + SCORE_MATCH_SLASH, match("a", "/a"));
115 ASSERT_SCORE_EQ(SCORE_GAP_LEADING*2 + SCORE_MATCH_SLASH, match("a", "*/a"));
116 ASSERT_SCORE_EQ(SCORE_GAP_LEADING*2 + SCORE_MATCH_SLASH + SCORE_MATCH_CONSECUTIVE, match("aa", "a/aa"));
117 PASS();
118 }
119
120 TEST score_capital() {
121 ASSERT_SCORE_EQ(SCORE_GAP_LEADING + SCORE_MATCH_CAPITAL, match("a", "bA"));
122 ASSERT_SCORE_EQ(SCORE_GAP_LEADING*2 + SCORE_MATCH_CAPITAL, match("a", "baA"));
123 ASSERT_SCORE_EQ(SCORE_GAP_LEADING*2 + SCORE_MATCH_CAPITAL + SCORE_MATCH_CONSECUTIVE, match("aa", "baAa"));
124 PASS();
125 }
126
127 TEST score_dot() {
128 ASSERT_SCORE_EQ(SCORE_GAP_LEADING + SCORE_MATCH_DOT, match("a", ".a"));
129 ASSERT_SCORE_EQ(SCORE_GAP_LEADING*3 + SCORE_MATCH_DOT, match("a", "*a.a"));
130 ASSERT_SCORE_EQ(SCORE_GAP_LEADING + SCORE_GAP_INNER + SCORE_MATCH_DOT, match("a", "*a.a"));
131 PASS();
132 }
133
134 TEST score_long_string() {
135 char string[4096];
136 memset(string, 'a', sizeof(string) - 1);
137 string[sizeof(string) - 1] = '\0';
138
139 ASSERT_SCORE_EQ(SCORE_MIN, match("aa", string));
140 ASSERT_SCORE_EQ(SCORE_MIN, match(string, "aa"));
141 ASSERT_SCORE_EQ(SCORE_MIN, match(string, string));
142
143 PASS();
144 }
145
146 TEST positions_consecutive() {
147 size_t positions[3];
148 match_positions("amo", "app/models/foo", positions);
149 ASSERT_SIZE_T_EQ(0, positions[0]);
150 ASSERT_SIZE_T_EQ(4, positions[1]);
151 ASSERT_SIZE_T_EQ(5, positions[2]);
152
153 PASS();
154 }
155
156 TEST positions_start_of_word() {
157 /*
158 * We should prefer matching the 'o' in order, since it's the beginning
159 * of a word.
160 */
161 size_t positions[4];
162 match_positions("amor", "app/models/order", positions);
163 ASSERT_SIZE_T_EQ(0, positions[0]);
164 ASSERT_SIZE_T_EQ(4, positions[1]);
165 ASSERT_SIZE_T_EQ(11, positions[2]);
166 ASSERT_SIZE_T_EQ(12, positions[3]);
167
168 PASS();
169 }
170
171 TEST positions_no_bonuses() {
172 size_t positions[2];
173 match_positions("as", "tags", positions);
174 ASSERT_SIZE_T_EQ(1, positions[0]);
175 ASSERT_SIZE_T_EQ(3, positions[1]);
176
177 match_positions("as", "examples.txt", positions);
178 ASSERT_SIZE_T_EQ(2, positions[0]);
179 ASSERT_SIZE_T_EQ(7, positions[1]);
180
181 PASS();
182 }
183
184 TEST positions_multiple_candidates_start_of_words() {
185 size_t positions[3];
186 match_positions("abc", "a/a/b/c/c", positions);
187 ASSERT_SIZE_T_EQ(2, positions[0]);
188 ASSERT_SIZE_T_EQ(4, positions[1]);
189 ASSERT_SIZE_T_EQ(6, positions[2]);
190
191 PASS();
192 }
193
194 TEST positions_exact_match() {
195 size_t positions[3];
196 match_positions("foo", "foo", positions);
197 ASSERT_SIZE_T_EQ(0, positions[0]);
198 ASSERT_SIZE_T_EQ(1, positions[1]);
199 ASSERT_SIZE_T_EQ(2, positions[2]);
200
201 PASS();
202 }
203
204 SUITE(match_suite) {
205 RUN_TEST(exact_match_should_return_true);
206 RUN_TEST(partial_match_should_return_true);
207 RUN_TEST(empty_query_should_always_match);
208 RUN_TEST(non_match_should_return_false);
209 RUN_TEST(match_with_delimiters_in_between);
210
211 RUN_TEST(should_prefer_starts_of_words);
212 RUN_TEST(should_prefer_consecutive_letters);
213 RUN_TEST(should_prefer_contiguous_over_letter_following_period);
214 RUN_TEST(should_prefer_shorter_matches);
215 RUN_TEST(should_prefer_shorter_candidates);
216 RUN_TEST(should_prefer_start_of_candidate);
217
218 RUN_TEST(score_exact_match);
219 RUN_TEST(score_empty_query);
220 RUN_TEST(score_gaps);
221 RUN_TEST(score_consecutive);
222 RUN_TEST(score_slash);
223 RUN_TEST(score_capital);
224 RUN_TEST(score_dot);
225 RUN_TEST(score_long_string);
226
227 RUN_TEST(positions_consecutive);
228 RUN_TEST(positions_start_of_word);
229 RUN_TEST(positions_no_bonuses);
230 RUN_TEST(positions_multiple_candidates_start_of_words);
231 RUN_TEST(positions_exact_match);
232 }