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
|
wire/gen_*_csv
|
||||||
cli/lightning-cli
|
cli/lightning-cli
|
||||||
coverage
|
coverage
|
||||||
|
# Coverage profiling data files
|
||||||
|
*.profraw
|
||||||
|
*.profdata
|
||||||
ccan/config.h
|
ccan/config.h
|
||||||
__pycache__
|
__pycache__
|
||||||
config.vars
|
config.vars
|
||||||
|
|||||||
15
Makefile
15
Makefile
@@ -674,6 +674,21 @@ coverage/coverage.info: check pytest
|
|||||||
coverage: coverage/coverage.info
|
coverage: coverage/coverage.info
|
||||||
genhtml coverage/coverage.info --output-directory coverage
|
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.
|
# We make libwallycore.la a dependency, so that it gets built normally, without ncc.
|
||||||
# Ncc can't handle the libwally source code (yet).
|
# Ncc can't handle the libwally source code (yet).
|
||||||
ncc: ${TARGET_DIR}/libwally-core-build/src/libwallycore.la
|
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):
|
def __init__(self, outputDir, verbose=True):
|
||||||
self.logs = []
|
self.logs = []
|
||||||
self.env = os.environ.copy()
|
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.proc = None
|
||||||
self.outputDir = outputDir
|
self.outputDir = outputDir
|
||||||
if not os.path.exists(outputDir):
|
if not os.path.exists(outputDir):
|
||||||
@@ -1635,6 +1658,10 @@ class NodeFactory(object):
|
|||||||
else:
|
else:
|
||||||
self.valgrind = VALGRIND
|
self.valgrind = VALGRIND
|
||||||
self.testname = testname
|
self.testname = testname
|
||||||
|
|
||||||
|
# Set test name in environment for coverage file organization
|
||||||
|
os.environ['CLN_TEST_NAME'] = testname
|
||||||
|
|
||||||
self.next_id = 1
|
self.next_id = 1
|
||||||
self.nodes = []
|
self.nodes = []
|
||||||
self.reserved_ports = []
|
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:
|
Finally, generate an HTML report from the profile. We have a script to make this easier:
|
||||||
```shell
|
```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
|
full_channel.profraw full_channel.html
|
||||||
firefox full_channel.html
|
firefox full_channel.html
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ dependencies = [
|
|||||||
"pyln-proto",
|
"pyln-proto",
|
||||||
"pyln-grpc-proto",
|
"pyln-grpc-proto",
|
||||||
"pytest-trackflaky",
|
"pytest-trackflaky",
|
||||||
"pyln-testing",
|
|
||||||
"pytest-rerunfailures>=16.0.1",
|
"pytest-rerunfailures>=16.0.1",
|
||||||
|
"pyln-testing",
|
||||||
|
"pyln-proto",
|
||||||
]
|
]
|
||||||
package-mode = false
|
package-mode = false
|
||||||
[dependency-groups]
|
[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 = "flake8" },
|
||||||
{ name = "flask-socketio" },
|
{ name = "flask-socketio" },
|
||||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
{ 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-benchmark" },
|
||||||
{ name = "pytest-custom-exit-code" },
|
{ name = "pytest-custom-exit-code" },
|
||||||
{ name = "pytest-test-groups" },
|
{ name = "pytest-test-groups" },
|
||||||
@@ -1441,7 +1441,7 @@ dev = [
|
|||||||
{ name = "pyln-bolt7" },
|
{ name = "pyln-bolt7" },
|
||||||
{ name = "pyln-proto" },
|
{ name = "pyln-proto" },
|
||||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
{ 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]
|
[package.metadata]
|
||||||
@@ -1501,7 +1501,7 @@ dependencies = [
|
|||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
{ 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]
|
[package.metadata]
|
||||||
@@ -1529,7 +1529,7 @@ dependencies = [
|
|||||||
{ name = "psycopg2-binary" },
|
{ name = "psycopg2-binary" },
|
||||||
{ name = "pyln-client" },
|
{ name = "pyln-client" },
|
||||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
{ 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 = "python-bitcoinlib" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
]
|
]
|
||||||
@@ -1601,7 +1601,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "9.0.1"
|
version = "9.0.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
"python_full_version >= '3.10'",
|
"python_full_version >= '3.10'",
|
||||||
@@ -1615,9 +1615,9 @@ dependencies = [
|
|||||||
{ name = "pygments", marker = "python_full_version >= '3.10'" },
|
{ name = "pygments", marker = "python_full_version >= '3.10'" },
|
||||||
{ name = "tomli", 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 = [
|
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]]
|
[[package]]
|
||||||
@@ -1627,7 +1627,7 @@ source = { registry = "https://pypi.org/simple" }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "py-cpuinfo" },
|
{ name = "py-cpuinfo" },
|
||||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
{ 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" }
|
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 = [
|
wheels = [
|
||||||
@@ -1640,7 +1640,7 @@ version = "0.3.0"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
{ 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" }
|
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 = [
|
wheels = [
|
||||||
@@ -1672,7 +1672,7 @@ resolution-markers = [
|
|||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "packaging", marker = "python_full_version >= '3.10'" },
|
{ 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" }
|
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 = [
|
wheels = [
|
||||||
@@ -1685,7 +1685,7 @@ version = "1.2.1"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
{ 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" }
|
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 = [
|
wheels = [
|
||||||
@@ -1698,7 +1698,7 @@ version = "2.4.0"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
{ 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" }
|
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 = [
|
wheels = [
|
||||||
@@ -1711,7 +1711,7 @@ version = "0.1.0"
|
|||||||
source = { editable = "contrib/pytest-trackflaky" }
|
source = { editable = "contrib/pytest-trackflaky" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
{ 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]
|
[package.metadata]
|
||||||
@@ -1724,7 +1724,7 @@ source = { registry = "https://pypi.org/simple" }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "execnet" },
|
{ name = "execnet" },
|
||||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
{ 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" }
|
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 = [
|
wheels = [
|
||||||
@@ -2207,11 +2207,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urllib3"
|
name = "urllib3"
|
||||||
version = "2.5.0"
|
version = "2.6.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
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 = [
|
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]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user