mirror of
https://git.jabber.space/devs/cproof.git
synced 2026-07-17 23:36:22 +00:00
Inject system flags from /etc/makepkg.conf into the CI environment to detect build collisions caused by Pikaur's configuration bug. Pikaur's cascading logic causes flags from /etc/makepkg.conf to be merged into the build environment. This creates collisions with flags defined in the project's Makefile.am (e.g., duplicate -D_FORTIFY_SOURCE definitions), which can cause builds to fail for users. By exporting these flags in the CI environment, we ensure that any code change that is sensitive to flag duplication will trigger a failure in our Arch Linux CI matrix, preventing broken builds from reaching users. Implementation details: - Detects Arch Linux via /etc/os-release. - Uses a sed-based flattener to handle multi-line variables and trailing backslashes in makepkg.conf. - Exports the flags to the shell environment so that 'configure' and 'make' inherit them naturally, maintaining parity with a real Pikaur session.
479 lines
17 KiB
Bash
Executable File
479 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 "==> 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
|
|
|
|
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
|