Skip to content
Draft
2 changes: 1 addition & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,4 @@ jobs:
if [[ "${{ matrix.python-version }}" =~ t$ ]]; then
export PYTHON_GIL=0
fi
uv run pytest tests/integration/standard/ tests/integration/cqlengine/
uv run pytest -v tests/integration/standard/ tests/integration/cqlengine/
12 changes: 10 additions & 2 deletions cassandra/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -3439,7 +3439,7 @@ def pool_finished_setting_keyspace(pool, host_errors):
errors[pool.host] = host_errors

if not remaining_callbacks:
callback(host_errors)
callback(errors)

for pool in tuple(self._pools.values()):
pool._set_keyspace_for_all_conns(keyspace, pool_finished_setting_keyspace)
Expand Down Expand Up @@ -3482,6 +3482,14 @@ def wait_for_schema_agreement(self, wait_time: Optional[float] = None,
if wait_time is not None and wait_time <= 0:
raise ValueError("wait_time must be greater than 0")

try:
scope = SchemaAgreementScope(scope)
except ValueError:
raise ValueError(
"scope must be one of %s" % (
[s.value for s in SchemaAgreementScope],)
)

total_timeout = wait_time if wait_time is not None else self.cluster.max_schema_agreement_wait
if total_timeout <= 0:
raise ValueError("total_timeout must be greater than 0")
Expand Down Expand Up @@ -5325,7 +5333,7 @@ def _execute_after_prepare(self, host, connection, pool, response):
new_metadata_id = response.result_metadata_id
if new_metadata_id is not None:
self.prepared_statement.result_metadata_id = new_metadata_id

# use self._query to re-use the same host and
# at the same time properly borrow the connection
if pool is None and connection is not None and connection.is_control_connection:
Expand Down
57 changes: 52 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dev = [
"gevent",
"eventlet>=0.33.3",
"cython>=3.2",
"setuptools",
"packaging>=25.0",
"futurist",
"pyyaml",
Expand Down Expand Up @@ -120,7 +121,12 @@ log_format = "%(asctime)s.%(msecs)03d %(levelname)s [%(module)s:%(lineno)s]: %(m
log_level = "DEBUG"
log_date_format = "%Y-%m-%d %H:%M:%S"
xfail_strict = true
addopts = "-rf"
# Use 'append' import mode so the installed package (e.g. the compiled wheel
# under cibuildwheel) takes precedence on sys.path over the in-tree ``cassandra``
# source directory. With the default 'prepend' mode pytest inserts the project
# root at sys.path[0], which shadows the installed wheel with the uncompiled
# source tree and makes every C-extension test (libev, cmurmur3, Cython) skip.
addopts = "-rf --import-mode=append"
markers = [
"last: mark test to run last within its module group",
]
Expand Down Expand Up @@ -156,22 +162,63 @@ enable = ["pypy"]
[tool.cibuildwheel.linux]

before-build = "rm -rf ~/.pyxbld && rpm --import https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux && yum install -y libffi-devel libev libev-devel openssl openssl-devel"
# Install the optional lz4 compression dependency so the lz4 segment tests run
# (and fail loudly under CASS_DRIVER_NO_SKIP) instead of skipping silently.
test-extras = ["compress-lz4"]
# Extensions are mandatory on Linux (CASS_DRIVER_BUILD_EXTENSIONS_ARE_MUST=yes),
# so skipping is disabled (CASS_DRIVER_NO_SKIP=1): a missing dependency such as
# libev fails loudly instead of being silently skipped. Tests that cannot run in
# the default configuration are listed explicitly:
# * event-loop reactor tests are run separately with the matching
# EVENT_LOOP_MANAGER (gevent/eventlet/asyncio);
# * asyncore is deprecated and unavailable on modern Python, so it is ignored;
# * column_encryption is disabled upstream (scylladb/python-driver#365);
# * test_deserialize_date_range_month is disabled upstream (PYTHON-912).
# eventlet is skipped on PyPy (@notpypy), so CASS_DRIVER_NO_SKIP is not set for it.
test-command = [
"pytest {package}/tests/unit",
"EVENT_LOOP_MANAGER=gevent pytest {package}/tests/unit/io/test_geventreactor.py",
"CASS_DRIVER_NO_SKIP=1 pytest {package}/tests/unit -v --ignore={package}/tests/unit/column_encryption --ignore={package}/tests/unit/io/test_geventreactor.py --ignore={package}/tests/unit/io/test_eventletreactor.py --ignore={package}/tests/unit/io/test_asyncioreactor.py --ignore={package}/tests/unit/io/test_asyncorereactor.py -k 'not test_deserialize_date_range_month'",
"EVENT_LOOP_MANAGER=gevent CASS_DRIVER_NO_SKIP=1 pytest {package}/tests/unit/io/test_geventreactor.py -v",
"EVENT_LOOP_MANAGER=asyncio CASS_DRIVER_NO_SKIP=1 pytest {package}/tests/unit/io/test_asyncioreactor.py -v",
"EVENT_LOOP_MANAGER=eventlet pytest {package}/tests/unit/io/test_eventletreactor.py -v",
]

[tool.cibuildwheel.macos]
build-frontend = "build"
# Install lz4 so the lz4 segment tests run instead of skipping (see Linux note).
test-extras = ["compress-lz4"]
# Same policy as Linux (extensions are mandatory here too, libev comes from
# Homebrew). The extra -k exclusions are timing-sensitive tests that are flaky
# on macOS runners. The gevent/eventlet/asyncio reactor test files only contain
# those timing-sensitive timer tests, so they are not run separately here.
test-command = [
"pytest {project}/tests/unit -k 'not (test_multi_timer_validation or test_empty_connections or test_timer_cancellation)'",
"CASS_DRIVER_NO_SKIP=1 pytest {project}/tests/unit -v --ignore={project}/tests/unit/column_encryption --ignore={project}/tests/unit/io/test_geventreactor.py --ignore={project}/tests/unit/io/test_eventletreactor.py --ignore={project}/tests/unit/io/test_asyncioreactor.py --ignore={project}/tests/unit/io/test_asyncorereactor.py -k 'not (test_multi_timer_validation or test_empty_connections or test_timer_cancellation or test_deserialize_date_range_month)'",
]

[tool.cibuildwheel.windows]
build-frontend = "build"
# On Windows the C extensions are optional (CASS_DRIVER_BUILD_EXTENSIONS_ARE_MUST
# is overridden to "no" below), so extension-dependent tests (e.g. libev) are
# legitimately skipped here. CASS_DRIVER_NO_SKIP is therefore NOT enabled on
# Windows; we only add -v so skips are visible in the log.
test-command = [
"pytest {project}/tests/unit -k \"not (test_deserialize_date_range_year or test_datetype or test_libevreactor)\"",
"pytest {project}/tests/unit -v -k \"not (test_deserialize_date_range_year or test_datetype or test_libevreactor)\"",
]

# TODO: set CASS_DRIVER_BUILD_EXTENSIONS_ARE_MUST to yes when https://github.com/scylladb/python-driver/issues/429 is fixed
environment = { CASS_DRIVER_BUILD_CONCURRENCY = "2", CASS_DRIVER_BUILD_EXTENSIONS_ARE_MUST = "no" }

# PyPy never builds the libev/cmurmur3/Cython C extensions (setup.py forces
# is_pypy to skip them even when CASS_DRIVER_BUILD_EXTENSIONS_ARE_MUST=yes), so
# the tests that depend on those extensions legitimately skip. Enforcing
# CASS_DRIVER_NO_SKIP would turn those expected skips into failures, so it is
# NOT enabled for PyPy (same reasoning as Windows). The reactor tests are not
# run separately here because eventlet is unsupported on PyPy (@notpypy) and the
# extension-backed reactors are unavailable; with no-skip off they simply skip.
# test-extras is cleared (no compress-lz4): PyPy has no prebuilt lz4 wheel, so
# pip would try to compile it from source and fail. The lz4 tests just skip here.
[[tool.cibuildwheel.overrides]]
select = "pp*"
test-extras = []
test-command = [
"pytest {package}/tests/unit -v --ignore={package}/tests/unit/column_encryption -k 'not test_deserialize_date_range_month'",
]
34 changes: 34 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,43 @@
import os
import warnings

import pytest

# Directory containing the Cython-compiled driver modules.
_CASSANDRA_DIR = os.path.join(os.path.dirname(__file__), os.pardir, "cassandra")

# When set (e.g. in CI) a skipped test is turned into a failure. Tests skip
# themselves when their requirements are missing (a library is not installed,
# the wrong event loop is selected, ...). That is convenient locally, but in CI
# it is a footgun: a test may be silently skipped because we forgot to install
# something. Enabling this forces every skip to be explicit on the command line
# (via -k / --ignore / --deselect) instead of being hidden in the output.
_NO_SKIP = bool(os.environ.get("CASS_DRIVER_NO_SKIP"))


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""Turn skips into failures when CASS_DRIVER_NO_SKIP is set.

xfailed tests (which are reported as skipped) are left untouched so that
``xfail_strict`` keeps working as configured.
"""
outcome = yield
if not _NO_SKIP:
return
report = outcome.get_result()
if report.skipped and not hasattr(report, "wasxfail"):
reason = ""
if isinstance(report.longrepr, tuple) and len(report.longrepr) == 3:
reason = report.longrepr[2]
report.outcome = "failed"
report.longrepr = (
"Test was skipped but skipping is disabled in this environment "
"(CASS_DRIVER_NO_SKIP is set). Run it in a suitable configuration "
"or deselect it explicitly on the command line. "
"Original skip reason: {!r}".format(reason)
)


def pytest_configure(config):
"""Warn when a compiled Cython extension is older than its .py source.
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/io/test_libevreactor.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ def test_watchers_are_finished(self):
assert conn._read_watcher.stop.mock_calls

_global_loop._shutdown = False
# _cleanup stopped the prepare watcher; restart it so the shared
# singleton loop is left in a working state for subsequent tests
# (otherwise timers would never be scheduled and tests would hang).
_global_loop._preparer.start()


class LibevTimerPatcher(unittest.TestCase):
Expand Down
7 changes: 5 additions & 2 deletions tests/unit/io/test_libevreactor_shutdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,11 @@ def test_shutdown_cleanup_works_with_fix(self):
import sys
import os

# Add the driver path
sys.path.insert(0, {driver_path!r})
# Add the driver path as a fallback only. Append (not insert at 0) so that an
# installed build of the driver (e.g. the compiled wheel under cibuildwheel)
# takes precedence over the in-tree pure-Python source, which lacks the libev
# C extension and would make the import fail.
sys.path.append({driver_path!r})

# Import and setup
from cassandra.io import libevreactor
Expand Down
Loading