ci: speed up builds 4x with parallel tests, coverage, and ccache
All checks were successful
CI Code / Check spelling (push) Successful in 18s
CI Code / Check coding style (push) Successful in 31s
CI Code / Code Coverage (push) Successful in 15m25s
CI Code / Linux (debian) (push) Successful in 15m57s
CI Code / Linux (ubuntu) (push) Successful in 16m0s
CI Code / Linux (arch) (push) Successful in 16m6s

Split functional tests into 4 parallel groups and add check-functional-parallel target (~3x faster CI runs).
Add branch-aware LCOV coverage reporting with new --enable-coverage option and lcov summary in CI pipeline.
Enable ccache via -C configure flag for faster recompilations.
Install lcov in all Docker images and use --depth 1 git clones + parallel make -j$(nproc) for quicker container builds.
Update CONTRIBUTING.md with instructions for parallel test groups and adding new ones.

All changes are tightly related CI/performance improvements developed in sequence. No external service uploads (e.g. Codecov skipped due to Gitea incompatibility).
This commit is contained in:
2026-01-21 16:35:17 +01:00
parent a90eef1cb2
commit 85c817ee8c
14 changed files with 418 additions and 118 deletions

View File

@@ -14,21 +14,31 @@
* flaky tests caused by leftover state. The overhead is acceptable since
* functional tests run less frequently than unit tests.
*
* Tests are organized into groups for better maintainability:
* Group 1: Connection, Ping, Rooms, Presence
* Group 2: Messages, Receipts, Roster management
* Group 3: MUC (Multi-User Chat) functionality
* Group 4: Carbons, Chat sessions, Software version, Disconnect
* Tests are organized into groups for better maintainability and parallel execution:
* Group 1: Connect, Ping, Rooms, Software
* Group 2: Message, Receipts, Roster, Chat Session
* Group 3: Presence, Disconnect
* Group 4: MUC, Carbons
*
* Parallel execution:
* ./functionaltests - run all tests sequentially
* ./functionaltests N - run group N only (N = 1..num_groups)
*
* For parallel execution, run multiple groups simultaneously:
* ./functionaltests 1 & ./functionaltests 2 & ./functionaltests 3 & ./functionaltests 4 & wait
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include "prof_cmocka.h"
#include <sys/stat.h>
#include "config.h"
#include "common.h"
#include "proftest.h"
#include "test_connect.h"
#include "test_ping.h"
@@ -49,13 +59,21 @@
int
main(int argc, char* argv[])
{
const struct CMUnitTest all_tests[] = {
/* ============================================================
* GROUP 1: Connect, Ping, Rooms, Presence
* Basic XMPP session establishment and presence management
* ============================================================ */
int group = 0; /* 0 = all groups */
if (argc > 1) {
group = atoi(argv[1]);
if (group < 1 || group > 4) {
fprintf(stderr, "Usage: %s [group]\n", argv[0]);
fprintf(stderr, " group: 1-4 to run specific group, or omit for all\n");
return 1;
}
}
/* ============================================================
* GROUP 1: Connect, Ping, Rooms, Software
* Basic XMPP session establishment and server queries
* ============================================================ */
const struct CMUnitTest group1_tests[] = {
/* Connection tests - verify login, roster, bookmarks */
PROF_FUNC_TEST(connect_jid_requests_roster),
PROF_FUNC_TEST(connect_jid_sends_presence_after_receiving_roster),
@@ -73,28 +91,20 @@ main(int argc, char* argv[])
/* Room discovery - XEP-0045 */
PROF_FUNC_TEST(rooms_query),
/* Presence tests - online/away/xa/dnd/chat status */
PROF_FUNC_TEST(presence_online),
PROF_FUNC_TEST(presence_online_with_message),
PROF_FUNC_TEST(presence_away),
PROF_FUNC_TEST(presence_away_with_message),
PROF_FUNC_TEST(presence_xa),
PROF_FUNC_TEST(presence_xa_with_message),
PROF_FUNC_TEST(presence_dnd),
PROF_FUNC_TEST(presence_dnd_with_message),
PROF_FUNC_TEST(presence_chat),
PROF_FUNC_TEST(presence_chat_with_message),
PROF_FUNC_TEST(presence_set_priority),
PROF_FUNC_TEST(presence_includes_priority),
PROF_FUNC_TEST(presence_keeps_status),
PROF_FUNC_TEST(presence_received),
PROF_FUNC_TEST(presence_missing_resource_defaults),
/* ============================================================
* GROUP 2: Message, Receipts, Roster
* Core messaging and contact management
* ============================================================ */
/* Software Version - XEP-0092 */
PROF_FUNC_TEST(send_software_version_request),
PROF_FUNC_TEST(display_software_version_result),
PROF_FUNC_TEST(shows_message_when_software_version_error),
PROF_FUNC_TEST(display_software_version_result_when_from_domainpart),
PROF_FUNC_TEST(show_message_in_chat_window_when_no_resource),
PROF_FUNC_TEST(display_software_version_result_in_chat),
};
/* ============================================================
* GROUP 2: Message, Receipts, Roster, Chat Session
* Core messaging and contact management
* ============================================================ */
const struct CMUnitTest group2_tests[] = {
/* Basic message send/receive */
PROF_FUNC_TEST(message_send),
PROF_FUNC_TEST(message_receive_console),
@@ -112,21 +122,54 @@ main(int argc, char* argv[])
PROF_FUNC_TEST(sends_remove_item_nick),
PROF_FUNC_TEST(sends_nick_change),
/* ============================================================
* GROUP 3: MUC (Multi-User Chat)
* XEP-0045 conference room functionality
* ============================================================ */
/* Chat session management - bare/full JID routing */
PROF_FUNC_TEST(sends_message_to_barejid_when_contact_offline),
PROF_FUNC_TEST(sends_message_to_barejid_when_contact_online),
PROF_FUNC_TEST(sends_message_to_fulljid_when_received_from_fulljid),
PROF_FUNC_TEST(sends_subsequent_messages_to_fulljid),
PROF_FUNC_TEST(resets_to_barejid_after_presence_received),
PROF_FUNC_TEST(new_session_when_message_received_from_different_fulljid),
};
/* Room join with various options */
/* ============================================================
* GROUP 3: Presence, Disconnect
* Online/away/xa/dnd/chat status management
* ============================================================ */
const struct CMUnitTest group3_tests[] = {
PROF_FUNC_TEST(presence_online),
PROF_FUNC_TEST(presence_online_with_message),
PROF_FUNC_TEST(presence_away),
PROF_FUNC_TEST(presence_away_with_message),
PROF_FUNC_TEST(presence_xa),
PROF_FUNC_TEST(presence_xa_with_message),
PROF_FUNC_TEST(presence_dnd),
PROF_FUNC_TEST(presence_dnd_with_message),
PROF_FUNC_TEST(presence_chat),
PROF_FUNC_TEST(presence_chat_with_message),
PROF_FUNC_TEST(presence_set_priority),
PROF_FUNC_TEST(presence_includes_priority),
PROF_FUNC_TEST(presence_keeps_status),
PROF_FUNC_TEST(presence_received),
PROF_FUNC_TEST(presence_missing_resource_defaults),
/* Disconnect - clean session termination */
PROF_FUNC_TEST(disconnect_ends_session),
};
/* ============================================================
* GROUP 4: MUC, Carbons
* Multi-user chat and message synchronization
* ============================================================ */
const struct CMUnitTest group4_tests[] = {
/* MUC room join with various options - XEP-0045 */
PROF_FUNC_TEST(sends_room_join),
PROF_FUNC_TEST(sends_room_join_with_nick),
PROF_FUNC_TEST(sends_room_join_with_password),
PROF_FUNC_TEST(sends_room_join_with_nick_and_password),
/* Room information display */
/* MUC room information display */
PROF_FUNC_TEST(shows_role_and_affiliation_on_join),
PROF_FUNC_TEST(shows_subject_on_join),
// PROF_FUNC_TEST(shows_history_message), // temporarily disabled due to timing issues in CI
PROF_FUNC_TEST(shows_occupant_join),
/* MUC messaging */
@@ -134,16 +177,11 @@ main(int argc, char* argv[])
PROF_FUNC_TEST(shows_me_message_from_occupant),
PROF_FUNC_TEST(shows_me_message_from_self),
/* Console notification settings for MUC */
/* MUC console notification settings */
PROF_FUNC_TEST(shows_all_messages_in_console_when_window_not_focussed),
PROF_FUNC_TEST(shows_first_message_in_console_when_window_not_focussed),
PROF_FUNC_TEST(shows_no_message_in_console_when_window_not_focussed),
/* ============================================================
* GROUP 4: Carbons, Chat Session, Software, Disconnect
* Message synchronization and session management
* ============================================================ */
/* Message Carbons - XEP-0280 (message sync across devices) */
PROF_FUNC_TEST(send_enable_carbons),
PROF_FUNC_TEST(connect_with_carbons_enabled),
@@ -151,26 +189,34 @@ main(int argc, char* argv[])
PROF_FUNC_TEST(receive_carbon),
PROF_FUNC_TEST(receive_self_carbon),
PROF_FUNC_TEST(receive_private_carbon),
/* Chat session management - bare/full JID routing */
PROF_FUNC_TEST(sends_message_to_barejid_when_contact_offline),
PROF_FUNC_TEST(sends_message_to_barejid_when_contact_online),
PROF_FUNC_TEST(sends_message_to_fulljid_when_received_from_fulljid),
PROF_FUNC_TEST(sends_subsequent_messages_to_fulljid),
PROF_FUNC_TEST(resets_to_barejid_after_presence_received),
PROF_FUNC_TEST(new_session_when_message_received_from_different_fulljid),
/* Software Version - XEP-0092 */
PROF_FUNC_TEST(send_software_version_request),
PROF_FUNC_TEST(display_software_version_result),
PROF_FUNC_TEST(shows_message_when_software_version_error),
PROF_FUNC_TEST(display_software_version_result_when_from_domainpart),
PROF_FUNC_TEST(show_message_in_chat_window_when_no_resource),
PROF_FUNC_TEST(display_software_version_result_in_chat),
/* Disconnect - clean session termination */
PROF_FUNC_TEST(disconnect_ends_session),
};
return cmocka_run_group_tests(all_tests, NULL, NULL);
/* Test group registry for easy extension */
struct {
const char* name;
const struct CMUnitTest* tests;
size_t count;
} groups[] = {
{ "Group 1: Connect/Ping/Rooms/Software", group1_tests, ARRAY_SIZE(group1_tests) },
{ "Group 2: Message/Receipts/Roster/Session", group2_tests, ARRAY_SIZE(group2_tests) },
{ "Group 3: Presence/Disconnect", group3_tests, ARRAY_SIZE(group3_tests) },
{ "Group 4: MUC/Carbons", group4_tests, ARRAY_SIZE(group4_tests) },
};
const int num_groups = ARRAY_SIZE(groups);
int result = 0;
if (group > 0 && group <= num_groups) {
/* Run specific group */
result = _cmocka_run_group_tests(groups[group - 1].name, groups[group - 1].tests,
groups[group - 1].count, NULL, NULL);
} else {
/* Run all groups sequentially */
for (int i = 0; i < num_groups; i++) {
result |= _cmocka_run_group_tests(groups[i].name, groups[i].tests,
groups[i].count, NULL, NULL);
}
}
return result;
}

View File

@@ -24,6 +24,13 @@ int fd = 0;
int stub_port = 5230;
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
@@ -77,7 +84,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)) {
@@ -90,7 +97,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)) {
@@ -103,7 +110,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)) {
@@ -116,7 +123,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)) {
@@ -129,7 +136,9 @@ _create_logs_dir(void)
void
_cleanup_dirs(void)
{
int res = system("rm -rf ./tests/functionaltests/files");
char cmd[512];
snprintf(cmd, sizeof(cmd), "rm -rf ./tests/functionaltests/files/%d", stub_port);
int res = system(cmd);
if (res == -1) {
assert_true(FALSE);
}
@@ -231,14 +240,20 @@ init_prof_test(void **state)
return -1;
}
// Generate unique XDG paths based on stub_port for parallel execution
snprintf(xdg_config_home, sizeof(xdg_config_home),
"./tests/functionaltests/files/%d/xdg_config_home", stub_port);
snprintf(xdg_data_home, sizeof(xdg_data_home),
"./tests/functionaltests/files/%d/xdg_data_home", stub_port);
// Give stabber server thread time to start listening
usleep(100000); // 100ms
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);
setenv("XDG_CONFIG_HOME", xdg_config_home, 1);
setenv("XDG_DATA_HOME", xdg_data_home, 1);
_cleanup_dirs();

View File

@@ -1,8 +1,13 @@
#ifndef __H_PROFTEST
#define __H_PROFTEST
#define XDG_CONFIG_HOME "./tests/functionaltests/files/xdg_config_home"
#define XDG_DATA_HOME "./tests/functionaltests/files/xdg_data_home"
/*
* XDG paths are dynamic and generated per-test based on stub_port.
* Each test instance uses unique directories (./tests/functionaltests/files/{port}/xdg_*)
* to allow parallel test execution without file conflicts.
*/
extern char xdg_config_home[256];
extern char xdg_data_home[256];
extern int stub_port;