#!/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 echo "Error ${ERR_CODE} with command '${BASH_COMMAND}' on line ${BASH_LINENO[0]}. Exiting." echo exit ${ERR_CODE} } trap error_handler ERR # 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 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)" echo "This means the test framework is NOT detecting failures correctly!" echo "--- Test output ---" cat /tmp/test_must_fail.log echo "--- End output ---" 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" echo "Parallel failure detection is broken!" 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 ./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 ;; 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 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-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/*' \ --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 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 # 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" 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+=("$!") 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 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 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" 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 fi done if [ ${#failed_builds[@]} -gt 0 ]; then echo "RESULT: FAILED (builds ${failed_builds[*]})" exit 1 else echo "RESULT: ALL 4 BUILDS PASSED ✓" fi