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 : /* Non-tty stdin path: getline on an empty/closed stream returns -1. */
91 1 : if (!terminal_is_tty(STDIN_FILENO)) {
92 1 : int n = terminal_read_password("test", pwbuf, sizeof(pwbuf));
93 1 : ASSERT(n == -1 || n >= 0,
94 : "terminal_read_password non-tty: must not crash");
95 : }
96 :
97 : /* Non-tty stdin with CRLF input: covers the \\r stripping step
98 : * (terminal.c line 214: if (slen > 0 && line[slen-1] == '\\r')). */
99 1 : if (!terminal_is_tty(STDIN_FILENO)) {
100 : int pfd[2];
101 1 : if (pipe(pfd) == 0) {
102 1 : const char *crlf_input = "secret\r\n";
103 1 : ssize_t _wr = write(pfd[1], crlf_input, 8); (void)_wr;
104 1 : close(pfd[1]);
105 1 : int saved = dup(STDIN_FILENO);
106 1 : dup2(pfd[0], STDIN_FILENO);
107 1 : close(pfd[0]);
108 1 : clearerr(stdin); /* clear EOF from any previous non-tty read */
109 : char pwbuf2[64];
110 1 : int n2 = terminal_read_password("Password", pwbuf2, sizeof(pwbuf2));
111 1 : dup2(saved, STDIN_FILENO);
112 1 : close(saved);
113 1 : ASSERT(n2 == 6 && strcmp(pwbuf2, "secret") == 0,
114 : "terminal_read_password: CRLF input strips \\r");
115 : }
116 : }
117 :
118 : /* ── platform_home_dir ──────────────────────────────────────────── */
119 :
120 1 : const char *home = platform_home_dir();
121 1 : ASSERT(home != NULL, "platform_home_dir should not return NULL");
122 1 : ASSERT(home[0] == '/', "platform_home_dir should return an absolute path");
123 :
124 : /* Must still work when HOME is unset (getpwuid fallback) */
125 1 : char saved_home[4096] = {0};
126 1 : const char *env_home = getenv("HOME");
127 1 : if (env_home) snprintf(saved_home, sizeof(saved_home), "%s", env_home);
128 1 : unsetenv("HOME");
129 1 : home = platform_home_dir();
130 1 : ASSERT(home != NULL, "platform_home_dir should fall back to getpwuid");
131 1 : if (saved_home[0]) setenv("HOME", saved_home, 1);
132 :
133 : /* ── platform_cache_dir ─────────────────────────────────────────── */
134 :
135 : /* Default: ~/.cache */
136 1 : unsetenv("XDG_CACHE_HOME");
137 1 : const char *cache = platform_cache_dir();
138 1 : ASSERT(cache != NULL, "platform_cache_dir should not return NULL");
139 1 : ASSERT(strstr(cache, ".cache") != NULL,
140 : "platform_cache_dir default should contain '.cache'");
141 :
142 : /* XDG override */
143 1 : setenv("XDG_CACHE_HOME", "/tmp/test-xdg-cache", 1);
144 1 : cache = platform_cache_dir();
145 1 : ASSERT(cache != NULL, "platform_cache_dir XDG should not return NULL");
146 1 : ASSERT(strcmp(cache, "/tmp/test-xdg-cache") == 0,
147 : "platform_cache_dir should respect XDG_CACHE_HOME");
148 1 : unsetenv("XDG_CACHE_HOME");
149 :
150 : /* ── platform_config_dir ────────────────────────────────────────── */
151 :
152 : /* Default: ~/.config */
153 1 : unsetenv("XDG_CONFIG_HOME");
154 1 : const char *cfg = platform_config_dir();
155 1 : ASSERT(cfg != NULL, "platform_config_dir should not return NULL");
156 1 : ASSERT(strstr(cfg, ".config") != NULL,
157 : "platform_config_dir default should contain '.config'");
158 :
159 : /* XDG override */
160 1 : setenv("XDG_CONFIG_HOME", "/tmp/test-xdg-config", 1);
161 1 : cfg = platform_config_dir();
162 1 : ASSERT(cfg != NULL, "platform_config_dir XDG should not return NULL");
163 1 : ASSERT(strcmp(cfg, "/tmp/test-xdg-config") == 0,
164 : "platform_config_dir should respect XDG_CONFIG_HOME");
165 1 : unsetenv("XDG_CONFIG_HOME");
166 : }
|