LCOV - code coverage report
Current view: top level - tests/pty - test_input_line_pty.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 98.8 % 337 333
Test Date: 2026-08-21 10:11:28 Functions: 100.0 % 26 26

            Line data    Source code
       1              : /**
       2              :  * @file test_input_line_pty.c
       3              :  * @brief PTY-based functional tests for InputLine and PathComplete.
       4              :  *
       5              :  * Usage: test-pty-input-line <path-to-input-line-harness>
       6              :  *
       7              :  * Each test launches the harness binary inside a PTY, sends keystrokes,
       8              :  * and inspects the virtual screen to verify correct behaviour.
       9              :  */
      10              : 
      11              : #define _DEFAULT_SOURCE
      12              : #define _XOPEN_SOURCE 600
      13              : 
      14              : #include "ptytest.h"
      15              : #include "pty_assert.h"
      16              : 
      17              : /* Minimal RUN_TEST compatible with g_tests_run / g_tests_failed globals */
      18              : #ifndef RUN_TEST
      19              : #define RUN_TEST(fn) do { \
      20              :     printf("  %-50s", #fn); \
      21              :     int _before = g_tests_failed; \
      22              :     fn(); \
      23              :     printf("%s\n", (g_tests_failed == _before) ? "OK" : "FAIL"); \
      24              : } while(0)
      25              : #endif
      26              : 
      27              : #include <stdio.h>
      28              : #include <stdlib.h>
      29              : #include <string.h>
      30              : #include <sys/stat.h>
      31              : #include <unistd.h>
      32              : 
      33              : /* ── Globals ─────────────────────────────────────────────────────────── */
      34              : 
      35              : int g_tests_run    = 0;
      36              : int g_tests_failed = 0;
      37              : 
      38              : static char g_harness_bin[512]; /* path to input-line-harness */
      39              : static char g_root[256];        /* per-run temp dir for filesystem tests */
      40              : 
      41              : #define WAIT_MS   3000
      42              : #define SETTLE_MS 150
      43              : #define ROWS 24
      44              : #define COLS 80
      45              : 
      46              : /* ── Temp filesystem helpers ─────────────────────────────────────────── */
      47              : 
      48           21 : static void fs_setup(void)
      49              : {
      50           21 :     snprintf(g_root, sizeof(g_root), "/tmp/il_pty_%d", (int)getpid());
      51           21 :     mkdir(g_root, 0755);
      52           21 : }
      53              : 
      54            1 : static void fs_teardown(void)
      55              : {
      56              :     char cmd[512];
      57            1 :     snprintf(cmd, sizeof(cmd), "rm -rf '/tmp/il_pty_%d'", (int)getpid());
      58            1 :     int r = system(cmd);
      59              :     (void)r;
      60            1 : }
      61              : 
      62          163 : static void make_file(const char *dir, const char *name)
      63              : {
      64              :     char path[512];
      65          163 :     snprintf(path, sizeof(path), "%s/%s", dir, name);
      66          163 :     FILE *f = fopen(path, "w");
      67          163 :     if (f) fclose(f);
      68          163 : }
      69              : 
      70            9 : static void make_subdir(const char *dir, const char *name)
      71              : {
      72              :     char path[512];
      73            9 :     snprintf(path, sizeof(path), "%s/%s", dir, name);
      74            9 :     mkdir(path, 0755);
      75            9 : }
      76              : 
      77              : /* ── Session helpers ─────────────────────────────────────────────────── */
      78              : 
      79              : /** Open a PTY session running the harness with an optional initial path. */
      80          230 : static PtySession *open_harness(const char *initial)
      81              : {
      82          230 :     PtySession *s = pty_open(COLS, ROWS);
      83          230 :     if (!s) return NULL;
      84              : 
      85              :     const char *argv[3];
      86          230 :     argv[0] = g_harness_bin;
      87          230 :     argv[1] = initial;  /* may be NULL */
      88          230 :     argv[2] = NULL;
      89              : 
      90          230 :     if (pty_run(s, argv) != 0) {
      91            0 :         pty_close(s);
      92            0 :         return NULL;
      93              :     }
      94          210 :     return s;
      95              : }
      96              : 
      97              : /* ── Tests: basic editing ────────────────────────────────────────────── */
      98              : 
      99              : /** Prompt appears on row 0 immediately after launch. */
     100              : 
     101              : /* ── UTF-8 input (accented characters) ──────────────────────────────── */
     102              : 
     103              : /* Typing non-ASCII must insert the whole code point, not a placeholder byte.
     104              :  *
     105              :  * terminal_last_printable() reports multi-byte input as the value 1; inserting
     106              :  * that produced a literal 0x01 in the buffer, so "hőcserélő" came out as
     107              :  * "h\x01cser\x01l\x01" — and because a control byte occupies no column, the
     108              :  * rendered cursor drifted away from the real insertion point, making editing
     109              :  * unreliable. */
     110            4 : static void test_utf8_accented_input(void)
     111              : {
     112            4 :     PtySession *s = open_harness(NULL);
     113            3 :     ASSERT(s != NULL, "utf8: harness launched");
     114            3 :     ASSERT_WAIT_FOR(s, "Path:", WAIT_MS);
     115              : 
     116              :     /* A real-world filename: Hungarian accents, including two-byte ő and é. */
     117            3 :     pty_send_str(s, "hőcserélő-számla.pdf");
     118            3 :     pty_settle(s, SETTLE_MS);
     119            3 :     ASSERT_SCREEN_CONTAINS(s, "hőcserélő-számla.pdf");
     120              : 
     121            3 :     pty_send_key(s, PTY_KEY_ENTER);
     122            3 :     ASSERT_WAIT_FOR(s, "RESULT:", WAIT_MS);
     123              :     /* The value handed back must be the text that was typed, byte for byte. */
     124            3 :     ASSERT_SCREEN_CONTAINS(s, "RESULT: hőcserélő-számla.pdf");
     125            3 :     pty_close(s);
     126              : }
     127              : 
     128              : /* Backspace after an accented character must delete the whole code point,
     129              :  * leaving valid UTF-8 rather than a dangling continuation byte. */
     130            3 : static void test_utf8_backspace_deletes_code_point(void)
     131              : {
     132            3 :     PtySession *s = open_harness(NULL);
     133            2 :     ASSERT(s != NULL, "utf8 bs: harness launched");
     134            2 :     ASSERT_WAIT_FOR(s, "Path:", WAIT_MS);
     135              : 
     136            2 :     pty_send_str(s, "árvíz");
     137            2 :     pty_settle(s, SETTLE_MS);
     138            2 :     pty_send_key(s, PTY_KEY_BACK);   /* remove 'z' */
     139            2 :     pty_send_key(s, PTY_KEY_BACK);   /* remove 'í' (two bytes) */
     140            2 :     pty_settle(s, SETTLE_MS);
     141              : 
     142            2 :     pty_send_key(s, PTY_KEY_ENTER);
     143            2 :     ASSERT_WAIT_FOR(s, "RESULT:", WAIT_MS);
     144            2 :     ASSERT_SCREEN_CONTAINS(s, "RESULT: árv");
     145            2 :     pty_close(s);
     146              : }
     147              : 
     148              : /* Editing in the middle of accented text: the cursor position the terminal
     149              :  * shows must match where the bytes actually go. */
     150            2 : static void test_utf8_insert_in_middle(void)
     151              : {
     152            2 :     PtySession *s = open_harness(NULL);
     153            1 :     ASSERT(s != NULL, "utf8 mid: harness launched");
     154            1 :     ASSERT_WAIT_FOR(s, "Path:", WAIT_MS);
     155              : 
     156            1 :     pty_send_str(s, "őz");
     157            1 :     pty_settle(s, SETTLE_MS);
     158            1 :     pty_send_key(s, PTY_KEY_LEFT);        /* between ő and z */
     159            1 :     pty_send_str(s, "é");
     160            1 :     pty_settle(s, SETTLE_MS);
     161              : 
     162            1 :     pty_send_key(s, PTY_KEY_ENTER);
     163            1 :     ASSERT_WAIT_FOR(s, "RESULT:", WAIT_MS);
     164            1 :     ASSERT_SCREEN_CONTAINS(s, "RESULT: őéz");
     165            1 :     pty_close(s);
     166              : }
     167              : 
     168           21 : static void test_prompt_displayed(void)
     169              : {
     170           21 :     PtySession *s = open_harness(NULL);
     171           20 :     ASSERT(s != NULL, "harness launched");
     172              : 
     173           20 :     ASSERT_WAIT_FOR(s, "Path:", WAIT_MS);
     174           20 :     ASSERT_ROW_CONTAINS(s, 0, "Path:");
     175              : 
     176           20 :     pty_send_key(s, PTY_KEY_ESC);
     177           20 :     ASSERT_WAIT_FOR(s, "CANCELLED", WAIT_MS);
     178           20 :     pty_close(s);
     179              : }
     180              : 
     181              : /** Typing characters echoes them in the input row. */
     182           20 : static void test_typing_appears_on_screen(void)
     183              : {
     184           20 :     PtySession *s = open_harness(NULL);
     185           19 :     ASSERT(s != NULL, "harness launched");
     186           19 :     ASSERT_WAIT_FOR(s, "Path:", WAIT_MS);
     187              : 
     188           19 :     pty_send_str(s, "hello");
     189           19 :     pty_settle(s, SETTLE_MS);
     190           19 :     ASSERT_ROW_CONTAINS(s, 0, "hello");
     191              : 
     192           19 :     pty_send_key(s, PTY_KEY_ESC);
     193           19 :     ASSERT_WAIT_FOR(s, "CANCELLED", WAIT_MS);
     194           19 :     pty_close(s);
     195              : }
     196              : 
     197              : /** Backspace removes the last character. */
     198           19 : static void test_backspace_deletes_char(void)
     199              : {
     200           19 :     PtySession *s = open_harness(NULL);
     201           18 :     ASSERT(s != NULL, "harness launched");
     202           18 :     ASSERT_WAIT_FOR(s, "Path:", WAIT_MS);
     203              : 
     204           18 :     pty_send_str(s, "abc");
     205           18 :     pty_settle(s, SETTLE_MS);
     206           18 :     pty_send_key(s, PTY_KEY_BACK);
     207           18 :     pty_settle(s, SETTLE_MS);
     208              : 
     209              :     /* 'c' should be gone; 'ab' remains */
     210           18 :     ASSERT_ROW_CONTAINS(s, 0, "ab");
     211           18 :     ASSERT(pty_row_contains(s, 0, "abc") == 0, "'abc' gone after backspace");
     212              : 
     213           18 :     pty_send_key(s, PTY_KEY_ESC);
     214           18 :     ASSERT_WAIT_FOR(s, "CANCELLED", WAIT_MS);
     215           18 :     pty_close(s);
     216              : }
     217              : 
     218              : /** Initial text is shown and confirmed verbatim on Enter. */
     219           18 : static void test_initial_text_confirmed(void)
     220              : {
     221           18 :     PtySession *s = open_harness("/some/path.txt");
     222           17 :     ASSERT(s != NULL, "harness launched");
     223           17 :     ASSERT_WAIT_FOR(s, "/some/path.txt", WAIT_MS);
     224              : 
     225           17 :     pty_send_key(s, PTY_KEY_ENTER);
     226           17 :     ASSERT_WAIT_FOR(s, "RESULT: /some/path.txt", WAIT_MS);
     227           17 :     pty_close(s);
     228              : }
     229              : 
     230              : /** ESC returns CANCELLED without result. */
     231           17 : static void test_esc_cancels(void)
     232              : {
     233           17 :     PtySession *s = open_harness("/cancel/me");
     234           16 :     ASSERT(s != NULL, "harness launched");
     235           16 :     ASSERT_WAIT_FOR(s, "Path:", WAIT_MS);
     236              : 
     237           16 :     pty_send_key(s, PTY_KEY_ESC);
     238           16 :     ASSERT_WAIT_FOR(s, "CANCELLED", WAIT_MS);
     239           16 :     ASSERT(pty_screen_contains(s, "RESULT:") == 0, "no RESULT on ESC");
     240           16 :     pty_close(s);
     241              : }
     242              : 
     243              : /** Typed text is returned as the result on Enter. */
     244           16 : static void test_typed_text_in_result(void)
     245              : {
     246           16 :     PtySession *s = open_harness(NULL);
     247           15 :     ASSERT(s != NULL, "harness launched");
     248           15 :     ASSERT_WAIT_FOR(s, "Path:", WAIT_MS);
     249              : 
     250           15 :     pty_send_str(s, "/tmp/myfile.txt");
     251           15 :     pty_send_key(s, PTY_KEY_ENTER);
     252           15 :     ASSERT_WAIT_FOR(s, "RESULT: /tmp/myfile.txt", WAIT_MS);
     253           15 :     pty_close(s);
     254              : }
     255              : 
     256              : /** Left/right arrow keys move the cursor (no crash). */
     257           15 : static void test_arrow_keys_move_cursor(void)
     258              : {
     259           15 :     PtySession *s = open_harness("abc");
     260           14 :     ASSERT(s != NULL, "harness launched");
     261           14 :     ASSERT_WAIT_FOR(s, "abc", WAIT_MS);
     262              : 
     263           14 :     pty_send_key(s, PTY_KEY_LEFT);
     264           14 :     pty_send_key(s, PTY_KEY_LEFT);
     265           14 :     pty_settle(s, SETTLE_MS);
     266              : 
     267              :     /* Insert 'X' before last char: should produce 'aXbc' */
     268           14 :     pty_send_str(s, "X");
     269           14 :     pty_settle(s, SETTLE_MS);
     270           14 :     ASSERT_ROW_CONTAINS(s, 0, "aXbc");
     271              : 
     272           14 :     pty_send_key(s, PTY_KEY_ESC);
     273           14 :     ASSERT_WAIT_FOR(s, "CANCELLED", WAIT_MS);
     274           14 :     pty_close(s);
     275              : }
     276              : 
     277              : /** Home/End jump to start and end of line. */
     278           14 : static void test_home_end_keys(void)
     279              : {
     280           14 :     PtySession *s = open_harness("foo");
     281           13 :     ASSERT(s != NULL, "harness launched");
     282           13 :     ASSERT_WAIT_FOR(s, "foo", WAIT_MS);
     283              : 
     284              :     /* Home → insert at start */
     285           13 :     pty_send_key(s, PTY_KEY_HOME);
     286           13 :     pty_send_str(s, "Z");
     287           13 :     pty_settle(s, SETTLE_MS);
     288           13 :     ASSERT_ROW_CONTAINS(s, 0, "Zfoo");
     289              : 
     290              :     /* End → append at end */
     291           13 :     pty_send_key(s, PTY_KEY_END);
     292           13 :     pty_send_str(s, "Y");
     293           13 :     pty_settle(s, SETTLE_MS);
     294           13 :     ASSERT_ROW_CONTAINS(s, 0, "ZfooY");
     295              : 
     296           13 :     pty_send_key(s, PTY_KEY_ESC);
     297           13 :     ASSERT_WAIT_FOR(s, "CANCELLED", WAIT_MS);
     298           13 :     pty_close(s);
     299              : }
     300              : 
     301              : /* ── Tests: Tab completion ───────────────────────────────────────────── */
     302              : 
     303              : /** Tab with a single matching file completes it fully. */
     304           13 : static void test_tab_single_match(void)
     305              : {
     306           13 :     make_file(g_root, "report.pdf");
     307              : 
     308              :     char prefix[512];
     309           13 :     snprintf(prefix, sizeof(prefix), "%s/rep", g_root);
     310              : 
     311           13 :     PtySession *s = open_harness(prefix);
     312           12 :     ASSERT(s != NULL, "harness launched");
     313           12 :     ASSERT_WAIT_FOR(s, prefix, WAIT_MS);
     314              : 
     315           12 :     pty_send_key(s, PTY_KEY_TAB);
     316           12 :     pty_settle(s, SETTLE_MS);
     317              : 
     318              :     /* Completion row (row 0) should contain the full filename */
     319           12 :     ASSERT_ROW_CONTAINS(s, 0, "report.pdf");
     320              : 
     321           12 :     pty_send_key(s, PTY_KEY_ENTER);
     322              :     char expected[512];
     323           12 :     snprintf(expected, sizeof(expected), "RESULT: %s/report.pdf", g_root);
     324           12 :     ASSERT_WAIT_FOR(s, expected, WAIT_MS);
     325           12 :     pty_close(s);
     326              : }
     327              : 
     328              : /** Completion bar appears at row 1 when there are multiple matches. */
     329           12 : static void test_tab_shows_completion_bar(void)
     330              : {
     331           12 :     make_file(g_root, "aaa.txt");
     332           12 :     make_file(g_root, "aab.txt");
     333           12 :     make_file(g_root, "aac.txt");
     334              : 
     335              :     char prefix[512];
     336           12 :     snprintf(prefix, sizeof(prefix), "%s/aa", g_root);
     337              : 
     338           12 :     PtySession *s = open_harness(prefix);
     339           11 :     ASSERT(s != NULL, "harness launched");
     340           11 :     ASSERT_WAIT_FOR(s, prefix, WAIT_MS);
     341              : 
     342           11 :     pty_send_key(s, PTY_KEY_TAB);
     343           11 :     pty_settle(s, SETTLE_MS);
     344              : 
     345              :     /* Completion bar on row 1 must list all three names */
     346           11 :     ASSERT_ROW_CONTAINS(s, 1, "aaa.txt");
     347           11 :     ASSERT_ROW_CONTAINS(s, 1, "aab.txt");
     348           11 :     ASSERT_ROW_CONTAINS(s, 1, "aac.txt");
     349              : 
     350           11 :     pty_send_key(s, PTY_KEY_ESC);
     351           11 :     ASSERT_WAIT_FOR(s, "CANCELLED", WAIT_MS);
     352           11 :     pty_close(s);
     353              : }
     354              : 
     355              : /** Repeated Tab presses cycle forward through alphabetically sorted matches. */
     356           11 : static void test_tab_cycles_forward(void)
     357              : {
     358           11 :     make_file(g_root, "zz_alpha.txt");
     359           11 :     make_file(g_root, "zz_beta.txt");
     360           11 :     make_file(g_root, "zz_gamma.txt");
     361              : 
     362              :     char prefix[512];
     363           11 :     snprintf(prefix, sizeof(prefix), "%s/zz_", g_root);
     364              : 
     365           11 :     PtySession *s = open_harness(prefix);
     366           10 :     ASSERT(s != NULL, "harness launched");
     367           10 :     ASSERT_WAIT_FOR(s, prefix, WAIT_MS);
     368              : 
     369              :     /* 1st Tab → zz_alpha */
     370           10 :     pty_send_key(s, PTY_KEY_TAB);
     371           10 :     pty_settle(s, SETTLE_MS);
     372           10 :     ASSERT_ROW_CONTAINS(s, 0, "zz_alpha.txt");
     373              : 
     374              :     /* 2nd Tab → zz_beta */
     375           10 :     pty_send_key(s, PTY_KEY_TAB);
     376           10 :     pty_settle(s, SETTLE_MS);
     377           10 :     ASSERT_ROW_CONTAINS(s, 0, "zz_beta.txt");
     378              : 
     379              :     /* 3rd Tab → zz_gamma */
     380           10 :     pty_send_key(s, PTY_KEY_TAB);
     381           10 :     pty_settle(s, SETTLE_MS);
     382           10 :     ASSERT_ROW_CONTAINS(s, 0, "zz_gamma.txt");
     383              : 
     384              :     /* 4th Tab wraps back to alpha */
     385           10 :     pty_send_key(s, PTY_KEY_TAB);
     386           10 :     pty_settle(s, SETTLE_MS);
     387           10 :     ASSERT_ROW_CONTAINS(s, 0, "zz_alpha.txt");
     388              : 
     389           10 :     pty_send_key(s, PTY_KEY_ESC);
     390           10 :     ASSERT_WAIT_FOR(s, "CANCELLED", WAIT_MS);
     391           10 :     pty_close(s);
     392              : }
     393              : 
     394              : /** Shift+Tab cycles backward through completions. */
     395           10 : static void test_shift_tab_cycles_backward(void)
     396              : {
     397           10 :     make_file(g_root, "xx_one.txt");
     398           10 :     make_file(g_root, "xx_three.txt");
     399           10 :     make_file(g_root, "xx_two.txt");
     400              : 
     401              :     char prefix[512];
     402           10 :     snprintf(prefix, sizeof(prefix), "%s/xx_", g_root);
     403              : 
     404           10 :     PtySession *s = open_harness(prefix);
     405            9 :     ASSERT(s != NULL, "harness launched");
     406            9 :     ASSERT_WAIT_FOR(s, prefix, WAIT_MS);
     407              : 
     408              :     /* First Tab → xx_one (idx 0) */
     409            9 :     pty_send_key(s, PTY_KEY_TAB);
     410            9 :     pty_settle(s, SETTLE_MS);
     411            9 :     ASSERT_ROW_CONTAINS(s, 0, "xx_one.txt");
     412              : 
     413              :     /* Shift+Tab backward-wraps to xx_two (last, idx 2) */
     414            9 :     pty_send(s, "\033[Z", 3);
     415            9 :     pty_settle(s, SETTLE_MS);
     416            9 :     ASSERT_ROW_CONTAINS(s, 0, "xx_two.txt");
     417              : 
     418              :     /* Another Shift+Tab → xx_three */
     419            9 :     pty_send(s, "\033[Z", 3);
     420            9 :     pty_settle(s, SETTLE_MS);
     421            9 :     ASSERT_ROW_CONTAINS(s, 0, "xx_three.txt");
     422              : 
     423              :     /* Another Shift+Tab → xx_one */
     424            9 :     pty_send(s, "\033[Z", 3);
     425            9 :     pty_settle(s, SETTLE_MS);
     426            9 :     ASSERT_ROW_CONTAINS(s, 0, "xx_one.txt");
     427              : 
     428            9 :     pty_send_key(s, PTY_KEY_ESC);
     429            9 :     ASSERT_WAIT_FOR(s, "CANCELLED", WAIT_MS);
     430            9 :     pty_close(s);
     431              : }
     432              : 
     433              : /** Typing '/' after a directory completion triggers a fresh scan inside it. */
     434            9 : static void test_slash_enters_subdirectory(void)
     435              : {
     436            9 :     make_subdir(g_root, "docs");
     437            9 :     make_file(g_root, "docs/guide.pdf");
     438              : 
     439              :     char prefix[512];
     440            9 :     snprintf(prefix, sizeof(prefix), "%s/doc", g_root);
     441              : 
     442            9 :     PtySession *s = open_harness(prefix);
     443            8 :     ASSERT(s != NULL, "harness launched");
     444            8 :     ASSERT_WAIT_FOR(s, prefix, WAIT_MS);
     445              : 
     446              :     /* Tab completes to "docs" */
     447            8 :     pty_send_key(s, PTY_KEY_TAB);
     448            8 :     pty_settle(s, SETTLE_MS);
     449            8 :     ASSERT_ROW_CONTAINS(s, 0, "docs");
     450              : 
     451              :     /* Append '/' */
     452            8 :     pty_send_str(s, "/");
     453            8 :     pty_settle(s, SETTLE_MS);
     454              : 
     455              :     /* Next Tab scans inside docs/ → guide.pdf */
     456            8 :     pty_send_key(s, PTY_KEY_TAB);
     457            8 :     pty_settle(s, SETTLE_MS);
     458            8 :     ASSERT_ROW_CONTAINS(s, 0, "guide.pdf");
     459              : 
     460              :     char expected[512];
     461            8 :     snprintf(expected, sizeof(expected), "%s/docs/guide.pdf", g_root);
     462            8 :     pty_send_key(s, PTY_KEY_ENTER);
     463              :     char want[600];
     464            8 :     snprintf(want, sizeof(want), "RESULT: %s", expected);
     465            8 :     ASSERT_WAIT_FOR(s, want, WAIT_MS);
     466            8 :     pty_close(s);
     467              : }
     468              : 
     469              : /** Suffix after cursor is preserved throughout Tab cycling. */
     470            8 : static void test_suffix_preserved(void)
     471              : {
     472            8 :     make_file(g_root, "report.pdf");
     473            8 :     make_file(g_root, "readme.txt");
     474              : 
     475              :     /* Initial path: "<root>/filename.pdf", cursor just after '<root>/' */
     476              :     char initial[512];
     477            8 :     snprintf(initial, sizeof(initial), "%s/filename.pdf", g_root);
     478              : 
     479            8 :     PtySession *s = open_harness(initial);
     480            7 :     ASSERT(s != NULL, "harness launched");
     481            7 :     ASSERT_WAIT_FOR(s, initial, WAIT_MS);
     482              : 
     483              :     /* Move cursor to just after the '/' (Home, then right to skip "<root>/") */
     484            7 :     pty_send_key(s, PTY_KEY_HOME);
     485              :     /* advance past every character in g_root + '/' */
     486            7 :     size_t steps = strlen(g_root) + 1;
     487          126 :     for (size_t i = 0; i < steps; i++)
     488          119 :         pty_send_key(s, PTY_KEY_RIGHT);
     489            7 :     pty_settle(s, SETTLE_MS);
     490              : 
     491              :     /* Tab with empty prefix → first alphabetical match; suffix stays */
     492            7 :     pty_send_key(s, PTY_KEY_TAB);
     493            7 :     pty_settle(s, SETTLE_MS);
     494              : 
     495              :     /* Suffix "filename.pdf" must still be visible on the input row */
     496            7 :     ASSERT_ROW_CONTAINS(s, 0, "filename.pdf");
     497              : 
     498            7 :     pty_send_key(s, PTY_KEY_ESC);
     499            7 :     ASSERT_WAIT_FOR(s, "CANCELLED", WAIT_MS);
     500            7 :     pty_close(s);
     501              : }
     502              : 
     503              : /** Hidden files (dot-files) are excluded when prefix is empty. */
     504            7 : static void test_hidden_files_excluded(void)
     505              : {
     506              :     char td[512];
     507            7 :     snprintf(td, sizeof(td), "%s/hidden_test", g_root);
     508            7 :     mkdir(td, 0755);
     509            7 :     make_file(td, ".hidden_secret");
     510            7 :     make_file(td, "visible.cfg");
     511              : 
     512              :     char prefix[768];
     513            7 :     snprintf(prefix, sizeof(prefix), "%s/", td);
     514              : 
     515            7 :     PtySession *s = open_harness(prefix);
     516            6 :     ASSERT(s != NULL, "harness launched");
     517            6 :     ASSERT_WAIT_FOR(s, prefix, WAIT_MS);
     518              : 
     519            6 :     pty_send_key(s, PTY_KEY_TAB);
     520            6 :     pty_settle(s, SETTLE_MS);
     521              : 
     522              :     /* Input row must show the visible file (first Tab selection) */
     523            6 :     ASSERT_ROW_CONTAINS(s, 0, "visible.cfg");
     524              :     /* Hidden file must not appear anywhere on screen */
     525            6 :     ASSERT(pty_screen_contains(s, ".hidden_secret") == 0,
     526              :            "hidden file not on screen");
     527              : 
     528            6 :     pty_send_key(s, PTY_KEY_ESC);
     529            6 :     ASSERT_WAIT_FOR(s, "CANCELLED", WAIT_MS);
     530            6 :     pty_close(s);
     531              : }
     532              : 
     533              : /** Dot prefix reveals hidden files. */
     534            6 : static void test_dot_prefix_shows_hidden(void)
     535              : {
     536            6 :     make_file(g_root, ".bashrc");
     537            6 :     make_file(g_root, ".profile");
     538              : 
     539              :     char prefix[512];
     540            6 :     snprintf(prefix, sizeof(prefix), "%s/.", g_root);
     541              : 
     542            6 :     PtySession *s = open_harness(prefix);
     543            5 :     ASSERT(s != NULL, "harness launched");
     544            5 :     ASSERT_WAIT_FOR(s, prefix, WAIT_MS);
     545              : 
     546            5 :     pty_send_key(s, PTY_KEY_TAB);
     547            5 :     pty_settle(s, SETTLE_MS);
     548              : 
     549              :     /* First alphabetical dot-file (.bashrc) should appear */
     550            5 :     ASSERT_ROW_CONTAINS(s, 0, ".bashrc");
     551              : 
     552            5 :     pty_send_key(s, PTY_KEY_ESC);
     553            5 :     ASSERT_WAIT_FOR(s, "CANCELLED", WAIT_MS);
     554            5 :     pty_close(s);
     555              : }
     556              : 
     557              : /** No match: buffer is unchanged after Tab. */
     558            5 : static void test_no_match_leaves_buf_unchanged(void)
     559              : {
     560              :     /* Use empty dir (g_root has only previously created files from other
     561              :      * tests — use a fresh subdir to guarantee it is empty). */
     562              :     char emptydir[512];
     563            5 :     snprintf(emptydir, sizeof(emptydir), "%s/empty_notest", g_root);
     564            5 :     mkdir(emptydir, 0755);
     565              : 
     566              :     char prefix[768];
     567            5 :     snprintf(prefix, sizeof(prefix), "%s/zzz", emptydir);
     568              : 
     569            5 :     PtySession *s = open_harness(prefix);
     570            4 :     ASSERT(s != NULL, "harness launched");
     571            4 :     ASSERT_WAIT_FOR(s, prefix, WAIT_MS);
     572              : 
     573            4 :     pty_send_key(s, PTY_KEY_TAB);
     574            4 :     pty_settle(s, SETTLE_MS);
     575              : 
     576              :     /* Input row must still show the unchanged prefix */
     577            4 :     ASSERT_ROW_CONTAINS(s, 0, prefix);
     578              :     /* Completion bar (row 1) must not appear / must be empty */
     579            4 :     ASSERT(pty_row_contains(s, 1, "zzz") == 0, "no completion bar on no match");
     580              : 
     581            4 :     pty_send_key(s, PTY_KEY_ESC);
     582            4 :     ASSERT_WAIT_FOR(s, "CANCELLED", WAIT_MS);
     583            4 :     pty_close(s);
     584              : }
     585              : 
     586              : /* ── Entry point ─────────────────────────────────────────────────────── */
     587              : 
     588           21 : int main(int argc, char *argv[])
     589              : {
     590           21 :     if (argc < 2) {
     591            0 :         fprintf(stderr, "Usage: %s <input-line-harness>\n", argv[0]);
     592            0 :         return 1;
     593              :     }
     594           21 :     snprintf(g_harness_bin, sizeof(g_harness_bin), "%s", argv[1]);
     595              : 
     596           21 :     printf("--- InputLine / PathComplete PTY Test Suite ---\n\n");
     597              : 
     598           21 :     fs_setup();
     599              : 
     600              :     /* Basic editing */
     601           21 :     printf("[Basic editing]\n");
     602           21 :     RUN_TEST(test_prompt_displayed);
     603           20 :     RUN_TEST(test_typing_appears_on_screen);
     604           19 :     RUN_TEST(test_backspace_deletes_char);
     605           18 :     RUN_TEST(test_initial_text_confirmed);
     606           17 :     RUN_TEST(test_esc_cancels);
     607           16 :     RUN_TEST(test_typed_text_in_result);
     608           15 :     RUN_TEST(test_arrow_keys_move_cursor);
     609           14 :     RUN_TEST(test_home_end_keys);
     610              : 
     611              :     /* Tab completion */
     612           13 :     printf("\n[Tab completion]\n");
     613           13 :     RUN_TEST(test_tab_single_match);
     614           12 :     RUN_TEST(test_tab_shows_completion_bar);
     615           11 :     RUN_TEST(test_tab_cycles_forward);
     616           10 :     RUN_TEST(test_shift_tab_cycles_backward);
     617            9 :     RUN_TEST(test_slash_enters_subdirectory);
     618            8 :     RUN_TEST(test_suffix_preserved);
     619            7 :     RUN_TEST(test_hidden_files_excluded);
     620            6 :     RUN_TEST(test_dot_prefix_shows_hidden);
     621            5 :     RUN_TEST(test_no_match_leaves_buf_unchanged);
     622            4 :     RUN_TEST(test_utf8_accented_input);
     623            3 :     RUN_TEST(test_utf8_backspace_deletes_code_point);
     624            2 :     RUN_TEST(test_utf8_insert_in_middle);
     625              : 
     626            1 :     fs_teardown();
     627              : 
     628            1 :     printf("\n--- Test Results ---\n");
     629            1 :     printf("Tests Run:    %d\n", g_tests_run);
     630            1 :     printf("Tests Passed: %d\n", g_tests_run - g_tests_failed);
     631            1 :     printf("Tests Failed: %d\n", g_tests_failed);
     632              : 
     633            1 :     return (g_tests_failed > 0) ? 1 : 0;
     634              : }
        

Generated by: LCOV version 2.0-1