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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: Code coverage

on:
push:
branches:
- master
- 'branch-**'
paths-ignore:
- docs/*
- examples/*
- .gitignore
- '*.rst'
- '*.ini'
- LICENSE
- .github/dependabot.yml
- .github/pull_request_template.md
- "*.md"
- .github/workflows/docs-*
Comment thread
roydahan marked this conversation as resolved.
pull_request:
paths-ignore:
- docs/*
- examples/*
- .gitignore
- '*.rst'
- '*.ini'
- LICENSE
- .github/dependabot.yml
- .github/pull_request_template.md
- "*.md"
- .github/workflows/docs-*
workflow_dispatch:

jobs:
coverage:
name: Measure code coverage
if: "!contains(github.event.pull_request.labels.*.name, 'disable-coverage-tests')"
runs-on: ubuntu-24.04
env:
SCYLLA_VERSION: release:2026.1
PROTOCOL_VERSION: 4
# Set once at job level so every step (including "Build driver" and the
# summary step, both of which invoke `uv run`) sees the same value.
# uv's cache-keys include this var, so a value that flips between steps
# makes each `uv run` re-detect a "changed" build config and rebuild the
# Cython extensions from scratch -- about two minutes wasted per flip.
CASS_DRIVER_NO_CYTHON: "1"

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Set up JDK 8
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 8
distribution: 'adopt'

- name: Install libev
run: sudo apt-get install libev4 libev-dev

- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
python-version: "3.13"

- name: Build driver
run: uv sync

- name: Cache Scylla download
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.ccm/repository
key: scylla-${{ env.SCYLLA_VERSION }}-${{ runner.os }}

- name: Download Scylla
run: |
uv run ccm create scylla-driver-temp -n 1 --scylla --version ${SCYLLA_VERSION}
uv run ccm remove

- name: Run tests with coverage
run: bash scripts/coverage.sh

- name: Publish coverage summary
if: always()
run: |
{
echo '## Coverage report'
echo '```'
uv run coverage report -m
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-report
path: |
htmlcov/
coverage.xml
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ tests/unit/cython/bytesio_testhelper.c

# Unit test / coverage reports
.coverage
.coverage.*
htmlcov/
coverage.xml
.tox

#iPython
*.ipynb

uv.lock
.venv/


Expand Down
22 changes: 22 additions & 0 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,28 @@ Use tee to capture logs and see them on your terminal::

uv run pytest -s tests/unit/ 2>&1 | tee test.log

Measuring Code Coverage
------------------------
``scripts/coverage.sh`` runs the unit suite (all event-loop reactors) and,
if a Scylla/Cassandra version is available, the integration suite, under
``coverage.py``, then combines and reports the result::

bash scripts/coverage.sh

# include the integration suite too
SCYLLA_VERSION="release:2026.1" bash scripts/coverage.sh

Open ``htmlcov/index.html`` afterwards for a line-by-line, browsable report.
``coverage.xml`` is also produced for tooling that consumes Cobertura-style
XML.

Note that ``cluster.py``, ``connection.py``, ``protocol.py`` and the other
modules that are optionally Cython-compiled (see ``Dev setup`` above) are
measured as plain Python here, since ``coverage.py`` cannot trace
into compiled extensions -- the script sets ``CASS_DRIVER_NO_CYTHON=1`` for
this reason. Modules that are Cython-only with no pure-Python fallback
(``obj_parser``, ``numpy_parser``, ``row_parser``, and similar) are not built
at all in that mode, so they are not measured by this script.

Running the Benchmarks
======================
Expand Down
19 changes: 19 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ dev = [
"pyyaml",
"numpy",
"objgraph",
"coverage[toml]>=7.6",
"ccm @ git+https://git@github.com/scylladb/scylla-ccm.git@master",
]

Expand Down Expand Up @@ -130,6 +131,24 @@ markers = [
version_file = "cassandra/_version.py"
tag_regex = '(?P<version>\d*?\.\d*?\.\d*?)-scylla'

[tool.coverage.run]
source = ["cassandra"]
branch = true
parallel = true
relative_files = true
omit = ["cassandra/_version.py"]

[tool.coverage.report]
show_missing = true
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"if TYPE_CHECKING:",
]

[tool.coverage.html]
directory = "htmlcov"

#### CI BUILDWHEEL CONFIG ####

[tool.cibuildwheel]
Expand Down
84 changes: 84 additions & 0 deletions scripts/coverage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
# Runs the unit suite (all event-loop reactors) and, if SCYLLA_VERSION or
# CASSANDRA_VERSION is set, the integration suite, all under coverage.py, then
# combines and reports.
#
# CASS_DRIVER_NO_CYTHON=1 forces cluster.py/connection.py/protocol.py/etc. to
# build as plain Python instead of Cython extensions, since coverage.py can't
# trace into compiled extensions. Cython-only modules with no .py fallback
# (obj_parser, numpy_parser, row_parser, ...) are not built at all in this mode
# and are therefore not measured by this script.
#
# Deliberately not `set -e`: a failing test must not skip report generation
# below, or a broken test leaves no coverage output at all to diagnose it
# with. Each test invocation instead records its own failure into $status,
# and the script exits with that status only after combine/report/html/xml
# have run.
set -uo pipefail
cd "$(dirname "$0")/.." || exit 1

rm -f .coverage .coverage.* || exit 1
export CASS_DRIVER_NO_CYTHON=1

# A previous plain `uv sync`/`uv run` may have left Cython-compiled .so/.pyd
# files in place from a normal (Cython-enabled) build. Python's import system
# prefers those over the .py source, so they must be removed -- otherwise
# CASS_DRIVER_NO_CYTHON=1 silently has no effect and coverage reports 0% for
# every affected module (and, for the Cython-only modules with no .py
# fallback like row_parser, HAVE_CYTHON would stay True off a stale .so,
# defeating CASS_DRIVER_NO_CYTHON entirely). Only extensions matching the
# *current* interpreter's own EXTENSION_SUFFIXES are removed -- the same
# mechanism tests/conftest.py already uses to detect staleness -- so this
# doesn't force a rebuild for other Python versions/venvs sharing this
# checkout. murmur3/libev are excluded by name since they're unaffected by
# CASS_DRIVER_NO_CYTHON. `--reinstall-package` then rebuilds from scratch,
# producing only the extensions CASS_DRIVER_NO_CYTHON=1 actually allows. If
# any of this setup fails, there's no point running any tests, so bail out
# immediately -- failure tolerance below is scoped to test/report commands
# only.
uv run python -c "
import importlib.machinery, pathlib
exclude = {'cmurmur3', 'libevwrapper'}
for path in pathlib.Path('cassandra').rglob('*'):
for suffix in importlib.machinery.EXTENSION_SUFFIXES:
if path.name.endswith(suffix):
if path.name[:-len(suffix)] not in exclude:
path.unlink()
break
" || exit 1
uv sync --reinstall-package scylla-driver || exit 1

status=0

# Unlike the gevent/eventlet/asyncio reactor tests below, tests/unit/io/
# test_asyncorereactor.py is deliberately NOT in the --ignore list: it needs
# no separate EVENT_LOOP_MANAGER run, since it self-skips via
# ASYNCCORE_AVAILABLE on Python 3.12+ (where the stdlib `asyncore` module was
# removed) and otherwise runs normally here, gaining coverage on 3.9-3.11.
uv run coverage run -m pytest tests/unit -v \
--ignore=tests/unit/column_encryption \
--ignore=tests/unit/io/test_geventreactor.py \
--ignore=tests/unit/io/test_eventletreactor.py \
--ignore=tests/unit/io/test_asyncioreactor.py \
|| status=1

# gevent/eventlet monkey-patch threading/sockets, which can confuse
# coverage.py's default sys.settrace-based collector; --concurrency tells it
# about the greenlet scheduler explicitly. asyncio and the default (thread)
# runs need no such hint.
EVENT_LOOP_MANAGER=gevent uv run coverage run --concurrency=gevent,thread -m pytest tests/unit/io/test_geventreactor.py -v || status=1
EVENT_LOOP_MANAGER=asyncio uv run coverage run -m pytest tests/unit/io/test_asyncioreactor.py -v || status=1
EVENT_LOOP_MANAGER=eventlet uv run coverage run --concurrency=eventlet,thread -m pytest tests/unit/io/test_eventletreactor.py -v || status=1

if [[ -n "${SCYLLA_VERSION:-}" || -n "${CASSANDRA_VERSION:-}" ]]; then
uv run coverage run -m pytest tests/integration/standard tests/integration/cqlengine/ -v || status=1
else
echo "SCYLLA_VERSION/CASSANDRA_VERSION not set -- skipping integration coverage."
fi

uv run coverage combine || status=1
uv run coverage report -m || status=1
uv run coverage html || status=1
uv run coverage xml || status=1

exit "$status"
Loading