LCOV - code coverage report
Current view: top level - tests/unit - test_platform.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 97.6 % 82 80
Test Date: 2026-08-21 10:11:28 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 <stdlib.h>
       5              : #include <string.h>
       6              : #include <unistd.h>
       7              : #include <fcntl.h>
       8              : #include <locale.h>
       9              : 
      10            1 : void test_platform(void) {
      11              : 
      12              :     /* ── terminal_wcwidth ───────────────────────────────────────────── */
      13              : 
      14            1 :     setlocale(LC_ALL, "");
      15              : 
      16              :     /* ASCII printable characters: width 1 */
      17            1 :     ASSERT(terminal_wcwidth('A')    == 1, "wcwidth: ASCII letter should be 1");
      18            1 :     ASSERT(terminal_wcwidth(' ')    == 1, "wcwidth: space should be 1");
      19            1 :     ASSERT(terminal_wcwidth('0')    == 1, "wcwidth: digit should be 1");
      20              : 
      21              :     /* Control characters: width 0 (not negative) */
      22            1 :     ASSERT(terminal_wcwidth('\n')   == 0, "wcwidth: newline should be 0");
      23            1 :     ASSERT(terminal_wcwidth('\t')   == 0, "wcwidth: tab should be 0");
      24            1 :     ASSERT(terminal_wcwidth(0x01)   == 0, "wcwidth: control char should be 0");
      25              : 
      26              :     /* Common accented Latin characters: width 1 */
      27            1 :     ASSERT(terminal_wcwidth(0x00E9) == 1, "wcwidth: e-acute (U+00E9) should be 1");
      28            1 :     ASSERT(terminal_wcwidth(0x00E1) == 1, "wcwidth: a-acute (U+00E1) should be 1");
      29            1 :     ASSERT(terminal_wcwidth(0x0151) == 1, "wcwidth: o-double-acute (U+0151) should be 1");
      30              : 
      31              :     /* CJK ideograph: width 2 */
      32            1 :     ASSERT(terminal_wcwidth(0x4E2D) == 2, "wcwidth: CJK U+4E2D should be 2");
      33            1 :     ASSERT(terminal_wcwidth(0x3042) == 2, "wcwidth: Hiragana U+3042 should be 2");
      34              : 
      35              :     /* ── terminal_is_tty ────────────────────────────────────────────── */
      36              : 
      37              :     /* When stdout is a pipe (as in test runner), fd 1 is not a tty */
      38              :     /* We cannot assert a specific value since it depends on the test
      39              :      * environment, but the function must return 0 or 1 without crashing. */
      40            1 :     int r = terminal_is_tty(STDOUT_FILENO);
      41            1 :     ASSERT(r == 0 || r == 1, "terminal_is_tty must return 0 or 1");
      42              : 
      43              :     /* Invalid fd must return 0 */
      44            1 :     ASSERT(terminal_is_tty(-1) == 0, "terminal_is_tty(-1) should return 0");
      45            1 :     ASSERT(terminal_is_tty(9999) == 0, "terminal_is_tty(9999) should return 0");
      46              : 
      47              :     /* ── terminal_cols ──────────────────────────────────────────────── */
      48              : 
      49              :     /* When stdout is not a tty (e.g. piped in CI), must fall back to 80. */
      50            1 :     if (!terminal_is_tty(STDOUT_FILENO)) {
      51            1 :         ASSERT(terminal_cols() == 80,
      52              :                "terminal_cols() should return 80 when stdout is not a tty");
      53              :     } else {
      54              :         /* On a real terminal it must be a positive value. */
      55            0 :         ASSERT(terminal_cols() > 0, "terminal_cols() must be positive");
      56              :     }
      57              : 
      58              :     /* ── terminal_rows ──────────────────────────────────────────────── */
      59              : 
      60            1 :     int rows = terminal_rows();
      61            1 :     if (!terminal_is_tty(STDOUT_FILENO)) {
      62            1 :         ASSERT(rows == 0, "terminal_rows() should return 0 when stdout is not a tty");
      63              :     } else {
      64            0 :         ASSERT(rows > 0, "terminal_rows() must be positive on a real terminal");
      65              :     }
      66              : 
      67              :     /* ── terminal_raw_enter / terminal_raw_exit ─────────────────────── */
      68              : 
      69              :     /* When stdin is not a tty (as in test runner), raw_enter must return NULL
      70              :      * gracefully (tcgetattr will fail). */
      71            1 :     if (!terminal_is_tty(STDIN_FILENO)) {
      72            1 :         TermRawState *s = terminal_raw_enter();
      73            1 :         ASSERT(s == NULL,
      74              :                "terminal_raw_enter should return NULL when stdin is not a tty");
      75              :         /* terminal_raw_exit(NULL) and terminal_raw_exit(&NULL) must be safe. */
      76            1 :         terminal_raw_exit(NULL);
      77            1 :         terminal_raw_exit(&s);   /* s is already NULL */
      78              :     }
      79              : 
      80              :     /* ── terminal_read_password — guard clauses ─────────────────────── */
      81              : 
      82              :     char pwbuf[64];
      83              :     /* NULL buf → -1 */
      84            1 :     ASSERT(terminal_read_password("test", NULL, 64) == -1,
      85              :            "terminal_read_password: NULL buf should return -1");
      86              :     /* size == 0 → -1 */
      87            1 :     ASSERT(terminal_read_password("test", pwbuf, 0) == -1,
      88              :            "terminal_read_password: size 0 should return -1");
      89              : 
      90              :     /* EOF on stdin → -1.
      91              :      *
      92              :      * This must read from a stream we closed ourselves, never the inherited
      93              :      * stdin: "not a tty" does not imply "empty or closed".  When the test
      94              :      * runner is started with a socket on stdin — as happens under a job
      95              :      * runner — getline() blocks forever and the whole suite hangs with no
      96              :      * output, which is exactly what it did before. */
      97              :     {
      98              :         int pfd[2];
      99            1 :         if (pipe(pfd) == 0) {
     100            1 :             close(pfd[1]);                    /* immediate EOF */
     101            1 :             int saved = dup(STDIN_FILENO);
     102            1 :             dup2(pfd[0], STDIN_FILENO);
     103            1 :             close(pfd[0]);
     104            1 :             clearerr(stdin);
     105            1 :             int n = terminal_read_password("test", pwbuf, sizeof(pwbuf));
     106            1 :             dup2(saved, STDIN_FILENO);
     107            1 :             close(saved);
     108            1 :             clearerr(stdin);
     109            1 :             ASSERT(n == -1, "terminal_read_password: EOF on stdin returns -1");
     110              :         }
     111              :     }
     112              : 
     113              :     /* Non-tty stdin with CRLF input: covers the \\r stripping step
     114              :      * (terminal.c line 214: if (slen > 0 && line[slen-1] == '\\r')). */
     115              :     {
     116              :         int pfd[2];
     117            1 :         if (pipe(pfd) == 0) {
     118            1 :             const char *crlf_input = "secret\r\n";
     119            1 :             ssize_t _wr = write(pfd[1], crlf_input, 8); (void)_wr;
     120            1 :             close(pfd[1]);
     121            1 :             int saved = dup(STDIN_FILENO);
     122            1 :             dup2(pfd[0], STDIN_FILENO);
     123            1 :             close(pfd[0]);
     124            1 :             clearerr(stdin); /* clear EOF from any previous non-tty read */
     125              :             char pwbuf2[64];
     126            1 :             int n2 = terminal_read_password("Password", pwbuf2, sizeof(pwbuf2));
     127            1 :             dup2(saved, STDIN_FILENO);
     128            1 :             close(saved);
     129            1 :             ASSERT(n2 == 6 && strcmp(pwbuf2, "secret") == 0,
     130              :                    "terminal_read_password: CRLF input strips \\r");
     131              :         }
     132              :     }
     133              : 
     134              :     /* ── platform_home_dir ──────────────────────────────────────────── */
     135              : 
     136            1 :     const char *home = platform_home_dir();
     137            1 :     ASSERT(home != NULL, "platform_home_dir should not return NULL");
     138            1 :     ASSERT(home[0] == '/', "platform_home_dir should return an absolute path");
     139              : 
     140              :     /* Must still work when HOME is unset (getpwuid fallback) */
     141            1 :     char saved_home[4096] = {0};
     142            1 :     const char *env_home = getenv("HOME");
     143            1 :     if (env_home) snprintf(saved_home, sizeof(saved_home), "%s", env_home);
     144            1 :     unsetenv("HOME");
     145            1 :     home = platform_home_dir();
     146            1 :     ASSERT(home != NULL, "platform_home_dir should fall back to getpwuid");
     147            1 :     if (saved_home[0]) setenv("HOME", saved_home, 1);
     148              : 
     149              :     /* ── platform_cache_dir ─────────────────────────────────────────── */
     150              : 
     151              :     /* Default: ~/.cache */
     152            1 :     unsetenv("XDG_CACHE_HOME");
     153            1 :     const char *cache = platform_cache_dir();
     154            1 :     ASSERT(cache != NULL, "platform_cache_dir should not return NULL");
     155            1 :     ASSERT(strstr(cache, ".cache") != NULL,
     156              :            "platform_cache_dir default should contain '.cache'");
     157              : 
     158              :     /* XDG override */
     159            1 :     setenv("XDG_CACHE_HOME", "/tmp/test-xdg-cache", 1);
     160            1 :     cache = platform_cache_dir();
     161            1 :     ASSERT(cache != NULL, "platform_cache_dir XDG should not return NULL");
     162            1 :     ASSERT(strcmp(cache, "/tmp/test-xdg-cache") == 0,
     163              :            "platform_cache_dir should respect XDG_CACHE_HOME");
     164            1 :     unsetenv("XDG_CACHE_HOME");
     165              : 
     166              :     /* ── platform_config_dir ────────────────────────────────────────── */
     167              : 
     168              :     /* Default: ~/.config */
     169            1 :     unsetenv("XDG_CONFIG_HOME");
     170            1 :     const char *cfg = platform_config_dir();
     171            1 :     ASSERT(cfg != NULL, "platform_config_dir should not return NULL");
     172            1 :     ASSERT(strstr(cfg, ".config") != NULL,
     173              :            "platform_config_dir default should contain '.config'");
     174              : 
     175              :     /* XDG override */
     176            1 :     setenv("XDG_CONFIG_HOME", "/tmp/test-xdg-config", 1);
     177            1 :     cfg = platform_config_dir();
     178            1 :     ASSERT(cfg != NULL, "platform_config_dir XDG should not return NULL");
     179            1 :     ASSERT(strcmp(cfg, "/tmp/test-xdg-config") == 0,
     180              :            "platform_config_dir should respect XDG_CONFIG_HOME");
     181            1 :     unsetenv("XDG_CONFIG_HOME");
     182              : }
        

Generated by: LCOV version 2.0-1