LCOV - code coverage report
Current view: top level - tests/unit - test_path_complete.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 218 218
Test Date: 2026-04-15 21:12:52 Functions: 100.0 % 20 20

            Line data    Source code
       1              : #include "test_helpers.h"
       2              : #include "input_line.h"
       3              : #include "path_complete.h"
       4              : #include <stdio.h>
       5              : #include <stdlib.h>
       6              : #include <string.h>
       7              : #include <sys/stat.h>
       8              : #include <unistd.h>
       9              : 
      10              : /* ── Test fixture ─────────────────────────────────────────���──────────── */
      11              : 
      12              : static char g_root[256]; /* per-run temp root, removed at end */
      13              : 
      14            1 : static void fixture_setup(void) {
      15            1 :     snprintf(g_root, sizeof(g_root), "/tmp/pc_test_%d", getpid());
      16            1 :     mkdir(g_root, 0755);
      17            1 : }
      18              : 
      19            1 : static void fixture_teardown(void) {
      20              :     /* Recursively remove the temp tree via shell to keep test code short. */
      21            1 :     char cmd[512];
      22            1 :     snprintf(cmd, sizeof(cmd), "rm -rf '%s'", g_root);
      23            1 :     if (system(cmd)) { /* cleanup failure ignored in tests */ }
      24            1 : }
      25              : 
      26              : /* Create a test sub-directory under g_root; return its path in out[outsz]. */
      27              : #pragma GCC diagnostic push
      28              : #pragma GCC diagnostic ignored "-Wformat-truncation"
      29           11 : static void td_make(const char *name, char *out, size_t outsz) {
      30           11 :     snprintf(out, outsz, "%s/%s", g_root, name);
      31           11 :     mkdir(out, 0755);
      32           11 : }
      33              : #pragma GCC diagnostic pop
      34              : 
      35              : /* Create an empty regular file at dir/name. */
      36           20 : static void make_file(const char *dir, const char *name) {
      37           20 :     char path[512];
      38           20 :     snprintf(path, sizeof(path), "%s/%s", dir, name);
      39           20 :     FILE *f = fopen(path, "w");
      40           20 :     if (f) fclose(f);
      41           20 : }
      42              : 
      43              : /* Create a sub-directory at dir/name. */
      44            1 : static void make_subdir(const char *dir, const char *name) {
      45            1 :     char path[512];
      46            1 :     snprintf(path, sizeof(path), "%s/%s", dir, name);
      47            1 :     mkdir(path, 0755);
      48            1 : }
      49              : 
      50              : /* Initialise an InputLine with path_complete_attach and buf = text. */
      51           10 : static void il_setup(InputLine *il, char *buf, size_t bufsz, const char *text) {
      52           10 :     input_line_init(il, buf, bufsz, text);
      53           10 :     path_complete_attach(il);
      54           10 : }
      55              : 
      56              : /* ── Tests ───────────────────────────────────────────────────────────── */
      57              : 
      58            1 : static void test_attach_sets_callbacks(void) {
      59            1 :     char buf[256] = "";
      60            1 :     InputLine il;
      61            1 :     input_line_init(&il, buf, sizeof(buf), "");
      62              : 
      63            1 :     ASSERT(il.tab_fn       == NULL, "tab_fn NULL before attach");
      64            1 :     ASSERT(il.shift_tab_fn == NULL, "shift_tab_fn NULL before attach");
      65            1 :     ASSERT(il.render_below == NULL, "render_below NULL before attach");
      66              : 
      67            1 :     path_complete_attach(&il);
      68              : 
      69            1 :     ASSERT(il.tab_fn       != NULL, "tab_fn set after attach");
      70            1 :     ASSERT(il.shift_tab_fn != NULL, "shift_tab_fn set after attach");
      71            1 :     ASSERT(il.render_below != NULL, "render_below set after attach");
      72              : }
      73              : 
      74            1 : static void test_reset_idempotent(void) {
      75            1 :     path_complete_reset(); /* no crash on empty state */
      76            1 :     path_complete_reset(); /* double reset safe */
      77            1 : }
      78              : 
      79            1 : static void test_no_match_leaves_buf_unchanged(void) {
      80            1 :     char td[256]; td_make("no_match", td, sizeof(td));
      81              :     /* dir is empty; type a prefix that matches nothing */
      82            1 :     char path[512]; snprintf(path, sizeof(path), "%s/zzz", td);
      83              : 
      84            1 :     char buf[512]; InputLine il;
      85            1 :     il_setup(&il, buf, sizeof(buf), path);
      86            1 :     il.tab_fn(&il);
      87              : 
      88            1 :     ASSERT(strcmp(il.buf, path) == 0, "buf unchanged when no match");
      89            1 :     ASSERT(il.cur == strlen(path),    "cur unchanged when no match");
      90            1 :     path_complete_reset();
      91              : }
      92              : 
      93            1 : static void test_single_match_completes_fully(void) {
      94            1 :     char td[256]; td_make("single", td, sizeof(td));
      95            1 :     make_file(td, "report.pdf");
      96              : 
      97            1 :     char prefix[512]; snprintf(prefix, sizeof(prefix), "%s/rep", td);
      98            1 :     char buf[512]; InputLine il;
      99            1 :     il_setup(&il, buf, sizeof(buf), prefix);
     100            1 :     il.tab_fn(&il);
     101              : 
     102            1 :     char expected[512]; snprintf(expected, sizeof(expected), "%s/report.pdf", td);
     103            1 :     ASSERT(strcmp(il.buf, expected) == 0, "single match: full completion");
     104            1 :     ASSERT(il.cur == strlen(expected),    "cursor at end after single match");
     105            1 :     path_complete_reset();
     106              : }
     107              : 
     108            1 : static void test_cycles_forward_through_matches(void) {
     109            1 :     char td[256]; td_make("cycle_fwd", td, sizeof(td));
     110            1 :     make_file(td, "aaa.txt");
     111            1 :     make_file(td, "aab.txt");
     112            1 :     make_file(td, "aac.txt");
     113              : 
     114            1 :     char prefix[512]; snprintf(prefix, sizeof(prefix), "%s/aa", td);
     115            1 :     char buf[512]; InputLine il;
     116            1 :     il_setup(&il, buf, sizeof(buf), prefix);
     117              : 
     118            1 :     char e1[512], e2[512], e3[512];
     119            1 :     snprintf(e1, sizeof(e1), "%s/aaa.txt", td);
     120            1 :     snprintf(e2, sizeof(e2), "%s/aab.txt", td);
     121            1 :     snprintf(e3, sizeof(e3), "%s/aac.txt", td);
     122              : 
     123            1 :     il.tab_fn(&il);
     124            1 :     ASSERT(strcmp(il.buf, e1) == 0, "1st Tab → aaa.txt");
     125              : 
     126            1 :     il.tab_fn(&il);
     127            1 :     ASSERT(strcmp(il.buf, e2) == 0, "2nd Tab → aab.txt");
     128              : 
     129            1 :     il.tab_fn(&il);
     130            1 :     ASSERT(strcmp(il.buf, e3) == 0, "3rd Tab → aac.txt");
     131              : 
     132            1 :     il.tab_fn(&il); /* wrap */
     133            1 :     ASSERT(strcmp(il.buf, e1) == 0, "4th Tab wraps to aaa.txt");
     134            1 :     path_complete_reset();
     135              : }
     136              : 
     137            1 : static void test_cycles_backward_with_shift_tab(void) {
     138            1 :     char td[256]; td_make("cycle_bwd", td, sizeof(td));
     139            1 :     make_file(td, "baa.txt");
     140            1 :     make_file(td, "bab.txt");
     141            1 :     make_file(td, "bac.txt");
     142              : 
     143            1 :     char prefix[512]; snprintf(prefix, sizeof(prefix), "%s/ba", td);
     144            1 :     char buf[512]; InputLine il;
     145            1 :     il_setup(&il, buf, sizeof(buf), prefix);
     146              : 
     147            1 :     char e1[512], e2[512], e3[512];
     148            1 :     snprintf(e1, sizeof(e1), "%s/baa.txt", td);
     149            1 :     snprintf(e2, sizeof(e2), "%s/bab.txt", td);
     150            1 :     snprintf(e3, sizeof(e3), "%s/bac.txt", td);
     151              : 
     152            1 :     il.tab_fn(&il);              /* → baa.txt (idx 0) */
     153            1 :     il.shift_tab_fn(&il);        /* backward wrap → bac.txt (idx 2) */
     154            1 :     ASSERT(strcmp(il.buf, e3) == 0, "Shift+Tab wraps backward to bac.txt");
     155              : 
     156            1 :     il.shift_tab_fn(&il);        /* → bab.txt */
     157            1 :     ASSERT(strcmp(il.buf, e2) == 0, "Shift+Tab → bab.txt");
     158              : 
     159            1 :     il.shift_tab_fn(&il);        /* → baa.txt */
     160            1 :     ASSERT(strcmp(il.buf, e1) == 0, "Shift+Tab → baa.txt");
     161            1 :     path_complete_reset();
     162              : }
     163              : 
     164            1 : static void test_shift_tab_without_active_list_is_noop(void) {
     165            1 :     char td[256]; td_make("stab_noop", td, sizeof(td));
     166            1 :     make_file(td, "x.txt");
     167              : 
     168            1 :     char path[512]; snprintf(path, sizeof(path), "%s/x.txt", td);
     169            1 :     char buf[512]; InputLine il;
     170            1 :     il_setup(&il, buf, sizeof(buf), path);
     171              : 
     172              :     /* No Tab pressed yet; shift_tab_fn should be a no-op */
     173            1 :     il.shift_tab_fn(&il);
     174            1 :     ASSERT(strcmp(il.buf, path) == 0, "Shift+Tab no-op without active list");
     175            1 :     path_complete_reset();
     176              : }
     177              : 
     178            1 : static void test_suffix_preserved_when_cursor_in_middle(void) {
     179            1 :     char td[256]; td_make("suffix", td, sizeof(td));
     180            1 :     make_file(td, "report.pdf");
     181            1 :     make_file(td, "readme.txt");
     182              : 
     183              :     /* buf = "<td>/filename.pdf", cursor right after "<td>/" */
     184            1 :     char buf[512];
     185            1 :     snprintf(buf, sizeof(buf), "%s/filename.pdf", td);
     186            1 :     InputLine il;
     187            1 :     input_line_init(&il, buf, sizeof(buf), buf);
     188            1 :     il.cur = strlen(td) + 1; /* cursor points just after the '/' */
     189            1 :     path_complete_attach(&il);
     190              : 
     191            1 :     il.tab_fn(&il); /* prefix="" → first alphabetical match selected */
     192              : 
     193              :     /* The suffix "filename.pdf" must appear at the end of the result */
     194            1 :     size_t blen = strlen(il.buf);
     195            1 :     const char *suffix = "filename.pdf";
     196            1 :     size_t slen = strlen(suffix);
     197            1 :     ASSERT(blen > slen, "result longer than suffix");
     198            1 :     ASSERT(strcmp(il.buf + blen - slen, suffix) == 0,
     199              :            "suffix 'filename.pdf' preserved after completion");
     200              : 
     201              :     /* Cursor is at the end of the completed prefix, not at the end of buf */
     202            1 :     ASSERT(il.cur < il.len, "cursor before end when suffix present");
     203            1 :     path_complete_reset();
     204              : }
     205              : 
     206            1 : static void test_edit_after_cycle_triggers_fresh_scan(void) {
     207            1 :     char td[256]; td_make("rescan", td, sizeof(td));
     208            1 :     make_file(td, "cat.txt");
     209            1 :     make_file(td, "car.txt");
     210              : 
     211            1 :     char prefix[512]; snprintf(prefix, sizeof(prefix), "%s/ca", td);
     212            1 :     char buf[512]; InputLine il;
     213            1 :     il_setup(&il, buf, sizeof(buf), prefix);
     214              : 
     215            1 :     il.tab_fn(&il); /* first match: car.txt (alphabetical) */
     216              : 
     217              :     /* Simulate user editing the buffer back to the original prefix */
     218            1 :     snprintf(il.buf, il.bufsz, "%s/ca", td);
     219            1 :     il.len = strlen(il.buf);
     220            1 :     il.cur = il.len;
     221              : 
     222              :     /* Next Tab must do a fresh scan, not rely on stale expected */
     223            1 :     il.tab_fn(&il);
     224            1 :     char expected[512]; snprintf(expected, sizeof(expected), "%s/car.txt", td);
     225            1 :     ASSERT(strcmp(il.buf, expected) == 0, "fresh scan after edit → car.txt");
     226            1 :     path_complete_reset();
     227              : }
     228              : 
     229            1 : static void test_typing_slash_enters_subdirectory(void) {
     230            1 :     char td[256]; td_make("enter_dir", td, sizeof(td));
     231            1 :     make_subdir(td, "docs");
     232            1 :     make_file(td, "docs/guide.pdf"); /* nested file */
     233              : 
     234            1 :     char prefix[512]; snprintf(prefix, sizeof(prefix), "%s/doc", td);
     235            1 :     char buf[512]; InputLine il;
     236            1 :     il_setup(&il, buf, sizeof(buf), prefix);
     237              : 
     238            1 :     il.tab_fn(&il); /* completes to "<td>/docs" */
     239            1 :     char exp_docs[512]; snprintf(exp_docs, sizeof(exp_docs), "%s/docs", td);
     240            1 :     ASSERT(strcmp(il.buf, exp_docs) == 0, "completes to 'docs'");
     241              : 
     242              :     /* User types '/' → buf becomes "<td>/docs/" */
     243            1 :     size_t len = il.len;
     244            1 :     il.buf[len] = '/'; il.buf[len+1] = '\0';
     245            1 :     il.len = len + 1; il.cur = il.len;
     246              : 
     247              :     /* Next Tab must scan inside docs/, not cycle old list */
     248            1 :     il.tab_fn(&il);
     249            1 :     char expected[512]; snprintf(expected, sizeof(expected), "%s/docs/guide.pdf", td);
     250            1 :     ASSERT(strcmp(il.buf, expected) == 0, "Tab after '/' enters subdirectory");
     251            1 :     path_complete_reset();
     252              : }
     253              : 
     254            1 : static void test_results_are_sorted_alphabetically(void) {
     255            1 :     char td[256]; td_make("sorted", td, sizeof(td));
     256            1 :     make_file(td, "zebra.txt");
     257            1 :     make_file(td, "alpha.txt");
     258            1 :     make_file(td, "mango.txt");
     259              : 
     260            1 :     char prefix[512]; snprintf(prefix, sizeof(prefix), "%s/", td);
     261            1 :     char buf[512]; InputLine il;
     262            1 :     il_setup(&il, buf, sizeof(buf), prefix);
     263              : 
     264            1 :     char prev[512] = "";
     265            4 :     for (int i = 0; i < 3; i++) {
     266            3 :         il.tab_fn(&il);
     267              :         /* Extract just the filename part */
     268            3 :         const char *name = il.buf + strlen(td) + 1;
     269            3 :         if (i > 0)
     270            2 :             ASSERT(strcmp(prev, name) < 0, "each match strictly after previous (sorted)");
     271            3 :         strncpy(prev, name, sizeof(prev) - 1);
     272              :     }
     273            1 :     path_complete_reset();
     274              : }
     275              : 
     276            1 : static void test_hidden_files_excluded_with_empty_prefix(void) {
     277            1 :     char td[256]; td_make("hidden", td, sizeof(td));
     278            1 :     make_file(td, ".hidden");
     279            1 :     make_file(td, "visible.txt");
     280              : 
     281            1 :     char prefix[512]; snprintf(prefix, sizeof(prefix), "%s/", td);
     282            1 :     char buf[512]; InputLine il;
     283            1 :     il_setup(&il, buf, sizeof(buf), prefix);
     284              : 
     285            1 :     il.tab_fn(&il);
     286            1 :     ASSERT(strstr(il.buf, ".hidden") == NULL, "hidden file excluded when prefix empty");
     287            1 :     ASSERT(strstr(il.buf, "visible.txt") != NULL, "visible file included");
     288            1 :     path_complete_reset();
     289              : }
     290              : 
     291            1 : static void test_hidden_files_included_with_dot_prefix(void) {
     292            1 :     char td[256]; td_make("hidden_dot", td, sizeof(td));
     293            1 :     make_file(td, ".bashrc");
     294            1 :     make_file(td, ".profile");
     295              : 
     296            1 :     char prefix[512]; snprintf(prefix, sizeof(prefix), "%s/.", td);
     297            1 :     char buf[512]; InputLine il;
     298            1 :     il_setup(&il, buf, sizeof(buf), prefix);
     299              : 
     300            1 :     il.tab_fn(&il);
     301              :     /* First alphabetical match starting with '.' should appear */
     302            1 :     const char *name = il.buf + strlen(td) + 1;
     303            1 :     ASSERT(name[0] == '.', "hidden file included when prefix starts with '.'");
     304            1 :     path_complete_reset();
     305              : }
     306              : 
     307              : /* ── Entry point ─────────────────────────────────────────────────────── */
     308              : 
     309            1 : void test_path_complete(void) {
     310            1 :     fixture_setup();
     311              : 
     312            1 :     RUN_TEST(test_attach_sets_callbacks);
     313            1 :     RUN_TEST(test_reset_idempotent);
     314            1 :     RUN_TEST(test_no_match_leaves_buf_unchanged);
     315            1 :     RUN_TEST(test_single_match_completes_fully);
     316            1 :     RUN_TEST(test_cycles_forward_through_matches);
     317            1 :     RUN_TEST(test_cycles_backward_with_shift_tab);
     318            1 :     RUN_TEST(test_shift_tab_without_active_list_is_noop);
     319            1 :     RUN_TEST(test_suffix_preserved_when_cursor_in_middle);
     320            1 :     RUN_TEST(test_edit_after_cycle_triggers_fresh_scan);
     321            1 :     RUN_TEST(test_typing_slash_enters_subdirectory);
     322            1 :     RUN_TEST(test_results_are_sorted_alphabetically);
     323            1 :     RUN_TEST(test_hidden_files_excluded_with_empty_prefix);
     324            1 :     RUN_TEST(test_hidden_files_included_with_dot_prefix);
     325              : 
     326            1 :     fixture_teardown();
     327            1 : }
        

Generated by: LCOV version 2.0-1