From aae83c7c67ab0d916a423ee761011022f0820851 Mon Sep 17 00:00:00 2001 From: sylwiaszunejko Date: Thu, 18 Jun 2026 11:46:41 +0200 Subject: [PATCH 1/5] test_libevreactor: Restart preparer after cleanup test_watchers_are_finished calls libev__cleanup which stops the preparer, which causes the test_multi_timer_validation to hang if it runs later. Simplest fix is to not only restore _global_loop._shutdown but also restart the prepared with _global_loop._preparer.start(). --- tests/unit/io/test_libevreactor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/unit/io/test_libevreactor.py b/tests/unit/io/test_libevreactor.py index cf7e7caf77..7c93d54ff5 100644 --- a/tests/unit/io/test_libevreactor.py +++ b/tests/unit/io/test_libevreactor.py @@ -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): From 0ccd02e1ea3f148f5f0af03e5363815948ad2414 Mon Sep 17 00:00:00 2001 From: sylwiaszunejko Date: Thu, 18 Jun 2026 11:47:39 +0200 Subject: [PATCH 2/5] pyproject.toml: Add setuptools to dev group It looks like some recent version of Python removed distutils from stdlib, which causes test collection to fail for me because of missing import. The fix is to add setuptools to dev dependencies. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 4a40af5378..c77e8c52a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ dev = [ "gevent", "eventlet>=0.33.3", "cython>=3.2", + "setuptools", "packaging>=25.0", "futurist", "pyyaml", From da203304d3312afc72c27668b45be5a597fd3ee6 Mon Sep 17 00:00:00 2001 From: sylwiaszunejko Date: Thu, 18 Jun 2026 12:04:38 +0200 Subject: [PATCH 3/5] CI: turn silent unit-test skips into failures Tests skip themselves when their requirements are missing, which is a footgun in CI: a libev/lz4/event-loop test could be silently skipped and nobody would notice. Add a CASS_DRIVER_NO_SKIP-gated pytest hook in conftest.py that converts skips into failures (leaving xfail untouched), and enable it in the cibuildwheel test-commands with precise -k/--ignore filters plus -v output. Install the lz4 test-extra so the compression tests actually run, and add -v to the integration-tests workflow. --- .github/workflows/integration-tests.yml | 2 +- pyproject.toml | 33 +++++++++++++++++++++--- tests/conftest.py | 34 +++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 5e76d6bbb4..acebb1d617 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -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/ diff --git a/pyproject.toml b/pyproject.toml index c77e8c52a7..5ef1791d18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -157,21 +157,46 @@ 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 diff --git a/tests/conftest.py b/tests/conftest.py index 8fd2fc923b..d59829a5fa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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. From da4d3c4d0f21fe8d02bc578a8cf257312c284fb9 Mon Sep 17 00:00:00 2001 From: sylwiaszunejko Date: Thu, 18 Jun 2026 12:05:14 +0200 Subject: [PATCH 4/5] Fix Session._set_keyspace_for_all_pools to report all pools' errors The final callback was invoked with host_errors (the errors from only the last pool to finish) instead of the accumulated errors dict. If the last pool succeeded, failures from other pools were silently lost. Pass the aggregated errors dict, matching the method's docstring. --- cassandra/cluster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 1181c6f686..fdbdfb8d70 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -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) From 9d24352e6513c6609fb03445bea43b1b0f445128 Mon Sep 17 00:00:00 2001 From: sylwiaszunejko Date: Thu, 18 Jun 2026 12:05:14 +0200 Subject: [PATCH 5/5] Validate scope in Session.wait_for_schema_agreement The docstring promised ValueError for an invalid scope, but no check existed: an unknown value silently fell through to cluster-wide behaviour. Coerce/validate the argument against SchemaAgreementScope up front (also accepting the plain string values) and raise a clear ValueError listing the valid scopes. --- cassandra/cluster.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index fdbdfb8d70..34e07567a9 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -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") @@ -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: