All checks were successful
CI Code / Check spelling (push) Successful in 18s
CI Code / Check coding style (push) Successful in 34s
CI Code / Code Coverage (push) Successful in 2m36s
CI Code / Linux (debian) (push) Successful in 4m41s
CI Code / Linux (ubuntu) (push) Successful in 4m52s
CI Code / Linux (arch) (push) Successful in 5m40s
Sync with upstream profanity-im/profanity.
Major upstream changes incorporated:
Memory management
- Replace malloc+memset with g_new0 throughout codebase
- Adopt auto_gchar / auto_gcharv / auto_gerror / auto_jid cleanup macros
- Replace free() with g_free() for GAlloc'd memory
Editor rewrite
- Remove pthread-based async editor; use GChildWatch callback API
- New launch_editor(initial_content, callback, user_data) interface
- Proper signal handling (SIGINT, SIGTSTP, SIGPIPE reset in child)
- ui_suspend()/ui_resume() integration for TTY management
OMEMO improvements
- Dual backend support: libsignal-protocol-c and libomemo-c
- Proper pre-key removal after use (XEP-0384 compliance)
- Automatic pre-key regeneration when store drops below threshold
- New functions: omemo_is_device_active(), omemo_is_jid_trusted()
- omemo_get_jid_untrusted_fingerprints() for better error messages
- Fingerprint notifications on new device identity discovery
- Deterministic pre-key ID generation tracking max_pre_key_id
- omemo_trust_changed() UI updates on trust state changes
JID validation
- RFC 6122-compliant validation in jid_is_valid()
- Character-level checks (RFC 6122 forbidden chars: & ' / : < > @)
- Length limits: 1023 per component, 3071 total
- New jid_is_valid_user_jid() for user vs. service JID distinction
Database
- Schema migration v3: UNIQUE constraint on archive_id for deduplication
- Triggers for corrected message tracking (replaces_db_id / replaced_by_db_id)
- db_history_result_t return type, _truncate_datetime_suffix()
UI / console
- win_warn_needed() / win_warn_sent() warning deduplication hash table
- PAD_MIN_HEIGHT dynamic pad sizing with PAD_THRESHOLD auto-cleanup
- Spellcheck integration in input field with Unicode word detection
- cons_spellcheck_setting() for /settings ui output
- /[command]? shortcut for command help
Account config
- Account name sanitization for GKeyFile special chars ([ ] = # \n \r)
- Replace popen() with g_spawn_sync() for eval_password
- TLS policy: add "direct" option alongside legacy
Connection
- Port validation with g_assert (0–65535)
- SHA-256 certificate fingerprint support (XMPP_CERT_PUBKEY_FINGERPRINT_SHA256)
- "direct" TLS policy alias for legacy SSL
Common utilities
- str_xml_sanitize() for XML 1.0 illegal character removal
- string_matches_one_of() with formatted error messages
- valid_tls_policy_option() helper
- prof_date_time_format_iso8601() utility
- Improved strip_arg_quotes() with backslash unescaping
- prof_occurrences() uses g_slist_prepend + reverse for performance
PGP / OX
- Proper GPGME resource cleanup with goto-cleanup pattern
- g_string_free(xmppuri) leak fix in _ox_key_lookup
CSV export
- Use GString + g_file_set_contents instead of raw write() syscalls
Tests
- Restructured into subdirectories: command/, config/, xmpp/, ui/, omemo/, otr/, pgp/
- New test_cmd_ac.c for autocompleter unit tests
- Updated stubs for new UI suspend/resume functions
License headers
- Migrate to SPDX-3.0 identifiers (GPL-3.0-or-later WITH OpenSSL-exception)
────────────────────────────────────────────────────
cproof-specific preservations:
- XEP-0308 LMC: replace_id ?: id logic in message/stanza/omemo
- Force encryption: cmd_force_encryption, test_forced_encryption
- CWE-134: format string protection (cons_show("%s", ...))
- y_start_pos-based paging in window.c
- db_history_result_t return type, _truncate_datetime_suffix()
Merge-time fixes:
- common.c: format-security (-Werror) — cons_show(errmsg) → cons_show("%s", errmsg)
- database.c: null-deref guard — !msg->timestamp → msg && !msg->timestamp
- console.c: implicit size_t → int cast — (int)(maxlen + 1)
- tlscerts.c: %d for size_t — %zu
Build system:
- Kept autotools (Makefile.am, configure.ac); upstream uses Meson
- Restored deleted files: bootstrap.sh, autogen.sh, ax_valgrind_check.m4, configure-debug
- Updated Makefile.am test paths for subdirectory structure
- Added test_cmd_ac, test_forced_encryption to test sources
Functional tests:
- Use cproof version; upstream requires stbbr_for_xmlns from updated stabber
- Not yet available in devs/stabber fork
Closes #64
Merge author: jabber.developer2
Commits authors:
Michael Vetter <jubalh@iodoru.org>
& Steffen Jaeckel <s@jaeckel.eu>
632 lines
18 KiB
C
632 lines
18 KiB
C
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
#include <glib.h>
|
|
|
|
#include <stdlib.h>
|
|
#include "prof_cmocka.h"
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <string.h>
|
|
#include <pty.h>
|
|
#include <fcntl.h>
|
|
#include <sys/select.h>
|
|
#include <regex.h>
|
|
#include <signal.h>
|
|
#include <time.h>
|
|
|
|
#include <stabber.h>
|
|
|
|
#include "proftest.h"
|
|
|
|
/* Number of parallel test groups for CI builds */
|
|
#define TEST_GROUPS 5
|
|
|
|
/* Number of ports reserved per test group; allows skipping TIME_WAIT ports */
|
|
#define PORTS_PER_GROUP 50
|
|
|
|
/* Base port and fallback scan range for stabber */
|
|
#define BASE_PORT 5230
|
|
#define FALLBACK_PORT_RANGE (TEST_GROUPS * PORTS_PER_GROUP)
|
|
|
|
/* Shutdown polling: interval and maximum attempts before escalating signals */
|
|
#define MS_TO_US 1000
|
|
#define SHUTDOWN_POLL_MS 50
|
|
#define SHUTDOWN_POLL_MAX 100 /* 100 x 50ms = 5s */
|
|
#define SIGTERM_POLL_MAX 20 /* 20 x 50ms = 1s */
|
|
#define QUIT_GRACE_MS 100 /* grace for /quit to be read from pty */
|
|
#define INIT_WAIT_MS 50 /* wait for child process to initialize */
|
|
#define INPUT_DELAY_MS 10 /* let profanity process input */
|
|
#define OUTPUT_POLL_MS 50 /* polling interval for output checks */
|
|
#define READ_TIMEOUT_MS 100 /* select() timeout for pty reads */
|
|
|
|
/* Expect timeouts (seconds) */
|
|
#define EXPECT_TIMEOUT_DEFAULT 30
|
|
#define EXPECT_TIMEOUT_CONNECT 60
|
|
|
|
/* Test duration thresholds (seconds) */
|
|
#define SLOW_TEST_THRESHOLD_S 20
|
|
#define FAST_TEST_THRESHOLD_S 0.3
|
|
|
|
/* Sentinel "infinity" for min-elapsed tracking (any real test is faster) */
|
|
#define MIN_ELAPSED_INIT 1e9
|
|
|
|
/* Terminal dimensions for forkpty */
|
|
#define PTY_ROWS 24
|
|
#define PTY_COLS 300
|
|
|
|
/* Preprocessor stringification (for setenv from numeric #define) */
|
|
#define STRINGIFY_(x) #x
|
|
#define STRINGIFY(x) STRINGIFY_(x)
|
|
|
|
char* config_orig;
|
|
char* data_orig;
|
|
|
|
int fd = 0;
|
|
int stub_port = BASE_PORT;
|
|
pid_t child_pid = 0;
|
|
|
|
/*
|
|
* Dynamic XDG paths based on stub_port for parallel test execution.
|
|
* Each test instance gets unique directories to avoid file conflicts.
|
|
*/
|
|
char xdg_config_home[256];
|
|
char xdg_data_home[256];
|
|
|
|
/*
|
|
* 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 = EXPECT_TIMEOUT_DEFAULT;
|
|
|
|
/* Per-test wall-clock timer */
|
|
static struct timespec test_start_ts;
|
|
static const char* current_test_name;
|
|
static double min_elapsed = MIN_ELAPSED_INIT;
|
|
static const char* min_test_name = "(none)";
|
|
|
|
/* Per-test threshold overrides (0 = use default) */
|
|
double prof_test_slow_threshold = 0;
|
|
double prof_test_fast_threshold = 0;
|
|
|
|
/* Timezone for profanity child process (see proftest.h) */
|
|
const char* prof_test_tz = "UTC";
|
|
|
|
gboolean
|
|
_create_dir(const char* name)
|
|
{
|
|
struct stat sb;
|
|
|
|
if (stat(name, &sb) != 0) {
|
|
if (errno != ENOENT || mkdir(name, S_IRWXU) != 0) {
|
|
return FALSE;
|
|
}
|
|
} else {
|
|
if ((sb.st_mode & S_IFDIR) != S_IFDIR) {
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
_mkdir_recursive(const char* dir)
|
|
{
|
|
int i;
|
|
gboolean result = TRUE;
|
|
|
|
for (i = 1; i <= strlen(dir); i++) {
|
|
if (dir[i] == '/' || dir[i] == '\0') {
|
|
gchar* next_dir = g_strndup(dir, i);
|
|
result = _create_dir(next_dir);
|
|
g_free(next_dir);
|
|
if (!result) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void
|
|
_create_config_dir(void)
|
|
{
|
|
GString* profanity_dir = g_string_new(xdg_config_home);
|
|
g_string_append(profanity_dir, "/profanity");
|
|
|
|
if (!_mkdir_recursive(profanity_dir->str)) {
|
|
assert_true(FALSE);
|
|
}
|
|
|
|
g_string_free(profanity_dir, TRUE);
|
|
}
|
|
|
|
void
|
|
_create_data_dir(void)
|
|
{
|
|
GString* profanity_dir = g_string_new(xdg_data_home);
|
|
g_string_append(profanity_dir, "/profanity");
|
|
|
|
if (!_mkdir_recursive(profanity_dir->str)) {
|
|
assert_true(FALSE);
|
|
}
|
|
|
|
g_string_free(profanity_dir, TRUE);
|
|
}
|
|
|
|
void
|
|
_create_chatlogs_dir(void)
|
|
{
|
|
GString* chatlogs_dir = g_string_new(xdg_data_home);
|
|
g_string_append(chatlogs_dir, "/profanity/chatlogs");
|
|
|
|
if (!_mkdir_recursive(chatlogs_dir->str)) {
|
|
assert_true(FALSE);
|
|
}
|
|
|
|
g_string_free(chatlogs_dir, TRUE);
|
|
}
|
|
|
|
void
|
|
_create_logs_dir(void)
|
|
{
|
|
GString* logs_dir = g_string_new(xdg_data_home);
|
|
g_string_append(logs_dir, "/profanity/logs");
|
|
|
|
if (!_mkdir_recursive(logs_dir->str)) {
|
|
assert_true(FALSE);
|
|
}
|
|
|
|
g_string_free(logs_dir, TRUE);
|
|
}
|
|
|
|
void
|
|
_cleanup_dirs(void)
|
|
{
|
|
const char* group_env = getenv("PROF_TEST_GROUP");
|
|
int group = group_env ? atoi(group_env) : 0;
|
|
int dir_id = (group >= 1 && group <= TEST_GROUPS) ? group : stub_port;
|
|
|
|
printf("[PROF_TEST] Cleaning up directories for group %d (dir_id %d)\n", group, dir_id);
|
|
|
|
char cmd[512];
|
|
snprintf(cmd, sizeof(cmd), "rm -rf ./test-files/%d", dir_id);
|
|
int res = system(cmd);
|
|
if (res == -1) {
|
|
assert_true(FALSE);
|
|
}
|
|
}
|
|
|
|
static void
|
|
sleep_ms(int ms)
|
|
{
|
|
usleep(ms * MS_TO_US);
|
|
}
|
|
|
|
/*
|
|
* Read available data from fd into output_buffer with timeout.
|
|
* Returns number of bytes read, 0 on timeout, -1 on error.
|
|
*/
|
|
static ssize_t
|
|
_read_output(int timeout_ms)
|
|
{
|
|
fd_set readfds;
|
|
struct timeval tv;
|
|
|
|
FD_ZERO(&readfds);
|
|
FD_SET(fd, &readfds);
|
|
|
|
tv.tv_sec = timeout_ms / MS_TO_US;
|
|
tv.tv_usec = (timeout_ms % MS_TO_US) * MS_TO_US;
|
|
|
|
int ret = select(fd + 1, &readfds, NULL, NULL, &tv);
|
|
if (ret <= 0) {
|
|
return ret;
|
|
}
|
|
|
|
/* Check size before subtracting so OUTPUT_BUF_SIZE - output_len - 1
|
|
* never goes negative / underflows. Reserves one byte for the
|
|
* trailing NUL. */
|
|
if (output_len >= OUTPUT_BUF_SIZE - 1) {
|
|
/* Buffer full, shift content */
|
|
memmove(output_buffer, output_buffer + OUTPUT_BUF_SIZE / 2, OUTPUT_BUF_SIZE / 2);
|
|
output_len = OUTPUT_BUF_SIZE / 2;
|
|
}
|
|
size_t space = OUTPUT_BUF_SIZE - output_len - 1;
|
|
|
|
ssize_t n = read(fd, output_buffer + output_len, space);
|
|
if (n > 0) {
|
|
output_len += (size_t)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)
|
|
{
|
|
struct winsize ws;
|
|
ws.ws_row = PTY_ROWS;
|
|
ws.ws_col = PTY_COLS;
|
|
ws.ws_xpixel = 0;
|
|
ws.ws_ypixel = 0;
|
|
|
|
/* Reset output buffer */
|
|
output_len = 0;
|
|
output_buffer[0] = '\0';
|
|
|
|
/* Force UTC so delay-stamp timestamps render deterministically
|
|
* regardless of the host timezone (needed for functional tests
|
|
* that verify rendered timestamp strings). */
|
|
setenv("TZ", prof_test_tz, 1);
|
|
tzset();
|
|
|
|
child_pid = forkpty(&fd, NULL, NULL, &ws);
|
|
|
|
if (child_pid < 0) {
|
|
fd = -1;
|
|
return;
|
|
}
|
|
|
|
if (child_pid == 0) {
|
|
/* Child process */
|
|
setenv("COLUMNS", STRINGIFY(PTY_COLS), 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);
|
|
|
|
/* Brief wait for process to initialize */
|
|
sleep_ms(INIT_WAIT_MS);
|
|
}
|
|
|
|
int
|
|
init_prof_test(void** state)
|
|
{
|
|
clock_gettime(CLOCK_MONOTONIC, &test_start_ts);
|
|
current_test_name = state && *state ? (const char*)*state : "unknown";
|
|
prof_test_slow_threshold = 0;
|
|
prof_test_fast_threshold = 0;
|
|
/* Get test group from environment for static resource allocation */
|
|
const char* group_env = getenv("PROF_TEST_GROUP");
|
|
int group = group_env ? atoi(group_env) : 0;
|
|
|
|
/* Get build index for port offset (for parallel CI builds) */
|
|
const char* build_env = getenv("PROF_BUILD_INDEX");
|
|
int build_idx = build_env ? atoi(build_env) : 0;
|
|
|
|
/* Calculate port base: each build uses a different range of
|
|
* TEST_GROUPS * PORTS_PER_GROUP ports.
|
|
* Build 0 (local/default): 5230-5429, Full: 5230-5429, Minimal: 5430-5629, etc.
|
|
* Build 0 and Full share the same range because build 0 is for local runs
|
|
* or sequential run (no parallel builds), while Full/Minimal/NoEncrypt/Default
|
|
* are used in CI where they run in parallel. */
|
|
int port_base = BASE_PORT + ((build_idx > 0 ? build_idx - 1 : 0) * TEST_GROUPS * PORTS_PER_GROUP);
|
|
|
|
/* Each group gets a dedicated range of ports for parallel execution.
|
|
* Group 1: port_base..port_base+PORTS_PER_GROUP-1
|
|
* Group 2: port_base+PORTS_PER_GROUP..port_base+2*PORTS_PER_GROUP-1, etc.
|
|
* Ports in TIME_WAIT from previous tests are skipped automatically. */
|
|
gboolean started = FALSE;
|
|
|
|
if (group >= 1 && group <= TEST_GROUPS) {
|
|
int group_port_base = port_base + (group - 1) * PORTS_PER_GROUP;
|
|
|
|
for (int p = group_port_base; p < group_port_base + PORTS_PER_GROUP && !started; p++) {
|
|
if (stbbr_start(STBBR_LOGDEBUG, p, 0) == 0) {
|
|
stub_port = p;
|
|
started = TRUE;
|
|
}
|
|
}
|
|
if (!started) {
|
|
fprintf(stderr, "[PROF_TEST] Failed to start stabber on ports %d-%d\n",
|
|
group_port_base, group_port_base + PORTS_PER_GROUP - 1);
|
|
}
|
|
}
|
|
|
|
/* Fallback to dynamic allocation if static failed or group=0 */
|
|
if (!started) {
|
|
for (int p = port_base; p < port_base + FALLBACK_PORT_RANGE; ++p) {
|
|
if (stbbr_start(STBBR_LOGDEBUG, p, 0) == 0) {
|
|
stub_port = p;
|
|
started = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!started) {
|
|
fprintf(stderr, "[PROF_TEST] ERROR: could not start stabber on any port\n");
|
|
return -1;
|
|
}
|
|
|
|
/* Generate unique XDG paths based on group for parallel execution.
|
|
* Use ./test-files/ in current (build) directory for out-of-tree builds compatibility. */
|
|
int dir_id = (group >= 1 && group <= TEST_GROUPS) ? group : stub_port;
|
|
snprintf(xdg_config_home, sizeof(xdg_config_home),
|
|
"./test-files/%d/xdg_config_home", dir_id);
|
|
snprintf(xdg_data_home, sizeof(xdg_data_home),
|
|
"./test-files/%d/xdg_data_home", dir_id);
|
|
|
|
printf("[PROF_TEST] Group %d using directories: config=%s, data=%s\n",
|
|
group, xdg_config_home, xdg_data_home);
|
|
|
|
config_orig = getenv("XDG_CONFIG_HOME");
|
|
data_orig = getenv("XDG_DATA_HOME");
|
|
|
|
setenv("XDG_CONFIG_HOME", xdg_config_home, 1);
|
|
setenv("XDG_DATA_HOME", xdg_data_home, 1);
|
|
|
|
_cleanup_dirs();
|
|
|
|
_create_config_dir();
|
|
_create_data_dir();
|
|
_create_chatlogs_dir();
|
|
_create_logs_dir();
|
|
|
|
/* Pre-write profrc with UI/notification defaults to ensure consistent,
|
|
* deterministic output across tests (no timestamps, no roster/occupants
|
|
* panels, low input-blocking delay for fast command processing). */
|
|
char profrc_path[512];
|
|
snprintf(profrc_path, sizeof(profrc_path),
|
|
"%s/profanity/profrc", xdg_config_home);
|
|
FILE* prc = fopen(profrc_path, "w");
|
|
if (prc) {
|
|
fprintf(prc,
|
|
"[ui]\n"
|
|
"inpblock=5\n"
|
|
"inpblock.dynamic=false\n"
|
|
"wrap=false\n"
|
|
"roster=false\n"
|
|
"occupants=false\n"
|
|
"time.console=off\n"
|
|
"time.chat=off\n"
|
|
"time.muc=off\n"
|
|
"time.config=off\n"
|
|
"time.private=off\n"
|
|
"time.xmlconsole=off\n"
|
|
"[notifications]\n"
|
|
"message=false\n"
|
|
"room=false\n");
|
|
fclose(prc);
|
|
}
|
|
|
|
prof_start();
|
|
int prof_started = prof_output_regex("CProof\\. Type /help for help information\\.");
|
|
assert_true(prof_started);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
close_prof_test(void** state)
|
|
{
|
|
if (fd > 0 && child_pid > 0) {
|
|
prof_input("/quit");
|
|
/* Close pty master after brief grace — child gets SIGHUP which
|
|
* accelerates shutdown instead of waiting for XMPP disconnect
|
|
* timeout (~5s). */
|
|
sleep_ms(QUIT_GRACE_MS);
|
|
close(fd);
|
|
fd = 0;
|
|
int exited = 0;
|
|
for (int i = 0; i < SHUTDOWN_POLL_MAX; i++) {
|
|
if (waitpid(child_pid, NULL, WNOHANG) != 0) {
|
|
exited = 1;
|
|
break;
|
|
}
|
|
sleep_ms(SHUTDOWN_POLL_MS);
|
|
}
|
|
if (!exited) {
|
|
kill(child_pid, SIGTERM);
|
|
for (int i = 0; i < SIGTERM_POLL_MAX; i++) {
|
|
if (waitpid(child_pid, NULL, WNOHANG) != 0) {
|
|
exited = 1;
|
|
break;
|
|
}
|
|
sleep_ms(SHUTDOWN_POLL_MS);
|
|
}
|
|
if (!exited) {
|
|
kill(child_pid, SIGKILL);
|
|
waitpid(child_pid, NULL, 0);
|
|
}
|
|
}
|
|
child_pid = 0;
|
|
}
|
|
|
|
if (config_orig) {
|
|
setenv("XDG_CONFIG_HOME", config_orig, 1);
|
|
}
|
|
if (data_orig) {
|
|
setenv("XDG_DATA_HOME", data_orig, 1);
|
|
}
|
|
|
|
stbbr_stop();
|
|
|
|
struct timespec now;
|
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
double elapsed = (now.tv_sec - test_start_ts.tv_sec)
|
|
+ (now.tv_nsec - test_start_ts.tv_nsec) / 1e9;
|
|
if (elapsed < min_elapsed) {
|
|
min_elapsed = elapsed;
|
|
min_test_name = current_test_name;
|
|
}
|
|
printf("[PROF_TEST] test '%s' took %.3f s (min: %.3fs '%s')\n",
|
|
current_test_name, elapsed, min_elapsed, min_test_name);
|
|
double slow_thr = prof_test_slow_threshold > 0 ? prof_test_slow_threshold : SLOW_TEST_THRESHOLD_S;
|
|
double fast_thr = prof_test_fast_threshold > 0 ? prof_test_fast_threshold : FAST_TEST_THRESHOLD_S;
|
|
if (elapsed >= slow_thr) {
|
|
print_message("[PROF_TEST] WARNING: slow test '%s' \u2014 %.1fs (threshold %.1fs)\n",
|
|
current_test_name, elapsed, slow_thr);
|
|
} else if (elapsed < fast_thr) {
|
|
print_message("[PROF_TEST] WARNING: suspiciously fast test '%s' \u2014 %.3fs (min %.3fs)\n",
|
|
current_test_name, elapsed, fast_thr);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
prof_input(const char* input)
|
|
{
|
|
GString* inp_str = g_string_new(input);
|
|
g_string_append(inp_str, "\r");
|
|
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 */
|
|
sleep_ms(INPUT_DELAY_MS);
|
|
}
|
|
|
|
/*
|
|
* Wait for exact text to appear in output.
|
|
* Returns 1 if found, 0 if timeout.
|
|
*/
|
|
int
|
|
prof_output_exact(const char* text)
|
|
{
|
|
time_t start = time(NULL);
|
|
|
|
while (time(NULL) - start < expect_timeout) {
|
|
/* Read any available output */
|
|
while (_read_output(READ_TIMEOUT_MS) > 0) {
|
|
/* Keep reading while data available */
|
|
}
|
|
|
|
/* Check if text is in buffer */
|
|
if (strstr(output_buffer, text) != NULL) {
|
|
return 1;
|
|
}
|
|
|
|
sleep_ms(OUTPUT_POLL_MS);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Wait for regex pattern to match in output.
|
|
* Returns 1 if found, 0 if timeout.
|
|
*/
|
|
int
|
|
prof_output_regex(const char* pattern)
|
|
{
|
|
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(READ_TIMEOUT_MS) > 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;
|
|
}
|
|
|
|
sleep_ms(OUTPUT_POLL_MS);
|
|
}
|
|
|
|
/* Timeout reached - log diagnostic info */
|
|
fprintf(stderr, "Timeout waiting for regex '%s' after %d seconds. Last output:\n", pattern, expect_timeout);
|
|
size_t len = strlen(output_buffer);
|
|
if (len > 500) {
|
|
fprintf(stderr, "...%s", output_buffer + len - 500);
|
|
} else {
|
|
fprintf(stderr, "%s", output_buffer);
|
|
}
|
|
fprintf(stderr, "\n");
|
|
|
|
regfree(®ex);
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
prof_connect_with_roster(const char* roster)
|
|
{
|
|
GString* roster_str = g_string_new(
|
|
"<iq type='result' to='stabber@localhost/profanity'>"
|
|
"<query xmlns='jabber:iq:roster' ver='362'>");
|
|
g_string_append(roster_str, roster);
|
|
g_string_append(roster_str,
|
|
"</query>"
|
|
"</iq>");
|
|
|
|
stbbr_for_query("jabber:iq:roster", roster_str->str);
|
|
g_string_free(roster_str, TRUE);
|
|
|
|
stbbr_auth_passwd("password");
|
|
|
|
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");
|
|
|
|
expect_timeout = EXPECT_TIMEOUT_CONNECT;
|
|
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 = EXPECT_TIMEOUT_CONNECT;
|
|
// 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
|
|
prof_timeout(int timeout)
|
|
{
|
|
expect_timeout = timeout;
|
|
}
|
|
|
|
void
|
|
prof_timeout_reset(void)
|
|
{
|
|
expect_timeout = EXPECT_TIMEOUT_CONNECT;
|
|
}
|
|
|
|
void
|
|
prof_connect(void)
|
|
{
|
|
prof_connect_with_roster(
|
|
"<item jid='buddy1@localhost' subscription='both' name='Buddy1'/>"
|
|
"<item jid='buddy2@localhost' subscription='both' name='Buddy2'/>");
|
|
}
|