LCOV - code coverage report
Current view: top level - tests/functional/pty - test_readline.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 96.4 % 169 163
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 12 12

            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              : }
        

Generated by: LCOV version 2.0-1