All checks were successful
CI Code / Check spelling (push) Successful in 20s
CI Code / Check coding style (push) Successful in 33s
CI Code / Code Coverage (push) Successful in 4m47s
CI Code / Linux (debian) (push) Successful in 6m9s
CI Code / Linux (ubuntu) (push) Successful in 6m13s
CI Code / Linux (arch) (push) Successful in 6m19s
Major changes: Run 4 build configurations in parallel with Valgrind on Linux Add test failure detection verification (meta-test) Port allocation per build to prevent conflicts in parallel runs Add --coverage-only flag for dedicated coverage builds Code quality: Add TEST_GROUPS constant, CMOCKA patterns, helper functions Organize ci-build.sh into sections
473 lines
17 KiB
Bash
Executable File
473 lines
17 KiB
Bash
Executable File
#!/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 <log_file>
|
|
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 <log_file> <pattern>
|
|
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
|
|
echo "==> Verifying test failure detection..."
|
|
|
|
# Create a simple failing test
|
|
cat > /tmp/test_must_fail.c << 'EOF'
|
|
#include <stdarg.h>
|
|
#include <stddef.h>
|
|
#include <setjmp.h>
|
|
#include <cmocka.h>
|
|
|
|
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
|
|
;;
|
|
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
|