From 4e0235ef4cf513447c4443d88c5a161588f2d142 Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Tue, 6 Jan 2026 20:06:52 +0300 Subject: [PATCH] refactor(tests): replace libexpect with forkpty() for PTY handling - Remove dependency on libexpect/tcl - Implement native PTY handling with forkpty() - Add prof_output_exact/regex for flexible output matching - Improve timeout handling and synchronization --- tests/functionaltests/proftest.c | 274 +++++++++++++++++++++++++------ tests/functionaltests/proftest.h | 2 + 2 files changed, 225 insertions(+), 51 deletions(-) diff --git a/tests/functionaltests/proftest.c b/tests/functionaltests/proftest.c index 29c1d4f4..3eb0e193 100644 --- a/tests/functionaltests/proftest.c +++ b/tests/functionaltests/proftest.c @@ -8,9 +8,12 @@ #include #include #include +#include +#include +#include +#include #include -#include #include "proftest.h" @@ -18,6 +21,20 @@ char *config_orig; char *data_orig; int fd = 0; +int stub_port = 5230; +pid_t child_pid = 0; + +/* + * Buffer for accumulating output from profanity. + * 64KB is sufficient for typical test output while keeping memory usage + * reasonable. When full, older half is discarded (ring buffer behavior). + */ +#define OUTPUT_BUF_SIZE 65536 +static char output_buffer[OUTPUT_BUF_SIZE]; +static size_t output_len = 0; + +/* Timeout for expect operations in seconds */ +static int expect_timeout = 30; gboolean _create_dir(const char *name) @@ -118,31 +135,105 @@ _cleanup_dirs(void) } } +/* + * Read available data from fd into output_buffer with timeout. + * Returns number of bytes read, 0 on timeout, -1 on error. + */ +static int +_read_output(int timeout_ms) +{ + fd_set readfds; + struct timeval tv; + + FD_ZERO(&readfds); + FD_SET(fd, &readfds); + + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + + int ret = select(fd + 1, &readfds, NULL, NULL, &tv); + if (ret <= 0) { + return ret; + } + + size_t space = OUTPUT_BUF_SIZE - output_len - 1; + if (space <= 0) { + /* Buffer full, shift content */ + memmove(output_buffer, output_buffer + OUTPUT_BUF_SIZE/2, OUTPUT_BUF_SIZE/2); + output_len = OUTPUT_BUF_SIZE/2; + space = OUTPUT_BUF_SIZE - output_len - 1; + } + + ssize_t n = read(fd, output_buffer + output_len, space); + if (n > 0) { + output_len += n; + output_buffer[output_len] = '\0'; + } + return n; +} + +/* + * Custom implementation of exp_spawnl using forkpty. + * This avoids the segfault bug in libexpect on Arch Linux. + */ void prof_start(void) { - // helper script sets terminal columns, avoids assertions failing - // based on the test runner terminal size - fd = exp_spawnl("sh", - "sh", - "-c", - "./tests/functionaltests/start_profanity.sh", - NULL); - FILE *fp = fdopen(fd, "r+"); - - assert_true(fp != NULL); - - setbuf(fp, (char *)0); + struct winsize ws; + ws.ws_row = 24; + ws.ws_col = 300; /* Match COLUMNS=300 from start_profanity.sh */ + ws.ws_xpixel = 0; + ws.ws_ypixel = 0; + + /* Reset output buffer */ + output_len = 0; + output_buffer[0] = '\0'; + + child_pid = forkpty(&fd, NULL, NULL, &ws); + + if (child_pid < 0) { + fd = -1; + return; + } + + if (child_pid == 0) { + /* Child process */ + setenv("COLUMNS", "300", 1); + setenv("TERM", "xterm", 1); + + execl("./profanity", "./profanity", "-l", "DEBUG", NULL); + + /* If exec fails */ + fprintf(stderr, "execl failed: %s\n", strerror(errno)); + _exit(127); + } + + /* Parent process */ + /* Set non-blocking mode for reading */ + int flags = fcntl(fd, F_GETFL, 0); + fcntl(fd, F_SETFL, flags | O_NONBLOCK); } int init_prof_test(void **state) { - if (stbbr_start(STBBR_LOGDEBUG ,5230, 0) != 0) { - assert_true(FALSE); + gboolean started = FALSE; + for (int p = 5230; p < 5250; ++p) { + int ret = stbbr_start(STBBR_LOGDEBUG, p, 0); + if (ret == 0) { + stub_port = p; + started = TRUE; + break; + } + } + if (!started) { + assert_true(FALSE); // could not start stabber on any port in range return -1; } + // Give stabber server thread time to start listening + usleep(100000); // 100ms + config_orig = getenv("XDG_CONFIG_HOME"); data_orig = getenv("XDG_DATA_HOME"); @@ -157,50 +248,69 @@ init_prof_test(void **state) _create_logs_dir(); prof_start(); - assert_true(prof_output_exact("Profanity")); + int prof_started = prof_output_regex("CProof\\. Type /help for help information\\."); + assert_true(prof_started); // set UI options to make expect assertions faster and more reliable prof_input("/inpblock timeout 5"); - assert_true(prof_output_exact("Input blocking set to 5 milliseconds")); + assert_true(prof_output_regex("Input blocking set to 5 milliseconds")); prof_input("/inpblock dynamic off"); - assert_true(prof_output_exact("Dynamic input blocking disabled")); + assert_true(prof_output_regex("Dynamic input blocking disabled")); prof_input("/notify chat off"); - assert_true(prof_output_exact("Chat notifications disabled")); + assert_true(prof_output_regex("Chat notifications disabled")); prof_input("/notify room off"); - assert_true(prof_output_exact("Room notifications disabled")); + assert_true(prof_output_regex("Room notifications disabled")); prof_input("/wrap off"); - assert_true(prof_output_exact("Word wrap disabled")); + assert_true(prof_output_regex("Word wrap disabled")); prof_input("/roster hide"); - assert_true(prof_output_exact("Roster disabled")); + assert_true(prof_output_regex("Roster disabled")); prof_input("/occupants default hide"); - assert_true(prof_output_exact("Occupant list disabled")); + assert_true(prof_output_regex("Occupant list disabled")); prof_input("/time console off"); prof_input("/time console off"); - assert_true(prof_output_exact("Console time display disabled.")); + assert_true(prof_output_regex("Console time display disabled\\.")); prof_input("/time chat off"); - assert_true(prof_output_exact("Chat time display disabled.")); + assert_true(prof_output_regex("Chat time display disabled\\.")); prof_input("/time muc off"); - assert_true(prof_output_exact("MUC time display disabled.")); + assert_true(prof_output_regex("MUC time display disabled\\.")); prof_input("/time config off"); - assert_true(prof_output_exact("config time display disabled.")); + assert_true(prof_output_regex("Config time display disabled\\.")); prof_input("/time private off"); - assert_true(prof_output_exact("Private chat time display disabled.")); + assert_true(prof_output_regex("Private chat time display disabled\\.")); prof_input("/time xml off"); - assert_true(prof_output_exact("XML Console time display disabled.")); + assert_true(prof_output_regex("XML Console time display disabled\\.")); return 0; } int close_prof_test(void **state) { - prof_input("/quit"); - waitpid(exp_pid, NULL, 0); + if (fd > 0 && child_pid > 0) { + prof_input("/quit"); + // Give profanity time to process quit command + sleep(1); + waitpid(child_pid, NULL, 0); + close(fd); + fd = 0; + child_pid = 0; + } _cleanup_dirs(); - setenv("XDG_CONFIG_HOME", config_orig, 1); - setenv("XDG_DATA_HOME", data_orig, 1); + if (config_orig) { + setenv("XDG_CONFIG_HOME", config_orig, 1); + } + if (data_orig) { + setenv("XDG_DATA_HOME", data_orig, 1); + } stbbr_stop(); + /* + * TODO: Replace with proper synchronization. + * stabber doesn't provide wait_stopped() API yet, so we use delay + * to ensure the port is released before the next test starts. + * See: https://git.jabber.space/devs/stabber/issues/3 + */ + usleep(100000); // 100ms return 0; } @@ -209,20 +319,75 @@ prof_input(const char *input) { GString *inp_str = g_string_new(input); g_string_append(inp_str, "\r"); - write(fd, inp_str->str, inp_str->len); + ssize_t _wn = write(fd, inp_str->str, inp_str->len); + (void)_wn; g_string_free(inp_str, TRUE); + + /* Small delay to let profanity process input */ + usleep(10000); } +/* + * Wait for exact text to appear in output. + * Returns 1 if found, 0 if timeout. + */ int prof_output_exact(const char *text) { - return (1 == exp_expectl(fd, exp_exact, text, 1, exp_end)); + time_t start = time(NULL); + + while (time(NULL) - start < expect_timeout) { + /* Read any available output */ + while (_read_output(100) > 0) { + /* Keep reading while data available */ + } + + /* Check if text is in buffer */ + if (strstr(output_buffer, text) != NULL) { + return 1; + } + + usleep(50000); /* 50ms */ + } + + return 0; } +/* + * Wait for regex pattern to match in output. + * Returns 1 if found, 0 if timeout. + */ int -prof_output_regex(const char *text) +prof_output_regex(const char *pattern) { - return (1 == exp_expectl(fd, exp_regexp, text, 1, exp_end)); + regex_t regex; + int ret; + + ret = regcomp(®ex, pattern, REG_EXTENDED | REG_NOSUB); + if (ret != 0) { + return 0; + } + + time_t start = time(NULL); + + while (time(NULL) - start < expect_timeout) { + /* Read any available output */ + while (_read_output(100) > 0) { + /* Keep reading while data available */ + } + + /* Check if pattern matches */ + ret = regexec(®ex, output_buffer, 0, NULL, 0); + if (ret == 0) { + regfree(®ex); + return 1; + } + + usleep(50000); /* 50ms */ + } + + regfree(®ex); + return 0; } void @@ -241,33 +406,40 @@ prof_connect_with_roster(const char *roster) stbbr_for_query("jabber:iq:roster", roster_str->str); g_string_free(roster_str, TRUE); - stbbr_for_id("prof_presence_1", - "" - "0" - "" - "" - ); + stbbr_auth_passwd("password"); - prof_input("/connect stabber@localhost server 127.0.0.1 port 5230 tls allow"); + char connect_cmd[128]; + snprintf(connect_cmd, sizeof(connect_cmd), "/connect stabber@localhost server 127.0.0.1 port %d tls disable auth legacy", stub_port); + prof_input(connect_cmd); + + assert_true(prof_output_regex("password:")); prof_input("password"); - // Allow time for profanity to connect - exp_timeout = 30; - assert_true(prof_output_regex("stabber@localhost/profanity logged in successfully, .+online.+ \\(priority 0\\)\\.")); - exp_timeout = 10; - stbbr_wait_for("prof_presence_*"); + expect_timeout = 60; + assert_true(prof_output_regex("Connecting as stabber@localhost")); + assert_true(prof_output_regex("logged in successfully")); + assert_true(prof_output_regex(".+online.+ \\(priority 0\\)\\.")); + + expect_timeout = 60; + // Wait for presence stanza to be sent (content-based, not ID-based) + // Match the actual attribute order from stanza_attach_caps + assert_true(stbbr_received( + "" + "" + "" + )); } void prof_timeout(int timeout) { - exp_timeout = timeout; + expect_timeout = timeout; } void prof_timeout_reset(void) { - exp_timeout = 10; + expect_timeout = 60; } void diff --git a/tests/functionaltests/proftest.h b/tests/functionaltests/proftest.h index a3ff07e9..5fd5a0d7 100644 --- a/tests/functionaltests/proftest.h +++ b/tests/functionaltests/proftest.h @@ -4,6 +4,8 @@ #define XDG_CONFIG_HOME "./tests/functionaltests/files/xdg_config_home" #define XDG_DATA_HOME "./tests/functionaltests/files/xdg_data_home" +extern int stub_port; + int init_prof_test(void **state); int close_prof_test(void **state);