Add comprehensive coverage infrastructure with clang source-based coverage
This commit introduces a modern coverage infrastructure for Core Lightning: - Migrate from ad-hoc coverage script to integrated Makefile targets - Add LLVM source-based coverage support with per-test profraw organization - Integrate coverage collection into pytest framework via TailableProc - Add GitHub Actions workflow for nightly coverage reports - Add Taskfile.yml for convenient task automation - Add codecov.yml for Codecov integration - Add comprehensive coverage documentation in COVERAGE.md - Update contributor workflow docs with new coverage script path - Add coverage data files to .gitignore (*.profraw, *.profdata) - Remove obsolete contrib/clang-coverage-report.sh - Remove obsolete tests/conftest.py (now using pyln-testing markers) - Update pyproject.toml to include pyln-testing in main dependencies The new infrastructure automatically collects coverage data when CLN_COVERAGE_DIR is set, organizing profraw files by test name for granular analysis. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
177
.github/workflows/coverage-nightly.yaml
vendored
Normal file
177
.github/workflows/coverage-nightly.yaml
vendored
Normal file
@@ -0,0 +1,177 @@
|
||||
name: Coverage (Nightly)
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run at 2 AM UTC every day
|
||||
- cron: '0 2 * * *'
|
||||
# Allow manual triggers for testing
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: coverage-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
compile:
|
||||
name: Build with Coverage
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
|
||||
- name: Install dependencies
|
||||
run: bash -x .github/scripts/setup.sh
|
||||
|
||||
- name: Build with coverage instrumentation
|
||||
run: |
|
||||
./configure --enable-debugbuild --enable-coverage CC=clang
|
||||
uv run make -j $(nproc) testpack.tar.bz2
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cln-coverage-build
|
||||
path: testpack.tar.bz2
|
||||
|
||||
test:
|
||||
name: Test (${{ matrix.name }})
|
||||
runs-on: ubuntu-22.04
|
||||
needs: compile
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: sqlite
|
||||
db: sqlite3
|
||||
pytest_par: 10
|
||||
- name: postgres
|
||||
db: postgres
|
||||
pytest_par: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
|
||||
- name: Install dependencies
|
||||
run: bash -x .github/scripts/setup.sh
|
||||
|
||||
- name: Install Bitcoin Core
|
||||
run: bash -x .github/scripts/install-bitcoind.sh
|
||||
|
||||
- name: Download build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cln-coverage-build
|
||||
|
||||
- name: Unpack build
|
||||
run: tar -xaf testpack.tar.bz2
|
||||
|
||||
- name: Run tests with coverage
|
||||
env:
|
||||
CLN_COVERAGE_DIR: ${{ github.workspace }}/coverage-raw
|
||||
TEST_DB_PROVIDER: ${{ matrix.db }}
|
||||
PYTEST_PAR: ${{ matrix.pytest_par }}
|
||||
SLOW_MACHINE: 1
|
||||
TIMEOUT: 900
|
||||
run: |
|
||||
mkdir -p "$CLN_COVERAGE_DIR"
|
||||
uv run eatmydata pytest tests/ -n ${PYTEST_PAR} -vvv
|
||||
|
||||
- name: Upload coverage data
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: coverage-raw-${{ matrix.name }}
|
||||
path: coverage-raw/*.profraw
|
||||
if-no-files-found: error
|
||||
|
||||
report:
|
||||
name: Generate Coverage Report
|
||||
runs-on: ubuntu-22.04
|
||||
needs: test
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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
|
||||
|
||||
- name: Download build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cln-coverage-build
|
||||
|
||||
- name: Unpack build
|
||||
run: tar -xaf testpack.tar.bz2
|
||||
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: coverage-raw-*
|
||||
path: coverage-artifacts
|
||||
|
||||
- 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
|
||||
|
||||
- name: Generate HTML report
|
||||
run: |
|
||||
chmod +x contrib/coverage/generate-coverage-report.sh
|
||||
./contrib/coverage/generate-coverage-report.sh
|
||||
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: coverage/merged.profdata
|
||||
flags: integration-tests
|
||||
name: cln-nightly-coverage
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-html-report
|
||||
path: coverage/html
|
||||
retention-days: 90
|
||||
|
||||
- name: Add summary to job
|
||||
run: |
|
||||
echo "## Coverage Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
cat coverage/summary.txt >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "📊 Download detailed HTML report from workflow artifacts" >> $GITHUB_STEP_SUMMARY
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -24,6 +24,9 @@ gen_*.h
|
||||
wire/gen_*_csv
|
||||
cli/lightning-cli
|
||||
coverage
|
||||
# Coverage profiling data files
|
||||
*.profraw
|
||||
*.profdata
|
||||
ccan/config.h
|
||||
__pycache__
|
||||
config.vars
|
||||
|
||||
15
Makefile
15
Makefile
@@ -674,6 +674,21 @@ coverage/coverage.info: check pytest
|
||||
coverage: coverage/coverage.info
|
||||
genhtml coverage/coverage.info --output-directory coverage
|
||||
|
||||
# Clang coverage targets (source-based coverage)
|
||||
coverage-clang-collect:
|
||||
@./contrib/coverage/collect-coverage.sh "$(CLN_COVERAGE_DIR)" coverage/merged.profdata
|
||||
|
||||
coverage-clang-report: coverage/merged.profdata
|
||||
@./contrib/coverage/generate-coverage-report.sh coverage/merged.profdata coverage/html
|
||||
|
||||
coverage-clang: coverage-clang-collect coverage-clang-report
|
||||
@echo "Coverage report: coverage/html/index.html"
|
||||
|
||||
coverage-clang-clean:
|
||||
rm -rf coverage/ "$(CLN_COVERAGE_DIR)"
|
||||
|
||||
.PHONY: coverage-clang-collect coverage-clang-report coverage-clang coverage-clang-clean
|
||||
|
||||
# We make libwallycore.la a dependency, so that it gets built normally, without ncc.
|
||||
# Ncc can't handle the libwally source code (yet).
|
||||
ncc: ${TARGET_DIR}/libwally-core-build/src/libwallycore.la
|
||||
|
||||
85
Taskfile.yml
Normal file
85
Taskfile.yml
Normal file
@@ -0,0 +1,85 @@
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
PYTEST_PAR: 4
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- uv run make cln-grpc/proto/node.proto
|
||||
- uv run make default -j {{ .PYTEST_PAR }}
|
||||
test:
|
||||
dir: '.'
|
||||
deps:
|
||||
- build
|
||||
cmds:
|
||||
- uv run pytest --force-flaky -vvv -n {{ .PYTEST_PAR }} tests {{ .CLI_ARGS }}
|
||||
|
||||
test-liquid:
|
||||
env:
|
||||
TEST_NETWORK: "liquid-regtest"
|
||||
cmds:
|
||||
- sed -i 's/TEST_NETWORK=regtest/TEST_NETWORK=liquid-regtest/g' config.vars
|
||||
- uv run make cln-grpc/proto/node.proto
|
||||
- uv run make default -j {{ .PYTEST_PAR }}
|
||||
- uv run pytest --color=yes --force-flaky -vvv -n {{ .PYTEST_PAR }} tests {{ .CLI_ARGS }}
|
||||
|
||||
clean:
|
||||
cmds:
|
||||
- poetry run make distclean
|
||||
|
||||
|
||||
tester-docker-image:
|
||||
cmds:
|
||||
- docker build --build-arg DOCKER_USER=$(whoami) --build-arg UID=$(id -u) --build-arg GID=$(id -g) --network=host -t cln-tester - <contrib/docker/Dockerfile.tester
|
||||
|
||||
isotest:
|
||||
dir: '.'
|
||||
deps:
|
||||
- tester-docker-image
|
||||
cmds:
|
||||
- docker run -ti -v $(pwd):/repo cln-tester bash -c 'task -t /repo/Taskfile.yml in-docker-test'
|
||||
|
||||
in-docker-build-deps:
|
||||
cmds:
|
||||
- sudo apt-get update -q
|
||||
- sudo apt-get install -y clang jq libsqlite3-dev libpq-dev systemtap-sdt-dev autoconf libtool zlib1g-dev libsodium-dev gettext git
|
||||
|
||||
in-docker-init:
|
||||
# pre-flight tasks, independent of the source code.
|
||||
dir: '/'
|
||||
cmds:
|
||||
- git config --global --add safe.directory /repo/.git
|
||||
- mkdir -p /test
|
||||
- python3 -m venv /test/.venv
|
||||
- /test/.venv/bin/pip3 install wheel
|
||||
- /test/.venv/bin/pip3 install 'poetry>=1.8,<2'
|
||||
|
||||
in-docker-test:
|
||||
# Just the counterpart called by `isotest` to actually initialize,
|
||||
# build and test CLN.
|
||||
dir: '/test'
|
||||
deps:
|
||||
- in-docker-init
|
||||
- in-docker-build-deps
|
||||
cmds:
|
||||
# This way of copying allows us to copy the dirty tree, without
|
||||
# triggering any of the potentially configured hooks which might
|
||||
# not be available in the docker image.
|
||||
- (cd /repo && git archive --format tar $(git stash create)) | tar -xvf -
|
||||
# Yes, this is not that smart, but the `Makefile` relies on
|
||||
# `git` being able to tell us about the version.
|
||||
- cp -R /repo/.git /test
|
||||
- git submodule update --init --recursive
|
||||
- python3 -m pip install poetry
|
||||
- poetry run make distclean
|
||||
- poetry install --with=dev
|
||||
- poetry run ./configure --disable-valgrind CC='clang'
|
||||
- poetry run make -j 4
|
||||
- poetry run pytest --color=yes -vvv -n {{ .PYTEST_PAR }} tests {{ .CLI_ARGS }}
|
||||
|
||||
kill:
|
||||
cmds:
|
||||
- killall -v bitcoind || true
|
||||
- killall -v elementsd || true
|
||||
- killall -v valgrind.bin || true
|
||||
31
codecov.yml
Normal file
31
codecov.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# Coverage can decrease by up to 1% and still pass
|
||||
target: auto
|
||||
threshold: 1%
|
||||
patch:
|
||||
default:
|
||||
# New code should maintain coverage
|
||||
target: auto
|
||||
|
||||
comment:
|
||||
# Post coverage comments on PRs (if we add PR coverage later)
|
||||
behavior: default
|
||||
layout: "header, diff, files"
|
||||
require_changes: false
|
||||
|
||||
# Ignore files that shouldn't affect coverage metrics
|
||||
ignore:
|
||||
- "external/**"
|
||||
- "ccan/**"
|
||||
- "*/test/**"
|
||||
- "tools/**"
|
||||
- "contrib/**"
|
||||
- "doc/**"
|
||||
- "devtools/**"
|
||||
|
||||
# Don't fail if coverage data is incomplete
|
||||
codecov:
|
||||
require_ci_to_pass: false
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/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}"
|
||||
@@ -197,6 +197,29 @@ class TailableProc(object):
|
||||
def __init__(self, outputDir, verbose=True):
|
||||
self.logs = []
|
||||
self.env = os.environ.copy()
|
||||
|
||||
# Add coverage support: inject LLVM_PROFILE_FILE if CLN_COVERAGE_DIR is set
|
||||
if os.getenv('CLN_COVERAGE_DIR'):
|
||||
coverage_dir = os.getenv('CLN_COVERAGE_DIR')
|
||||
|
||||
# Organize profraw files by test name for per-test coverage analysis
|
||||
test_name = os.getenv('CLN_TEST_NAME')
|
||||
if test_name:
|
||||
test_coverage_dir = os.path.join(coverage_dir, test_name)
|
||||
os.makedirs(test_coverage_dir, exist_ok=True)
|
||||
profraw_path = test_coverage_dir
|
||||
else:
|
||||
os.makedirs(coverage_dir, exist_ok=True)
|
||||
profraw_path = coverage_dir
|
||||
|
||||
# %p=PID, %m=binary signature prevents collisions across parallel processes
|
||||
# Note: We don't use %c (continuous mode) as it causes "__llvm_profile_counter_bias"
|
||||
# errors with our multi-binary setup. Instead, we validate and filter corrupt files
|
||||
# during collection (see contrib/coverage/collect-coverage.sh)
|
||||
self.env['LLVM_PROFILE_FILE'] = os.path.join(
|
||||
profraw_path, '%p-%m.profraw'
|
||||
)
|
||||
|
||||
self.proc = None
|
||||
self.outputDir = outputDir
|
||||
if not os.path.exists(outputDir):
|
||||
@@ -1635,6 +1658,10 @@ class NodeFactory(object):
|
||||
else:
|
||||
self.valgrind = VALGRIND
|
||||
self.testname = testname
|
||||
|
||||
# Set test name in environment for coverage file organization
|
||||
os.environ['CLN_TEST_NAME'] = testname
|
||||
|
||||
self.next_id = 1
|
||||
self.nodes = []
|
||||
self.reserved_ports = []
|
||||
|
||||
BIN
devtools/check-bolt
Executable file
BIN
devtools/check-bolt
Executable file
Binary file not shown.
298
doc/COVERAGE.md
Normal file
298
doc/COVERAGE.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# Code Coverage Guide
|
||||
|
||||
This guide explains how to measure code coverage for Core Lightning's test suite.
|
||||
|
||||
## Overview
|
||||
|
||||
Core Lightning uses Clang's source-based coverage instrumentation to measure which lines of code are executed during tests. This is particularly challenging because:
|
||||
|
||||
- CLN is a multi-process application (lightningd + 8 daemon executables)
|
||||
- Each test spawns multiple nodes, each running multiple daemon processes
|
||||
- Tests run in parallel (10+ workers)
|
||||
- Test processes run in temporary directories
|
||||
|
||||
Our solution uses `LLVM_PROFILE_FILE` environment variable with unique naming patterns to prevent profile file collisions across parallel processes.
|
||||
|
||||
## Local Development Workflow
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Clang compiler (clang-15 or later)
|
||||
- LLVM tools: `llvm-profdata`, `llvm-cov`
|
||||
|
||||
Install on Ubuntu/Debian:
|
||||
```bash
|
||||
sudo apt-get install clang llvm
|
||||
```
|
||||
|
||||
### Step 1: Build with Coverage Instrumentation
|
||||
|
||||
```bash
|
||||
./configure --enable-coverage CC=clang
|
||||
make clean # Important: clean previous builds
|
||||
make
|
||||
```
|
||||
|
||||
This compiles all binaries with `-fprofile-instr-generate -fcoverage-mapping` flags.
|
||||
|
||||
### Step 2: Run Tests with Coverage Collection
|
||||
|
||||
Set the coverage directory and run tests:
|
||||
|
||||
```bash
|
||||
export CLN_COVERAGE_DIR=/tmp/cln-coverage
|
||||
mkdir -p "$CLN_COVERAGE_DIR"
|
||||
uv run pytest tests/ -n 10
|
||||
```
|
||||
|
||||
You can run a subset of tests for faster iteration:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/test_pay.py -n 10
|
||||
```
|
||||
|
||||
All test processes will write `.profraw` files to `$CLN_COVERAGE_DIR` with unique names like `12345-67890abcdef.profraw` (PID-signature).
|
||||
|
||||
### Step 3: Generate Coverage Reports
|
||||
|
||||
Merge all profile files and generate HTML report:
|
||||
|
||||
```bash
|
||||
make coverage-clang
|
||||
```
|
||||
|
||||
This runs two scripts:
|
||||
1. `contrib/coverage/collect-coverage.sh` - Merges all `.profraw` files into `coverage/merged.profdata`
|
||||
2. `contrib/coverage/generate-coverage-report.sh` - Generates HTML report from merged profile
|
||||
|
||||
### Step 4: View the Report
|
||||
|
||||
Open the HTML report in your browser:
|
||||
|
||||
```bash
|
||||
xdg-open coverage/html/index.html
|
||||
```
|
||||
|
||||
Or on macOS:
|
||||
|
||||
```bash
|
||||
open coverage/html/index.html
|
||||
```
|
||||
|
||||
The report shows:
|
||||
- **Per-file coverage**: Which files have been tested
|
||||
- **Line-by-line coverage**: Which lines were executed and how many times
|
||||
- **Summary statistics**: Overall coverage percentage
|
||||
|
||||
You can also view the text summary:
|
||||
|
||||
```bash
|
||||
cat coverage/summary.txt
|
||||
```
|
||||
|
||||
### Step 5: Clean Up
|
||||
|
||||
```bash
|
||||
make coverage-clang-clean
|
||||
```
|
||||
|
||||
This removes the `coverage/` directory and `$CLN_COVERAGE_DIR`.
|
||||
|
||||
## Complete Example
|
||||
|
||||
```bash
|
||||
# Build
|
||||
./configure --enable-coverage CC=clang
|
||||
make
|
||||
|
||||
# Test
|
||||
export CLN_COVERAGE_DIR=/tmp/cln-coverage
|
||||
mkdir -p "$CLN_COVERAGE_DIR"
|
||||
uv run pytest tests/test_pay.py tests/test_invoice.py -n 10
|
||||
|
||||
# Report
|
||||
make coverage-clang
|
||||
xdg-open coverage/html/index.html
|
||||
|
||||
# Clean
|
||||
make coverage-clang-clean
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Running Specific Test Files
|
||||
|
||||
For faster development iteration, run only the tests you're working on:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/test_plugin.py -n 5
|
||||
```
|
||||
|
||||
### Per-Test Coverage
|
||||
|
||||
Coverage data is automatically organized by test name, allowing you to see which code each test exercises:
|
||||
|
||||
```bash
|
||||
export CLN_COVERAGE_DIR=/tmp/cln-coverage
|
||||
mkdir -p "$CLN_COVERAGE_DIR"
|
||||
uv run pytest tests/test_pay.py tests/test_invoice.py -n 10
|
||||
```
|
||||
|
||||
This creates a directory structure like:
|
||||
```
|
||||
/tmp/cln-coverage/
|
||||
├── test_pay/
|
||||
│ ├── 12345-abc.profraw
|
||||
│ └── 67890-def.profraw
|
||||
└── test_invoice/
|
||||
├── 11111-ghi.profraw
|
||||
└── 22222-jkl.profraw
|
||||
```
|
||||
|
||||
Generate per-test coverage reports:
|
||||
```bash
|
||||
# Generate text summaries
|
||||
./contrib/coverage/per-test-coverage.sh
|
||||
|
||||
# Generate HTML reports (optional)
|
||||
./contrib/coverage/per-test-coverage-html.sh
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `coverage/per-test/<test>.profdata` - Merged profile for each test
|
||||
- `coverage/per-test/<test>.txt` - Text summary for each test
|
||||
- `coverage/per-test-html/<test>/index.html` - HTML report for each test (if generated)
|
||||
|
||||
### Merging Multiple Test Runs
|
||||
|
||||
You can accumulate coverage across multiple test runs by reusing the same `CLN_COVERAGE_DIR`:
|
||||
|
||||
```bash
|
||||
export CLN_COVERAGE_DIR=/tmp/cln-coverage
|
||||
mkdir -p "$CLN_COVERAGE_DIR"
|
||||
|
||||
# Run different test subsets
|
||||
uv run pytest tests/test_pay.py -n 10
|
||||
uv run pytest tests/test_invoice.py -n 10
|
||||
uv run pytest tests/test_plugin.py -n 10
|
||||
|
||||
# Generate combined report (merges all tests)
|
||||
make coverage-clang
|
||||
|
||||
# Or generate per-test reports
|
||||
./contrib/coverage/per-test-coverage.sh
|
||||
```
|
||||
|
||||
### Manual Collection and Reporting
|
||||
|
||||
If you want more control:
|
||||
|
||||
```bash
|
||||
# Collect and merge
|
||||
./contrib/coverage/collect-coverage.sh /tmp/cln-coverage coverage/merged.profdata
|
||||
|
||||
# Generate report
|
||||
./contrib/coverage/generate-coverage-report.sh coverage/merged.profdata coverage/html
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
Coverage is automatically measured nightly on the master branch via the `coverage-nightly.yaml` GitHub Actions workflow. The workflow:
|
||||
|
||||
1. Builds CLN with coverage instrumentation
|
||||
2. Runs tests with both sqlite and postgres databases
|
||||
3. Merges coverage from all test runs
|
||||
4. Uploads results to Codecov.io
|
||||
5. Saves HTML reports as artifacts (90-day retention)
|
||||
|
||||
You can view:
|
||||
- **Codecov dashboard**: [codecov.io/gh/ElementsProject/lightning](https://codecov.io/gh/ElementsProject/lightning)
|
||||
- **HTML artifacts**: Download from GitHub Actions workflow runs
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No .profraw files created
|
||||
|
||||
**Problem**: `make coverage-clang` reports "No .profraw files found"
|
||||
|
||||
**Solutions**:
|
||||
1. Verify `CLN_COVERAGE_DIR` is set: `echo $CLN_COVERAGE_DIR`
|
||||
2. Verify you built with coverage: `./configure --enable-coverage CC=clang && make`
|
||||
3. Check that tests actually ran successfully
|
||||
|
||||
### llvm-profdata not found
|
||||
|
||||
**Problem**: `llvm-profdata: command not found`
|
||||
|
||||
**Solution**: Install LLVM tools:
|
||||
```bash
|
||||
sudo apt-get install llvm
|
||||
# Or on macOS:
|
||||
brew install llvm
|
||||
```
|
||||
|
||||
### Binary not found errors in generate-coverage-report.sh
|
||||
|
||||
**Problem**: Script complains about missing binaries
|
||||
|
||||
**Solution**: Make sure you've run `make` to build all CLN executables
|
||||
|
||||
### Coverage shows 0% for some files
|
||||
|
||||
**Causes**:
|
||||
1. Those files weren't executed by your tests (expected)
|
||||
2. The binary wasn't instrumented (check build flags)
|
||||
3. The profile data is incomplete
|
||||
|
||||
### Corrupt .profraw files
|
||||
|
||||
**Problem**: `llvm-profdata merge` fails with "invalid instrumentation profile data (file header is corrupt)"
|
||||
|
||||
**Cause**: When test processes crash or timeout, they may leave incomplete/corrupt `.profraw` files.
|
||||
|
||||
**Solution**: The `collect-coverage.sh` script automatically validates and filters out bad files:
|
||||
- **Empty files** - Processes that crash immediately
|
||||
- **Incomplete files** (< 1KB) - Processes killed before writing enough data
|
||||
- **Corrupt files** - Files with invalid headers or structure
|
||||
|
||||
You'll see output like:
|
||||
```
|
||||
Found 1250 profile files
|
||||
Skipping empty file: /tmp/cln-coverage/12345-abc.profraw
|
||||
Skipping incomplete file (512 bytes): /tmp/cln-coverage/67890-def.profraw
|
||||
Skipping corrupt file: /tmp/cln-coverage/11111-ghi.profraw
|
||||
Valid files: 1247
|
||||
Filtered out: 3 files
|
||||
- Empty: 1
|
||||
- Incomplete (< 1KB): 1
|
||||
- Corrupt/invalid: 1
|
||||
✓ Merged profile: coverage/merged.profdata
|
||||
```
|
||||
|
||||
To manually review and clean up corrupt files:
|
||||
```bash
|
||||
./contrib/coverage/cleanup-corrupt-profraw.sh
|
||||
```
|
||||
|
||||
This will show you which files are corrupt and offer to delete them.
|
||||
|
||||
**Prevention**: Incomplete/corrupt files are unavoidable when tests crash/timeout. The collection script handles this automatically by filtering them out during merge.
|
||||
|
||||
## Understanding Coverage Metrics
|
||||
|
||||
- **Lines**: Percentage of source code lines executed
|
||||
- **Functions**: Percentage of functions that were called
|
||||
- **Regions**: Percentage of code regions (blocks) executed
|
||||
- **Hit count**: Number of times each line was executed
|
||||
|
||||
Aim for:
|
||||
- **>80% line coverage** for core functionality
|
||||
- **>60% overall** given the complexity of CLN
|
||||
|
||||
Remember: 100% coverage doesn't mean bug-free code, but low coverage means untested code paths.
|
||||
|
||||
## References
|
||||
|
||||
- [LLVM Source-Based Code Coverage](https://clang.llvm.org/docs/SourceBasedCodeCoverage.html)
|
||||
- [llvm-profdata documentation](https://llvm.org/docs/CommandGuide/llvm-profdata.html)
|
||||
- [llvm-cov documentation](https://llvm.org/docs/CommandGuide/llvm-cov.html)
|
||||
@@ -91,7 +91,7 @@ LLVM_PROFILE_FILE="full_channel.profraw" ./channeld/test/run-full_channel
|
||||
|
||||
Finally, generate an HTML report from the profile. We have a script to make this easier:
|
||||
```shell
|
||||
./contrib/clang-coverage-report.sh channeld/test/run-full_channel \
|
||||
./contrib/coverage/clang-coverage-report.sh channeld/test/run-full_channel \
|
||||
full_channel.profraw full_channel.html
|
||||
firefox full_channel.html
|
||||
```
|
||||
|
||||
@@ -16,8 +16,9 @@ dependencies = [
|
||||
"pyln-proto",
|
||||
"pyln-grpc-proto",
|
||||
"pytest-trackflaky",
|
||||
"pyln-testing",
|
||||
"pytest-rerunfailures>=16.0.1",
|
||||
"pyln-testing",
|
||||
"pyln-proto",
|
||||
]
|
||||
package-mode = false
|
||||
[dependency-groups]
|
||||
|
||||
BIN
tools/lightning-downgrade
Executable file
BIN
tools/lightning-downgrade
Executable file
Binary file not shown.
34
uv.lock
generated
34
uv.lock
generated
@@ -458,7 +458,7 @@ dev = [
|
||||
{ name = "flake8" },
|
||||
{ name = "flask-socketio" },
|
||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pytest-benchmark" },
|
||||
{ name = "pytest-custom-exit-code" },
|
||||
{ name = "pytest-test-groups" },
|
||||
@@ -1441,7 +1441,7 @@ dev = [
|
||||
{ name = "pyln-bolt7" },
|
||||
{ name = "pyln-proto" },
|
||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -1501,7 +1501,7 @@ dependencies = [
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -1529,7 +1529,7 @@ dependencies = [
|
||||
{ name = "psycopg2-binary" },
|
||||
{ name = "pyln-client" },
|
||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "python-bitcoinlib" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
@@ -1601,7 +1601,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.1"
|
||||
version = "9.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.10'",
|
||||
@@ -1615,9 +1615,9 @@ dependencies = [
|
||||
{ name = "pygments", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "tomli", marker = "python_full_version == '3.10.*'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1627,7 +1627,7 @@ source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "py-cpuinfo" },
|
||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" }
|
||||
wheels = [
|
||||
@@ -1640,7 +1640,7 @@ version = "0.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/92/9d/e1eb0af5e96a5c34f59b9aa69dfb680764420fe60f2ec28cfbc5339f99f8/pytest-custom_exit_code-0.3.0.tar.gz", hash = "sha256:51ffff0ee2c1ddcc1242e2ddb2a5fd02482717e33a2326ef330e3aa430244635", size = 3633, upload-time = "2019-08-07T09:45:15.781Z" }
|
||||
wheels = [
|
||||
@@ -1672,7 +1672,7 @@ resolution-markers = [
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "packaging", marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/04/71e9520551fc8fe2cf5c1a1842e4e600265b0815f2016b7c27ec85688682/pytest_rerunfailures-16.1.tar.gz", hash = "sha256:c38b266db8a808953ebd71ac25c381cb1981a78ff9340a14bcb9f1b9bff1899e", size = 30889, upload-time = "2025-10-10T07:06:01.238Z" }
|
||||
wheels = [
|
||||
@@ -1685,7 +1685,7 @@ version = "1.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/5a/c7874fe15e03d86a1109a3274b57a2473edb8a1dda4a4d27f25d848b6ff5/pytest_test_groups-1.2.1.tar.gz", hash = "sha256:67576b295522fc144b3a42fa1801f50ae962389e984b48bab4336686d09032f1", size = 8137, upload-time = "2025-05-08T16:28:19.627Z" }
|
||||
wheels = [
|
||||
@@ -1698,7 +1698,7 @@ version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" }
|
||||
wheels = [
|
||||
@@ -1711,7 +1711,7 @@ version = "0.1.0"
|
||||
source = { editable = "contrib/pytest-trackflaky" }
|
||||
dependencies = [
|
||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -1724,7 +1724,7 @@ source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "execnet" },
|
||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
|
||||
wheels = [
|
||||
@@ -2207,11 +2207,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/43/554c2569b62f49350597348fc3ac70f786e3c32e7f19d266e19817812dd3/urllib3-2.6.0.tar.gz", hash = "sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1", size = 432585, upload-time = "2025-12-05T15:08:47.885Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/1a/9ffe814d317c5224166b23e7c47f606d6e473712a2fad0f704ea9b99f246/urllib3-2.6.0-py3-none-any.whl", hash = "sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f", size = 131083, upload-time = "2025-12-05T15:08:45.983Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user