Line data Source code
1 : #include "test_helpers.h"
2 : #include "input_line.h"
3 : #include <string.h>
4 : #include <stddef.h>
5 : #include <unistd.h>
6 : #include <fcntl.h>
7 :
8 : /* ── stdin/stdout redirection helpers ──────────────────────────────────── */
9 :
10 : /**
11 : * Redirect STDIN_FILENO from a pipe containing the given bytes.
12 : * Saves the old stdin fd into *saved_stdin.
13 : * Returns the read-end fd (already dup2'd to STDIN_FILENO; close it after
14 : * restoring).
15 : */
16 27 : static int stdin_push(const char *bytes, size_t len, int *saved_stdin) {
17 : int pipefd[2];
18 27 : if (pipe(pipefd) != 0) return -1;
19 : /* Write all bytes then close write-end so read side sees EOF */
20 27 : if (write(pipefd[1], bytes, len) != (ssize_t)len) {
21 0 : close(pipefd[0]); close(pipefd[1]); return -1;
22 : }
23 27 : close(pipefd[1]);
24 27 : *saved_stdin = dup(STDIN_FILENO);
25 27 : dup2(pipefd[0], STDIN_FILENO);
26 27 : close(pipefd[0]);
27 27 : return 0;
28 : }
29 :
30 : /** Restore STDIN_FILENO from saved_stdin fd. */
31 27 : static void stdin_pop(int saved_stdin) {
32 27 : dup2(saved_stdin, STDIN_FILENO);
33 27 : close(saved_stdin);
34 27 : }
35 :
36 : /**
37 : * Suppress stdout and stderr for the duration of input_line_run
38 : * (which emits ANSI escape codes we don't want in test output).
39 : * On return, *saved_out and *saved_err hold the real fds.
40 : */
41 27 : static void output_suppress(int *saved_out, int *saved_err) {
42 27 : fflush(stdout); fflush(stderr);
43 27 : int null_fd = open("/dev/null", O_WRONLY);
44 27 : *saved_out = dup(STDOUT_FILENO);
45 27 : *saved_err = dup(STDERR_FILENO);
46 27 : if (null_fd >= 0) {
47 27 : dup2(null_fd, STDOUT_FILENO);
48 27 : dup2(null_fd, STDERR_FILENO);
49 27 : close(null_fd);
50 : }
51 27 : }
52 :
53 27 : static void output_restore(int saved_out, int saved_err) {
54 27 : fflush(stdout); fflush(stderr);
55 27 : dup2(saved_out, STDOUT_FILENO); close(saved_out);
56 27 : dup2(saved_err, STDERR_FILENO); close(saved_err);
57 27 : }
58 :
59 : /**
60 : * Run input_line_run with injected keypress bytes.
61 : * Suppresses TUI output. Returns the result of input_line_run.
62 : */
63 27 : static int run_with_keys(InputLine *il, int trow, const char *prompt,
64 : const char *keys, size_t keylen) {
65 : int saved_stdin;
66 27 : if (stdin_push(keys, keylen, &saved_stdin) != 0) return -99;
67 :
68 : int saved_out, saved_err;
69 27 : output_suppress(&saved_out, &saved_err);
70 :
71 27 : int result = input_line_run(il, trow, prompt);
72 :
73 27 : output_restore(saved_out, saved_err);
74 27 : stdin_pop(saved_stdin);
75 27 : return result;
76 : }
77 :
78 1 : void test_input_line(void) {
79 : char buf[64];
80 :
81 : /* 1. Init with NULL initial_text → empty buffer, cursor at 0 */
82 : InputLine il;
83 1 : input_line_init(&il, buf, sizeof(buf), NULL);
84 1 : ASSERT(il.buf == buf, "buf pointer set");
85 1 : ASSERT(il.bufsz == sizeof(buf), "bufsz set");
86 1 : ASSERT(il.len == 0, "len 0 for NULL initial");
87 1 : ASSERT(il.cur == 0, "cur 0 for NULL initial");
88 1 : ASSERT(buf[0] == '\0', "buf NUL-terminated");
89 :
90 : /* 2. Init with empty string → same as NULL */
91 1 : input_line_init(&il, buf, sizeof(buf), "");
92 1 : ASSERT(il.len == 0, "len 0 for empty initial");
93 1 : ASSERT(il.cur == 0, "cur 0 for empty initial");
94 :
95 : /* 3. Init with text → len and cur at end */
96 1 : input_line_init(&il, buf, sizeof(buf), "hello");
97 1 : ASSERT(il.len == 5, "len 5 for 'hello'");
98 1 : ASSERT(il.cur == 5, "cur at end after init");
99 1 : ASSERT(strcmp(buf, "hello") == 0, "buf contains 'hello'");
100 :
101 : /* 4. Init truncates when text >= bufsz */
102 : char small[4];
103 1 : input_line_init(&il, small, sizeof(small), "toolong");
104 1 : ASSERT(il.len == 3, "len truncated to bufsz-1");
105 1 : ASSERT(small[3] == '\0', "buf NUL-terminated after truncation");
106 :
107 : /* 5. All callbacks NULL after init */
108 1 : input_line_init(&il, buf, sizeof(buf), "x");
109 1 : ASSERT(il.tab_fn == NULL, "tab_fn NULL after init");
110 1 : ASSERT(il.shift_tab_fn == NULL, "shift_tab_fn NULL after init");
111 1 : ASSERT(il.render_below == NULL, "render_below NULL after init");
112 :
113 : /* 6. trow is 0 after init */
114 1 : ASSERT(il.trow == 0, "trow 0 after init");
115 :
116 : /* 7. UTF-8 multi-byte initial text */
117 1 : input_line_init(&il, buf, sizeof(buf), "héllo");
118 : /* 'é' is 2 bytes (0xC3 0xA9) → len = 6, cur = 6 */
119 1 : ASSERT(il.len == 6, "len accounts for UTF-8 bytes");
120 1 : ASSERT(il.cur == 6, "cur at byte end for UTF-8 text");
121 1 : ASSERT(strcmp(buf, "héllo") == 0, "UTF-8 content preserved");
122 :
123 : /* ── input_line_run tests ──────────────────────────────────────────── */
124 :
125 : /* 8. Enter on pre-filled buffer → returns 1, buffer unchanged */
126 : {
127 : char rbuf[64];
128 : InputLine ril;
129 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "hello");
130 1 : int res = run_with_keys(&ril, 5, "> ", "\r", 1);
131 1 : ASSERT(res == 1, "input_line_run: Enter returns 1");
132 1 : ASSERT(strcmp(rbuf, "hello") == 0, "input_line_run: buffer unchanged after Enter");
133 : }
134 :
135 : /* 9. ESC on pre-filled buffer → returns 0, buffer unchanged */
136 : {
137 : char rbuf[64];
138 : InputLine ril;
139 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "hello");
140 : /* Bare ESC: send ESC then a non-'[' byte so terminal_read_key treats it
141 : * as TERM_KEY_ESC (the fallback in the else branch). We use a second \r
142 : * which won't be read because input_line_run returns on ESC. */
143 1 : int res = run_with_keys(&ril, 5, "> ", "\033x", 2);
144 1 : ASSERT(res == 0, "input_line_run: ESC returns 0");
145 1 : ASSERT(strcmp(rbuf, "hello") == 0, "input_line_run: buffer unchanged after ESC");
146 : }
147 :
148 : /* 10. Ctrl-C → returns 0 */
149 : {
150 : char rbuf[64];
151 : InputLine ril;
152 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "test");
153 1 : int res = run_with_keys(&ril, 5, "> ", "\x03", 1);
154 1 : ASSERT(res == 0, "input_line_run: Ctrl-C returns 0");
155 : }
156 :
157 : /* 11. Enter on empty buffer → returns 1 with empty buffer */
158 : {
159 : char rbuf[64];
160 : InputLine ril;
161 1 : input_line_init(&ril, rbuf, sizeof(rbuf), NULL);
162 1 : int res = run_with_keys(&ril, 5, "Prompt: ", "\r", 1);
163 1 : ASSERT(res == 1, "input_line_run: Enter on empty buffer returns 1");
164 1 : ASSERT(rbuf[0] == '\0', "input_line_run: empty buffer stays empty");
165 : }
166 :
167 : /* 12. Type characters then Enter → buffer contains typed text */
168 : {
169 : char rbuf[64];
170 : InputLine ril;
171 1 : input_line_init(&ril, rbuf, sizeof(rbuf), NULL);
172 : /* inject: 'h','i','\r' */
173 1 : int res = run_with_keys(&ril, 5, "> ", "hi\r", 3);
174 1 : ASSERT(res == 1, "input_line_run: typing then Enter returns 1");
175 1 : ASSERT(strcmp(rbuf, "hi") == 0, "input_line_run: typed chars in buffer");
176 : }
177 :
178 : /* 13. Backspace deletes last character */
179 : {
180 : char rbuf[64];
181 : InputLine ril;
182 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "ab");
183 : /* DEL byte (backspace) then Enter */
184 1 : int res = run_with_keys(&ril, 5, "> ", "\x7f\r", 2);
185 1 : ASSERT(res == 1, "input_line_run: backspace+Enter returns 1");
186 1 : ASSERT(strcmp(rbuf, "a") == 0, "input_line_run: backspace deletes last char");
187 : }
188 :
189 : /* 14. Backspace at start of buffer → no change */
190 : {
191 : char rbuf[64];
192 : InputLine ril;
193 1 : input_line_init(&ril, rbuf, sizeof(rbuf), NULL);
194 : /* backspace on empty → no-op, then Enter */
195 1 : int res = run_with_keys(&ril, 5, "> ", "\x7f\r", 2);
196 1 : ASSERT(res == 1, "input_line_run: backspace on empty returns 1");
197 1 : ASSERT(rbuf[0] == '\0', "input_line_run: backspace on empty is no-op");
198 : }
199 :
200 : /* 15. Left arrow then Backspace → deletes char before new cursor position */
201 : {
202 : char rbuf[64];
203 : InputLine ril;
204 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "ab");
205 : /* Left arrow: ESC [ D (3 bytes), then backspace, then Enter */
206 1 : int res = run_with_keys(&ril, 5, "> ", "\033[D\x7f\r", 6);
207 1 : ASSERT(res == 1, "input_line_run: left+backspace returns 1");
208 1 : ASSERT(strcmp(rbuf, "b") == 0, "input_line_run: left+backspace deletes 'a'");
209 : }
210 :
211 : /* 16. Right arrow at end → no change, then Enter */
212 : {
213 : char rbuf[64];
214 : InputLine ril;
215 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "hi");
216 : /* Right arrow at end is a no-op */
217 1 : int res = run_with_keys(&ril, 5, "> ", "\033[C\r", 4);
218 1 : ASSERT(res == 1, "input_line_run: right at end+Enter returns 1");
219 1 : ASSERT(strcmp(rbuf, "hi") == 0, "input_line_run: right at end is no-op");
220 : }
221 :
222 : /* 17. Home key moves cursor to start, then type char prepends it */
223 : {
224 : char rbuf[64];
225 : InputLine ril;
226 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "bc");
227 : /* Home: ESC [ H, then type 'a', then Enter */
228 1 : int res = run_with_keys(&ril, 5, "> ", "\033[Ha\r", 5);
229 1 : ASSERT(res == 1, "input_line_run: Home+insert+Enter returns 1");
230 1 : ASSERT(strcmp(rbuf, "abc") == 0, "input_line_run: Home+insert prepends char");
231 : }
232 :
233 : /* 18. End key moves cursor to end, then type appends */
234 : {
235 : char rbuf[64];
236 : InputLine ril;
237 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "hi");
238 : /* Move to start first, then End, then type '!' */
239 : /* Home: ESC[H, End: ESC[F, type '!', Enter */
240 1 : int res = run_with_keys(&ril, 5, "> ", "\033[H\033[F!\r", 8);
241 1 : ASSERT(res == 1, "input_line_run: Home+End+insert+Enter returns 1");
242 1 : ASSERT(strcmp(rbuf, "hi!") == 0, "input_line_run: End+insert appends");
243 : }
244 :
245 : /* 19. Delete key (ESC[3~) removes char at cursor */
246 : {
247 : char rbuf[64];
248 : InputLine ril;
249 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "abc");
250 : /* Home, then Delete: ESC[H ESC[3~, Enter */
251 1 : int res = run_with_keys(&ril, 5, "> ", "\033[H\033[3~\r", 8);
252 1 : ASSERT(res == 1, "input_line_run: Home+Delete+Enter returns 1");
253 1 : ASSERT(strcmp(rbuf, "bc") == 0, "input_line_run: Delete removes char at cursor");
254 : }
255 :
256 : /* 20. Delete at end (no-op) then Enter */
257 : {
258 : char rbuf[64];
259 : InputLine ril;
260 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "hi");
261 : /* Delete at end = no-op */
262 1 : int res = run_with_keys(&ril, 5, "> ", "\033[3~\r", 5);
263 1 : ASSERT(res == 1, "input_line_run: Delete at end+Enter returns 1");
264 1 : ASSERT(strcmp(rbuf, "hi") == 0, "input_line_run: Delete at end is no-op");
265 : }
266 :
267 : /* 21. Buffer full: inserting beyond bufsz is silently ignored */
268 : {
269 : char rbuf[4]; /* holds 3 chars + NUL */
270 : InputLine ril;
271 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "abc");
272 : /* Try to insert 'd' when buffer is full, then Enter */
273 1 : int res = run_with_keys(&ril, 5, "> ", "d\r", 2);
274 1 : ASSERT(res == 1, "input_line_run: insert into full buffer returns 1");
275 1 : ASSERT(strcmp(rbuf, "abc") == 0, "input_line_run: insert into full buffer is no-op");
276 : }
277 :
278 : /* 22. Pre-filled 2-byte UTF-8 'é' in buffer, press Enter
279 : * → il_render calls display_cols on the buffer → exercises 2-byte path
280 : * (input_line.c:38-39) and cp_len 2-byte return (line 16) */
281 : {
282 : char rbuf[64];
283 : InputLine ril;
284 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "\xC3\xA9");
285 1 : int res = run_with_keys(&ril, 5, "> ", "\r", 1);
286 1 : ASSERT(res == 1, "input_line_run: UTF-8 2-byte pre-filled + Enter returns 1");
287 1 : ASSERT((unsigned char)rbuf[0] == 0xC3,
288 : "input_line_run: 2-byte UTF-8 pre-filled preserved");
289 : }
290 :
291 : /* 23. Pre-filled 3-byte UTF-8 '中' in buffer, press Enter
292 : * → exercises display_cols 3-byte path (input_line.c:40-43) */
293 : {
294 : char rbuf[64];
295 : InputLine ril;
296 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "\xE4\xB8\xAD");
297 1 : int res = run_with_keys(&ril, 5, "> ", "\r", 1);
298 1 : ASSERT(res == 1, "input_line_run: UTF-8 3-byte pre-filled + Enter returns 1");
299 1 : ASSERT((unsigned char)rbuf[0] == 0xE4,
300 : "input_line_run: 3-byte UTF-8 pre-filled preserved");
301 : }
302 :
303 : /* 24. Pre-filled 4-byte UTF-8 😀 in buffer, press Enter
304 : * → exercises display_cols 4-byte path (input_line.c:44-48) */
305 : {
306 : char rbuf[64];
307 : InputLine ril;
308 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "\xF0\x9F\x98\x80");
309 1 : int res = run_with_keys(&ril, 5, "> ", "\r", 1);
310 1 : ASSERT(res == 1, "input_line_run: UTF-8 4-byte pre-filled + Enter returns 1");
311 1 : ASSERT((unsigned char)rbuf[0] == 0xF0,
312 : "input_line_run: 4-byte UTF-8 pre-filled preserved");
313 : }
314 :
315 : /* 25. Left over 2-byte UTF-8 → exercises cp_len 2-byte (input_line.c:16)
316 : * and il_backspace over multi-byte sequence */
317 : {
318 : char rbuf[64];
319 : InputLine ril;
320 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "\xC3\xA9");
321 : /* Left arrow moves cursor back over é (2 bytes), then Enter */
322 1 : int res = run_with_keys(&ril, 5, "> ", "\033[D\r", 4);
323 1 : ASSERT(res == 1, "input_line_run: left over 2-byte returns 1");
324 : }
325 :
326 : /* 26. Delete on 3-byte UTF-8 → exercises il_delete_fwd cp_len 3-byte
327 : * (input_line.c:17,87) */
328 : {
329 : char rbuf[64];
330 : InputLine ril;
331 : /* "中x": Home, Delete removes '中' (3 bytes), leaving "x" */
332 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "\xE4\xB8\xAD" "x");
333 1 : int res = run_with_keys(&ril, 5, "> ",
334 : "\033[H" "\033[3~" "\r", 8);
335 1 : ASSERT(res == 1, "input_line_run: delete 3-byte UTF-8 returns 1");
336 1 : ASSERT(rbuf[0] == 'x' && rbuf[1] == '\0',
337 : "input_line_run: 3-byte UTF-8 deleted");
338 : }
339 :
340 : /* 27. Shift-Tab (ESC[Z) with no shift_tab_fn → no-op, then Enter
341 : * → exercises terminal.c case 'Z' (TERM_KEY_SHIFT_TAB) */
342 : {
343 : char rbuf[64];
344 : InputLine ril;
345 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "hi");
346 : /* ESC [ Z = Shift-Tab, then Enter */
347 1 : int res = run_with_keys(&ril, 5, "> ", "\033[Z\r", 4);
348 1 : ASSERT(res == 1, "input_line_run: Shift-Tab+Enter returns 1");
349 1 : ASSERT(strcmp(rbuf, "hi") == 0, "input_line_run: Shift-Tab is no-op");
350 : }
351 :
352 : /* 28. ESC[1~ → TERM_KEY_HOME (nx == '~') → cursor to start
353 : * → exercises terminal.c case '1' / nx=='~' branch */
354 : {
355 : char rbuf[64];
356 : InputLine ril;
357 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "bc");
358 : /* ESC[1~ moves to start, type 'a', Enter → "abc" */
359 1 : int res = run_with_keys(&ril, 5, "> ", "\033[1~a\r", 6);
360 1 : ASSERT(res == 1, "input_line_run: ESC[1~+insert+Enter returns 1");
361 1 : ASSERT(strcmp(rbuf, "abc") == 0, "input_line_run: ESC[1~ moves to start");
362 : }
363 :
364 : /* 29. ESC[1x → TERM_KEY_IGNORE (nx != '~')
365 : * → exercises terminal.c case '1' / nx!='~' branch */
366 : {
367 : char rbuf[64];
368 : InputLine ril;
369 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "hi");
370 : /* ESC[1x: c3='1', nx='x' → IGNORE, then Enter */
371 1 : int res = run_with_keys(&ril, 5, "> ", "\033[1x\r", 5);
372 1 : ASSERT(res == 1, "input_line_run: ESC[1x (IGNORE)+Enter returns 1");
373 1 : ASSERT(strcmp(rbuf, "hi") == 0, "input_line_run: ESC[1x is no-op");
374 : }
375 :
376 : /* 30. ESC[4~ → TERM_KEY_END → cursor to end
377 : * → exercises terminal.c case '4' */
378 : {
379 : char rbuf[64];
380 : InputLine ril;
381 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "hi");
382 : /* Home, then ESC[4~ (End), type '!', Enter → "hi!" */
383 1 : int res = run_with_keys(&ril, 5, "> ", "\033[H\033[4~!\r", 9);
384 1 : ASSERT(res == 1, "input_line_run: ESC[4~+insert+Enter returns 1");
385 1 : ASSERT(strcmp(rbuf, "hi!") == 0, "input_line_run: ESC[4~ moves to end");
386 : }
387 :
388 : /* 31. ESC[7~ → TERM_KEY_HOME → cursor to start
389 : * → exercises terminal.c case '7' */
390 : {
391 : char rbuf[64];
392 : InputLine ril;
393 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "bc");
394 : /* ESC[7~ moves to start, type 'a', Enter → "abc" */
395 1 : int res = run_with_keys(&ril, 5, "> ", "\033[7~a\r", 6);
396 1 : ASSERT(res == 1, "input_line_run: ESC[7~+insert+Enter returns 1");
397 1 : ASSERT(strcmp(rbuf, "abc") == 0, "input_line_run: ESC[7~ moves to start");
398 : }
399 :
400 : /* 32. ESC[8~ → TERM_KEY_END → cursor to end
401 : * → exercises terminal.c case '8' */
402 : {
403 : char rbuf[64];
404 : InputLine ril;
405 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "hi");
406 : /* Home, then ESC[8~ (End), type '!', Enter → "hi!" */
407 1 : int res = run_with_keys(&ril, 5, "> ", "\033[H\033[8~!\r", 9);
408 1 : ASSERT(res == 1, "input_line_run: ESC[8~+insert+Enter returns 1");
409 1 : ASSERT(strcmp(rbuf, "hi!") == 0, "input_line_run: ESC[8~ moves to end");
410 : }
411 :
412 : /* 33. Unknown ESC sequence → default drain → TERM_KEY_IGNORE → no-op
413 : * → exercises terminal.c default case drain loop */
414 : {
415 : char rbuf[64];
416 : InputLine ril;
417 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "hi");
418 : /* ESC[9x: c3='9' → default, drains until 'x' (letter), then Enter */
419 1 : int res = run_with_keys(&ril, 5, "> ", "\033[9x\r", 5);
420 1 : ASSERT(res == 1, "input_line_run: unknown ESC seq+Enter returns 1");
421 1 : ASSERT(strcmp(rbuf, "hi") == 0, "input_line_run: unknown ESC seq is no-op");
422 : }
423 :
424 : /* 34. TAB key → TERM_KEY_TAB → no-op in input_line
425 : * → exercises terminal.c line 146: result = TERM_KEY_TAB */
426 : {
427 : char rbuf[64];
428 : InputLine ril;
429 1 : input_line_init(&ril, rbuf, sizeof(rbuf), "hi");
430 : /* TAB then Enter: TAB is TERM_KEY_TAB which input_line ignores */
431 1 : int res = run_with_keys(&ril, 5, "> ", "\t\r", 2);
432 1 : ASSERT(res == 1, "input_line_run: TAB+Enter returns 1");
433 1 : ASSERT(strcmp(rbuf, "hi") == 0, "input_line_run: TAB is no-op");
434 : }
435 : }
|