Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file test_terminal_coverage.c
6 : * @brief Coverage-boosting PTY tests for src/platform/posix/terminal.c.
7 : *
8 : * Drives terminal_coverage_harness through various modes to exercise the
9 : * lines not reached by the existing password-prompt and readline PTY tests:
10 : *
11 : * - terminal_cols() / terminal_rows() success path (ioctl returns real size)
12 : * - terminal_raw_enter() / terminal_raw_exit()
13 : * - terminal_read_key() for all key codes (arrows, ESC sequences, Ctrl keys)
14 : * - terminal_wait_key()
15 : * - terminal_install_cleanup_handlers()
16 : * - terminal_enable_resize_notifications() / terminal_consume_resize()
17 : * - terminal_read_password() non-TTY (piped stdin) path
18 : */
19 :
20 : #include "ptytest.h"
21 : #include "pty_assert.h"
22 :
23 : #include <signal.h>
24 : #include <stdio.h>
25 : #include <stdlib.h>
26 : #include <string.h>
27 : #include <unistd.h>
28 : #include <sys/types.h>
29 : #include <sys/wait.h>
30 :
31 : /* ── Globals ─────────────────────────────────────────────────────────── */
32 :
33 : static int g_tests_run = 0;
34 : static int g_tests_failed = 0;
35 :
36 : #ifndef TERMINAL_COVERAGE_HARNESS
37 : #define TERMINAL_COVERAGE_HARNESS "terminal_coverage_harness"
38 : #endif
39 :
40 : /* ── Helpers ─────────────────────────────────────────────────────────── */
41 :
42 : #define RUN_TEST(fn) do { \
43 : printf(" running %s ...\n", #fn); \
44 : fn(); \
45 : } while(0)
46 :
47 : #define CHECK_WAIT_FOR(s, text, timeout_ms, label) do { \
48 : g_tests_run++; \
49 : if (pty_wait_for((s), (text), (timeout_ms)) != 0) { \
50 : printf(" [FAIL] %s:%d: wait_for(\"%s\", %d ms) timed out\n", \
51 : __FILE__, __LINE__, (text), (timeout_ms)); \
52 : g_tests_failed++; \
53 : goto label; \
54 : } \
55 : } while(0)
56 :
57 : #define CHECK(cond, msg, label) do { \
58 : g_tests_run++; \
59 : if (!(cond)) { \
60 : printf(" [FAIL] %s:%d: %s\n", __FILE__, __LINE__, (msg)); \
61 : g_tests_failed++; \
62 : goto label; \
63 : } \
64 : } while(0)
65 :
66 : /** Open 80x24 PTY running harness in the given mode. */
67 35 : static PtySession *open_harness(const char *mode) {
68 35 : PtySession *s = pty_open(80, 24);
69 35 : if (!s) return NULL;
70 35 : const char *argv[] = { TERMINAL_COVERAGE_HARNESS, mode, NULL };
71 35 : if (pty_run(s, argv) != 0) {
72 0 : pty_close(s);
73 0 : return NULL;
74 : }
75 28 : return s;
76 : }
77 :
78 : /* ── Test: terminal_cols / terminal_rows ─────────────────────────────── */
79 :
80 : /**
81 : * @brief terminal_cols() / terminal_rows() return the PTY size (not fallback).
82 : *
83 : * The PTY is opened 80×24 so ioctl(TIOCGWINSZ) should return ws_col=80 and
84 : * ws_row=24. That covers the success-return branches on lines 30 and 37.
85 : */
86 9 : static void test_cols_rows_on_pty(void) {
87 9 : PtySession *s = pty_open(80, 24);
88 9 : CHECK(s != NULL, "pty_open should succeed", done_no_session);
89 :
90 9 : const char *argv[] = { TERMINAL_COVERAGE_HARNESS, "cols_rows", NULL };
91 9 : CHECK(pty_run(s, argv) == 0, "pty_run should succeed", done);
92 :
93 : /* Should report real PTY dimensions — not the 80 / 0 fallback. */
94 8 : CHECK_WAIT_FOR(s, "COLS:", 3000, done);
95 8 : CHECK_WAIT_FOR(s, "ROWS:", 3000, done);
96 :
97 8 : int exit_code = pty_wait_exit(s, 3000);
98 8 : CHECK(exit_code == 0, "harness must exit 0 for cols_rows mode", done);
99 8 : done:
100 8 : pty_close(s);
101 8 : done_no_session:
102 8 : return;
103 : }
104 :
105 : /* ── Test: terminal_raw_enter / terminal_raw_exit ────────────────────── */
106 :
107 : /**
108 : * @brief terminal_raw_enter() succeeds and terminal_raw_exit() cleans up.
109 : *
110 : * Covers lines 54-60 (raw_enter) and 65-68 (raw_exit).
111 : */
112 8 : static void test_raw_enter_exit(void) {
113 8 : PtySession *s = open_harness("raw_enter_exit");
114 7 : CHECK(s != NULL, "open_harness(raw_enter_exit) should succeed",
115 : done_no_session);
116 :
117 7 : CHECK_WAIT_FOR(s, "RAW_ENTER:OK", 3000, done);
118 7 : CHECK_WAIT_FOR(s, "RAW_EXIT:OK", 3000, done);
119 :
120 7 : int exit_code = pty_wait_exit(s, 3000);
121 7 : CHECK(exit_code == 0, "harness must exit 0 for raw_enter_exit mode", done);
122 7 : done:
123 7 : pty_close(s);
124 7 : done_no_session:
125 7 : return;
126 : }
127 :
128 : /* ── Test: terminal_read_key — basic printable and control keys ──────── */
129 :
130 : /**
131 : * @brief Exercise terminal_read_key paths.
132 : *
133 : * Sends a representative mix of keys through the PTY and asserts the harness
134 : * echoes the expected KEY:* lines. This covers:
135 : * - read_byte() and its caller (lines 83–96)
136 : * - printable ASCII branch (lines 173–175)
137 : * - \n/\r → TERM_KEY_ENTER (line 157)
138 : * - Ctrl-A/E/K/W/D (lines 163–169)
139 : * - Backspace (line 171)
140 : * - terminal_last_printable() (line 91)
141 : * - terminal_wait_key() (lines 187–195)
142 : */
143 7 : static void test_read_key_basic(void) {
144 7 : PtySession *s = open_harness("read_key");
145 6 : CHECK(s != NULL, "open_harness(read_key) should succeed", done_no_session);
146 :
147 6 : CHECK_WAIT_FOR(s, "READY", 3000, done);
148 :
149 : /* Printable 'a' → KEY:CHAR:a */
150 6 : pty_send_str(s, "a");
151 6 : CHECK_WAIT_FOR(s, "KEY:CHAR:a", 3000, done);
152 :
153 : /* Enter (\r) → KEY:ENTER */
154 6 : pty_send_key(s, PTY_KEY_ENTER);
155 6 : CHECK_WAIT_FOR(s, "KEY:ENTER", 3000, done);
156 :
157 : /* Backspace → KEY:BACK */
158 6 : pty_send_key(s, PTY_KEY_BACK);
159 6 : CHECK_WAIT_FOR(s, "KEY:BACK", 3000, done);
160 :
161 : /* Ctrl-A (0x01) → KEY:CTRL_A */
162 6 : pty_send(s, "\x01", 1);
163 6 : CHECK_WAIT_FOR(s, "KEY:CTRL_A", 3000, done);
164 :
165 : /* Ctrl-E (0x05) → KEY:CTRL_E */
166 6 : pty_send(s, "\x05", 1);
167 6 : CHECK_WAIT_FOR(s, "KEY:CTRL_E", 3000, done);
168 :
169 : /* Ctrl-K (0x0b) → KEY:CTRL_K */
170 6 : pty_send(s, "\x0b", 1);
171 6 : CHECK_WAIT_FOR(s, "KEY:CTRL_K", 3000, done);
172 :
173 : /* Ctrl-W (0x17) → KEY:CTRL_W */
174 6 : pty_send(s, "\x17", 1);
175 6 : CHECK_WAIT_FOR(s, "KEY:CTRL_W", 3000, done);
176 :
177 : /* 'q' (printable) causes the harness to exit cleanly */
178 6 : pty_send_str(s, "q");
179 6 : CHECK_WAIT_FOR(s, "KEY:q", 3000, done);
180 :
181 6 : int exit_code = pty_wait_exit(s, 3000);
182 6 : CHECK(exit_code == 0, "harness must exit 0 after 'q'", done);
183 6 : done:
184 6 : pty_close(s);
185 6 : done_no_session:
186 6 : return;
187 : }
188 :
189 : /* ── Test: terminal_read_key — escape sequences ──────────────────────── */
190 :
191 : /**
192 : * @brief Exercise ESC-sequence paths in terminal_read_key.
193 : *
194 : * Covers lines 99–156 (the ESC prefix, CSI sequences, SS3 sequences, bare ESC).
195 : */
196 6 : static void test_read_key_escape_sequences(void) {
197 6 : PtySession *s = open_harness("read_key");
198 5 : CHECK(s != NULL, "open_harness(read_key) should succeed", done_no_session);
199 :
200 5 : CHECK_WAIT_FOR(s, "READY", 3000, done);
201 :
202 : /* Up arrow → ESC [ A → KEY:UP */
203 5 : pty_send_key(s, PTY_KEY_UP);
204 5 : CHECK_WAIT_FOR(s, "KEY:UP", 3000, done);
205 :
206 : /* Down arrow → ESC [ B → KEY:DOWN */
207 5 : pty_send_key(s, PTY_KEY_DOWN);
208 5 : CHECK_WAIT_FOR(s, "KEY:DOWN", 3000, done);
209 :
210 : /* Right arrow → ESC [ C → KEY:RIGHT */
211 5 : pty_send_key(s, PTY_KEY_RIGHT);
212 5 : CHECK_WAIT_FOR(s, "KEY:RIGHT", 3000, done);
213 :
214 : /* Left arrow → ESC [ D → KEY:LEFT */
215 5 : pty_send_key(s, PTY_KEY_LEFT);
216 5 : CHECK_WAIT_FOR(s, "KEY:LEFT", 3000, done);
217 :
218 : /* Home (ESC [ H) → KEY:HOME */
219 5 : pty_send(s, "\033[H", 3);
220 5 : CHECK_WAIT_FOR(s, "KEY:HOME", 3000, done);
221 :
222 : /* End (ESC [ F) → KEY:END */
223 5 : pty_send(s, "\033[F", 3);
224 5 : CHECK_WAIT_FOR(s, "KEY:END", 3000, done);
225 :
226 : /* PgUp (ESC [ 5 ~) → KEY:PGUP */
227 5 : pty_send_key(s, PTY_KEY_PGUP);
228 5 : CHECK_WAIT_FOR(s, "KEY:PGUP", 3000, done);
229 :
230 : /* PgDn (ESC [ 6 ~) → KEY:PGDN */
231 5 : pty_send_key(s, PTY_KEY_PGDN);
232 5 : CHECK_WAIT_FOR(s, "KEY:PGDN", 3000, done);
233 :
234 : /* Delete (ESC [ 3 ~) → KEY:DELETE */
235 5 : pty_send(s, "\033[3~", 4);
236 5 : CHECK_WAIT_FOR(s, "KEY:DELETE", 3000, done);
237 :
238 : /* Home via ESC [ 1 ~ → KEY:HOME */
239 5 : pty_send(s, "\033[1~", 4);
240 5 : CHECK_WAIT_FOR(s, "KEY:HOME", 3000, done);
241 :
242 : /* End via ESC [ 4 ~ → KEY:END */
243 5 : pty_send(s, "\033[4~", 4);
244 5 : CHECK_WAIT_FOR(s, "KEY:END", 3000, done);
245 :
246 : /* Home via ESC [ 7 ~ → KEY:HOME */
247 5 : pty_send(s, "\033[7~", 4);
248 5 : CHECK_WAIT_FOR(s, "KEY:HOME", 3000, done);
249 :
250 : /* End via ESC [ 8 ~ → KEY:END */
251 5 : pty_send(s, "\033[8~", 4);
252 5 : CHECK_WAIT_FOR(s, "KEY:END", 3000, done);
253 :
254 : /* Home via ESC O H (SS3 sequence) → KEY:HOME */
255 5 : pty_send(s, "\033OH", 3);
256 5 : CHECK_WAIT_FOR(s, "KEY:HOME", 3000, done);
257 :
258 : /* End via ESC O F (SS3 sequence) → KEY:END */
259 5 : pty_send(s, "\033OF", 3);
260 5 : CHECK_WAIT_FOR(s, "KEY:END", 3000, done);
261 :
262 : /* Bare ESC followed by timeout → KEY:ESC */
263 5 : pty_send(s, "\033", 1);
264 5 : CHECK_WAIT_FOR(s, "KEY:ESC", 3000, done);
265 :
266 : /* Unknown CSI sequence (ESC [ 2 0 m) → IGNORE (drains to letter) */
267 5 : pty_send(s, "\033[20m", 5);
268 5 : CHECK_WAIT_FOR(s, "KEY:IGNORE", 3000, done);
269 :
270 : /* Unknown SS3 (ESC O Z) → IGNORE */
271 5 : pty_send(s, "\033OZ", 3);
272 5 : CHECK_WAIT_FOR(s, "KEY:IGNORE", 3000, done);
273 :
274 : /* Ctrl-D → exits harness */
275 5 : pty_send_key(s, PTY_KEY_CTRL_D);
276 5 : CHECK_WAIT_FOR(s, "KEY:CTRL_D", 3000, done);
277 :
278 5 : int exit_code = pty_wait_exit(s, 3000);
279 5 : CHECK(exit_code == 0, "harness must exit 0 after Ctrl-D", done);
280 5 : done:
281 5 : pty_close(s);
282 5 : done_no_session:
283 5 : return;
284 : }
285 :
286 : /* ── Test: terminal_wait_key ─────────────────────────────────────────── */
287 :
288 : /**
289 : * @brief terminal_wait_key() returns 1 when input is available.
290 : *
291 : * Covers lines 187-195.
292 : */
293 5 : static void test_wait_key(void) {
294 5 : PtySession *s = open_harness("wait_key");
295 4 : CHECK(s != NULL, "open_harness(wait_key) should succeed", done_no_session);
296 :
297 4 : CHECK_WAIT_FOR(s, "READY", 3000, done);
298 :
299 : /* Send a complete line so wait_key's poll() sees POLLIN in canonical mode. */
300 4 : pty_send_str(s, "x");
301 4 : pty_send_key(s, PTY_KEY_ENTER);
302 4 : CHECK_WAIT_FOR(s, "WAIT_KEY:READY", 3000, done);
303 :
304 4 : int exit_code = pty_wait_exit(s, 3000);
305 4 : CHECK(exit_code == 0, "harness must exit 0 for wait_key mode", done);
306 4 : done:
307 4 : pty_close(s);
308 4 : done_no_session:
309 4 : return;
310 : }
311 :
312 : /* ── Test: terminal_install_cleanup_handlers ─────────────────────────── */
313 :
314 : /**
315 : * @brief terminal_install_cleanup_handlers() installs signal handlers.
316 : *
317 : * Covers lines 240-253 (the function itself). The signal handler code
318 : * (lines 217-238) is reached by delivering SIGTERM to the harness.
319 : */
320 4 : static void test_install_cleanup_handlers(void) {
321 4 : PtySession *s = open_harness("install_handlers");
322 3 : CHECK(s != NULL, "open_harness(install_handlers) should succeed",
323 : done_no_session);
324 :
325 3 : CHECK_WAIT_FOR(s, "HANDLERS:OK", 3000, done);
326 :
327 3 : int exit_code = pty_wait_exit(s, 3000);
328 3 : CHECK(exit_code == 0, "harness must exit 0 for install_handlers mode",
329 : done);
330 3 : done:
331 3 : pty_close(s);
332 3 : done_no_session:
333 3 : return;
334 : }
335 :
336 : /* ── Test: terminal_enable_resize_notifications / consume_resize ─────── */
337 :
338 : /**
339 : * @brief Resize notifications are detected via SIGWINCH.
340 : *
341 : * Covers lines 261-285: terminal_enable_resize_notifications(),
342 : * the resize_handler signal handler, and terminal_consume_resize().
343 : */
344 3 : static void test_resize_notifications(void) {
345 3 : PtySession *s = open_harness("resize_notify");
346 2 : CHECK(s != NULL, "open_harness(resize_notify) should succeed",
347 : done_no_session);
348 :
349 2 : CHECK_WAIT_FOR(s, "RESIZE_READY", 3000, done);
350 :
351 : /* Issue a PTY resize — ptytest sends TIOCSWINSZ + SIGWINCH. */
352 2 : int rc = pty_resize(s, 100, 30);
353 2 : CHECK(rc == 0, "pty_resize should succeed", done);
354 :
355 2 : CHECK_WAIT_FOR(s, "RESIZE_DETECTED", 5000, done);
356 :
357 2 : int exit_code = pty_wait_exit(s, 3000);
358 2 : CHECK(exit_code == 0, "harness must exit 0 after resize", done);
359 2 : done:
360 2 : pty_close(s);
361 2 : done_no_session:
362 2 : return;
363 : }
364 :
365 : /* ── Test: terminal_read_password non-TTY path ───────────────────────── */
366 :
367 : /**
368 : * @brief terminal_read_password() reads from piped stdin (non-TTY path).
369 : *
370 : * Runs the harness via fork+exec with stdin replaced by a pipe.
371 : * Covers lines 328-346 (the else-branch in terminal_read_password).
372 : */
373 2 : static void test_passwd_nontty(void) {
374 : int pipefd[2];
375 2 : g_tests_run++;
376 2 : if (pipe(pipefd) != 0) {
377 0 : printf(" [FAIL] %s:%d: pipe() failed\n", __FILE__, __LINE__);
378 0 : g_tests_failed++;
379 0 : return;
380 : }
381 :
382 2 : pid_t pid = fork();
383 3 : if (pid < 0) {
384 0 : printf(" [FAIL] %s:%d: fork() failed\n", __FILE__, __LINE__);
385 0 : g_tests_failed++;
386 0 : close(pipefd[0]);
387 0 : close(pipefd[1]);
388 0 : return;
389 : }
390 :
391 3 : if (pid == 0) {
392 : /* Child: replace stdin with read end of the pipe. */
393 1 : close(pipefd[1]);
394 1 : dup2(pipefd[0], STDIN_FILENO);
395 1 : close(pipefd[0]);
396 1 : const char *argv[] = {
397 : TERMINAL_COVERAGE_HARNESS, "passwd_nontty", NULL
398 : };
399 1 : execv(argv[0], (char *const *)argv);
400 1 : _exit(127);
401 : }
402 :
403 : /* Parent: write a password line then close the write end. */
404 2 : close(pipefd[0]);
405 2 : const char *pw = "piped_secret\n";
406 2 : ssize_t written = write(pipefd[1], pw, strlen(pw));
407 2 : close(pipefd[1]);
408 : (void)written;
409 :
410 : /* Collect the child's stdout by reading from its pipe-backed stdout.
411 : * Since we can't easily capture stdout here, we just check exit status
412 : * and trust GCOV records the lines. An exit 0 means no error. */
413 2 : int status = 0;
414 2 : waitpid(pid, &status, 0);
415 2 : if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
416 0 : printf(" [FAIL] %s:%d: passwd_nontty harness exited with %d\n",
417 0 : __FILE__, __LINE__, WEXITSTATUS(status));
418 0 : g_tests_failed++;
419 : }
420 : }
421 :
422 : /**
423 : * @brief Non-TTY path with EOF immediately → returns -1.
424 : *
425 : * Covers the getline-returns-(-1) branch inside the else block.
426 : */
427 2 : static void test_passwd_nontty_eof(void) {
428 : int pipefd[2];
429 2 : g_tests_run++;
430 2 : if (pipe(pipefd) != 0) {
431 0 : printf(" [FAIL] %s:%d: pipe() failed\n", __FILE__, __LINE__);
432 0 : g_tests_failed++;
433 0 : return;
434 : }
435 :
436 2 : pid_t pid = fork();
437 3 : if (pid < 0) {
438 0 : printf(" [FAIL] %s:%d: fork() failed\n", __FILE__, __LINE__);
439 0 : g_tests_failed++;
440 0 : close(pipefd[0]);
441 0 : close(pipefd[1]);
442 0 : return;
443 : }
444 :
445 3 : if (pid == 0) {
446 1 : close(pipefd[1]);
447 1 : dup2(pipefd[0], STDIN_FILENO);
448 1 : close(pipefd[0]);
449 1 : const char *argv[] = {
450 : TERMINAL_COVERAGE_HARNESS, "passwd_nontty", NULL
451 : };
452 1 : execv(argv[0], (char *const *)argv);
453 1 : _exit(127);
454 : }
455 :
456 : /* Close write end immediately → child sees EOF → harness exits 1. */
457 2 : close(pipefd[0]);
458 2 : close(pipefd[1]);
459 :
460 2 : int status = 0;
461 2 : waitpid(pid, &status, 0);
462 : /* Exit 1 means terminal_read_password returned -1 as expected. */
463 2 : if (!WIFEXITED(status) || WEXITSTATUS(status) != 1) {
464 0 : printf(" [FAIL] %s:%d: expected exit 1 on EOF, got %d\n",
465 0 : __FILE__, __LINE__, WEXITSTATUS(status));
466 0 : g_tests_failed++;
467 : }
468 : }
469 :
470 : /* ── Test: Ctrl-C triggers signal handler path ───────────────────────── */
471 :
472 : /**
473 : * @brief SIGTERM exercices the cleanup_signal_handler.
474 : *
475 : * Opens install_handlers mode, waits for it to install the handlers,
476 : * then sends SIGTERM via pty_close (which will SIGTERM the child). This
477 : * exercises the cleanup_signal_handler code (lines 217-238).
478 : * We use a separate PTY session so the signal is sent to the child
479 : * while it is in raw mode.
480 : */
481 2 : static void test_signal_handler_path(void) {
482 2 : PtySession *s = open_harness("read_key");
483 1 : CHECK(s != NULL, "open_harness(read_key) should succeed", done_no_session);
484 :
485 1 : CHECK_WAIT_FOR(s, "READY", 3000, done);
486 :
487 : /* Send Ctrl-C (0x03). In raw mode (ISIG cleared) this arrives as
488 : * TERM_KEY_QUIT rather than raising SIGINT. The harness will print
489 : * KEY:QUIT and exit cleanly. This exercises the Ctrl-C branch (line 159). */
490 1 : pty_send_key(s, PTY_KEY_CTRL_C);
491 1 : CHECK_WAIT_FOR(s, "KEY:QUIT", 3000, done);
492 :
493 1 : int exit_code = pty_wait_exit(s, 3000);
494 1 : CHECK(exit_code == 0, "harness must exit 0 after QUIT key", done);
495 1 : done:
496 1 : pty_close(s);
497 1 : done_no_session:
498 1 : return;
499 : }
500 :
501 : /* ── Entry point ─────────────────────────────────────────────────────── */
502 :
503 9 : int main(void) {
504 9 : printf("Terminal coverage PTY tests (%s)\n", TERMINAL_COVERAGE_HARNESS);
505 :
506 9 : RUN_TEST(test_cols_rows_on_pty);
507 8 : RUN_TEST(test_raw_enter_exit);
508 7 : RUN_TEST(test_read_key_basic);
509 6 : RUN_TEST(test_read_key_escape_sequences);
510 5 : RUN_TEST(test_wait_key);
511 4 : RUN_TEST(test_install_cleanup_handlers);
512 3 : RUN_TEST(test_resize_notifications);
513 2 : RUN_TEST(test_passwd_nontty);
514 2 : RUN_TEST(test_passwd_nontty_eof);
515 2 : RUN_TEST(test_signal_handler_path);
516 :
517 1 : printf("\n%d tests run, %d failed\n", g_tests_run, g_tests_failed);
518 1 : return g_tests_failed > 0 ? 1 : 0;
519 : }
|