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
This commit is contained in:
@@ -8,9 +8,12 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <pty.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/select.h>
|
||||||
|
#include <regex.h>
|
||||||
|
|
||||||
#include <stabber.h>
|
#include <stabber.h>
|
||||||
#include <expect.h>
|
|
||||||
|
|
||||||
#include "proftest.h"
|
#include "proftest.h"
|
||||||
|
|
||||||
@@ -18,6 +21,20 @@ char *config_orig;
|
|||||||
char *data_orig;
|
char *data_orig;
|
||||||
|
|
||||||
int fd = 0;
|
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
|
gboolean
|
||||||
_create_dir(const char *name)
|
_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
|
void
|
||||||
prof_start(void)
|
prof_start(void)
|
||||||
{
|
{
|
||||||
// helper script sets terminal columns, avoids assertions failing
|
struct winsize ws;
|
||||||
// based on the test runner terminal size
|
ws.ws_row = 24;
|
||||||
fd = exp_spawnl("sh",
|
ws.ws_col = 300; /* Match COLUMNS=300 from start_profanity.sh */
|
||||||
"sh",
|
ws.ws_xpixel = 0;
|
||||||
"-c",
|
ws.ws_ypixel = 0;
|
||||||
"./tests/functionaltests/start_profanity.sh",
|
|
||||||
NULL);
|
/* Reset output buffer */
|
||||||
FILE *fp = fdopen(fd, "r+");
|
output_len = 0;
|
||||||
|
output_buffer[0] = '\0';
|
||||||
assert_true(fp != NULL);
|
|
||||||
|
child_pid = forkpty(&fd, NULL, NULL, &ws);
|
||||||
setbuf(fp, (char *)0);
|
|
||||||
|
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
|
int
|
||||||
init_prof_test(void **state)
|
init_prof_test(void **state)
|
||||||
{
|
{
|
||||||
if (stbbr_start(STBBR_LOGDEBUG ,5230, 0) != 0) {
|
gboolean started = FALSE;
|
||||||
assert_true(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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Give stabber server thread time to start listening
|
||||||
|
usleep(100000); // 100ms
|
||||||
|
|
||||||
config_orig = getenv("XDG_CONFIG_HOME");
|
config_orig = getenv("XDG_CONFIG_HOME");
|
||||||
data_orig = getenv("XDG_DATA_HOME");
|
data_orig = getenv("XDG_DATA_HOME");
|
||||||
|
|
||||||
@@ -157,50 +248,69 @@ init_prof_test(void **state)
|
|||||||
_create_logs_dir();
|
_create_logs_dir();
|
||||||
|
|
||||||
prof_start();
|
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
|
// set UI options to make expect assertions faster and more reliable
|
||||||
prof_input("/inpblock timeout 5");
|
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");
|
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");
|
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");
|
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");
|
prof_input("/wrap off");
|
||||||
assert_true(prof_output_exact("Word wrap disabled"));
|
assert_true(prof_output_regex("Word wrap disabled"));
|
||||||
prof_input("/roster hide");
|
prof_input("/roster hide");
|
||||||
assert_true(prof_output_exact("Roster disabled"));
|
assert_true(prof_output_regex("Roster disabled"));
|
||||||
prof_input("/occupants default hide");
|
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");
|
||||||
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");
|
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");
|
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");
|
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");
|
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");
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
close_prof_test(void **state)
|
close_prof_test(void **state)
|
||||||
{
|
{
|
||||||
prof_input("/quit");
|
if (fd > 0 && child_pid > 0) {
|
||||||
waitpid(exp_pid, NULL, 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();
|
_cleanup_dirs();
|
||||||
|
|
||||||
setenv("XDG_CONFIG_HOME", config_orig, 1);
|
if (config_orig) {
|
||||||
setenv("XDG_DATA_HOME", data_orig, 1);
|
setenv("XDG_CONFIG_HOME", config_orig, 1);
|
||||||
|
}
|
||||||
|
if (data_orig) {
|
||||||
|
setenv("XDG_DATA_HOME", data_orig, 1);
|
||||||
|
}
|
||||||
|
|
||||||
stbbr_stop();
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,20 +319,75 @@ prof_input(const char *input)
|
|||||||
{
|
{
|
||||||
GString *inp_str = g_string_new(input);
|
GString *inp_str = g_string_new(input);
|
||||||
g_string_append(inp_str, "\r");
|
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);
|
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
|
int
|
||||||
prof_output_exact(const char *text)
|
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
|
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
|
void
|
||||||
@@ -241,33 +406,40 @@ prof_connect_with_roster(const char *roster)
|
|||||||
stbbr_for_query("jabber:iq:roster", roster_str->str);
|
stbbr_for_query("jabber:iq:roster", roster_str->str);
|
||||||
g_string_free(roster_str, TRUE);
|
g_string_free(roster_str, TRUE);
|
||||||
|
|
||||||
stbbr_for_id("prof_presence_1",
|
stbbr_auth_passwd("password");
|
||||||
"<presence id='prof_presence_1' lang='en' to='stabber@localhost/profanity' from='stabber@localhost/profanity'>"
|
|
||||||
"<priority>0</priority>"
|
|
||||||
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io/' ver='f8mrtdyAmhnj8Ca+630bThSL718='/>"
|
|
||||||
"</presence>"
|
|
||||||
);
|
|
||||||
|
|
||||||
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");
|
prof_input("password");
|
||||||
|
|
||||||
// Allow time for profanity to connect
|
expect_timeout = 60;
|
||||||
exp_timeout = 30;
|
assert_true(prof_output_regex("Connecting as stabber@localhost"));
|
||||||
assert_true(prof_output_regex("stabber@localhost/profanity logged in successfully, .+online.+ \\(priority 0\\)\\."));
|
assert_true(prof_output_regex("logged in successfully"));
|
||||||
exp_timeout = 10;
|
assert_true(prof_output_regex(".+online.+ \\(priority 0\\)\\."));
|
||||||
stbbr_wait_for("prof_presence_*");
|
|
||||||
|
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(
|
||||||
|
"<presence id=\"*\">"
|
||||||
|
"<c xmlns=\"http://jabber.org/protocol/caps\" hash=\"sha-1\" node=\"http://profanity-im.github.io\" ver=\"*\"/>"
|
||||||
|
"</presence>"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
prof_timeout(int timeout)
|
prof_timeout(int timeout)
|
||||||
{
|
{
|
||||||
exp_timeout = timeout;
|
expect_timeout = timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
prof_timeout_reset(void)
|
prof_timeout_reset(void)
|
||||||
{
|
{
|
||||||
exp_timeout = 10;
|
expect_timeout = 60;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
#define XDG_CONFIG_HOME "./tests/functionaltests/files/xdg_config_home"
|
#define XDG_CONFIG_HOME "./tests/functionaltests/files/xdg_config_home"
|
||||||
#define XDG_DATA_HOME "./tests/functionaltests/files/xdg_data_home"
|
#define XDG_DATA_HOME "./tests/functionaltests/files/xdg_data_home"
|
||||||
|
|
||||||
|
extern int stub_port;
|
||||||
|
|
||||||
int init_prof_test(void **state);
|
int init_prof_test(void **state);
|
||||||
int close_prof_test(void **state);
|
int close_prof_test(void **state);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user