diff --git a/tests/functionaltests/proftest.c b/tests/functionaltests/proftest.c index 37813327..88502893 100644 --- a/tests/functionaltests/proftest.c +++ b/tests/functionaltests/proftest.c @@ -12,6 +12,7 @@ #include #include #include +#include #include @@ -20,6 +21,14 @@ /* 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 + +/* Shutdown polling: interval and maximum attempts before escalating signals */ +#define SHUTDOWN_POLL_MS 50 +#define SHUTDOWN_POLL_MAX 100 /* 100 x 50ms = 5s */ +#define SIGTERM_POLL_MAX 20 /* 20 x 50ms = 1s */ + char *config_orig; char *data_orig; @@ -246,38 +255,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 = 5230 + ((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 + 200; ++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,8 +311,8 @@ 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 + /* No sleep needed: server_run() completes bind()+listen() before returning. + * Incoming connections queue in kernel backlog until accept() runs. */ config_orig = getenv("XDG_CONFIG_HOME"); data_orig = getenv("XDG_DATA_HOME"); @@ -315,38 +327,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 +366,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). */ + usleep(100000); /* 100ms grace for /quit to be read from pty buffer */ 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; + } + usleep(SHUTDOWN_POLL_MS * 1000); + } + 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; } + usleep(SHUTDOWN_POLL_MS * 1000); + } + if (!exited) { + kill(child_pid, SIGKILL); + waitpid(child_pid, NULL, 0); + } + } child_pid = 0; } @@ -371,13 +402,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; }