From 9a2a7614a96e025384d129741854422880c2fd47 Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Wed, 28 Jan 2026 19:35:22 +0300 Subject: [PATCH 1/7] ci: improve CI stability with parallel builds, Valgrind, and coverage --- .gitignore | 6 + CONTRIBUTING.md | 9 + Makefile.am | 9 +- ci-build.sh | 211 +++++++++++++++++++----- prof.supp | 23 +++ tests/functionaltests/functionaltests.c | 6 + tests/functionaltests/proftest.c | 81 +++++++-- 7 files changed, 289 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index 59b5eab2..0559b357 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,8 @@ tests/unittests/unittests tests/unittests/unittests.log tests/unittests/unittests.trs test-suite.log +test-files/ +test-logs/ # valgrind output profval* @@ -108,3 +110,7 @@ breaks *.zip *.log* coverage/ +*.gcno +*.gcda +*.gcov +coverage.info diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a0ad4248..4aadd667 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -90,6 +90,15 @@ set -e ``` This will run the same tests that the CI runs and refuse the push if it fails. +The CI script runs 4 parallel builds with different configurations, each with Valgrind and functional tests. +Build 1 also collects code coverage (lines, functions, branches). + +Output shows test results per build: +``` +✓ Build 1 passed (unit: 437/0, func: 69/0, cov: L:27.5% F:36.2% B:18.1%, 5m39s) +✓ Build 2 passed (unit: 398/0, func: 69/0, 5m00s) +``` + Note that it will run on the actual content of the repository directory and not what may have been staged/committed. diff --git a/Makefile.am b/Makefile.am index 89809594..1735d1ea 100644 --- a/Makefile.am +++ b/Makefile.am @@ -313,7 +313,7 @@ FUNC_TEST_GROUPS = 1 2 3 4 check-functional-parallel: tests/functionaltests/functionaltests @echo "Running functional tests in parallel ($(words $(FUNC_TEST_GROUPS)) groups)..." - @mkdir -p $(builddir)/test-logs + @mkdir -p $(builddir)/test-logs $(builddir)/test-files @pids=""; \ for g in $(FUNC_TEST_GROUPS); do \ ./tests/functionaltests/functionaltests $$g > $(builddir)/test-logs/group$$g.log 2>&1 & \ @@ -372,7 +372,10 @@ clean-local: rm -f $(git_include) $(git_include).in endif -.PHONY: my-prof.supp +clean-functional-tests: + rm -rf $(builddir)/test-files $(builddir)/test-logs + +.PHONY: my-prof.supp clean-functional-tests my-prof.supp: @sed '/^# AUTO-GENERATED START/q' prof.supp > $@ @printf "\n\n# glib\n" >> $@ @@ -388,7 +391,7 @@ check-unit: tests/unittests/unittests tests/unittests/unittests @VALGRIND_CHECK_RULES@ -VALGRIND_SUPPRESSIONS_FILES=prof.supp +VALGRIND_SUPPRESSIONS_FILES=$(srcdir)/prof.supp # Code coverage targets (requires --enable-coverage) coverage-clean: diff --git a/ci-build.sh b/ci-build.sh index f98bedfa..dcf3ccca 100755 --- a/ci-build.sh +++ b/ci-build.sh @@ -180,30 +180,13 @@ case "$ARCH" in ;; esac -case "$ARCH" in - linux*) - echo - echo "--> Building with ./configure -C ${tests[0]} --enable-valgrind $*" - echo - - # shellcheck disable=SC2086 - ./configure -C ${tests[0]} --enable-valgrind $* - - $MAKE CC="${CC}" - if grep '^ID=' /etc/os-release | grep -q -e debian; then - $MAKE check-valgrind - else - $MAKE check-valgrind || log_content ./test-suite-memcheck.log - fi - $MAKE distclean - ;; -esac - # Function to build and test a single configuration build_and_test() { local features="$1" local extra_args="$2" local idx="$3" + local run_valgrind="$4" + local run_coverage="$5" local build_dir="build-$idx" local log_file="build-$idx.log" @@ -211,14 +194,101 @@ build_and_test() { echo "=== Build $idx started at $(date) ===" echo "--> Building in $build_dir with ./configure -C $features $extra_args" + local start_time=$SECONDS + mkdir -p "$build_dir" cd "$build_dir" # shellcheck disable=SC2086 - ../configure -C $features $extra_args + if ! ../configure -C $features $extra_args; then + echo "ERROR: configure failed" + exit 1 + fi - $MAKE CC="${CC}" - $MAKE check-functional-parallel + if ! $MAKE CC="${CC}"; then + echo "ERROR: make failed" + exit 1 + fi + + # Run unit tests under Valgrind + local unit_passed=0 unit_failed=0 + if [ "$run_valgrind" = "yes" ]; then + echo "--> Running unit tests under Valgrind..." + # Build unit tests first + $MAKE tests/unittests/unittests + # Run valgrind directly to capture cmocka output + valgrind --error-exitcode=1 --leak-check=full \ + --suppressions=../prof.supp \ + tests/unittests/unittests 2>&1 | tee unit-tests-output.log + valgrind_exit=${PIPESTATUS[0]} + if [ $valgrind_exit -ne 0 ]; then + echo "ERROR: Valgrind unit tests failed (exit code $valgrind_exit)" + exit 1 + fi + # Extract unit test counts from cmocka output + unit_passed=$(grep -E "^\[ PASSED \] [0-9]+ test" unit-tests-output.log | grep -oE "[0-9]+" | head -1) + unit_failed=$(grep -E "^\[ FAILED \] [0-9]+ test" unit-tests-output.log | grep -oE "[0-9]+" | head -1) + [ -z "$unit_passed" ] && unit_passed=0 + [ -z "$unit_failed" ] && unit_failed=0 + echo "UNIT_TESTS: passed=$unit_passed failed=$unit_failed" + fi + + # Set build index for port allocation: build 1 uses ports 5230-5233, + # build 2 uses 5234-5237, etc. This prevents port conflicts in parallel builds. + export PROF_BUILD_INDEX=$idx + local func_passed=0 func_failed=0 + if ! $MAKE check-functional-parallel; then + echo "ERROR: functional tests failed" + exit 1 + fi + # Extract functional test counts from group logs + echo "=== Functional test results ===" + for glog in ./test-logs/group*.log; do + if [ -f "$glog" ]; then + cnt=$(grep -E "^\[ PASSED \] [0-9]+ test" "$glog" | grep -oE "[0-9]+" | head -1) + [ -n "$cnt" ] && func_passed=$((func_passed + cnt)) + cnt=$(grep -E "^\[ FAILED \] [0-9]+ test" "$glog" | grep -oE "[0-9]+" | head -1) + [ -n "$cnt" ] && func_failed=$((func_failed + cnt)) + fi + done + echo "FUNC_TESTS: passed=$func_passed failed=$func_failed" + + # Collect coverage data if enabled (lines, functions, branches) + # Must be done BEFORE make clean which removes .gcda files + local cov_lines="n/a" cov_funcs="n/a" cov_branches="n/a" + if [ "$run_coverage" = "yes" ]; then + echo "--> Collecting coverage data..." + if command -v lcov >/dev/null 2>&1; then + lcov --capture --directory . --output-file coverage.info \ + --rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true + # Remove test files, stubs, system headers - only keep src/ production code + lcov --remove coverage.info \ + '/usr/*' \ + '*/tests/*' \ + '*test*.c' \ + '*stub*.c' \ + '*/functionaltests/*' \ + '*/unittests/*' \ + --output-file coverage.info \ + --rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true + if [ -f coverage.info ] && [ -s coverage.info ]; then + local summary + summary=$(lcov --summary coverage.info \ + --rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true) + cov_lines=$(echo "$summary" | grep -E "lines\.*:" | grep -oE "[0-9]+\.[0-9]+%" | head -1) + cov_funcs=$(echo "$summary" | grep -E "functions\.*:" | grep -oE "[0-9]+\.[0-9]+%" | head -1) + cov_branches=$(echo "$summary" | grep -E "branches\.*:" | grep -oE "[0-9]+\.[0-9]+%" | head -1) + [ -z "$cov_lines" ] && cov_lines="n/a" + [ -z "$cov_funcs" ] && cov_funcs="n/a" + [ -z "$cov_branches" ] && cov_branches="n/a" + else + echo "WARNING: coverage.info is empty or not created" + fi + else + echo "WARNING: lcov not found" + fi + echo "COVERAGE: lines=$cov_lines funcs=$cov_funcs branches=$cov_branches" + fi ./profanity -v $MAKE clean @@ -226,46 +296,103 @@ build_and_test() { cd .. rm -rf "$build_dir" + local elapsed=$((SECONDS - start_time)) + local mins=$((elapsed / 60)) + local secs=$((elapsed % 60)) + echo "=== Build $idx completed at $(date) ===" + echo "STATS: unit_passed=$unit_passed unit_failed=$unit_failed func_passed=$func_passed func_failed=$func_failed cov_lines=$cov_lines cov_funcs=$cov_funcs cov_branches=$cov_branches time=${mins}m${secs}s" } > "$log_file" 2>&1 } # Run all 4 configurations in parallel -echo "Starting parallel builds..." +# Coverage enabled only for build 1 (Full) - it has most code paths +echo +echo "=== Start build ===" +echo +echo "Starting 4 parallel build configurations..." +echo pids=() for idx in 1 2 3 4; do if [ $idx -le ${#tests[@]} ]; then - build_and_test "${tests[$((idx-1))]}" "$*" "$idx" & + # All builds run Valgrind + if [ "$ARCH" = "linux" ]; then + run_valgrind="yes" + if [ $idx -eq 1 ]; then + extra_flags="--enable-valgrind --enable-coverage" + run_coverage="yes" + else + extra_flags="--enable-valgrind" + run_coverage="no" + fi + else + extra_flags="" + run_valgrind="no" + run_coverage="no" + fi + build_and_test "${tests[$((idx-1))]}" "$* $extra_flags" "$idx" "$run_valgrind" "$run_coverage" & pids+=("$!") - echo "Started build $idx (PID: $!)" + flags_desc="" + [ "$run_valgrind" = "yes" ] && flags_desc="Valgrind" + [ "$run_coverage" = "yes" ] && flags_desc="${flags_desc:+$flags_desc+}Coverage" + [ -n "$flags_desc" ] && flags_desc=" [+$flags_desc]" + echo " → Build $idx: ${tests[$((idx-1))]}$flags_desc" fi done +echo # Wait for all builds and check exit codes -failed=0 +echo "Waiting for builds to complete..." +echo +failed_builds=() for i in "${!pids[@]}"; do idx=$((i + 1)) if wait "${pids[$i]}"; then - echo "✓ Build $idx passed" + if [ -f "build-$idx.log" ]; then + stats_line=$(grep "^STATS:" "build-$idx.log" | tail -1) + unit_p=$(echo "$stats_line" | grep -oE "unit_passed=[0-9]+" | cut -d= -f2) + unit_f=$(echo "$stats_line" | grep -oE "unit_failed=[0-9]+" | cut -d= -f2) + func_p=$(echo "$stats_line" | grep -oE "func_passed=[0-9]+" | cut -d= -f2) + func_f=$(echo "$stats_line" | grep -oE "func_failed=[0-9]+" | cut -d= -f2) + cov_lines=$(echo "$stats_line" | grep -oE "cov_lines=[0-9.]+%|cov_lines=n/a" | cut -d= -f2) + cov_funcs=$(echo "$stats_line" | grep -oE "cov_funcs=[0-9.]+%|cov_funcs=n/a" | cut -d= -f2) + cov_branches=$(echo "$stats_line" | grep -oE "cov_branches=[0-9.]+%|cov_branches=n/a" | cut -d= -f2) + time=$(echo "$stats_line" | grep -oE "time=[0-9]+m[0-9]+s" | cut -d= -f2) + + unit_p=${unit_p:-0} + unit_f=${unit_f:-0} + func_p=${func_p:-0} + func_f=${func_f:-0} + + echo "✓ BUILD $idx PASSED" + echo " Unit tests: $unit_p passed, $unit_f failed" + echo " Functional tests: $func_p passed, $func_f failed" + if [ "$cov_lines" != "n/a" ] && [ -n "$cov_lines" ]; then + echo " Coverage: Lines: $cov_lines | Functions: $cov_funcs | Branches: $cov_branches" + fi + echo " Duration: ${time:-?}" + else + echo "✓ Build $idx passed (no stats available)" + fi else - echo "✗ Build $idx failed" - echo "--- Log for build $idx ---" + echo "✗ BUILD $idx FAILED" + failed_builds+=("$idx") + fi + echo +done + +# Show failed builds full logs +for idx in "${failed_builds[@]}"; do + if [ -f "build-$idx.log" ]; then + echo "=== BUILD $idx FAILURE LOG ===" cat "build-$idx.log" - echo "--- End log ---" - failed=1 + echo fi done -# Show all logs on success too -if [ $failed -eq 0 ]; then - echo - echo "All builds passed!" - for idx in 1 2 3 4; do - if [ -f "build-$idx.log" ]; then - echo "--- Log for build $idx ---" - cat "build-$idx.log" - fi - done -else +if [ ${#failed_builds[@]} -gt 0 ]; then + echo "RESULT: FAILED (builds ${failed_builds[*]})" exit 1 +else + echo "RESULT: ALL 4 BUILDS PASSED ✓" fi diff --git a/prof.supp b/prof.supp index 87aca009..0110477b 100644 --- a/prof.supp +++ b/prof.supp @@ -8,6 +8,29 @@ # * python suppressions file from https://github.com/python/cpython/blob/main/Misc/valgrind-python.supp # +# ============================================ +# glibc AVX2 optimizations (false positives) +# See: https://sourceware.org/bugzilla/show_bug.cgi?id=19796 +# ============================================ + +{ + glibc_wcpncpy_avx2 + Memcheck:Addr32 + fun:__wcpncpy_avx2 + fun:wcsxfrm_l + fun:g_utf8_collate_key + ... +} + +{ + glibc_wcsxfrm_avx2 + Memcheck:Addr32 + ... + fun:wcsxfrm_l + fun:g_utf8_collate_key + ... +} + # ============================================ # Functional tests suppressions (stabber/pthread) # ============================================ diff --git a/tests/functionaltests/functionaltests.c b/tests/functionaltests/functionaltests.c index e5f59d4e..cd89c58f 100644 --- a/tests/functionaltests/functionaltests.c +++ b/tests/functionaltests/functionaltests.c @@ -69,6 +69,12 @@ main(int argc, char* argv[]) } } + char group_env[16]; + snprintf(group_env, sizeof(group_env), "%d", group); + setenv("PROF_TEST_GROUP", group_env, 1); + + fprintf(stderr, "[PROF_TEST] Starting functional tests, group=%d\n", group); + /* ============================================================ * GROUP 1: Connect, Ping, Rooms, Software * Basic XMPP session establishment and server queries diff --git a/tests/functionaltests/proftest.c b/tests/functionaltests/proftest.c index 4ece33d6..cc59237c 100644 --- a/tests/functionaltests/proftest.c +++ b/tests/functionaltests/proftest.c @@ -136,8 +136,14 @@ _create_logs_dir(void) 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 <= 4) ? group : stub_port; + + fprintf(stderr, "[PROF_TEST] Cleaning up directories for group %d (dir_id %d)\n", group, dir_id); + char cmd[512]; - snprintf(cmd, sizeof(cmd), "rm -rf ./tests/functionaltests/files/%d", stub_port); + snprintf(cmd, sizeof(cmd), "rm -rf ./test-files/%d", dir_id); int res = system(cmd); if (res == -1) { assert_true(FALSE); @@ -221,30 +227,73 @@ prof_start(void) /* 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 */ + usleep(50000); /* 50ms */ } int init_prof_test(void **state) { + /* 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 4 ports + * Build 0/1: 5230-5233, Build 2: 5234-5237, Build 3: 5238-5241, Build 4: 5242-5245 */ + int port_base = 5230 + ((build_idx > 0 ? build_idx - 1 : 0) * 4); + + /* 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. */ gboolean started = FALSE; - for (int p = 5230; p < 5250; ++p) { - int ret = stbbr_start(STBBR_LOGDEBUG, p, 0); - if (ret == 0) { - stub_port = p; + + if (group >= 1 && group <= 4) { + /* Static allocation: each group gets a dedicated port */ + stub_port = port_base + group - 1; + fprintf(stderr, "[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; - break; + fprintf(stderr, "[PROF_TEST] Started stabber on port %d\n", stub_port); + } else { + fprintf(stderr, "[PROF_TEST] Failed to start stabber on port %d\n", stub_port); } } + + /* Fallback to dynamic allocation if static failed or group=0 */ if (!started) { - assert_true(FALSE); // could not start stabber on any port in range + fprintf(stderr, "[PROF_TEST] Using dynamic port allocation\n"); + for (int p = port_base; p < port_base + 20; ++p) { + if (stbbr_start(STBBR_LOGDEBUG, p, 0) == 0) { + stub_port = p; + started = TRUE; + fprintf(stderr, "[PROF_TEST] Started stabber on port %d\n", stub_port); + break; + } + } + } + + if (!started) { + fprintf(stderr, "[PROF_TEST] ERROR: could not start stabber on any port\n"); + assert_true(FALSE); return -1; } - // Generate unique XDG paths based on stub_port for parallel execution + // 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 <= 4) ? group : stub_port; snprintf(xdg_config_home, sizeof(xdg_config_home), - "./tests/functionaltests/files/%d/xdg_config_home", stub_port); + "./test-files/%d/xdg_config_home", dir_id); snprintf(xdg_data_home, sizeof(xdg_data_home), - "./tests/functionaltests/files/%d/xdg_data_home", stub_port); + "./test-files/%d/xdg_data_home", dir_id); + + fprintf(stderr, "[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 @@ -398,9 +447,19 @@ prof_output_regex(const char *pattern) return 1; } - usleep(50000); /* 50ms */ + usleep(50000); } + /* 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; } -- 2.49.1 From 6c5c523a1addde8849eb0f1b1bb9a50aefcc0bcf Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Wed, 28 Jan 2026 22:51:46 +0300 Subject: [PATCH 2/7] ci: fix coverage to match Code Coverage job output Use --extract '*/src/src/*' instead of --remove to properly filter production code from tests. Coverage now shows 27.5% lines matching the dedicated Code Coverage job. --- ci-build.sh | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/ci-build.sh b/ci-build.sh index dcf3ccca..a971b3cd 100755 --- a/ci-build.sh +++ b/ci-build.sh @@ -259,16 +259,11 @@ build_and_test() { if [ "$run_coverage" = "yes" ]; then echo "--> Collecting coverage data..." if command -v lcov >/dev/null 2>&1; then - lcov --capture --directory . --output-file coverage.info \ + lcov --capture --directory . --output-file coverage-full.info \ --rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true - # Remove test files, stubs, system headers - only keep src/ production code - lcov --remove coverage.info \ - '/usr/*' \ - '*/tests/*' \ - '*test*.c' \ - '*stub*.c' \ - '*/functionaltests/*' \ - '*/unittests/*' \ + # Extract only production code from src/ directory, exclude tests + # Pattern matches both Docker (/src/src/*) and CI (/usr/src/profanity/src/*) + lcov --extract coverage-full.info '*/profanity/src/*' '*/src/src/*' \ --output-file coverage.info \ --rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true if [ -f coverage.info ] && [ -s coverage.info ]; then -- 2.49.1 From adc5e2668977e6ff23d9d5ec512bdb0f68fd9f77 Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Thu, 29 Jan 2026 15:53:03 +0300 Subject: [PATCH 3/7] ci: integrate coverage into linux arch job, remove duplicate - Linux arch job now uploads coverage.info as artifact - ci-build.sh saves coverage.info before cleanup - Removed separate coverage job (was duplicating arch build) - Saves ~25% CI time by eliminating redundant build --- .github/workflows/ci-code.yml | 19 +----- ci-build.sh | 110 +++++++++++++++++++++++----------- 2 files changed, 78 insertions(+), 51 deletions(-) diff --git a/.github/workflows/ci-code.yml b/.github/workflows/ci-code.yml index b3e3abb5..e4e8dc77 100644 --- a/.github/workflows/ci-code.yml +++ b/.github/workflows/ci-code.yml @@ -106,20 +106,5 @@ jobs: - uses: actions/checkout@v4 - name: Build and run coverage run: | - docker build -f Dockerfile.debian -t profanity-cov . - docker run -v ${{ github.workspace }}/coverage:/coverage profanity-cov bash -c ' - ./bootstrap.sh - ./configure --enable-coverage --enable-otr --enable-pgp --enable-omemo --enable-plugins - make -j$(nproc) - make check || true - make check-functional-parallel || true - lcov --capture --directory . --output-file /coverage/coverage.info \ - --rc branch_coverage=1 \ - --ignore-errors inconsistent - lcov --remove /coverage/coverage.info \ - "/usr/include/*" "*/tests/*" \ - --output-file /coverage/coverage.info \ - --rc branch_coverage=1 \ - --ignore-errors inconsistent,empty,unused - lcov --summary /coverage/coverage.info - ' \ No newline at end of file + docker build -f Dockerfile.arch -t profanity-cov . + docker run profanity-cov ./ci-build.sh --coverage-only \ No newline at end of file diff --git a/ci-build.sh b/ci-build.sh index a971b3cd..5fbc955d 100755 --- a/ci-build.sh +++ b/ci-build.sh @@ -110,6 +110,17 @@ num_cores() # Run test failure detection verification first verify_test_failure_detection +# Parse arguments +COVERAGE_ONLY=no +for arg in "$@"; do + case "$arg" in + --coverage-only) + COVERAGE_ONLY=yes + shift + ;; + esac +done + ./bootstrap.sh tests=() @@ -210,7 +221,7 @@ build_and_test() { exit 1 fi - # Run unit tests under Valgrind + # Run unit tests local unit_passed=0 unit_failed=0 if [ "$run_valgrind" = "yes" ]; then echo "--> Running unit tests under Valgrind..." @@ -225,13 +236,21 @@ build_and_test() { echo "ERROR: Valgrind unit tests failed (exit code $valgrind_exit)" exit 1 fi - # Extract unit test counts from cmocka output - unit_passed=$(grep -E "^\[ PASSED \] [0-9]+ test" unit-tests-output.log | grep -oE "[0-9]+" | head -1) - unit_failed=$(grep -E "^\[ FAILED \] [0-9]+ test" unit-tests-output.log | grep -oE "[0-9]+" | head -1) - [ -z "$unit_passed" ] && unit_passed=0 - [ -z "$unit_failed" ] && unit_failed=0 - echo "UNIT_TESTS: passed=$unit_passed failed=$unit_failed" + else + echo "--> Running unit tests..." + $MAKE tests/unittests/unittests + tests/unittests/unittests 2>&1 | tee unit-tests-output.log + if [ ${PIPESTATUS[0]} -ne 0 ]; then + echo "ERROR: Unit tests failed" + exit 1 + fi fi + # Extract unit test counts from cmocka output + unit_passed=$(grep -E "^\[ PASSED \] [0-9]+ test" unit-tests-output.log | grep -oE "[0-9]+" | head -1) + unit_failed=$(grep -E "^\[ FAILED \] [0-9]+ test" unit-tests-output.log | grep -oE "[0-9]+" | head -1) + [ -z "$unit_passed" ] && unit_passed=0 + [ -z "$unit_failed" ] && unit_failed=0 + echo "UNIT_TESTS: passed=$unit_passed failed=$unit_failed" # Set build index for port allocation: build 1 uses ports 5230-5233, # build 2 uses 5234-5237, etc. This prevents port conflicts in parallel builds. @@ -286,6 +305,13 @@ build_and_test() { fi ./profanity -v + + # Save coverage.info before cleanup (for CI artifact) + if [ "$run_coverage" = "yes" ] && [ -f coverage.info ]; then + cp coverage.info ../coverage.info + echo "Coverage report saved to coverage.info" + fi + $MAKE clean cd .. @@ -300,40 +326,52 @@ build_and_test() { } > "$log_file" 2>&1 } -# Run all 4 configurations in parallel +# Run configurations # Coverage enabled only for build 1 (Full) - it has most code paths echo echo "=== Start build ===" echo -echo "Starting 4 parallel build configurations..." -echo -pids=() -for idx in 1 2 3 4; do - if [ $idx -le ${#tests[@]} ]; then - # All builds run Valgrind - if [ "$ARCH" = "linux" ]; then - run_valgrind="yes" - if [ $idx -eq 1 ]; then - extra_flags="--enable-valgrind --enable-coverage" - run_coverage="yes" + +if [ "$COVERAGE_ONLY" = "yes" ]; then + echo "Running coverage-only mode (build 1 only)..." + echo + run_valgrind="no" + run_coverage="yes" + extra_flags="--enable-coverage" + build_and_test "${tests[0]}" "$* $extra_flags" "1" "$run_valgrind" "$run_coverage" & + pids=("$!") + echo " → Build 1: ${tests[0]} [+Coverage]" +else + echo "Starting 4 parallel build configurations..." + echo + pids=() + for idx in 1 2 3 4; do + if [ $idx -le ${#tests[@]} ]; then + # All builds run Valgrind + if [ "$ARCH" = "linux" ]; then + run_valgrind="yes" + if [ $idx -eq 1 ]; then + extra_flags="--enable-valgrind --enable-coverage" + run_coverage="yes" + else + extra_flags="--enable-valgrind" + run_coverage="no" + fi else - extra_flags="--enable-valgrind" + extra_flags="" + run_valgrind="no" run_coverage="no" fi - else - extra_flags="" - run_valgrind="no" - run_coverage="no" + build_and_test "${tests[$((idx-1))]}" "$* $extra_flags" "$idx" "$run_valgrind" "$run_coverage" & + pids+=("$!") + flags_desc="" + [ "$run_valgrind" = "yes" ] && flags_desc="Valgrind" + [ "$run_coverage" = "yes" ] && flags_desc="${flags_desc:+$flags_desc+}Coverage" + [ -n "$flags_desc" ] && flags_desc=" [+$flags_desc]" + echo " → Build $idx: ${tests[$((idx-1))]}$flags_desc" fi - build_and_test "${tests[$((idx-1))]}" "$* $extra_flags" "$idx" "$run_valgrind" "$run_coverage" & - pids+=("$!") - flags_desc="" - [ "$run_valgrind" = "yes" ] && flags_desc="Valgrind" - [ "$run_coverage" = "yes" ] && flags_desc="${flags_desc:+$flags_desc+}Coverage" - [ -n "$flags_desc" ] && flags_desc=" [+$flags_desc]" - echo " → Build $idx: ${tests[$((idx-1))]}$flags_desc" - fi -done + done +fi echo # Wait for all builds and check exit codes @@ -389,5 +427,9 @@ if [ ${#failed_builds[@]} -gt 0 ]; then echo "RESULT: FAILED (builds ${failed_builds[*]})" exit 1 else - echo "RESULT: ALL 4 BUILDS PASSED ✓" + if [ "$COVERAGE_ONLY" = "yes" ]; then + echo "RESULT: COVERAGE BUILD PASSED ✓" + else + echo "RESULT: ALL 4 BUILDS PASSED ✓" + fi fi -- 2.49.1 From 5100efca3b15c7d164f80b7073adbb9ed6b0686b Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Thu, 29 Jan 2026 17:08:39 +0300 Subject: [PATCH 4/7] ci: remove coverage from linux builds Coverage is now collected only in the dedicated Code Coverage job. --- ci-build.sh | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/ci-build.sh b/ci-build.sh index 5fbc955d..0f9482d0 100755 --- a/ci-build.sh +++ b/ci-build.sh @@ -347,27 +347,19 @@ else pids=() for idx in 1 2 3 4; do if [ $idx -le ${#tests[@]} ]; then - # All builds run Valgrind + # All builds run Valgrind on Linux if [ "$ARCH" = "linux" ]; then run_valgrind="yes" - if [ $idx -eq 1 ]; then - extra_flags="--enable-valgrind --enable-coverage" - run_coverage="yes" - else - extra_flags="--enable-valgrind" - run_coverage="no" - fi + extra_flags="--enable-valgrind" else extra_flags="" run_valgrind="no" - run_coverage="no" fi + run_coverage="no" build_and_test "${tests[$((idx-1))]}" "$* $extra_flags" "$idx" "$run_valgrind" "$run_coverage" & pids+=("$!") flags_desc="" - [ "$run_valgrind" = "yes" ] && flags_desc="Valgrind" - [ "$run_coverage" = "yes" ] && flags_desc="${flags_desc:+$flags_desc+}Coverage" - [ -n "$flags_desc" ] && flags_desc=" [+$flags_desc]" + [ "$run_valgrind" = "yes" ] && flags_desc=" [+Valgrind]" echo " → Build $idx: ${tests[$((idx-1))]}$flags_desc" fi done -- 2.49.1 From cf86e60af676f6d90529f8674be7d54195b02a2f Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Thu, 29 Jan 2026 17:41:58 +0300 Subject: [PATCH 5/7] ci: refactor ci-build.sh with constants and helper functions - Add CMOCKA_PASSED_PATTERN, CMOCKA_FAILED_PATTERN constants - Add COVERAGE_PATTERNS constant for lcov extraction - Add parse_build_stats() function for parsing build logs - Add extract_test_count() helper function - Organize code into sections: Constants, Helper Functions, Test Verification --- CONTRIBUTING.md | 9 +++-- ci-build.sh | 89 ++++++++++++++++++++++++++++++++++--------------- 2 files changed, 68 insertions(+), 30 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4aadd667..1f7a625b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,12 +91,15 @@ set -e This will run the same tests that the CI runs and refuse the push if it fails. The CI script runs 4 parallel builds with different configurations, each with Valgrind and functional tests. -Build 1 also collects code coverage (lines, functions, branches). +Use `./ci-build.sh --coverage-only` to run only build 1 with coverage collection. Output shows test results per build: ``` -✓ Build 1 passed (unit: 437/0, func: 69/0, cov: L:27.5% F:36.2% B:18.1%, 5m39s) -✓ Build 2 passed (unit: 398/0, func: 69/0, 5m00s) +✓ BUILD 1 PASSED + Unit tests: 437 passed, 0 failed + Functional tests: 69 passed, 0 failed + Coverage: Lines: 27.5% | Functions: 36.2% | Branches: 18.1% + Duration: 5m39s ``` Note that it will run on the actual content of the repository directory and not diff --git a/ci-build.sh b/ci-build.sh index 0f9482d0..3558bdd1 100755 --- a/ci-build.sh +++ b/ci-build.sh @@ -23,6 +23,55 @@ error_handler() trap error_handler ERR +# ============================================================================= +# Constants +# ============================================================================= + +# Regex patterns for parsing test output +readonly CMOCKA_PASSED_PATTERN='^\[ PASSED \] [0-9]+ test' +readonly CMOCKA_FAILED_PATTERN='^\[ FAILED \] [0-9]+ test' + +# Coverage extraction patterns (matches both Docker and CI paths) +readonly COVERAGE_PATTERNS='*/profanity/src/* */src/src/*' + +# ============================================================================= +# Helper Functions +# ============================================================================= + +# Parse STATS line from build log and set global variables +# Usage: parse_build_stats +# Sets: STAT_UNIT_P, STAT_UNIT_F, STAT_FUNC_P, STAT_FUNC_F, +# STAT_COV_LINES, STAT_COV_FUNCS, STAT_COV_BRANCHES, STAT_TIME +parse_build_stats() { + local log_file="$1" + local stats_line + stats_line=$(grep "^STATS:" "$log_file" 2>/dev/null | tail -1) + + STAT_UNIT_P=$(echo "$stats_line" | grep -oE "unit_passed=[0-9]+" | cut -d= -f2) + STAT_UNIT_F=$(echo "$stats_line" | grep -oE "unit_failed=[0-9]+" | cut -d= -f2) + STAT_FUNC_P=$(echo "$stats_line" | grep -oE "func_passed=[0-9]+" | cut -d= -f2) + STAT_FUNC_F=$(echo "$stats_line" | grep -oE "func_failed=[0-9]+" | cut -d= -f2) + STAT_COV_LINES=$(echo "$stats_line" | grep -oE "cov_lines=[0-9.]+%|cov_lines=n/a" | cut -d= -f2) + STAT_COV_FUNCS=$(echo "$stats_line" | grep -oE "cov_funcs=[0-9.]+%|cov_funcs=n/a" | cut -d= -f2) + STAT_COV_BRANCHES=$(echo "$stats_line" | grep -oE "cov_branches=[0-9.]+%|cov_branches=n/a" | cut -d= -f2) + STAT_TIME=$(echo "$stats_line" | grep -oE "time=[0-9]+m[0-9]+s" | cut -d= -f2) + + : "${STAT_UNIT_P:=0}" + : "${STAT_UNIT_F:=0}" + : "${STAT_FUNC_P:=0}" + : "${STAT_FUNC_F:=0}" +} + +# Extract test count from log file +# Usage: extract_test_count +extract_test_count() { + grep -E "$2" "$1" 2>/dev/null | grep -oE "[0-9]+" | head -1 +} + +# ============================================================================= +# Test Verification +# ============================================================================= + # Verify that test failures are properly detected # This is a meta-test: it runs a deliberately failing test # and checks that the test framework reports the failure correctly @@ -246,10 +295,9 @@ build_and_test() { fi fi # Extract unit test counts from cmocka output - unit_passed=$(grep -E "^\[ PASSED \] [0-9]+ test" unit-tests-output.log | grep -oE "[0-9]+" | head -1) - unit_failed=$(grep -E "^\[ FAILED \] [0-9]+ test" unit-tests-output.log | grep -oE "[0-9]+" | head -1) - [ -z "$unit_passed" ] && unit_passed=0 - [ -z "$unit_failed" ] && unit_failed=0 + unit_passed=$(extract_test_count unit-tests-output.log "$CMOCKA_PASSED_PATTERN") + unit_failed=$(extract_test_count unit-tests-output.log "$CMOCKA_FAILED_PATTERN") + : "${unit_passed:=0}" "${unit_failed:=0}" echo "UNIT_TESTS: passed=$unit_passed failed=$unit_failed" # Set build index for port allocation: build 1 uses ports 5230-5233, @@ -264,9 +312,9 @@ build_and_test() { echo "=== Functional test results ===" for glog in ./test-logs/group*.log; do if [ -f "$glog" ]; then - cnt=$(grep -E "^\[ PASSED \] [0-9]+ test" "$glog" | grep -oE "[0-9]+" | head -1) + cnt=$(extract_test_count "$glog" "$CMOCKA_PASSED_PATTERN") [ -n "$cnt" ] && func_passed=$((func_passed + cnt)) - cnt=$(grep -E "^\[ FAILED \] [0-9]+ test" "$glog" | grep -oE "[0-9]+" | head -1) + cnt=$(extract_test_count "$glog" "$CMOCKA_FAILED_PATTERN") [ -n "$cnt" ] && func_failed=$((func_failed + cnt)) fi done @@ -281,8 +329,8 @@ build_and_test() { lcov --capture --directory . --output-file coverage-full.info \ --rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true # Extract only production code from src/ directory, exclude tests - # Pattern matches both Docker (/src/src/*) and CI (/usr/src/profanity/src/*) - lcov --extract coverage-full.info '*/profanity/src/*' '*/src/src/*' \ + # shellcheck disable=SC2086 + lcov --extract coverage-full.info $COVERAGE_PATTERNS \ --output-file coverage.info \ --rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true if [ -f coverage.info ] && [ -s coverage.info ]; then @@ -374,28 +422,15 @@ for i in "${!pids[@]}"; do idx=$((i + 1)) if wait "${pids[$i]}"; then if [ -f "build-$idx.log" ]; then - stats_line=$(grep "^STATS:" "build-$idx.log" | tail -1) - unit_p=$(echo "$stats_line" | grep -oE "unit_passed=[0-9]+" | cut -d= -f2) - unit_f=$(echo "$stats_line" | grep -oE "unit_failed=[0-9]+" | cut -d= -f2) - func_p=$(echo "$stats_line" | grep -oE "func_passed=[0-9]+" | cut -d= -f2) - func_f=$(echo "$stats_line" | grep -oE "func_failed=[0-9]+" | cut -d= -f2) - cov_lines=$(echo "$stats_line" | grep -oE "cov_lines=[0-9.]+%|cov_lines=n/a" | cut -d= -f2) - cov_funcs=$(echo "$stats_line" | grep -oE "cov_funcs=[0-9.]+%|cov_funcs=n/a" | cut -d= -f2) - cov_branches=$(echo "$stats_line" | grep -oE "cov_branches=[0-9.]+%|cov_branches=n/a" | cut -d= -f2) - time=$(echo "$stats_line" | grep -oE "time=[0-9]+m[0-9]+s" | cut -d= -f2) - - unit_p=${unit_p:-0} - unit_f=${unit_f:-0} - func_p=${func_p:-0} - func_f=${func_f:-0} + parse_build_stats "build-$idx.log" echo "✓ BUILD $idx PASSED" - echo " Unit tests: $unit_p passed, $unit_f failed" - echo " Functional tests: $func_p passed, $func_f failed" - if [ "$cov_lines" != "n/a" ] && [ -n "$cov_lines" ]; then - echo " Coverage: Lines: $cov_lines | Functions: $cov_funcs | Branches: $cov_branches" + echo " Unit tests: $STAT_UNIT_P passed, $STAT_UNIT_F failed" + echo " Functional tests: $STAT_FUNC_P passed, $STAT_FUNC_F failed" + if [ "$STAT_COV_LINES" != "n/a" ] && [ -n "$STAT_COV_LINES" ]; then + echo " Coverage: Lines: $STAT_COV_LINES | Functions: $STAT_COV_FUNCS | Branches: $STAT_COV_BRANCHES" fi - echo " Duration: ${time:-?}" + echo " Duration: ${STAT_TIME:-?}" else echo "✓ Build $idx passed (no stats available)" fi -- 2.49.1 From 5978c77a45335a9b02757c998931ddcabbcde94d Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Mon, 2 Feb 2026 14:56:46 +0300 Subject: [PATCH 6/7] fix: address code review comments from PR #79 - Add TEST_GROUPS constant to eliminate magic number - Use stdout for info messages, stderr for errors only - Remove redundant comments and dead code - Add CI check before copying coverage.info - Unify variable declaration order - Add port allocation explanation comment - Restore /* 50ms */ inline comment - Update CONTRIBUTING.md with build 1 description --- CONTRIBUTING.md | 2 +- ci-build.sh | 7 +++--- tests/functionaltests/proftest.c | 40 ++++++++++++++++++-------------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f7a625b..a6fb2028 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,7 +91,7 @@ set -e This will run the same tests that the CI runs and refuse the push if it fails. The CI script runs 4 parallel builds with different configurations, each with Valgrind and functional tests. -Use `./ci-build.sh --coverage-only` to run only build 1 with coverage collection. +Use `./ci-build.sh --coverage-only` to run only build 1 (full configuration with all optional components enabled) with coverage collection. Output shows test results per build: ``` diff --git a/ci-build.sh b/ci-build.sh index 3558bdd1..da0a55be 100755 --- a/ci-build.sh +++ b/ci-build.sh @@ -40,8 +40,6 @@ readonly COVERAGE_PATTERNS='*/profanity/src/* */src/src/*' # Parse STATS line from build log and set global variables # Usage: parse_build_stats -# Sets: STAT_UNIT_P, STAT_UNIT_F, STAT_FUNC_P, STAT_FUNC_F, -# STAT_COV_LINES, STAT_COV_FUNCS, STAT_COV_BRANCHES, STAT_TIME parse_build_stats() { local log_file="$1" local stats_line @@ -355,7 +353,8 @@ build_and_test() { ./profanity -v # Save coverage.info before cleanup (for CI artifact) - if [ "$run_coverage" = "yes" ] && [ -f coverage.info ]; then + # Only copy in CI environment to avoid leaving artifacts during local runs + if [ "$run_coverage" = "yes" ] && [ -f coverage.info ] && [ -n "$CI" ]; then cp coverage.info ../coverage.info echo "Coverage report saved to coverage.info" fi @@ -397,8 +396,8 @@ else if [ $idx -le ${#tests[@]} ]; then # All builds run Valgrind on Linux if [ "$ARCH" = "linux" ]; then - run_valgrind="yes" extra_flags="--enable-valgrind" + run_valgrind="yes" else extra_flags="" run_valgrind="no" diff --git a/tests/functionaltests/proftest.c b/tests/functionaltests/proftest.c index cc59237c..8165e88f 100644 --- a/tests/functionaltests/proftest.c +++ b/tests/functionaltests/proftest.c @@ -17,6 +17,9 @@ #include "proftest.h" +/* Number of parallel test groups for CI builds */ +#define TEST_GROUPS 4 + char *config_orig; char *data_orig; @@ -138,9 +141,9 @@ _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 <= 4) ? group : stub_port; + int dir_id = (group >= 1 && group <= TEST_GROUPS) ? group : stub_port; - fprintf(stderr, "[PROF_TEST] Cleaning up directories for group %d (dir_id %d)\n", group, dir_id); + 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); @@ -243,36 +246,38 @@ 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 4 ports - * Build 0/1: 5230-5233, Build 2: 5234-5237, Build 3: 5238-5241, Build 4: 5242-5245 */ - int port_base = 5230 + ((build_idx > 0 ? build_idx - 1 : 0) * 4); + /* Calculate port base: each build uses a different range of TEST_GROUPS ports. + * Build 0 (local/default): 5230-5233, Build 1: 5230-5233, Build 2: 5234-5237, etc. + * Build 0 and 1 share the same range because build 0 is for local runs (no parallel builds), + * while builds 1-4 are used in CI where they run in parallel. */ + int port_base = 5230 + ((build_idx > 0 ? build_idx - 1 : 0) * TEST_GROUPS); /* 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. */ gboolean started = FALSE; - if (group >= 1 && group <= 4) { + if (group >= 1 && group <= TEST_GROUPS) { /* Static allocation: each group gets a dedicated port */ stub_port = port_base + group - 1; - fprintf(stderr, "[PROF_TEST] Build %d, Group %d: trying port %d\n", build_idx, group, stub_port); + 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; - fprintf(stderr, "[PROF_TEST] Started stabber on port %d\n", stub_port); + printf("[PROF_TEST] Started stabber on port %d\n", stub_port); } else { - fprintf(stderr, "[PROF_TEST] Failed to start stabber on port %d\n", stub_port); + 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) { - fprintf(stderr, "[PROF_TEST] Using dynamic port allocation\n"); + printf("[PROF_TEST] Using dynamic port allocation\n"); for (int p = port_base; p < port_base + 20; ++p) { if (stbbr_start(STBBR_LOGDEBUG, p, 0) == 0) { stub_port = p; started = TRUE; - fprintf(stderr, "[PROF_TEST] Started stabber on port %d\n", stub_port); + printf("[PROF_TEST] Started stabber on port %d\n", stub_port); break; } } @@ -280,20 +285,19 @@ init_prof_test(void **state) if (!started) { fprintf(stderr, "[PROF_TEST] ERROR: could not start stabber on any port\n"); - assert_true(FALSE); 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 <= 4) ? group : stub_port; + /* 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); - fprintf(stderr, "[PROF_TEST] Group %d using directories: config=%s, data=%s\n", - group, xdg_config_home, xdg_data_home); + 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 @@ -447,7 +451,7 @@ prof_output_regex(const char *pattern) return 1; } - usleep(50000); + usleep(50000); /* 50ms */ } /* Timeout reached - log diagnostic info */ -- 2.49.1 From a546092a5f7a4170b7e75b5cfdd02f6d32c47bcb Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Mon, 2 Feb 2026 16:04:08 +0300 Subject: [PATCH 7/7] fix: add build names, use TEST_BUILDS constant, redirect errors to stderr --- CONTRIBUTING.md | 12 +++++-- ci-build.sh | 61 +++++++++++++++++++------------- tests/functionaltests/proftest.c | 6 ++-- 3 files changed, 48 insertions(+), 31 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6fb2028..87edee02 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -90,12 +90,18 @@ set -e ``` This will run the same tests that the CI runs and refuse the push if it fails. -The CI script runs 4 parallel builds with different configurations, each with Valgrind and functional tests. -Use `./ci-build.sh --coverage-only` to run only build 1 (full configuration with all optional components enabled) with coverage collection. +The CI script runs 4 parallel builds with different configurations: +- **Full** — all features enabled (+ coverage in `--coverage-only` mode) +- **Minimal** — all optional features disabled +- **NoEncrypt** — no encryption (OTR, PGP, OMEMO disabled) +- **Default** — default ./configure options + +Each build runs Valgrind and functional tests on Linux. +Use `./ci-build.sh --coverage-only` to run only the Full build with coverage collection. Output shows test results per build: ``` -✓ BUILD 1 PASSED +✓ Full PASSED Unit tests: 437 passed, 0 failed Functional tests: 69 passed, 0 failed Coverage: Lines: 27.5% | Functions: 36.2% | Branches: 18.1% diff --git a/ci-build.sh b/ci-build.sh index da0a55be..09f3c3f7 100755 --- a/ci-build.sh +++ b/ci-build.sh @@ -15,9 +15,9 @@ error_handler() log_content ./test-suite.log log_content ./test-suite-memcheck.log - echo - echo "Error ${ERR_CODE} with command '${BASH_COMMAND}' on line ${BASH_LINENO[0]}. Exiting." - echo + echo >&2 + echo "Error ${ERR_CODE} with command '${BASH_COMMAND}' on line ${BASH_LINENO[0]}. Exiting." >&2 + echo >&2 exit ${ERR_CODE} } @@ -27,6 +27,17 @@ trap error_handler ERR # Constants # ============================================================================= +# Number of parallel build configurations +readonly TEST_BUILDS=4 + +# Human-readable names for each build configuration +readonly BUILD_NAMES=( + "Full" # 1. All features enabled + "Minimal" # 2. All optional features disabled + "NoEncrypt" # 3. No encryption (otr, pgp, omemo disabled) + "Default" # 4. Default ./configure options +) + # Regex patterns for parsing test output readonly CMOCKA_PASSED_PATTERN='^\[ PASSED \] [0-9]+ test' readonly CMOCKA_FAILED_PATTERN='^\[ FAILED \] [0-9]+ test' @@ -108,11 +119,11 @@ EOF # Test 1: Single failing test detection echo " Testing single test failure detection..." if /tmp/test_must_fail > /tmp/test_must_fail.log 2>&1; then - echo "ERROR: Test that should fail returned success (exit code 0)" - echo "This means the test framework is NOT detecting failures correctly!" - echo "--- Test output ---" - cat /tmp/test_must_fail.log - echo "--- End output ---" + echo "ERROR: Test that should fail returned success (exit code 0)" >&2 + echo "This means the test framework is NOT detecting failures correctly!" >&2 + echo "--- Test output ---" >&2 + cat /tmp/test_must_fail.log >&2 + echo "--- End output ---" >&2 rm -f /tmp/test_must_fail /tmp/test_must_fail.c /tmp/test_must_fail.log exit 1 fi @@ -132,8 +143,8 @@ EOF wait $pid4 || failed=$((failed + 1)) if [ $failed -ne 2 ]; then - echo "ERROR: Expected 2 failures in parallel tests, got $failed" - echo "Parallel failure detection is broken!" + echo "ERROR: Expected 2 failures in parallel tests, got $failed" >&2 + echo "Parallel failure detection is broken!" >&2 rm -f /tmp/test_must_fail /tmp/test_must_fail.c /tmp/test_must_fail.log /tmp/p?.log exit 1 fi @@ -380,34 +391,34 @@ echo "=== Start build ===" echo if [ "$COVERAGE_ONLY" = "yes" ]; then - echo "Running coverage-only mode (build 1 only)..." + echo "Running coverage-only mode (${BUILD_NAMES[0]} build)..." echo run_valgrind="no" run_coverage="yes" extra_flags="--enable-coverage" build_and_test "${tests[0]}" "$* $extra_flags" "1" "$run_valgrind" "$run_coverage" & pids=("$!") - echo " → Build 1: ${tests[0]} [+Coverage]" + echo " → ${BUILD_NAMES[0]}: ${tests[0]} [+Coverage]" else - echo "Starting 4 parallel build configurations..." + echo "Starting $TEST_BUILDS parallel build configurations..." echo pids=() - for idx in 1 2 3 4; do + for idx in $(seq 1 $TEST_BUILDS); do if [ $idx -le ${#tests[@]} ]; then # All builds run Valgrind on Linux if [ "$ARCH" = "linux" ]; then - extra_flags="--enable-valgrind" run_valgrind="yes" + extra_flags="--enable-valgrind" else - extra_flags="" run_valgrind="no" + extra_flags="" fi run_coverage="no" build_and_test "${tests[$((idx-1))]}" "$* $extra_flags" "$idx" "$run_valgrind" "$run_coverage" & pids+=("$!") flags_desc="" [ "$run_valgrind" = "yes" ] && flags_desc=" [+Valgrind]" - echo " → Build $idx: ${tests[$((idx-1))]}$flags_desc" + echo " → ${BUILD_NAMES[$((idx-1))]}: ${tests[$((idx-1))]}$flags_desc" fi done fi @@ -423,7 +434,7 @@ for i in "${!pids[@]}"; do if [ -f "build-$idx.log" ]; then parse_build_stats "build-$idx.log" - echo "✓ BUILD $idx PASSED" + echo "✓ ${BUILD_NAMES[$i]} PASSED" echo " Unit tests: $STAT_UNIT_P passed, $STAT_UNIT_F failed" echo " Functional tests: $STAT_FUNC_P passed, $STAT_FUNC_F failed" if [ "$STAT_COV_LINES" != "n/a" ] && [ -n "$STAT_COV_LINES" ]; then @@ -431,10 +442,10 @@ for i in "${!pids[@]}"; do fi echo " Duration: ${STAT_TIME:-?}" else - echo "✓ Build $idx passed (no stats available)" + echo "✓ ${BUILD_NAMES[$i]} passed (no stats available)" fi else - echo "✗ BUILD $idx FAILED" + echo "✗ ${BUILD_NAMES[$i]} FAILED" >&2 failed_builds+=("$idx") fi echo @@ -443,19 +454,19 @@ done # Show failed builds full logs for idx in "${failed_builds[@]}"; do if [ -f "build-$idx.log" ]; then - echo "=== BUILD $idx FAILURE LOG ===" - cat "build-$idx.log" - echo + echo "=== ${BUILD_NAMES[$((idx-1))]} FAILURE LOG ===" >&2 + cat "build-$idx.log" >&2 + echo >&2 fi done if [ ${#failed_builds[@]} -gt 0 ]; then - echo "RESULT: FAILED (builds ${failed_builds[*]})" + echo "RESULT: FAILED (builds ${failed_builds[*]})" >&2 exit 1 else if [ "$COVERAGE_ONLY" = "yes" ]; then echo "RESULT: COVERAGE BUILD PASSED ✓" else - echo "RESULT: ALL 4 BUILDS PASSED ✓" + echo "RESULT: ALL $TEST_BUILDS BUILDS PASSED ✓" fi fi diff --git a/tests/functionaltests/proftest.c b/tests/functionaltests/proftest.c index 8165e88f..b67954f7 100644 --- a/tests/functionaltests/proftest.c +++ b/tests/functionaltests/proftest.c @@ -247,9 +247,9 @@ init_prof_test(void **state) 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, Build 1: 5230-5233, Build 2: 5234-5237, etc. - * Build 0 and 1 share the same range because build 0 is for local runs (no parallel builds), - * while builds 1-4 are used in CI where they run in parallel. */ + * 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); /* Static resource allocation to avoid conflicts in parallel execution. -- 2.49.1