Line data Source code
1 : #include "test_helpers.h"
2 : #include "input_line.h"
3 : #include "path_complete.h"
4 : #include <stdio.h>
5 : #include <stdlib.h>
6 : #include <string.h>
7 : #include <sys/stat.h>
8 : #include <unistd.h>
9 :
10 : /* ── Test fixture ─────────────────────────────────────────���──────────── */
11 :
12 : static char g_root[256]; /* per-run temp root, removed at end */
13 :
14 1 : static void fixture_setup(void) {
15 1 : snprintf(g_root, sizeof(g_root), "/tmp/pc_test_%d", getpid());
16 1 : mkdir(g_root, 0755);
17 1 : }
18 :
19 1 : static void fixture_teardown(void) {
20 : /* Recursively remove the temp tree via shell to keep test code short. */
21 1 : char cmd[512];
22 1 : snprintf(cmd, sizeof(cmd), "rm -rf '%s'", g_root);
23 1 : if (system(cmd)) { /* cleanup failure ignored in tests */ }
24 1 : }
25 :
26 : /* Create a test sub-directory under g_root; return its path in out[outsz]. */
27 : #pragma GCC diagnostic push
28 : #pragma GCC diagnostic ignored "-Wformat-truncation"
29 11 : static void td_make(const char *name, char *out, size_t outsz) {
30 11 : snprintf(out, outsz, "%s/%s", g_root, name);
31 11 : mkdir(out, 0755);
32 11 : }
33 : #pragma GCC diagnostic pop
34 :
35 : /* Create an empty regular file at dir/name. */
36 20 : static void make_file(const char *dir, const char *name) {
37 20 : char path[512];
38 20 : snprintf(path, sizeof(path), "%s/%s", dir, name);
39 20 : FILE *f = fopen(path, "w");
40 20 : if (f) fclose(f);
41 20 : }
42 :
43 : /* Create a sub-directory at dir/name. */
44 1 : static void make_subdir(const char *dir, const char *name) {
45 1 : char path[512];
46 1 : snprintf(path, sizeof(path), "%s/%s", dir, name);
47 1 : mkdir(path, 0755);
48 1 : }
49 :
50 : /* Initialise an InputLine with path_complete_attach and buf = text. */
51 10 : static void il_setup(InputLine *il, char *buf, size_t bufsz, const char *text) {
52 10 : input_line_init(il, buf, bufsz, text);
53 10 : path_complete_attach(il);
54 10 : }
55 :
56 : /* ── Tests ───────────────────────────────────────────────────────────── */
57 :
58 1 : static void test_attach_sets_callbacks(void) {
59 1 : char buf[256] = "";
60 1 : InputLine il;
61 1 : input_line_init(&il, buf, sizeof(buf), "");
62 :
63 1 : ASSERT(il.tab_fn == NULL, "tab_fn NULL before attach");
64 1 : ASSERT(il.shift_tab_fn == NULL, "shift_tab_fn NULL before attach");
65 1 : ASSERT(il.render_below == NULL, "render_below NULL before attach");
66 :
67 1 : path_complete_attach(&il);
68 :
69 1 : ASSERT(il.tab_fn != NULL, "tab_fn set after attach");
70 1 : ASSERT(il.shift_tab_fn != NULL, "shift_tab_fn set after attach");
71 1 : ASSERT(il.render_below != NULL, "render_below set after attach");
72 : }
73 :
74 1 : static void test_reset_idempotent(void) {
75 1 : path_complete_reset(); /* no crash on empty state */
76 1 : path_complete_reset(); /* double reset safe */
77 1 : }
78 :
79 1 : static void test_no_match_leaves_buf_unchanged(void) {
80 1 : char td[256]; td_make("no_match", td, sizeof(td));
81 : /* dir is empty; type a prefix that matches nothing */
82 1 : char path[512]; snprintf(path, sizeof(path), "%s/zzz", td);
83 :
84 1 : char buf[512]; InputLine il;
85 1 : il_setup(&il, buf, sizeof(buf), path);
86 1 : il.tab_fn(&il);
87 :
88 1 : ASSERT(strcmp(il.buf, path) == 0, "buf unchanged when no match");
89 1 : ASSERT(il.cur == strlen(path), "cur unchanged when no match");
90 1 : path_complete_reset();
91 : }
92 :
93 1 : static void test_single_match_completes_fully(void) {
94 1 : char td[256]; td_make("single", td, sizeof(td));
95 1 : make_file(td, "report.pdf");
96 :
97 1 : char prefix[512]; snprintf(prefix, sizeof(prefix), "%s/rep", td);
98 1 : char buf[512]; InputLine il;
99 1 : il_setup(&il, buf, sizeof(buf), prefix);
100 1 : il.tab_fn(&il);
101 :
102 1 : char expected[512]; snprintf(expected, sizeof(expected), "%s/report.pdf", td);
103 1 : ASSERT(strcmp(il.buf, expected) == 0, "single match: full completion");
104 1 : ASSERT(il.cur == strlen(expected), "cursor at end after single match");
105 1 : path_complete_reset();
106 : }
107 :
108 1 : static void test_cycles_forward_through_matches(void) {
109 1 : char td[256]; td_make("cycle_fwd", td, sizeof(td));
110 1 : make_file(td, "aaa.txt");
111 1 : make_file(td, "aab.txt");
112 1 : make_file(td, "aac.txt");
113 :
114 1 : char prefix[512]; snprintf(prefix, sizeof(prefix), "%s/aa", td);
115 1 : char buf[512]; InputLine il;
116 1 : il_setup(&il, buf, sizeof(buf), prefix);
117 :
118 1 : char e1[512], e2[512], e3[512];
119 1 : snprintf(e1, sizeof(e1), "%s/aaa.txt", td);
120 1 : snprintf(e2, sizeof(e2), "%s/aab.txt", td);
121 1 : snprintf(e3, sizeof(e3), "%s/aac.txt", td);
122 :
123 1 : il.tab_fn(&il);
124 1 : ASSERT(strcmp(il.buf, e1) == 0, "1st Tab → aaa.txt");
125 :
126 1 : il.tab_fn(&il);
127 1 : ASSERT(strcmp(il.buf, e2) == 0, "2nd Tab → aab.txt");
128 :
129 1 : il.tab_fn(&il);
130 1 : ASSERT(strcmp(il.buf, e3) == 0, "3rd Tab → aac.txt");
131 :
132 1 : il.tab_fn(&il); /* wrap */
133 1 : ASSERT(strcmp(il.buf, e1) == 0, "4th Tab wraps to aaa.txt");
134 1 : path_complete_reset();
135 : }
136 :
137 1 : static void test_cycles_backward_with_shift_tab(void) {
138 1 : char td[256]; td_make("cycle_bwd", td, sizeof(td));
139 1 : make_file(td, "baa.txt");
140 1 : make_file(td, "bab.txt");
141 1 : make_file(td, "bac.txt");
142 :
143 1 : char prefix[512]; snprintf(prefix, sizeof(prefix), "%s/ba", td);
144 1 : char buf[512]; InputLine il;
145 1 : il_setup(&il, buf, sizeof(buf), prefix);
146 :
147 1 : char e1[512], e2[512], e3[512];
148 1 : snprintf(e1, sizeof(e1), "%s/baa.txt", td);
149 1 : snprintf(e2, sizeof(e2), "%s/bab.txt", td);
150 1 : snprintf(e3, sizeof(e3), "%s/bac.txt", td);
151 :
152 1 : il.tab_fn(&il); /* → baa.txt (idx 0) */
153 1 : il.shift_tab_fn(&il); /* backward wrap → bac.txt (idx 2) */
154 1 : ASSERT(strcmp(il.buf, e3) == 0, "Shift+Tab wraps backward to bac.txt");
155 :
156 1 : il.shift_tab_fn(&il); /* → bab.txt */
157 1 : ASSERT(strcmp(il.buf, e2) == 0, "Shift+Tab → bab.txt");
158 :
159 1 : il.shift_tab_fn(&il); /* → baa.txt */
160 1 : ASSERT(strcmp(il.buf, e1) == 0, "Shift+Tab → baa.txt");
161 1 : path_complete_reset();
162 : }
163 :
164 1 : static void test_shift_tab_without_active_list_is_noop(void) {
165 1 : char td[256]; td_make("stab_noop", td, sizeof(td));
166 1 : make_file(td, "x.txt");
167 :
168 1 : char path[512]; snprintf(path, sizeof(path), "%s/x.txt", td);
169 1 : char buf[512]; InputLine il;
170 1 : il_setup(&il, buf, sizeof(buf), path);
171 :
172 : /* No Tab pressed yet; shift_tab_fn should be a no-op */
173 1 : il.shift_tab_fn(&il);
174 1 : ASSERT(strcmp(il.buf, path) == 0, "Shift+Tab no-op without active list");
175 1 : path_complete_reset();
176 : }
177 :
178 1 : static void test_suffix_preserved_when_cursor_in_middle(void) {
179 1 : char td[256]; td_make("suffix", td, sizeof(td));
180 1 : make_file(td, "report.pdf");
181 1 : make_file(td, "readme.txt");
182 :
183 : /* buf = "<td>/filename.pdf", cursor right after "<td>/" */
184 1 : char buf[512];
185 1 : snprintf(buf, sizeof(buf), "%s/filename.pdf", td);
186 1 : InputLine il;
187 1 : input_line_init(&il, buf, sizeof(buf), buf);
188 1 : il.cur = strlen(td) + 1; /* cursor points just after the '/' */
189 1 : path_complete_attach(&il);
190 :
191 1 : il.tab_fn(&il); /* prefix="" → first alphabetical match selected */
192 :
193 : /* The suffix "filename.pdf" must appear at the end of the result */
194 1 : size_t blen = strlen(il.buf);
195 1 : const char *suffix = "filename.pdf";
196 1 : size_t slen = strlen(suffix);
197 1 : ASSERT(blen > slen, "result longer than suffix");
198 1 : ASSERT(strcmp(il.buf + blen - slen, suffix) == 0,
199 : "suffix 'filename.pdf' preserved after completion");
200 :
201 : /* Cursor is at the end of the completed prefix, not at the end of buf */
202 1 : ASSERT(il.cur < il.len, "cursor before end when suffix present");
203 1 : path_complete_reset();
204 : }
205 :
206 1 : static void test_edit_after_cycle_triggers_fresh_scan(void) {
207 1 : char td[256]; td_make("rescan", td, sizeof(td));
208 1 : make_file(td, "cat.txt");
209 1 : make_file(td, "car.txt");
210 :
211 1 : char prefix[512]; snprintf(prefix, sizeof(prefix), "%s/ca", td);
212 1 : char buf[512]; InputLine il;
213 1 : il_setup(&il, buf, sizeof(buf), prefix);
214 :
215 1 : il.tab_fn(&il); /* first match: car.txt (alphabetical) */
216 :
217 : /* Simulate user editing the buffer back to the original prefix */
218 1 : snprintf(il.buf, il.bufsz, "%s/ca", td);
219 1 : il.len = strlen(il.buf);
220 1 : il.cur = il.len;
221 :
222 : /* Next Tab must do a fresh scan, not rely on stale expected */
223 1 : il.tab_fn(&il);
224 1 : char expected[512]; snprintf(expected, sizeof(expected), "%s/car.txt", td);
225 1 : ASSERT(strcmp(il.buf, expected) == 0, "fresh scan after edit → car.txt");
226 1 : path_complete_reset();
227 : }
228 :
229 1 : static void test_typing_slash_enters_subdirectory(void) {
230 1 : char td[256]; td_make("enter_dir", td, sizeof(td));
231 1 : make_subdir(td, "docs");
232 1 : make_file(td, "docs/guide.pdf"); /* nested file */
233 :
234 1 : char prefix[512]; snprintf(prefix, sizeof(prefix), "%s/doc", td);
235 1 : char buf[512]; InputLine il;
236 1 : il_setup(&il, buf, sizeof(buf), prefix);
237 :
238 1 : il.tab_fn(&il); /* completes to "<td>/docs" */
239 1 : char exp_docs[512]; snprintf(exp_docs, sizeof(exp_docs), "%s/docs", td);
240 1 : ASSERT(strcmp(il.buf, exp_docs) == 0, "completes to 'docs'");
241 :
242 : /* User types '/' → buf becomes "<td>/docs/" */
243 1 : size_t len = il.len;
244 1 : il.buf[len] = '/'; il.buf[len+1] = '\0';
245 1 : il.len = len + 1; il.cur = il.len;
246 :
247 : /* Next Tab must scan inside docs/, not cycle old list */
248 1 : il.tab_fn(&il);
249 1 : char expected[512]; snprintf(expected, sizeof(expected), "%s/docs/guide.pdf", td);
250 1 : ASSERT(strcmp(il.buf, expected) == 0, "Tab after '/' enters subdirectory");
251 1 : path_complete_reset();
252 : }
253 :
254 1 : static void test_results_are_sorted_alphabetically(void) {
255 1 : char td[256]; td_make("sorted", td, sizeof(td));
256 1 : make_file(td, "zebra.txt");
257 1 : make_file(td, "alpha.txt");
258 1 : make_file(td, "mango.txt");
259 :
260 1 : char prefix[512]; snprintf(prefix, sizeof(prefix), "%s/", td);
261 1 : char buf[512]; InputLine il;
262 1 : il_setup(&il, buf, sizeof(buf), prefix);
263 :
264 1 : char prev[512] = "";
265 4 : for (int i = 0; i < 3; i++) {
266 3 : il.tab_fn(&il);
267 : /* Extract just the filename part */
268 3 : const char *name = il.buf + strlen(td) + 1;
269 3 : if (i > 0)
270 2 : ASSERT(strcmp(prev, name) < 0, "each match strictly after previous (sorted)");
271 3 : strncpy(prev, name, sizeof(prev) - 1);
272 : }
273 1 : path_complete_reset();
274 : }
275 :
276 1 : static void test_hidden_files_excluded_with_empty_prefix(void) {
277 1 : char td[256]; td_make("hidden", td, sizeof(td));
278 1 : make_file(td, ".hidden");
279 1 : make_file(td, "visible.txt");
280 :
281 1 : char prefix[512]; snprintf(prefix, sizeof(prefix), "%s/", td);
282 1 : char buf[512]; InputLine il;
283 1 : il_setup(&il, buf, sizeof(buf), prefix);
284 :
285 1 : il.tab_fn(&il);
286 1 : ASSERT(strstr(il.buf, ".hidden") == NULL, "hidden file excluded when prefix empty");
287 1 : ASSERT(strstr(il.buf, "visible.txt") != NULL, "visible file included");
288 1 : path_complete_reset();
289 : }
290 :
291 1 : static void test_hidden_files_included_with_dot_prefix(void) {
292 1 : char td[256]; td_make("hidden_dot", td, sizeof(td));
293 1 : make_file(td, ".bashrc");
294 1 : make_file(td, ".profile");
295 :
296 1 : char prefix[512]; snprintf(prefix, sizeof(prefix), "%s/.", td);
297 1 : char buf[512]; InputLine il;
298 1 : il_setup(&il, buf, sizeof(buf), prefix);
299 :
300 1 : il.tab_fn(&il);
301 : /* First alphabetical match starting with '.' should appear */
302 1 : const char *name = il.buf + strlen(td) + 1;
303 1 : ASSERT(name[0] == '.', "hidden file included when prefix starts with '.'");
304 1 : path_complete_reset();
305 : }
306 :
307 : /* ── Entry point ─────────────────────────────────────────────────────── */
308 :
309 1 : void test_path_complete(void) {
310 1 : fixture_setup();
311 :
312 1 : RUN_TEST(test_attach_sets_callbacks);
313 1 : RUN_TEST(test_reset_idempotent);
314 1 : RUN_TEST(test_no_match_leaves_buf_unchanged);
315 1 : RUN_TEST(test_single_match_completes_fully);
316 1 : RUN_TEST(test_cycles_forward_through_matches);
317 1 : RUN_TEST(test_cycles_backward_with_shift_tab);
318 1 : RUN_TEST(test_shift_tab_without_active_list_is_noop);
319 1 : RUN_TEST(test_suffix_preserved_when_cursor_in_middle);
320 1 : RUN_TEST(test_edit_after_cycle_triggers_fresh_scan);
321 1 : RUN_TEST(test_typing_slash_enters_subdirectory);
322 1 : RUN_TEST(test_results_are_sorted_alphabetically);
323 1 : RUN_TEST(test_hidden_files_excluded_with_empty_prefix);
324 1 : RUN_TEST(test_hidden_files_included_with_dot_prefix);
325 :
326 1 : fixture_teardown();
327 1 : }
|