#!/usr/bin/env bash log_content() { echo echo "Content of $1:" cat "$1" } error_handler() { ERR_CODE=$? log_content ./config.log log_content ./test-suite.log log_content ./test-suite-memcheck.log echo >&2 echo "Error ${ERR_CODE} with command '${BASH_COMMAND}' on line ${BASH_LINENO[0]}. Exiting." >&2 echo >&2 exit ${ERR_CODE} } 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' # 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 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 verify_test_failure_detection() { echo "==> Verifying test failure detection..." # Create a simple failing test cat > /tmp/test_must_fail.c << 'EOF' #include #include #include #include static void test_that_must_fail(void **state) { (void)state; assert_true(0); // This MUST fail } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_that_must_fail), }; return cmocka_run_group_tests(tests, NULL, NULL); } EOF # Compile the failing test if ! gcc -o /tmp/test_must_fail /tmp/test_must_fail.c -lcmocka 2>/dev/null; then echo "Warning: Could not compile test failure verification (cmocka not available?)" echo "Skipping test failure detection verification" return 0 fi # 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)" >&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 echo " ✓ Single test failure correctly detected" # Test 2: Parallel failure detection (simulates check-functional-parallel) echo " Testing parallel test failure detection..." failed=0 /tmp/test_must_fail > /tmp/p1.log 2>&1 & pid1=$! true > /tmp/p2.log 2>&1 & pid2=$! # This passes /tmp/test_must_fail > /tmp/p3.log 2>&1 & pid3=$! true > /tmp/p4.log 2>&1 & pid4=$! # This passes wait $pid1 || failed=$((failed + 1)) wait $pid2 || failed=$((failed + 1)) wait $pid3 || failed=$((failed + 1)) wait $pid4 || failed=$((failed + 1)) if [ $failed -ne 2 ]; then 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 echo " ✓ Parallel test failures correctly detected (2 of 4 failed as expected)" rm -f /tmp/test_must_fail /tmp/test_must_fail.c /tmp/test_must_fail.log /tmp/p?.log echo "✓ Test failure detection verified" } num_cores() { # Check for cores, for systems with: # Line 1. Linux w/ coreutils, or... # Line 2. OpenBSD, FreeBSD, NetBSD or macOS, or... # Line 3. Fallback for Linux w/o coreutils (glibc). nproc \ || sysctl -n hw.ncpu \ || getconf _NPROCESSORS_ONLN 2>/dev/null } # 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=() MAKE="make --quiet -j$(num_cores)" CC="gcc" ARCH="$(uname | tr '[:upper:]' '[:lower:]')" case "$ARCH" in linux*) # 4 configurations for parallel CI tests=( # 1. Full build (all features enabled) "--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp --enable-omemo --enable-plugins --enable-c-plugins --enable-python-plugins --with-xscreensaver --enable-omemo-qrcode --enable-gdk-pixbuf" # 2. Minimal build (all optional features disabled) "--disable-notifications --disable-icons-and-clipboard --disable-otr --disable-pgp --disable-omemo --disable-plugins --disable-c-plugins --disable-python-plugins --without-xscreensaver --disable-omemo-qrcode --disable-gdk-pixbuf" # 3. No encryption (disable otr, pgp, omemo) "--disable-pgp --disable-otr --disable-omemo --disable-omemo-qrcode" # 4. Default configuration "" ) source /etc/profile.d/debuginfod.sh 2>/dev/null || true if grep -q 'ID=arch' /etc/os-release 2>/dev/null && [ -f /etc/makepkg.conf ]; then echo "--> [Parity Mode] Simulating Pikaur collision..." set -a source /etc/makepkg.conf set +a fi ;; darwin*) # 4 configurations for parallel CI tests=( # 1. Full build (all features enabled) "--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp --enable-omemo --enable-plugins --enable-c-plugins --enable-python-plugins" # 2. Minimal build (all optional features disabled) "--disable-notifications --disable-icons-and-clipboard --disable-otr --disable-pgp --disable-omemo --disable-plugins --disable-c-plugins --disable-python-plugins" # 3. No encryption (disable otr, pgp, omemo) "--disable-pgp --disable-otr --disable-omemo" # 4. Default configuration "" ) ;; openbsd*) MAKE="gmake" # TODO(#1231): # `-std=gnu99 -fexec-charset=UTF-8` to silence: # src/event/server_events.c:1453:19: error: universal character names are only valid in C++ and C99 # src/event/server_events.c:1454:19: error: universal character names are only valid in C++ and C99 CC="egcc -std=gnu99 -fexec-charset=UTF-8" # 4 configurations for parallel CI tests=( # 1. Full build (all features enabled) "--enable-notifications --enable-icons-and-clipboard --enable-otr --enable-pgp --enable-omemo --enable-plugins --enable-c-plugins --enable-python-plugins" # 2. Minimal build (all optional features disabled) "--disable-notifications --disable-icons-and-clipboard --disable-otr --disable-pgp --disable-omemo --disable-plugins --disable-c-plugins --disable-python-plugins" # 3. No encryption (disable otr, pgp, omemo) "--disable-pgp --disable-otr --disable-omemo" # 4. Default configuration "" ) ;; 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" { 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 if ! ../configure -C $features $extra_args; then echo "ERROR: configure failed" exit 1 fi if ! $MAKE CC="${CC}"; then echo "ERROR: make failed" exit 1 fi # Run unit tests 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 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=$(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, # 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=$(extract_test_count "$glog" "$CMOCKA_PASSED_PATTERN") [ -n "$cnt" ] && func_passed=$((func_passed + cnt)) cnt=$(extract_test_count "$glog" "$CMOCKA_FAILED_PATTERN") [ -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-full.info \ --rc lcov_branch_coverage=1 --ignore-errors inconsistent 2>&1 || true # Extract only production code from src/ directory, exclude tests # 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 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 # Save coverage.info before cleanup (for CI artifact) # 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 $MAKE clean 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 configurations # Coverage enabled only for build 1 (Full) - it has most code paths echo echo "=== Start build ===" echo if [ "$COVERAGE_ONLY" = "yes" ]; then 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_NAMES[0]}: ${tests[0]} [+Coverage]" else echo "Starting $TEST_BUILDS parallel build configurations..." echo pids=() for idx in $(seq 1 $TEST_BUILDS); do if [ $idx -le ${#tests[@]} ]; then # All builds run Valgrind on Linux if [ "$ARCH" = "linux" ]; then run_valgrind="yes" extra_flags="--enable-valgrind" else 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_NAMES[$((idx-1))]}: ${tests[$((idx-1))]}$flags_desc" fi done fi echo # Wait for all builds and check exit codes echo "Waiting for builds to complete..." echo failed_builds=() for i in "${!pids[@]}"; do idx=$((i + 1)) if wait "${pids[$i]}"; then if [ -f "build-$idx.log" ]; then parse_build_stats "build-$idx.log" 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 echo " Coverage: Lines: $STAT_COV_LINES | Functions: $STAT_COV_FUNCS | Branches: $STAT_COV_BRANCHES" fi echo " Duration: ${STAT_TIME:-?}" else echo "✓ ${BUILD_NAMES[$i]} passed (no stats available)" fi else echo "✗ ${BUILD_NAMES[$i]} FAILED" >&2 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_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[*]})" >&2 exit 1 else if [ "$COVERAGE_ONLY" = "yes" ]; then echo "RESULT: COVERAGE BUILD PASSED ✓" else echo "RESULT: ALL $TEST_BUILDS BUILDS PASSED ✓" fi fi