Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file test_readline.c
6 : * @brief PTY-03 — PTY tests for the custom readline cursor/editing keys.
7 : *
8 : * Verifies that rl_readline correctly handles:
9 : * - Left/Right arrow cursor movement and mid-line character insertion.
10 : * - Home / End jumps.
11 : * - Backspace deletion.
12 : * - Ctrl-K (kill to end of line).
13 : * - Ctrl-W (kill previous word).
14 : * - Up / Down history navigation.
15 : * - Ctrl-D on an empty line causes clean exit.
16 : *
17 : * Approach:
18 : * The `rl_harness` binary runs rl_readline in a loop and prints
19 : * "ACCEPTED:<line>" after each Enter. The test drives the harness through a
20 : * PTY, sending keystroke sequences and asserting the expected ACCEPTED output
21 : * appears.
22 : *
23 : * Each test uses "goto cleanup" for ASAN-safe resource release on all exit
24 : * paths — ASSERT_WAIT_FOR would otherwise return early and leak the session.
25 : */
26 :
27 : #include "ptytest.h"
28 : #include "pty_assert.h"
29 :
30 : #include <stdio.h>
31 : #include <string.h>
32 : #include <unistd.h>
33 :
34 : /* ── Globals required by ASSERT / ASSERT_WAIT_FOR macros ──────────────── */
35 :
36 : static int g_tests_run = 0;
37 : static int g_tests_failed = 0;
38 :
39 : /* ── Helpers ──────────────────────────────────────────────────────────── */
40 :
41 : #define RUN_TEST(fn) do { \
42 : printf(" running %s ...\n", #fn); \
43 : fn(); \
44 : } while(0)
45 :
46 : /** Injected at build time by CMake. */
47 : #ifndef RL_HARNESS_BINARY
48 : #define RL_HARNESS_BINARY "rl_harness"
49 : #endif
50 :
51 : /**
52 : * @brief CHECK_WAIT: like ASSERT_WAIT_FOR but jumps to a label on failure.
53 : *
54 : * This avoids early-return leaks when the test has an open PtySession.
55 : */
56 : #define CHECK_WAIT_FOR(s, text, timeout_ms, label) do { \
57 : g_tests_run++; \
58 : if (pty_wait_for((s), (text), (timeout_ms)) != 0) { \
59 : printf(" [FAIL] %s:%d: wait_for(\"%s\", %d ms) timed out\n", \
60 : __FILE__, __LINE__, (text), (timeout_ms)); \
61 : g_tests_failed++; \
62 : goto label; \
63 : } \
64 : } while(0)
65 :
66 : /** Like ASSERT but jumps to a label on failure (no return). */
67 : #define CHECK(cond, msg, label) do { \
68 : g_tests_run++; \
69 : if (!(cond)) { \
70 : printf(" [FAIL] %s:%d: %s\n", __FILE__, __LINE__, (msg)); \
71 : g_tests_failed++; \
72 : goto label; \
73 : } \
74 : } while(0)
75 :
76 : /** Open a 80×24 PTY and start rl_harness in it. Returns session or NULL. */
77 54 : static PtySession *open_harness(void) {
78 54 : PtySession *s = pty_open(80, 24);
79 54 : if (!s) return NULL;
80 54 : const char *argv[] = { RL_HARNESS_BINARY, NULL };
81 54 : if (pty_run(s, argv) != 0) {
82 0 : pty_close(s);
83 0 : return NULL;
84 : }
85 : /* Wait for the initial prompt (search without trailing space — pty_row_text
86 : * trims trailing spaces, so "rl>" matches but "rl> " would not). */
87 45 : if (pty_wait_for(s, "rl>", 3000) != 0) {
88 0 : pty_close(s);
89 0 : return NULL;
90 : }
91 45 : return s;
92 : }
93 :
94 : /* ── Tests ────────────────────────────────────────────────────────────── */
95 :
96 : /**
97 : * @brief PTY-03-a: prompt "rl> " is visible on launch.
98 : */
99 11 : static void test_prompt_visible(void) {
100 11 : PtySession *s = pty_open(80, 24);
101 11 : CHECK(s != NULL, "pty_open should succeed", done_no_session);
102 :
103 11 : const char *argv[] = { RL_HARNESS_BINARY, NULL };
104 11 : CHECK(pty_run(s, argv) == 0, "pty_run(rl_harness) should succeed", done);
105 :
106 10 : CHECK_WAIT_FOR(s, "rl>", 3000, done);
107 :
108 10 : pty_send_key(s, PTY_KEY_CTRL_D);
109 10 : pty_wait_exit(s, 3000);
110 10 : done:
111 10 : pty_close(s);
112 10 : done_no_session:
113 10 : return;
114 : }
115 :
116 : /**
117 : * @brief PTY-03-b: plain text is accepted and echoed as ACCEPTED:<text>.
118 : */
119 10 : static void test_plain_text_accepted(void) {
120 10 : PtySession *s = open_harness();
121 9 : CHECK(s != NULL, "open_harness should succeed", done_no_session);
122 :
123 9 : pty_send_str(s, "hello");
124 9 : pty_send_key(s, PTY_KEY_ENTER);
125 :
126 9 : CHECK_WAIT_FOR(s, "ACCEPTED:hello", 3000, done);
127 :
128 9 : pty_send_key(s, PTY_KEY_CTRL_D);
129 9 : pty_wait_exit(s, 3000);
130 9 : done:
131 9 : pty_close(s);
132 9 : done_no_session:
133 9 : return;
134 : }
135 :
136 : /**
137 : * @brief PTY-03-c: Left arrow + insert places character mid-line.
138 : *
139 : * Type "helo", move left once (cursor before 'o'), type 'l' → "hello".
140 : */
141 9 : static void test_left_arrow_insert(void) {
142 9 : PtySession *s = open_harness();
143 8 : CHECK(s != NULL, "open_harness should succeed", done_no_session);
144 :
145 8 : pty_send_str(s, "helo");
146 8 : pty_send_key(s, PTY_KEY_LEFT); /* cursor before 'o' */
147 8 : pty_send_str(s, "l"); /* insert → "hello" */
148 8 : pty_send_key(s, PTY_KEY_ENTER);
149 :
150 8 : CHECK_WAIT_FOR(s, "ACCEPTED:hello", 3000, done);
151 :
152 8 : pty_send_key(s, PTY_KEY_CTRL_D);
153 8 : pty_wait_exit(s, 3000);
154 8 : done:
155 8 : pty_close(s);
156 8 : done_no_session:
157 8 : return;
158 : }
159 :
160 : /**
161 : * @brief PTY-03-d: Home then End move cursor; typing after Home inserts at start.
162 : *
163 : * Type "world", Home, type "hello " → "hello world".
164 : */
165 8 : static void test_home_end_insert(void) {
166 8 : PtySession *s = open_harness();
167 7 : CHECK(s != NULL, "open_harness should succeed", done_no_session);
168 :
169 7 : pty_send_str(s, "world");
170 7 : pty_send_key(s, PTY_KEY_HOME); /* cursor at position 0 */
171 7 : pty_send_str(s, "hello "); /* insert at start → "hello world" */
172 7 : pty_send_key(s, PTY_KEY_END); /* move to end — no-op here but exercises END */
173 7 : pty_send_key(s, PTY_KEY_ENTER);
174 :
175 7 : CHECK_WAIT_FOR(s, "ACCEPTED:hello world", 3000, done);
176 :
177 7 : pty_send_key(s, PTY_KEY_CTRL_D);
178 7 : pty_wait_exit(s, 3000);
179 7 : done:
180 7 : pty_close(s);
181 7 : done_no_session:
182 7 : return;
183 : }
184 :
185 : /**
186 : * @brief PTY-03-e: Backspace deletes the character before the cursor.
187 : *
188 : * Type "hellox", Backspace → "hello".
189 : */
190 7 : static void test_backspace(void) {
191 7 : PtySession *s = open_harness();
192 6 : CHECK(s != NULL, "open_harness should succeed", done_no_session);
193 :
194 6 : pty_send_str(s, "hellox");
195 6 : pty_send_key(s, PTY_KEY_BACK); /* delete 'x' */
196 6 : pty_send_key(s, PTY_KEY_ENTER);
197 :
198 6 : CHECK_WAIT_FOR(s, "ACCEPTED:hello", 3000, done);
199 :
200 6 : pty_send_key(s, PTY_KEY_CTRL_D);
201 6 : pty_wait_exit(s, 3000);
202 6 : done:
203 6 : pty_close(s);
204 6 : done_no_session:
205 6 : return;
206 : }
207 :
208 : /**
209 : * @brief PTY-03-f: Ctrl-K kills from cursor position to end of line.
210 : *
211 : * Type "helloXXX", Left×3, Ctrl-K → "hello".
212 : */
213 6 : static void test_ctrl_k_kill_to_end(void) {
214 6 : PtySession *s = open_harness();
215 5 : CHECK(s != NULL, "open_harness should succeed", done_no_session);
216 :
217 5 : pty_send_str(s, "helloXXX");
218 : /* Move cursor before the first 'X' */
219 5 : pty_send_key(s, PTY_KEY_LEFT);
220 5 : pty_send_key(s, PTY_KEY_LEFT);
221 5 : pty_send_key(s, PTY_KEY_LEFT);
222 : /* Kill to end (Ctrl-K = 0x0B) */
223 5 : pty_send(s, "\x0B", 1);
224 5 : pty_send_key(s, PTY_KEY_ENTER);
225 :
226 5 : CHECK_WAIT_FOR(s, "ACCEPTED:hello", 3000, done);
227 :
228 5 : pty_send_key(s, PTY_KEY_CTRL_D);
229 5 : pty_wait_exit(s, 3000);
230 5 : done:
231 5 : pty_close(s);
232 5 : done_no_session:
233 5 : return;
234 : }
235 :
236 : /**
237 : * @brief PTY-03-g: Ctrl-W kills the previous word.
238 : *
239 : * Type "foo bar", Ctrl-W → "foo ".
240 : */
241 5 : static void test_ctrl_w_kill_word(void) {
242 5 : PtySession *s = open_harness();
243 4 : CHECK(s != NULL, "open_harness should succeed", done_no_session);
244 :
245 4 : pty_send_str(s, "foo bar");
246 4 : pty_send(s, "\x17", 1); /* Ctrl-W = 0x17 */
247 4 : pty_send_key(s, PTY_KEY_ENTER);
248 :
249 : /* Search for "ACCEPTED:foo" (without trailing space) — pty_row_text trims
250 : * trailing spaces so "ACCEPTED:foo " would not be found. */
251 4 : CHECK_WAIT_FOR(s, "ACCEPTED:foo", 3000, done);
252 :
253 4 : pty_send_key(s, PTY_KEY_CTRL_D);
254 4 : pty_wait_exit(s, 3000);
255 4 : done:
256 4 : pty_close(s);
257 4 : done_no_session:
258 4 : return;
259 : }
260 :
261 : /**
262 : * @brief PTY-03-h: Up arrow recalls the most recent history entry.
263 : *
264 : * Submit "first", then on the next prompt press Up, then Enter →
265 : * "ACCEPTED:first" appears a second time.
266 : */
267 4 : static void test_up_arrow_history(void) {
268 4 : PtySession *s = open_harness();
269 3 : CHECK(s != NULL, "open_harness should succeed", done_no_session);
270 :
271 : /* Submit first line. */
272 3 : pty_send_str(s, "first");
273 3 : pty_send_key(s, PTY_KEY_ENTER);
274 3 : CHECK_WAIT_FOR(s, "ACCEPTED:first", 3000, done);
275 :
276 : /* Wait for the next prompt before pressing Up. */
277 3 : CHECK_WAIT_FOR(s, "rl>", 3000, done);
278 :
279 : /* Recall history with Up, then submit. */
280 3 : pty_send_key(s, PTY_KEY_UP);
281 3 : pty_settle(s, 100);
282 3 : pty_send_key(s, PTY_KEY_ENTER);
283 :
284 : /* The harness should print ACCEPTED:first a second time. */
285 3 : CHECK_WAIT_FOR(s, "ACCEPTED:first", 3000, done);
286 :
287 3 : pty_send_key(s, PTY_KEY_CTRL_D);
288 3 : pty_wait_exit(s, 3000);
289 3 : done:
290 3 : pty_close(s);
291 3 : done_no_session:
292 3 : return;
293 : }
294 :
295 : /**
296 : * @brief PTY-03-i: Down arrow after Up restores the current (empty) edit line.
297 : *
298 : * Submit "alpha", then on the next prompt press Up then Down, then "beta",
299 : * Enter → ACCEPTED:beta (not ACCEPTED:alpha).
300 : */
301 3 : static void test_down_arrow_restores_edit(void) {
302 3 : PtySession *s = open_harness();
303 2 : CHECK(s != NULL, "open_harness should succeed", done_no_session);
304 :
305 2 : pty_send_str(s, "alpha");
306 2 : pty_send_key(s, PTY_KEY_ENTER);
307 2 : CHECK_WAIT_FOR(s, "ACCEPTED:alpha", 3000, done);
308 :
309 2 : CHECK_WAIT_FOR(s, "rl>", 3000, done);
310 :
311 : /* Up (recall "alpha"), then Down (restore empty line). */
312 2 : pty_send_key(s, PTY_KEY_UP);
313 2 : pty_settle(s, 100);
314 2 : pty_send_key(s, PTY_KEY_DOWN);
315 2 : pty_settle(s, 100);
316 :
317 : /* Type "beta" on the restored blank line. */
318 2 : pty_send_str(s, "beta");
319 2 : pty_send_key(s, PTY_KEY_ENTER);
320 :
321 2 : CHECK_WAIT_FOR(s, "ACCEPTED:beta", 3000, done);
322 :
323 2 : pty_send_key(s, PTY_KEY_CTRL_D);
324 2 : pty_wait_exit(s, 3000);
325 2 : done:
326 2 : pty_close(s);
327 2 : done_no_session:
328 2 : return;
329 : }
330 :
331 : /**
332 : * @brief PTY-03-j: Ctrl-D on an empty line exits the harness with code 0.
333 : */
334 2 : static void test_ctrl_d_exits(void) {
335 2 : PtySession *s = open_harness();
336 1 : CHECK(s != NULL, "open_harness should succeed", done_no_session);
337 :
338 1 : pty_send_key(s, PTY_KEY_CTRL_D);
339 :
340 1 : int exit_code = pty_wait_exit(s, 3000);
341 1 : g_tests_run++;
342 1 : if (exit_code != 0) {
343 0 : printf(" [FAIL] %s:%d: Ctrl-D on empty line should cause exit 0, got %d\n",
344 : __FILE__, __LINE__, exit_code);
345 0 : g_tests_failed++;
346 : }
347 :
348 1 : pty_close(s);
349 1 : done_no_session:
350 1 : return;
351 : }
352 :
353 : /* ── Entry point ──────────────────────────────────────────────────────── */
354 :
355 11 : int main(void) {
356 11 : printf("PTY-03 readline navigation tests\n");
357 :
358 11 : RUN_TEST(test_prompt_visible);
359 10 : RUN_TEST(test_plain_text_accepted);
360 9 : RUN_TEST(test_left_arrow_insert);
361 8 : RUN_TEST(test_home_end_insert);
362 7 : RUN_TEST(test_backspace);
363 6 : RUN_TEST(test_ctrl_k_kill_to_end);
364 5 : RUN_TEST(test_ctrl_w_kill_word);
365 4 : RUN_TEST(test_up_arrow_history);
366 3 : RUN_TEST(test_down_arrow_restores_edit);
367 2 : RUN_TEST(test_ctrl_d_exits);
368 :
369 1 : printf("\n%d tests run, %d failed\n", g_tests_run, g_tests_failed);
370 1 : return g_tests_failed > 0 ? 1 : 0;
371 : }
|