Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 12 additions & 21 deletions .github/workflows/coverage-nightly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ jobs:
if: always()
with:
name: coverage-raw-${{ matrix.name }}
path: coverage-raw/*.profraw
# pyln-testing puts the profiles in per-test subdirectories
path: coverage-raw/
if-no-files-found: error

report:
Expand All @@ -114,13 +115,13 @@ jobs:
- name: Checkout
uses: actions/checkout@v6

# The raw profile format has no compatibility guarantees across LLVM
# releases, so llvm-profdata has to come from the same release as the
# clang that produced the profiles: the distro default in both cases.
- name: Install LLVM tools
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 18
sudo ln -sf /usr/bin/llvm-profdata-18 /usr/bin/llvm-profdata
sudo ln -sf /usr/bin/llvm-cov-18 /usr/bin/llvm-cov
sudo apt-get update
sudo apt-get install -y llvm

- name: Download build artifact
uses: actions/download-artifact@v8
Expand All @@ -136,28 +137,18 @@ jobs:
pattern: coverage-raw-*
path: coverage-artifacts

# collect-coverage.sh walks the tree itself and rejects the profiles
# that crashed tests left half-written, so it gets the whole download.
- name: Merge coverage data
run: |
mkdir -p coverage-raw coverage
find coverage-artifacts -name "*.profraw" -exec cp {} coverage-raw/ \;
PROFRAW_COUNT=$(ls -1 coverage-raw/*.profraw 2>/dev/null | wc -l)
echo "Found $PROFRAW_COUNT profile files"
if [ "$PROFRAW_COUNT" -eq 0 ]; then
echo "ERROR: No coverage data found"
exit 1
fi
chmod +x contrib/coverage/collect-coverage.sh
CLN_COVERAGE_DIR=coverage-raw ./contrib/coverage/collect-coverage.sh
run: ./contrib/coverage/collect-coverage.sh coverage-artifacts

- name: Generate HTML report
run: |
chmod +x contrib/coverage/generate-coverage-report.sh
./contrib/coverage/generate-coverage-report.sh
run: ./contrib/coverage/generate-coverage-report.sh

- name: Upload to Codecov
uses: codecov/codecov-action@v6
with:
files: coverage/merged.profdata
files: coverage/coverage.lcov
flags: integration-tests
name: cln-nightly-coverage
token: ${{ secrets.CODECOV_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ gen_*.c
gen_*.h
wire/gen_*_csv
cli/lightning-cli
coverage
/coverage
# Coverage profiling data files
*.profraw
*.profdata
Expand Down
27 changes: 27 additions & 0 deletions contrib/coverage/clang-coverage-report.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash -eu
#
# Generates an HTML coverage report from a raw Clang coverage profile. See
# https://clang.llvm.org/docs/SourceBasedCodeCoverage.html for more details.
#
# Example usage to create full_channel.html from full_channel.profraw for the
# run-full_channel unit test:
# ./contrib/clang-coverage-report.sh channeld/test/run-full_channel \
# full_channel.profraw full_channel.html

if [[ "$#" -ne 3 ]]; then
echo "Usage: $0 BINARY RAW_PROFILE_FILE TARGET_HTML_FILE"
exit 1
fi

readonly BINARY="$1"
readonly RAW_PROFILE_FILE="$2"
readonly TARGET_HTML_FILE="$3"

MERGED_PROFILE_FILE=$(mktemp)
readonly MERGED_PROFILE_FILE

llvm-profdata merge -sparse "${RAW_PROFILE_FILE}" -o "${MERGED_PROFILE_FILE}"
llvm-cov show "${BINARY}" -instr-profile="${MERGED_PROFILE_FILE}" -format=html \
> "${TARGET_HTML_FILE}"

rm "${MERGED_PROFILE_FILE}"
55 changes: 55 additions & 0 deletions contrib/coverage/cleanup-corrupt-profraw.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/bin/bash -eu
# Remove corrupt .profraw files from the coverage directory
# Usage: ./cleanup-corrupt-profraw.sh [COVERAGE_DIR]

COVERAGE_DIR="${1:-${CLN_COVERAGE_DIR:-/tmp/cln-coverage}}"

if [ ! -d "$COVERAGE_DIR" ]; then
echo "Coverage directory not found: $COVERAGE_DIR"
exit 1
fi

echo "Scanning for corrupt profraw files in: $COVERAGE_DIR"

# Find all profraw files
mapfile -t PROFRAW_FILES < <(find "$COVERAGE_DIR" -name "*.profraw" 2>/dev/null || true)

if [ ${#PROFRAW_FILES[@]} -eq 0 ]; then
echo "No .profraw files found"
exit 0
fi

echo "Found ${#PROFRAW_FILES[@]} profile files"

CORRUPT_FILES=()
for profraw in "${PROFRAW_FILES[@]}"; do
# Try to validate the file
if ! llvm-profdata show "$profraw" >/dev/null 2>&1; then
CORRUPT_FILES+=("$profraw")
fi
done

if [ ${#CORRUPT_FILES[@]} -eq 0 ]; then
echo "✓ No corrupt files found"
exit 0
fi

echo ""
echo "Found ${#CORRUPT_FILES[@]} corrupt file(s):"
for corrupt in "${CORRUPT_FILES[@]}"; do
echo " - $corrupt"
done

echo ""
read -p "Delete these corrupt files? [y/N] " -n 1 -r
echo

if [[ $REPLY =~ ^[Yy]$ ]]; then
for corrupt in "${CORRUPT_FILES[@]}"; do
rm -f "$corrupt"
echo " Deleted: $corrupt"
done
echo "✓ Removed ${#CORRUPT_FILES[@]} corrupt file(s)"
else
echo "No files were deleted"
fi
116 changes: 116 additions & 0 deletions contrib/coverage/collect-coverage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/bin/bash -eu
# Merge all .profraw files into a single .profdata file
# Usage: ./collect-coverage.sh [COVERAGE_DIR] [OUTPUT_FILE]

COVERAGE_DIR="${1:-${CLN_COVERAGE_DIR:-/tmp/cln-coverage}}"
OUTPUT="${2:-coverage/merged.profdata}"

echo "Collecting coverage from: $COVERAGE_DIR"

# Find all profraw files
mapfile -t PROFRAW_FILES < <(find "$COVERAGE_DIR" -name "*.profraw" 2>/dev/null || true)

if [ ${#PROFRAW_FILES[@]} -eq 0 ]; then
echo "ERROR: No .profraw files found in $COVERAGE_DIR"
exit 1
fi

echo "Found ${#PROFRAW_FILES[@]} profile files"

# Validate each profraw file and filter out corrupt/incomplete ones
# Define validation function for parallel execution
validate_file() {
local profraw="$1"

# Check if file is empty
if [ ! -s "$profraw" ]; then
return 1 # Empty
fi

# Check if file is suspiciously small (likely incomplete write)
# Valid profraw files are typically > 1KB
filesize=$(stat -c%s "$profraw" 2>/dev/null || stat -f%z "$profraw" 2>/dev/null)
if [ "$filesize" -lt 1024 ]; then
return 2 # Too small
fi

# Try to validate the file by checking if llvm-profdata can read it
if llvm-profdata show "$profraw" >/dev/null 2>&1; then
echo "$profraw" # Valid - output to stdout
return 0
else
return 3 # Corrupt
fi
}

# Export function for parallel execution
export -f validate_file

TOTAL=${#PROFRAW_FILES[@]}
NPROC=$(nproc 2>/dev/null || echo 4)
echo "Validating ${TOTAL} files in parallel (using ${NPROC} cores)..."

# Run validation in parallel and collect valid files
mapfile -t VALID_FILES < <(
printf '%s\n' "${PROFRAW_FILES[@]}" | \
xargs -P "$NPROC" -I {} bash -c 'validate_file "$@"' _ {}
)

# Calculate error counts
CORRUPT_COUNT=$((TOTAL - ${#VALID_FILES[@]}))

if [ ${#VALID_FILES[@]} -eq 0 ]; then
echo "ERROR: No valid .profraw files found (all $CORRUPT_COUNT files were corrupt/incomplete)"
exit 1
fi

echo "Valid files: ${#VALID_FILES[@]}"
if [ $CORRUPT_COUNT -gt 0 ]; then
echo "Filtered out: $CORRUPT_COUNT files (empty/small/corrupt)"
fi
mkdir -p "$(dirname "$OUTPUT")"

# Merge with -sparse flag for efficiency
# Use batched merging to avoid "Argument list too long" errors
BATCH_SIZE=500
TOTAL_FILES=${#VALID_FILES[@]}

if [ "$TOTAL_FILES" -le "$BATCH_SIZE" ]; then
# Small enough to merge in one go
echo "Merging ${TOTAL_FILES} files..."
llvm-profdata merge -sparse "${VALID_FILES[@]}" -o "$OUTPUT"
else
# Need to merge in batches
echo "Merging ${TOTAL_FILES} files in batches of ${BATCH_SIZE}..."

# Create temp directory for intermediate files
TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/profdata-merge.XXXXXX")
trap 'rm -rf "$TEMP_DIR"' EXIT

BATCH_NUM=0
INTERMEDIATE_FILES=()

# Merge files in batches
for ((i=0; i<TOTAL_FILES; i+=BATCH_SIZE)); do
BATCH_NUM=$((BATCH_NUM + 1))
END=$((i + BATCH_SIZE))
if [ "$END" -gt "$TOTAL_FILES" ]; then
END=$TOTAL_FILES
fi

BATCH_FILES=("${VALID_FILES[@]:$i:$BATCH_SIZE}")
INTERMEDIATE="$TEMP_DIR/batch-$BATCH_NUM.profdata"

echo " Batch $BATCH_NUM: merging files $((i+1))-$END..."
llvm-profdata merge -sparse "${BATCH_FILES[@]}" -o "$INTERMEDIATE"
INTERMEDIATE_FILES+=("$INTERMEDIATE")
done

# Merge all intermediate files into final output
echo "Merging ${#INTERMEDIATE_FILES[@]} intermediate files into final output..."
llvm-profdata merge -sparse "${INTERMEDIATE_FILES[@]}" -o "$OUTPUT"

# Cleanup handled by trap
fi

echo "✓ Merged profile: $OUTPUT"
70 changes: 70 additions & 0 deletions contrib/coverage/generate-coverage-report.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/bin/bash -eu
# Generate HTML and text coverage reports from merged profile data
# Usage: ./generate-coverage-report.sh [PROFDATA_FILE] [OUTPUT_DIR]

PROFDATA="${1:-coverage/merged.profdata}"
OUTPUT_DIR="${2:-coverage/html}"

if [ ! -f "$PROFDATA" ]; then
echo "ERROR: Profile not found: $PROFDATA"
echo "Run collect-coverage.sh first to create the merged profile"
exit 1
fi

# Get all binaries from Makefile (includes plugins, tools, test binaries)
echo "Discovering instrumented binaries from Makefile..."
mapfile -t BINARIES < <(make -qp 2>/dev/null | awk '/^ALL_PROGRAMS :=/ {$1=$2=""; print}' | tr ' ' '\n' | grep -v '^$')
mapfile -t TEST_BINARIES < <(make -qp 2>/dev/null | awk '/^ALL_TEST_PROGRAMS :=/ {$1=$2=""; print}' | tr ' ' '\n' | grep -v '^$')

# Combine all binaries
ALL_BINARIES=("${BINARIES[@]}" "${TEST_BINARIES[@]}")

# Build llvm-cov arguments
ARGS=()
for bin in "${ALL_BINARIES[@]}"; do
if [ -f "$bin" ]; then
if [ ${#ARGS[@]} -eq 0 ]; then
ARGS+=("$bin") # First binary is primary
else
ARGS+=("-object=$bin") # Others use -object=
fi
fi
done

if [ ${#ARGS[@]} -eq 0 ]; then
echo "ERROR: No instrumented binaries found"
echo "Make sure you've built with --enable-coverage"
exit 1
fi

echo "Generating coverage report for ${#ARGS[@]} binaries..."

# Generate HTML report
llvm-cov show "${ARGS[@]}" \
-instr-profile="$PROFDATA" \
-format=html \
-output-dir="$OUTPUT_DIR" \
-show-line-counts-or-regions \
-show-instantiations=false

echo "✓ HTML report: $OUTPUT_DIR/index.html"

# Generate text summary
mkdir -p coverage
llvm-cov report "${ARGS[@]}" \
-instr-profile="$PROFDATA" \
| tee coverage/summary.txt

echo "✓ Summary: coverage/summary.txt"

# Machine-readable export for tools that ingest lcov: Codecov, and
# lcov_cobertura for GitLab's coverage visualization. Both match the paths in
# the report against the repository tree, so strip the build directory off.
llvm-cov export "${ARGS[@]}" \
-instr-profile="$PROFDATA" \
-format=lcov > coverage/coverage.lcov.tmp

sed "s|^SF:$PWD/|SF:|" coverage/coverage.lcov.tmp > coverage/coverage.lcov
rm -f coverage/coverage.lcov.tmp

echo "✓ LCOV: coverage/coverage.lcov"
Loading
Loading