mirror of
https://git.jabber.space/devs/cproof.git
synced 2026-07-22 20:56:21 +00:00
Merge branch 'master' into merge/upstream-full
Resolve conflicts after master gained the flat-file database backend,
AI client, and other cproof-fork features:
- CHANGELOG: keep cproof-fork unreleased section atop full upstream
history (0.17.0 / 0.16.0 / 0.15.1)
- src/command/cmd_ac.c: keep both upstream spellcheck AC and cproof
/history switch|verify|export|import autocompletion
- src/config/preferences.c: merge upstream [spellcheck] group with the
cproof [ai]/[ai/<provider>] groups
- src/database.c: take master (thin dispatcher); upstream SQLite
improvements (v3 migration, auto_gchar) belong in database_sqlite.c
and will land in a follow-up commit
- src/tools/autocomplete.c: take upstream unescape of search_str
- src/ui/statusbar.c: keep guint signatures (per project preference)
and adapt _status_bar_draw_dbbackend to guint as well
- src/xmpp/jid.c: combine the new jid_is_valid/jid_is_valid_user_jid
with the "(null)" legacy-placeholder guard in
jid_create_from_bare_and_resource; use auto_gchar
- tests/functionaltests/proftest.c: keep unsigned-safe pre-check on
output_len before subtracting OUTPUT_BUF_SIZE
- tests/unittests/unittests.c: keep upstream subdirectory include
layout plus cproof-only test_ai_client/database_export/database_stress
- tests/unittests/test_jid.c: drop orphan top-level copy (the
restructured version lives in tests/unittests/xmpp/test_jid.c)
Upstream is included up to 1ac05754b (release 0.18.0 era). Follow-up:
pull 1ac05754b..upstream/master (esp. 42a849d16 db migration fix and
4749645c7 receipt-request restore), and adapt the SQLite improvements
into database_sqlite.c.
This commit is contained in:
@@ -13,46 +13,54 @@
|
||||
#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 4
|
||||
#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)
|
||||
#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 */
|
||||
#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
|
||||
#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
|
||||
#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;
|
||||
char* config_orig;
|
||||
char* data_orig;
|
||||
|
||||
int fd = 0;
|
||||
int stub_port = BASE_PORT;
|
||||
@@ -77,8 +85,21 @@ 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)
|
||||
_create_dir(const char* name)
|
||||
{
|
||||
struct stat sb;
|
||||
|
||||
@@ -96,14 +117,14 @@ _create_dir(const char *name)
|
||||
}
|
||||
|
||||
gboolean
|
||||
_mkdir_recursive(const char *dir)
|
||||
_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);
|
||||
gchar* next_dir = g_strndup(dir, i);
|
||||
result = _create_dir(next_dir);
|
||||
g_free(next_dir);
|
||||
if (!result) {
|
||||
@@ -118,7 +139,7 @@ _mkdir_recursive(const char *dir)
|
||||
void
|
||||
_create_config_dir(void)
|
||||
{
|
||||
GString *profanity_dir = g_string_new(xdg_config_home);
|
||||
GString* profanity_dir = g_string_new(xdg_config_home);
|
||||
g_string_append(profanity_dir, "/profanity");
|
||||
|
||||
if (!_mkdir_recursive(profanity_dir->str)) {
|
||||
@@ -131,7 +152,7 @@ _create_config_dir(void)
|
||||
void
|
||||
_create_data_dir(void)
|
||||
{
|
||||
GString *profanity_dir = g_string_new(xdg_data_home);
|
||||
GString* profanity_dir = g_string_new(xdg_data_home);
|
||||
g_string_append(profanity_dir, "/profanity");
|
||||
|
||||
if (!_mkdir_recursive(profanity_dir->str)) {
|
||||
@@ -144,7 +165,7 @@ _create_data_dir(void)
|
||||
void
|
||||
_create_chatlogs_dir(void)
|
||||
{
|
||||
GString *chatlogs_dir = g_string_new(xdg_data_home);
|
||||
GString* chatlogs_dir = g_string_new(xdg_data_home);
|
||||
g_string_append(chatlogs_dir, "/profanity/chatlogs");
|
||||
|
||||
if (!_mkdir_recursive(chatlogs_dir->str)) {
|
||||
@@ -157,7 +178,7 @@ _create_chatlogs_dir(void)
|
||||
void
|
||||
_create_logs_dir(void)
|
||||
{
|
||||
GString *logs_dir = g_string_new(xdg_data_home);
|
||||
GString* logs_dir = g_string_new(xdg_data_home);
|
||||
g_string_append(logs_dir, "/profanity/logs");
|
||||
|
||||
if (!_mkdir_recursive(logs_dir->str)) {
|
||||
@@ -170,12 +191,12 @@ _create_logs_dir(void)
|
||||
void
|
||||
_cleanup_dirs(void)
|
||||
{
|
||||
const char *group_env = getenv("PROF_TEST_GROUP");
|
||||
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);
|
||||
@@ -216,8 +237,8 @@ _read_output(int timeout_ms)
|
||||
* 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;
|
||||
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;
|
||||
|
||||
@@ -241,50 +262,60 @@ prof_start(void)
|
||||
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)
|
||||
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");
|
||||
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");
|
||||
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.
|
||||
@@ -292,7 +323,7 @@ init_prof_test(void **state)
|
||||
* 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.
|
||||
@@ -324,7 +355,7 @@ init_prof_test(void **state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!started) {
|
||||
fprintf(stderr, "[PROF_TEST] ERROR: could not start stabber on any port\n");
|
||||
return -1;
|
||||
@@ -337,8 +368,8 @@ init_prof_test(void **state)
|
||||
"./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",
|
||||
|
||||
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");
|
||||
@@ -389,7 +420,7 @@ init_prof_test(void **state)
|
||||
}
|
||||
|
||||
int
|
||||
close_prof_test(void **state)
|
||||
close_prof_test(void** state)
|
||||
{
|
||||
if (fd > 0 && child_pid > 0) {
|
||||
prof_input("/quit");
|
||||
@@ -410,7 +441,10 @@ close_prof_test(void **state)
|
||||
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; }
|
||||
if (waitpid(child_pid, NULL, WNOHANG) != 0) {
|
||||
exited = 1;
|
||||
break;
|
||||
}
|
||||
sleep_ms(SHUTDOWN_POLL_MS);
|
||||
}
|
||||
if (!exited) {
|
||||
@@ -429,18 +463,39 @@ close_prof_test(void **state)
|
||||
}
|
||||
|
||||
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)
|
||||
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");
|
||||
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);
|
||||
}
|
||||
@@ -450,24 +505,24 @@ prof_input(const char *input)
|
||||
* Returns 1 if found, 0 if timeout.
|
||||
*/
|
||||
int
|
||||
prof_output_exact(const char *text)
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -476,34 +531,34 @@ prof_output_exact(const char *text)
|
||||
* Returns 1 if found, 0 if timeout.
|
||||
*/
|
||||
int
|
||||
prof_output_regex(const char *pattern)
|
||||
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);
|
||||
@@ -513,23 +568,21 @@ prof_output_regex(const char *pattern)
|
||||
fprintf(stderr, "%s", output_buffer);
|
||||
}
|
||||
fprintf(stderr, "\n");
|
||||
|
||||
|
||||
regfree(®ex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
prof_connect_with_roster(const char *roster)
|
||||
prof_connect_with_roster(const char* roster)
|
||||
{
|
||||
GString *roster_str = g_string_new(
|
||||
GString* roster_str = g_string_new(
|
||||
"<iq type='result' to='stabber@localhost/profanity'>"
|
||||
"<query xmlns='jabber:iq:roster' ver='362'>"
|
||||
);
|
||||
"<query xmlns='jabber:iq:roster' ver='362'>");
|
||||
g_string_append(roster_str, roster);
|
||||
g_string_append(roster_str,
|
||||
"</query>"
|
||||
"</iq>"
|
||||
);
|
||||
"</query>"
|
||||
"</iq>");
|
||||
|
||||
stbbr_for_query("jabber:iq:roster", roster_str->str);
|
||||
g_string_free(roster_str, TRUE);
|
||||
@@ -539,7 +592,7 @@ prof_connect_with_roster(const char *roster)
|
||||
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");
|
||||
|
||||
@@ -547,15 +600,14 @@ prof_connect_with_roster(const char *roster)
|
||||
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>"
|
||||
));
|
||||
"<c xmlns=\"http://jabber.org/protocol/caps\" hash=\"sha-1\" node=\"http://profanity-im.github.io\" ver=\"*\"/>"
|
||||
"</presence>"));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -575,6 +627,5 @@ prof_connect(void)
|
||||
{
|
||||
prof_connect_with_roster(
|
||||
"<item jid='buddy1@localhost' subscription='both' name='Buddy1'/>"
|
||||
"<item jid='buddy2@localhost' subscription='both' name='Buddy2'/>"
|
||||
);
|
||||
"<item jid='buddy2@localhost' subscription='both' name='Buddy2'/>");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user