From f84ed1bf6ae67d402615d533b39a3d7879c6d9b6 Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Thu, 12 Mar 2026 13:06:56 +0300 Subject: [PATCH] =?UTF-8?q?perf(functests):=20speedup=20=E2=80=94=20profrc?= =?UTF-8?q?=20pre-baking,=20pty-close=20shutdown,=20parallel=20port=20pool?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace 16 interactive UI setup commands with pre-written profrc file containing [ui] and [notifications] sections (~1800ms saved per test). Replace sleep(1) + blocking waitpid with close(pty fd) → SIGHUP → polling waitpid(WNOHANG) → SIGTERM/SIGKILL fallback chain (~4900ms saved per test). Remove post-stbbr_start() and post-stbbr_stop() sleeps — bind+listen completes synchronously before stbbr_start() returns, and pthread_join() in stbbr_stop() guarantees socket cleanup (~200ms saved). Add PORTS_PER_GROUP=50 isolated port ranges per test group to enable safe parallel execution of 4 groups without port conflicts. --- tests/functionaltests/proftest.c | 215 +++++++++++++++++++------------ 1 file changed, 132 insertions(+), 83 deletions(-) diff --git a/tests/functionaltests/proftest.c b/tests/functionaltests/proftest.c index 37813327..525a7d01 100644 --- a/tests/functionaltests/proftest.c +++ b/tests/functionaltests/proftest.c @@ -12,6 +12,7 @@ #include #include #include +#include #include @@ -20,11 +21,41 @@ /* Number of parallel test groups for CI builds */ #define TEST_GROUPS 4 +/* 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 + +/* 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 = 5230; +int stub_port = BASE_PORT; pid_t child_pid = 0; /* @@ -44,7 +75,7 @@ static char output_buffer[OUTPUT_BUF_SIZE]; static size_t output_len = 0; /* Timeout for expect operations in seconds */ -static int expect_timeout = 30; +static int expect_timeout = EXPECT_TIMEOUT_DEFAULT; gboolean _create_dir(const char *name) @@ -153,6 +184,12 @@ _cleanup_dirs(void) } } +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. @@ -166,8 +203,8 @@ _read_output(int timeout_ms) FD_ZERO(&readfds); FD_SET(fd, &readfds); - tv.tv_sec = timeout_ms / 1000; - tv.tv_usec = (timeout_ms % 1000) * 1000; + 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) { @@ -198,8 +235,8 @@ void prof_start(void) { struct winsize ws; - ws.ws_row = 24; - ws.ws_col = 300; /* Match COLUMNS=300 from start_profanity.sh */ + ws.ws_row = PTY_ROWS; + ws.ws_col = PTY_COLS; ws.ws_xpixel = 0; ws.ws_ypixel = 0; @@ -216,7 +253,7 @@ prof_start(void) if (child_pid == 0) { /* Child process */ - setenv("COLUMNS", "300", 1); + setenv("COLUMNS", STRINGIFY(PTY_COLS), 1); setenv("TERM", "xterm", 1); execl("./profanity", "./profanity", "-l", "DEBUG", NULL); @@ -232,7 +269,7 @@ prof_start(void) fcntl(fd, F_SETFL, flags | O_NONBLOCK); /* Brief wait for process to initialize */ - usleep(50000); /* 50ms */ + sleep_ms(INIT_WAIT_MS); } int @@ -246,38 +283,41 @@ init_prof_test(void **state) 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. - * Build 0 (local/default): 5230-5233, Full: 5230-5233, Minimal: 5234-5237, 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 = 5230 + ((build_idx > 0 ? build_idx - 1 : 0) * TEST_GROUPS); + /* 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); - /* Static resource allocation to avoid conflicts in parallel execution. - * Group 1-4: use static port assignment. - * Group 0 (all groups): use dynamic allocation as fallback. */ + /* 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) { - /* Static allocation: each group gets a dedicated port */ - stub_port = port_base + group - 1; - printf("[PROF_TEST] Build %d, Group %d: trying port %d\n", build_idx, group, stub_port); - - if (stbbr_start(STBBR_LOGDEBUG, stub_port, 0) == 0) { - started = TRUE; - printf("[PROF_TEST] Started stabber on port %d\n", stub_port); - } else { - printf("[PROF_TEST] Failed to start stabber on port %d\n", stub_port); - } - } - - /* Fallback to dynamic allocation if static failed or group=0 */ - if (!started) { - printf("[PROF_TEST] Using dynamic port allocation\n"); - for (int p = port_base; p < port_base + 20; ++p) { + 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; - printf("[PROF_TEST] Started stabber on port %d\n", stub_port); break; } } @@ -299,9 +339,6 @@ init_prof_test(void **state) printf("[PROF_TEST] Group %d using directories: config=%s, data=%s\n", group, xdg_config_home, xdg_data_home); - // Give stabber server thread time to start listening - usleep(100000); // 100ms - config_orig = getenv("XDG_CONFIG_HOME"); data_orig = getenv("XDG_DATA_HOME"); @@ -315,38 +352,37 @@ init_prof_test(void **state) _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); - // set UI options to make expect assertions faster and more reliable - prof_input("/inpblock timeout 5"); - assert_true(prof_output_regex("Input blocking set to 5 milliseconds")); - prof_input("/inpblock dynamic off"); - assert_true(prof_output_regex("Dynamic input blocking disabled")); - prof_input("/notify chat off"); - assert_true(prof_output_regex("Chat notifications disabled")); - prof_input("/notify room off"); - assert_true(prof_output_regex("Room notifications disabled")); - prof_input("/wrap off"); - assert_true(prof_output_regex("Word wrap disabled")); - prof_input("/roster hide"); - assert_true(prof_output_regex("Roster disabled")); - prof_input("/occupants default hide"); - assert_true(prof_output_regex("Occupant list disabled")); - prof_input("/time console off"); - prof_input("/time console off"); - assert_true(prof_output_regex("Console time display disabled\\.")); - prof_input("/time chat off"); - assert_true(prof_output_regex("Chat time display disabled\\.")); - prof_input("/time muc off"); - assert_true(prof_output_regex("MUC time display disabled\\.")); - prof_input("/time config off"); - assert_true(prof_output_regex("Config time display disabled\\.")); - prof_input("/time private off"); - assert_true(prof_output_regex("Private chat time display disabled\\.")); - prof_input("/time xml off"); - assert_true(prof_output_regex("XML Console time display disabled\\.")); return 0; } @@ -355,11 +391,31 @@ close_prof_test(void **state) { if (fd > 0 && child_pid > 0) { prof_input("/quit"); - // Give profanity time to process quit command - sleep(1); - waitpid(child_pid, NULL, 0); + /* 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; } @@ -371,13 +427,6 @@ close_prof_test(void **state) } 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; } @@ -391,7 +440,7 @@ prof_input(const char *input) g_string_free(inp_str, TRUE); /* Small delay to let profanity process input */ - usleep(10000); + sleep_ms(INPUT_DELAY_MS); } /* @@ -405,7 +454,7 @@ prof_output_exact(const char *text) while (time(NULL) - start < expect_timeout) { /* Read any available output */ - while (_read_output(100) > 0) { + while (_read_output(READ_TIMEOUT_MS) > 0) { /* Keep reading while data available */ } @@ -414,7 +463,7 @@ prof_output_exact(const char *text) return 1; } - usleep(50000); /* 50ms */ + sleep_ms(OUTPUT_POLL_MS); } return 0; @@ -439,7 +488,7 @@ prof_output_regex(const char *pattern) while (time(NULL) - start < expect_timeout) { /* Read any available output */ - while (_read_output(100) > 0) { + while (_read_output(READ_TIMEOUT_MS) > 0) { /* Keep reading while data available */ } @@ -450,7 +499,7 @@ prof_output_regex(const char *pattern) return 1; } - usleep(50000); /* 50ms */ + sleep_ms(OUTPUT_POLL_MS); } /* Timeout reached - log diagnostic info */ @@ -492,12 +541,12 @@ prof_connect_with_roster(const char *roster) assert_true(prof_output_regex("password:")); prof_input("password"); - expect_timeout = 60; + 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 = 60; + 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( @@ -516,7 +565,7 @@ prof_timeout(int timeout) void prof_timeout_reset(void) { - expect_timeout = 60; + expect_timeout = EXPECT_TIMEOUT_CONNECT; } void