Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file test_ctrl_c_exit.c
6 : * @brief PTY-05 — Ctrl-C exits tg-tui cleanly without leaving raw-mode artefacts.
7 : *
8 : * Two tests:
9 : *
10 : * test_repl_ctrl_c:
11 : * - Launches tg-tui (REPL / no --tui flag).
12 : * - Waits for the "tg> " prompt.
13 : * - Sends Ctrl-C (byte 0x03).
14 : * - Asserts the child exits within 2 seconds with code 0 (graceful: the
15 : * readline layer returns -1 on Ctrl-C and repl() returns 0).
16 : * - Verifies that PTY dimensions are intact (terminal not trashed).
17 : *
18 : * test_tui_mode_ctrl_c:
19 : * - Launches tg-tui --tui.
20 : * - Waits for "[dialogs]" in the status row.
21 : * - Sends Ctrl-C.
22 : * - Asserts child exits within 2 seconds with status 130 (128+SIGINT) or 0.
23 : * (terminal_install_cleanup_handlers resets the handler to SIG_DFL and
24 : * re-raises SIGINT, so the shell sees 128+2=130.)
25 : * - Verifies PTY dimensions are intact after exit.
26 : *
27 : * Depends on:
28 : * - libptytest (PTY-01)
29 : * - pty_tel_stub (PTY-02 infrastructure)
30 : * - tg-tui binary with terminal_install_cleanup_handlers (FEAT-16)
31 : */
32 :
33 : #include "ptytest.h"
34 : #include "pty_assert.h"
35 : #include "pty_tel_stub.h"
36 :
37 : #include <stdio.h>
38 : #include <stdlib.h>
39 : #include <string.h>
40 : #include <unistd.h>
41 : #include <sys/stat.h>
42 :
43 : /* ── Test infrastructure ─────────────────────────────────────────────── */
44 :
45 : static int g_tests_run = 0;
46 : static int g_tests_failed = 0;
47 :
48 : #define RUN_TEST(fn) do { \
49 : printf(" running %s ...\n", #fn); \
50 : fn(); \
51 : } while(0)
52 :
53 : #ifndef TG_TUI_BINARY
54 : #define TG_TUI_BINARY "bin/tg-tui"
55 : #endif
56 :
57 : /**
58 : * @brief Like ASSERT but jumps to a label on failure (avoids early-return leaks).
59 : */
60 : #define CHECK(cond, msg, label) do { \
61 : g_tests_run++; \
62 : if (!(cond)) { \
63 : printf(" [FAIL] %s:%d: %s\n", __FILE__, __LINE__, (msg)); \
64 : g_tests_failed++; \
65 : goto label; \
66 : } \
67 : } while(0)
68 :
69 : /**
70 : * @brief Like ASSERT_WAIT_FOR but jumps to a label on failure.
71 : */
72 : #define CHECK_WAIT_FOR(s, text, timeout_ms, label) do { \
73 : g_tests_run++; \
74 : if (pty_wait_for((s), (text), (timeout_ms)) != 0) { \
75 : printf(" [FAIL] %s:%d: wait_for(\"%s\", %d ms) timed out\n", \
76 : __FILE__, __LINE__, (text), (timeout_ms)); \
77 : g_tests_failed++; \
78 : goto label; \
79 : } \
80 : } while(0)
81 :
82 : /**
83 : * @brief Common setup: unique tmp HOME, session seeded, stub started, env set.
84 : * @return 1 on success, 0 on failure.
85 : */
86 5 : static int setup_stub(PtyTelStub *stub) {
87 : char tmp[256];
88 5 : snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-pty-ctrlc-%d", (int)getpid());
89 :
90 5 : mkdir(tmp, 0700);
91 : char dot_config[512];
92 5 : snprintf(dot_config, sizeof(dot_config), "%s/.config", tmp);
93 5 : mkdir(dot_config, 0700);
94 : char cfg_dir[640];
95 5 : snprintf(cfg_dir, sizeof(cfg_dir), "%s/tg-cli", dot_config);
96 5 : mkdir(cfg_dir, 0700);
97 :
98 : char ini_path[768];
99 5 : snprintf(ini_path, sizeof(ini_path), "%s/config.ini", cfg_dir);
100 5 : FILE *f = fopen(ini_path, "w");
101 5 : if (!f) return 0;
102 5 : fprintf(f, "api_id=12345\napi_hash=deadbeefcafebabef00dbaadfeedc0de\n");
103 5 : fclose(f);
104 :
105 5 : setenv("HOME", tmp, 1);
106 :
107 5 : if (pty_tel_stub_start(stub) != 0) return 0;
108 :
109 5 : setenv("TG_CLI_DC_HOST", "127.0.0.1", 1);
110 : char port_str[16];
111 5 : snprintf(port_str, sizeof(port_str), "%d", stub->port);
112 5 : setenv("TG_CLI_DC_PORT", port_str, 1);
113 :
114 5 : setenv("TG_CLI_API_ID", "12345", 1);
115 5 : setenv("TG_CLI_API_HASH", "deadbeefcafebabef00dbaadfeedc0de", 1);
116 :
117 5 : return 1;
118 : }
119 :
120 : /* ── Tests ───────────────────────────────────────────────────────────── */
121 :
122 : /**
123 : * @brief PTY-05-a: REPL mode — Ctrl-C on the prompt exits cleanly (code 0).
124 : *
125 : * In REPL mode the terminal stays in cooked mode; rl_readline intercepts
126 : * Ctrl-C internally (raw mode is only for the custom readline widget) and
127 : * returns -1, which causes repl() to return 0 (clean exit).
128 : */
129 3 : static void test_repl_ctrl_c(void) {
130 : PtyTelStub stub;
131 3 : CHECK(setup_stub(&stub), "setup_stub failed", done_no_stub);
132 :
133 3 : PtySession *s = pty_open(80, 24);
134 3 : CHECK(s != NULL, "pty_open failed", done_no_session);
135 :
136 3 : const char *argv[] = { TG_TUI_BINARY, NULL };
137 3 : CHECK(pty_run(s, argv) == 0, "pty_run(tg-tui) failed", done);
138 :
139 : /* Wait for the REPL prompt. */
140 2 : CHECK_WAIT_FOR(s, "tg>", 5000, done);
141 :
142 : /* Send Ctrl-C. */
143 2 : pty_send_key(s, PTY_KEY_CTRL_C);
144 :
145 : /* The process should exit gracefully with code 0. */
146 2 : int code = pty_wait_exit(s, 2000);
147 2 : g_tests_run++;
148 2 : if (code != 0) {
149 0 : printf(" [FAIL] %s:%d: REPL Ctrl-C exit code: expected 0, got %d\n",
150 : __FILE__, __LINE__, code);
151 0 : g_tests_failed++;
152 : }
153 :
154 : /* PTY dimensions must be intact (terminal not left garbled). */
155 2 : int cols = 0, rows = 0;
156 2 : pty_get_size(s, &cols, &rows);
157 2 : g_tests_run++;
158 2 : if (cols != 80 || rows != 24) {
159 0 : printf(" [FAIL] %s:%d: PTY dimensions wrong after exit (%dx%d)\n",
160 : __FILE__, __LINE__, cols, rows);
161 0 : g_tests_failed++;
162 : }
163 :
164 2 : done:
165 2 : pty_close(s);
166 2 : done_no_session:
167 2 : pty_tel_stub_stop(&stub);
168 2 : done_no_stub:
169 2 : return;
170 : }
171 :
172 : /**
173 : * @brief PTY-05-b: TUI mode (--tui) — Ctrl-C exits and restores the terminal.
174 : *
175 : * In TUI mode the terminal is in raw mode. terminal_install_cleanup_handlers()
176 : * installs a SIGINT handler that restores the terminal and re-raises the signal
177 : * so the exit status is 130 (128+2). We also accept 0 in case the signal is
178 : * delivered while the TUI loop is processing a normal quit path.
179 : */
180 2 : static void test_tui_mode_ctrl_c(void) {
181 : PtyTelStub stub;
182 2 : CHECK(setup_stub(&stub), "setup_stub failed", done_no_stub);
183 :
184 2 : PtySession *s = pty_open(80, 24);
185 2 : CHECK(s != NULL, "pty_open failed", done_no_session);
186 :
187 2 : const char *argv[] = { TG_TUI_BINARY, "--tui", NULL };
188 2 : CHECK(pty_run(s, argv) == 0, "pty_run(tg-tui --tui) failed", done);
189 :
190 : /* Wait for the dialog-pane hint in the status row. */
191 1 : CHECK_WAIT_FOR(s, "[dialogs]", 5000, done);
192 :
193 : /* Send Ctrl-C to the TUI. */
194 1 : pty_send_key(s, PTY_KEY_CTRL_C);
195 :
196 : /* Allow up to 2 seconds for the process to exit. */
197 1 : int code = pty_wait_exit(s, 2000);
198 :
199 : /* Accept 130 (SIGINT with restored-then-re-raised handler) or 0 (graceful). */
200 1 : g_tests_run++;
201 1 : if (code != 130 && code != 0) {
202 0 : printf(" [FAIL] %s:%d: TUI Ctrl-C exit code: expected 130 or 0, got %d\n",
203 : __FILE__, __LINE__, code);
204 0 : g_tests_failed++;
205 : }
206 :
207 : /* PTY dimensions must be intact (terminal not left in raw mode). */
208 1 : int cols = 0, rows = 0;
209 1 : pty_get_size(s, &cols, &rows);
210 1 : g_tests_run++;
211 1 : if (cols != 80 || rows != 24) {
212 0 : printf(" [FAIL] %s:%d: PTY dimensions wrong after exit (%dx%d)\n",
213 : __FILE__, __LINE__, cols, rows);
214 0 : g_tests_failed++;
215 : }
216 :
217 1 : done:
218 1 : pty_close(s);
219 1 : done_no_session:
220 1 : pty_tel_stub_stop(&stub);
221 1 : done_no_stub:
222 1 : return;
223 : }
224 :
225 : /* ── Entry point ─────────────────────────────────────────────────────── */
226 :
227 3 : int main(void) {
228 3 : printf("PTY-05 Ctrl-C exit tests\n");
229 :
230 3 : RUN_TEST(test_repl_ctrl_c);
231 2 : RUN_TEST(test_tui_mode_ctrl_c);
232 :
233 1 : printf("\n%d tests run, %d failed\n", g_tests_run, g_tests_failed);
234 1 : return g_tests_failed > 0 ? 1 : 0;
235 : }
|