LCOV - code coverage report
Current view: top level - tests/unit - test_platform.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 93.1 % 87 81
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 1 1

            Line data    Source code
       1              : #include "test_helpers.h"
       2              : #include "platform/terminal.h"
       3              : #include "platform/path.h"
       4              : #include <signal.h>
       5              : #include <stdlib.h>
       6              : #include <string.h>
       7              : #include <unistd.h>
       8              : #include <locale.h>
       9              : #include <sys/wait.h>
      10              : 
      11            1 : void test_platform(void) {
      12              : 
      13              :     /* ── terminal_wcwidth ───────────────────────────────────────────── */
      14              : 
      15            1 :     setlocale(LC_ALL, "");
      16              : 
      17              :     /* ASCII printable characters: width 1 */
      18            1 :     ASSERT(terminal_wcwidth('A')    == 1, "wcwidth: ASCII letter should be 1");
      19            1 :     ASSERT(terminal_wcwidth(' ')    == 1, "wcwidth: space should be 1");
      20            1 :     ASSERT(terminal_wcwidth('0')    == 1, "wcwidth: digit should be 1");
      21              : 
      22              :     /* Control characters: width 0 (not negative) */
      23            1 :     ASSERT(terminal_wcwidth('\n')   == 0, "wcwidth: newline should be 0");
      24            1 :     ASSERT(terminal_wcwidth('\t')   == 0, "wcwidth: tab should be 0");
      25            1 :     ASSERT(terminal_wcwidth(0x01)   == 0, "wcwidth: control char should be 0");
      26              : 
      27              :     /* Common accented Latin characters: width 1 */
      28            1 :     ASSERT(terminal_wcwidth(0x00E9) == 1, "wcwidth: e-acute (U+00E9) should be 1");
      29            1 :     ASSERT(terminal_wcwidth(0x00E1) == 1, "wcwidth: a-acute (U+00E1) should be 1");
      30            1 :     ASSERT(terminal_wcwidth(0x0151) == 1, "wcwidth: o-double-acute (U+0151) should be 1");
      31              : 
      32              :     /* Combining diacritic: width 0 */
      33            1 :     ASSERT(terminal_wcwidth(0x0300) == 0, "wcwidth: combining grave (U+0300) should be 0");
      34              : 
      35              :     /* Zero-width joiner: width 0 */
      36            1 :     ASSERT(terminal_wcwidth(0x200D) == 0, "wcwidth: zero-width joiner (U+200D) should be 0");
      37              : 
      38              :     /* CJK ideograph: width 2 */
      39            1 :     ASSERT(terminal_wcwidth(0x4E2D) == 2, "wcwidth: CJK U+4E2D should be 2");
      40            1 :     ASSERT(terminal_wcwidth(0x3042) == 2, "wcwidth: Hiragana U+3042 should be 2");
      41              : 
      42              :     /* ── terminal_is_tty ────────────────────────────────────────────── */
      43              : 
      44              :     /* When stdout is a pipe (as in test runner), fd 1 is not a tty */
      45              :     /* We cannot assert a specific value since it depends on the test
      46              :      * environment, but the function must return 0 or 1 without crashing. */
      47            1 :     int r = terminal_is_tty(STDOUT_FILENO);
      48            1 :     ASSERT(r == 0 || r == 1, "terminal_is_tty must return 0 or 1");
      49              : 
      50              :     /* Invalid fd must return 0 */
      51            1 :     ASSERT(terminal_is_tty(-1) == 0, "terminal_is_tty(-1) should return 0");
      52            1 :     ASSERT(terminal_is_tty(9999) == 0, "terminal_is_tty(9999) should return 0");
      53              : 
      54              :     /* ── terminal_cols ──────────────────────────────────────────────── */
      55              : 
      56              :     /* When stdout is not a tty (e.g. piped in CI), must fall back to 80. */
      57            1 :     if (!terminal_is_tty(STDOUT_FILENO)) {
      58            1 :         ASSERT(terminal_cols() == 80,
      59              :                "terminal_cols() should return 80 when stdout is not a tty");
      60              :     } else {
      61              :         /* On a real terminal it must be a positive value. */
      62            0 :         ASSERT(terminal_cols() > 0, "terminal_cols() must be positive");
      63              :     }
      64              : 
      65              :     /* ── terminal_rows ──────────────────────────────────────────────── */
      66              : 
      67            1 :     int rows = terminal_rows();
      68            1 :     if (!terminal_is_tty(STDOUT_FILENO)) {
      69            1 :         ASSERT(rows == 0, "terminal_rows() should return 0 when stdout is not a tty");
      70              :     } else {
      71            0 :         ASSERT(rows > 0, "terminal_rows() must be positive on a real terminal");
      72              :     }
      73              : 
      74              :     /* ── terminal_raw_enter / terminal_raw_exit ─────────────────────── */
      75              : 
      76              :     /* When stdin is not a tty (as in test runner), raw_enter must return NULL
      77              :      * gracefully (tcgetattr will fail). */
      78            1 :     if (!terminal_is_tty(STDIN_FILENO)) {
      79            1 :         TermRawState *s = terminal_raw_enter();
      80            1 :         ASSERT(s == NULL,
      81              :                "terminal_raw_enter should return NULL when stdin is not a tty");
      82              :         /* terminal_raw_exit(NULL) and terminal_raw_exit(&NULL) must be safe. */
      83            1 :         terminal_raw_exit(NULL);
      84            1 :         terminal_raw_exit(&s);   /* s is already NULL */
      85              :     }
      86              : 
      87              :     /* ── terminal_read_password — guard clauses ─────────────────────── */
      88              : 
      89              :     char pwbuf[64];
      90              :     /* NULL buf → -1 */
      91            1 :     ASSERT(terminal_read_password("test", NULL, 64) == -1,
      92              :            "terminal_read_password: NULL buf should return -1");
      93              :     /* size == 0 → -1 */
      94            1 :     ASSERT(terminal_read_password("test", pwbuf, 0) == -1,
      95              :            "terminal_read_password: size 0 should return -1");
      96              : 
      97              :     /* Non-tty stdin path: getline on an empty/closed stream returns -1. */
      98            1 :     if (!terminal_is_tty(STDIN_FILENO)) {
      99            1 :         int n = terminal_read_password("test", pwbuf, sizeof(pwbuf));
     100            1 :         ASSERT(n == -1 || n >= 0,
     101              :                "terminal_read_password non-tty: must not crash");
     102              :     }
     103              : 
     104              :     /* ── platform_home_dir ──────────────────────────────────────────── */
     105              : 
     106            1 :     const char *home = platform_home_dir();
     107            1 :     ASSERT(home != NULL, "platform_home_dir should not return NULL");
     108            1 :     ASSERT(home[0] == '/', "platform_home_dir should return an absolute path");
     109              : 
     110              :     /* Must still work when HOME is unset (getpwuid fallback) */
     111            1 :     char saved_home[4096] = {0};
     112            1 :     const char *env_home = getenv("HOME");
     113            1 :     if (env_home) snprintf(saved_home, sizeof(saved_home), "%s", env_home);
     114            1 :     unsetenv("HOME");
     115            1 :     home = platform_home_dir();
     116            1 :     ASSERT(home != NULL, "platform_home_dir should fall back to getpwuid");
     117            1 :     if (saved_home[0]) setenv("HOME", saved_home, 1);
     118              : 
     119              :     /* ── platform_cache_dir ─────────────────────────────────────────── */
     120              : 
     121              :     /* Default: ~/.cache */
     122            1 :     unsetenv("XDG_CACHE_HOME");
     123            1 :     const char *cache = platform_cache_dir();
     124            1 :     ASSERT(cache != NULL, "platform_cache_dir should not return NULL");
     125            1 :     ASSERT(strstr(cache, ".cache") != NULL,
     126              :            "platform_cache_dir default should contain '.cache'");
     127              : 
     128              :     /* XDG override */
     129            1 :     setenv("XDG_CACHE_HOME", "/tmp/test-xdg-cache", 1);
     130            1 :     cache = platform_cache_dir();
     131            1 :     ASSERT(cache != NULL, "platform_cache_dir XDG should not return NULL");
     132            1 :     ASSERT(strcmp(cache, "/tmp/test-xdg-cache") == 0,
     133              :            "platform_cache_dir should respect XDG_CACHE_HOME");
     134            1 :     unsetenv("XDG_CACHE_HOME");
     135              : 
     136              :     /* ── platform_config_dir ────────────────────────────────────────── */
     137              : 
     138              :     /* Default: ~/.config */
     139            1 :     unsetenv("XDG_CONFIG_HOME");
     140            1 :     const char *cfg = platform_config_dir();
     141            1 :     ASSERT(cfg != NULL, "platform_config_dir should not return NULL");
     142            1 :     ASSERT(strstr(cfg, ".config") != NULL,
     143              :            "platform_config_dir default should contain '.config'");
     144              : 
     145              :     /* XDG override */
     146            1 :     setenv("XDG_CONFIG_HOME", "/tmp/test-xdg-config", 1);
     147            1 :     cfg = platform_config_dir();
     148            1 :     ASSERT(cfg != NULL, "platform_config_dir XDG should not return NULL");
     149            1 :     ASSERT(strcmp(cfg, "/tmp/test-xdg-config") == 0,
     150              :            "platform_config_dir should respect XDG_CONFIG_HOME");
     151            1 :     unsetenv("XDG_CONFIG_HOME");
     152              : 
     153              :     /* ── SIGWINCH resize notifications ──────────────────────────────── */
     154              : 
     155              :     /* Before enabling the handler, consume should be a no-op. */
     156            1 :     ASSERT(terminal_consume_resize() == 0,
     157              :            "consume_resize before enable returns 0");
     158              : 
     159            1 :     terminal_enable_resize_notifications();
     160              :     /* Idempotent. */
     161            1 :     terminal_enable_resize_notifications();
     162              : 
     163              :     /* Still nothing pending until a signal is delivered. */
     164            1 :     ASSERT(terminal_consume_resize() == 0,
     165              :            "no resize pending right after enable");
     166              : 
     167              :     /* Simulate a resize by raising SIGWINCH and let the handler run. */
     168            1 :     raise(SIGWINCH);
     169            1 :     ASSERT(terminal_consume_resize() == 1, "resize observed after SIGWINCH");
     170              :     /* Flag should clear on first consume. */
     171            1 :     ASSERT(terminal_consume_resize() == 0, "resize flag clears after read");
     172              : 
     173              :     /* ── terminal_wait_key ──────────────────────────────────────────── */
     174              : 
     175              :     /* With a 0ms timeout and no piped input pending, wait_key should
     176              :      * return 0 (timeout) quickly. The test runner's stdin is closed or
     177              :      * empty, so poll() either times out or reports a hangup — both
     178              :      * count as "no actionable key ready". */
     179            1 :     int wk = terminal_wait_key(0);
     180            1 :     ASSERT(wk == 0 || wk == 1 || wk == -1,
     181              :            "wait_key returns a valid sentinel");
     182              : 
     183              :     /* Raise SIGWINCH mid-wait: poll returns EINTR → we return -1. */
     184            1 :     raise(SIGWINCH);
     185              :     /* The handler already ran and set the flag; poll may or may not
     186              :      * actually be interrupted (depends on whether raise delivers
     187              :      * synchronously). Either way consume_resize should now be 1. */
     188            1 :     ASSERT(terminal_consume_resize() == 1,
     189              :            "SIGWINCH raised right before wait still observable");
     190              : 
     191              :     /* ── terminal_install_cleanup_handlers ──────────────────────────── */
     192              : 
     193              :     /* Passing NULL must be a safe no-op (no crash). */
     194            1 :     terminal_install_cleanup_handlers(NULL);
     195              : 
     196              :     /* Verify that after installing handlers, SIGTERM is caught (not the
     197              :      * default SIG_DFL which would terminate us) and re-raised with
     198              :      * SIG_DFL — we use a child process so the parent survives. */
     199              :     {
     200            1 :         pid_t pid = fork();
     201            1 :         ASSERT(pid >= 0, "fork() for SIGTERM test must succeed");
     202            1 :         if (pid == 0) {
     203              :             /* Child: set up a dummy TermRawState (not a real tty here,
     204              :              * so tcsetattr will fail silently — that is acceptable). */
     205            0 :             TermRawState *dummy = terminal_raw_enter();
     206              :             /* raw_enter returns NULL when stdin is not a tty (test env);
     207              :              * install_cleanup_handlers is documented to accept NULL (no-op)
     208              :              * so this is safe.  The important thing is it does not crash. */
     209            0 :             terminal_install_cleanup_handlers(dummy);
     210              :             /* Send ourselves SIGTERM — the handler should re-raise with
     211              :              * SIG_DFL, killing the child with SIGTERM. */
     212            0 :             raise(SIGTERM);
     213              :             /* Should not reach here after re-raise with SIG_DFL. */
     214            0 :             _exit(42);
     215              :         }
     216              :         /* Parent waits for the child. */
     217            1 :         int status = 0;
     218            1 :         waitpid(pid, &status, 0);
     219              :         /* Child must have been terminated by a signal (SIGTERM), not
     220              :          * exited normally with code 42. */
     221            1 :         ASSERT(WIFSIGNALED(status),
     222              :                "install_cleanup_handlers: child terminated by signal");
     223            1 :         ASSERT(WTERMSIG(status) == SIGTERM,
     224              :                "install_cleanup_handlers: child terminated by SIGTERM");
     225              :     }
     226              : }
        

Generated by: LCOV version 2.0-1