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:
Christian Decker
2025-11-29 19:04:12 +01:00
parent 7a1a363330
commit 4b9cffe183
13 changed files with 656 additions and 46 deletions

177
.github/workflows/coverage-nightly.yaml vendored Normal file
View 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
View File

@@ -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

View File

@@ -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
View 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
View 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

View File

@@ -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}"

View 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

Binary file not shown.

298
doc/COVERAGE.md Normal file
View 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)

View File

@@ -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
```

View File

@@ -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

Binary file not shown.

34
uv.lock generated
View File

@@ -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]]