From 186689d8cd8b7fa0f2d08b541979eb6067fac7af Mon Sep 17 00:00:00 2001 From: Israel Fruchter Date: Thu, 15 Oct 2026 09:24:36 +0300 Subject: [PATCH 1/5] tests: fix starting Cassandra 4.1+ and 5.0 clusters with CCM Cassandra 4.1+ and 5.0 clusters never came up, and CCM waited on the native port until the CI job timed out after 2 hours. - Cassandra41CCMCluster.set_configuration_options ran str() on every value, so booleans were written to cassandra.yaml as the string 'True'. Cassandra refuses to start with a string in a boolean field. Pass the values through unchanged; _get_config_val already formats the _in_ms / _in_kb ones. - scylla-ccm runs every Cassandra node with Java 8 (Node.get_env passes hardcode_java_version='8'), and Cassandra 5.0 exits with "Cassandra 5.0 requires Java 11 or Java 17". For 5.0+ (pre-releases included, and only when ccmlib is installed), use JAVA_HOME from the environment instead. Co-Authored-By: Claude Opus 5.5 --- tests/integration/__init__.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index a8424bfc57..c096d951bf 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -146,6 +146,17 @@ def cmd_line_args_to_dict(env_var): CASSANDRA_VERSION = Version(mcv_string) if mcv_string else cassandra_version CCM_VERSION = mcv_string if mcv_string else cv_string +if not SCYLLA_VERSION and CCMClusterFactory and CASSANDRA_VERSION.release[:2] >= (5, 0): + # scylla-ccm runs every Cassandra node with Java 8 (Node.get_env), but Cassandra 5.0 needs Java 11+. + # Use JAVA_HOME from the environment instead. Drop this once ccm picks the JDK by Cassandra version. + from ccmlib.node import Node as CCMNode + + def _get_env_with_java_home(self): + update_conf = not self._Node__conf_updated + self._Node__conf_updated = True + return common.make_cassandra_env(self.get_install_dir(), self.get_path(), update_conf) + CCMNode.get_env = _get_env_with_java_home + CASSANDRA_IP = os.getenv('CLUSTER_IP', '127.0.0.1') CASSANDRA_DIR = os.getenv('CASSANDRA_DIR', None) @@ -1028,5 +1039,6 @@ def _get_config_val(self, k, v): return v def set_configuration_options(self, values=None, *args, **kwargs): - new_values = {self._get_config_key(k, str(v)):self._get_config_val(k, str(v)) for (k,v) in values.items()} + # keep v's type: str(True) is written as the YAML string 'True', which Cassandra refuses to start with + new_values = {self._get_config_key(k, v): self._get_config_val(k, v) for (k, v) in values.items()} super(Cassandra41CCMCluster, self).set_configuration_options(values=new_values, *args, **kwargs) From 663afa019848a3aa6316654dc3154a2f1d15c65f Mon Sep 17 00:00:00 2001 From: Israel Fruchter Date: Mon, 19 Oct 2026 23:04:14 +0300 Subject: [PATCH 2/5] tests: make the standard integration tests pass on Cassandra - Add a scylla_only marker and use it for Scylla-only features: shard awareness, tablets, rate limit errors, client routes, the LWT flag in prepared metadata, USING TIMEOUT (on metadata queries and in the reported driver config), and null-in-collection errors (Cassandra 5.0 answers those with a NullPointerException server error instead of InvalidRequest). The rack-aware policy topology (2 racks, RF 3) also fails to start on Cassandra. - test_exit_without_shutdown: Scylla only. It builds a many-shard node with ScyllaNode.set_smp(), which ccm's Cassandra nodes don't have. - test_authentication: auth_superuser_name/_salted_password are Scylla options, and Cassandra refuses to start with them. Cassandra creates the cassandra/cassandra superuser on its own. - test_ip_change: only set api_address on Scylla, for the same reason. - test_replicas_are_queried: only add `tablets = {'enabled': false}` on Scylla. - test_replicas: Scylla's CCM names the rack RAC1 and Cassandra's names it r1. Assert the right one per backend instead of xfailing. - test_custom_protocol_handler: the two protocol v5 tests pass on Cassandra 4.0+, drop expectedFailure. They are skipped on Scylla. - test_no_connection_refused_on_timeout: Cassandra 4.0+ answers some contended LWTs with error 0x1700 (CAS_WRITE_UNKNOWN, "CAS operation result is unknown"), which the driver doesn't map to a class. Accept that code too. - test_single_interface: skip on Cassandra, the only backend it runs on. use_cluster() never passes use_single_interface to CCM (scylla-ccm doesn't support it), so the nodes never listen on the expected ports. - test_host_addresses and test_control_connection_port_discovery: strict xfail on Cassandra 4.0+. Since #528 the control connection reads system.local with a fixed column list that has no broadcast_port or rpc_port, so the local host's ports stay None. That's a driver bug, to be fixed separately. Co-Authored-By: Claude Opus 5.5 --- tests/integration/__init__.py | 4 ++++ tests/integration/standard/test_authentication.py | 8 +++++--- tests/integration/standard/test_client_routes.py | 3 +++ tests/integration/standard/test_cluster.py | 5 +++-- tests/integration/standard/test_control_connection.py | 4 +++- .../integration/standard/test_custom_protocol_handler.py | 2 -- tests/integration/standard/test_driver_config.py | 3 ++- tests/integration/standard/test_exit_without_shutdown.py | 5 ++++- tests/integration/standard/test_ip_change.py | 7 +++++-- tests/integration/standard/test_metadata.py | 9 ++++++--- tests/integration/standard/test_prepared_statements.py | 3 ++- tests/integration/standard/test_query.py | 3 +++ tests/integration/standard/test_rack_aware_policy.py | 5 ++++- tests/integration/standard/test_rate_limit_exceeded.py | 3 ++- tests/integration/standard/test_shard_aware.py | 3 ++- tests/integration/standard/test_single_interface.py | 8 +++++++- tests/integration/standard/test_tablets.py | 3 ++- tests/integration/standard/test_types.py | 3 ++- tests/integration/standard/test_use_keyspace.py | 3 ++- 19 files changed, 61 insertions(+), 23 deletions(-) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index c096d951bf..b1a3b5275a 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -320,6 +320,10 @@ def _has_vector_type(): _has_vector_type(), 'Cassandra >= 5.0 or Scylla >= 2025.4 required') xfail_scylla = lambda reason, *args, **kwargs: pytest.mark.xfail(SCYLLA_VERSION is not None, reason=reason, *args, **kwargs) +scylla_only = pytest.mark.skipif(SCYLLA_VERSION is None, reason='Scylla only test') +# system.local is read with a fixed column list since #528, without broadcast_port / rpc_port +xfail_cassandra_local_ports = pytest.mark.xfail(SCYLLA_VERSION is None and CASSANDRA_VERSION >= Version('4-a'), strict=True, + reason='broadcast_port / broadcast_rpc_port of the local host are not read') incorrect_test = lambda reason='This test seems to be incorrect and should be fixed', *args, **kwargs: pytest.mark.xfail(reason=reason, *args, **kwargs) pypy = unittest.skipUnless(platform.python_implementation() == "PyPy", "Test is skipped unless it's on PyPy") diff --git a/tests/integration/standard/test_authentication.py b/tests/integration/standard/test_authentication.py index f23cc324b1..b5091aa4c1 100644 --- a/tests/integration/standard/test_authentication.py +++ b/tests/integration/standard/test_authentication.py @@ -22,7 +22,7 @@ from cassandra.auth import PlainTextAuthProvider, SASLClient, SaslAuthProvider from tests.integration import use_singledc, get_cluster, remove_cluster, PROTOCOL_VERSION, \ - CASSANDRA_IP, CASSANDRA_VERSION, start_cluster_wait_for_up, TestCluster + CASSANDRA_IP, CASSANDRA_VERSION, start_cluster_wait_for_up, TestCluster, SCYLLA_VERSION from tests.integration.util import assert_quiescent_pool_state import unittest @@ -47,9 +47,11 @@ def setup_module(): config_options = { 'authenticator': 'PasswordAuthenticator', 'authorizer': 'CassandraAuthorizer', - 'auth_superuser_name': 'cassandra', - 'auth_superuser_salted_password': '$6$x7IFjiX5VCpvNiFk$2IfjTvSyGL7zerpV.wbY7mJjaRCrJ/68dtT3UpT.sSmNYz1bPjtn3mH.kJKFvaZ2T4SbVeBijjmwGjcb83LlV/' } + if SCYLLA_VERSION: + # Scylla only options, Cassandra creates the cassandra/cassandra superuser by default + config_options['auth_superuser_name'] = 'cassandra' + config_options['auth_superuser_salted_password'] = '$6$x7IFjiX5VCpvNiFk$2IfjTvSyGL7zerpV.wbY7mJjaRCrJ/68dtT3UpT.sSmNYz1bPjtn3mH.kJKFvaZ2T4SbVeBijjmwGjcb83LlV/' ccm_cluster.set_configuration_options(config_options) log.debug("Starting ccm test cluster with %s", config_options) start_cluster_wait_for_up(ccm_cluster) diff --git a/tests/integration/standard/test_client_routes.py b/tests/integration/standard/test_client_routes.py index 5fc97ce9d7..319df97902 100644 --- a/tests/integration/standard/test_client_routes.py +++ b/tests/integration/standard/test_client_routes.py @@ -47,7 +47,10 @@ use_cluster, wait_for_node_socket, skip_scylla_version_lt, + scylla_only, ) + +pytestmark = scylla_only # system.client_routes and CLIENT_ROUTES_CHANGE are Scylla only from tests.tcp_proxy import TcpProxy from tests.util import wait_until_not_raised diff --git a/tests/integration/standard/test_cluster.py b/tests/integration/standard/test_cluster.py index b0934b6efa..021a3b901b 100644 --- a/tests/integration/standard/test_cluster.py +++ b/tests/integration/standard/test_cluster.py @@ -43,7 +43,7 @@ from tests.integration import use_cluster, get_server_versions, CASSANDRA_VERSION, \ execute_until_pass, execute_with_long_wait_retry, get_node, MockLoggingHandler, get_unsupported_lower_protocol, \ get_unsupported_upper_protocol, local, CASSANDRA_IP, greaterthanorequalcass30, \ - lessthanorequalcass40, TestCluster, PROTOCOL_VERSION, incorrect_test + lessthanorequalcass40, TestCluster, PROTOCOL_VERSION, incorrect_test, SCYLLA_VERSION from tests.integration.util import assert_quiescent_pool_state from tests.util import assertListEqual import sys @@ -1254,7 +1254,8 @@ def test_replicas_are_queried(self): session.execute("DROP KEYSPACE IF EXISTS {}".format(ks_name)) session.execute( "CREATE KEYSPACE {} WITH replication = {{'class': 'NetworkTopologyStrategy', " - "'replication_factor': '1'}} AND tablets = {{'enabled': false}}".format(ks_name) + "'replication_factor': '1'}}".format(ks_name) + + (" AND tablets = {'enabled': false}" if SCYLLA_VERSION else "") ) session.execute(''' CREATE TABLE {}.table_with_big_key ( diff --git a/tests/integration/standard/test_control_connection.py b/tests/integration/standard/test_control_connection.py index f0c41dde14..28e3f44863 100644 --- a/tests/integration/standard/test_control_connection.py +++ b/tests/integration/standard/test_control_connection.py @@ -24,7 +24,7 @@ from cassandra.protocol import ConfigurationException from tests.integration import use_singledc, PROTOCOL_VERSION, TestCluster, greaterthanorequalcass40, \ - xfail_scylla_version_lt + xfail_scylla_version_lt, scylla_only, xfail_cassandra_local_ports from tests.integration.datatype_utils import update_datatypes @@ -109,6 +109,7 @@ def test_get_control_connection_host(self): assert new_host1 != new_host2 @greaterthanorequalcass40 + @xfail_cassandra_local_ports def test_control_connection_port_discovery(self): """ Test to validate that the correct port is discovered when peersV2 is used (C* 4.0+). @@ -134,6 +135,7 @@ def test_control_connection_port_discovery(self): assert 9042 == host.broadcast_rpc_port assert 7000 == host.broadcast_port + @scylla_only @xfail_scylla_version_lt(reason='scylladb/scylladb#26992 - system.client_routes is not yet supported', scylla_version="2026.1.0") def test_client_routes_change_event(self): diff --git a/tests/integration/standard/test_custom_protocol_handler.py b/tests/integration/standard/test_custom_protocol_handler.py index 59283a3b33..106c9f1fdf 100644 --- a/tests/integration/standard/test_custom_protocol_handler.py +++ b/tests/integration/standard/test_custom_protocol_handler.py @@ -134,7 +134,6 @@ def test_protocol_divergence_v4_fail_by_flag_uses_int(self): self._protocol_divergence_fail_by_flag_uses_int(ProtocolVersion.V4, uses_int_query_flag=False, int_flag=True) - @unittest.expectedFailure @greaterthanorequalcass40 def test_protocol_v5_uses_flag_int(self): """ @@ -148,7 +147,6 @@ def test_protocol_v5_uses_flag_int(self): self._protocol_divergence_fail_by_flag_uses_int(ProtocolVersion.V5, uses_int_query_flag=True, beta=True, int_flag=True) - @unittest.expectedFailure @greaterthanorequalcass40 def test_protocol_divergence_v5_fail_by_flag_uses_int(self): """ diff --git a/tests/integration/standard/test_driver_config.py b/tests/integration/standard/test_driver_config.py index 13ac3225f4..3f27ac7ad2 100644 --- a/tests/integration/standard/test_driver_config.py +++ b/tests/integration/standard/test_driver_config.py @@ -24,7 +24,7 @@ ConstantSpeculativeExecutionPolicy, FallthroughRetryPolicy) from tests.driver_config_schema import validate_report from tests.integration import (TestCluster, get_client_options, use_single_node, remove_cluster, - xfail_scylla_version_lt) + xfail_scylla_version_lt, scylla_only) def setup_module(): @@ -313,6 +313,7 @@ def test_the_reported_configuration_survives_the_round_trip(self): finally: cluster.shutdown() + @scylla_only def test_the_server_side_timeout_is_reported_against_scylla(self): """ USING TIMEOUT is a ScyllaDB extension, so this key is reported only on a diff --git a/tests/integration/standard/test_exit_without_shutdown.py b/tests/integration/standard/test_exit_without_shutdown.py index eecf554373..be31271f02 100644 --- a/tests/integration/standard/test_exit_without_shutdown.py +++ b/tests/integration/standard/test_exit_without_shutdown.py @@ -17,7 +17,10 @@ import sys from tests.integration import use_cluster, get_cluster, get_node, start_cluster_wait_for_up, \ - libevtest, PROTOCOL_VERSION, CASSANDRA_IP + libevtest, PROTOCOL_VERSION, CASSANDRA_IP, scylla_only + +# the many-shard node comes from ScyllaNode.set_smp(); ccm's Cassandra nodes have no shards +pytestmark = scylla_only # GH-1133: `cqlsh -e` drops its Session without shutdown(). When a GC pass # collects it, Session.__del__ closes all its connections on whatever thread diff --git a/tests/integration/standard/test_ip_change.py b/tests/integration/standard/test_ip_change.py index 53debfa1f5..d303628169 100644 --- a/tests/integration/standard/test_ip_change.py +++ b/tests/integration/standard/test_ip_change.py @@ -5,7 +5,7 @@ from cassandra.cluster import ExecutionProfile from cassandra.policies import WhiteListRoundRobinPolicy -from tests.integration import use_cluster, get_node, get_cluster, local, TestCluster +from tests.integration import use_cluster, get_node, get_cluster, local, TestCluster, SCYLLA_VERSION from tests.util import wait_until_not_raised LOGGER = logging.getLogger(__name__) @@ -46,7 +46,10 @@ def test_change_address_during_live_session(self): LOGGER.debug("Change IP address for node3") ip_prefix = get_cluster().get_ipprefix() new_ip = f'{ip_prefix}33' - node3.set_configuration_options(values={'listen_address': new_ip, 'rpc_address': new_ip, 'api_address': new_ip}) + new_addresses = {'listen_address': new_ip, 'rpc_address': new_ip} + if SCYLLA_VERSION: + new_addresses['api_address'] = new_ip + node3.set_configuration_options(values=new_addresses) node3.network_interfaces = {k: (new_ip, v[1]) for k, v in node3.network_interfaces.items()} LOGGER.debug(f"Start node3 again with ip address {new_ip}") node3.start(wait_for_binary_proto=True) diff --git a/tests/integration/standard/test_metadata.py b/tests/integration/standard/test_metadata.py index 9b014d6112..b96d7a2208 100644 --- a/tests/integration/standard/test_metadata.py +++ b/tests/integration/standard/test_metadata.py @@ -37,7 +37,8 @@ get_supported_protocol_versions, greaterthancass20, greaterthancass21, lessthancass40, TestCluster, requires_java_udf, requires_composite_type, - requires_collection_indexes, SCYLLA_VERSION, xfail_scylla, requirescompactstorage, get_tablets_disabled_ddl_suffix, execute_with_long_wait_retry) + requires_collection_indexes, SCYLLA_VERSION, xfail_scylla, requirescompactstorage, get_tablets_disabled_ddl_suffix, execute_with_long_wait_retry, + scylla_only, xfail_cassandra_local_ports) from tests.util import wait_until, assertRegex, assertDictEqual, assertListEqual, assert_startswith_diff @@ -50,6 +51,7 @@ def setup_module(): class HostMetaDataTests(BasicExistingKeyspaceUnitTestCase): @local + @xfail_cassandra_local_ports def test_host_addresses(self): """ Check to ensure that the broadcast_address, broadcast_rpc_address, @@ -1267,7 +1269,6 @@ def test_already_exists_exceptions(self): cluster.shutdown() @local - @pytest.mark.xfail(reason='AssertionError: \'RAC1\' != \'r1\' - probably a bug in driver or in Scylla') def test_replicas(self): """ Ensure cluster.metadata.get_replicas return correctly when not attached to keyspace @@ -1283,7 +1284,7 @@ def test_replicas(self): assert list(cluster.metadata.get_replicas('test3rf', b'key')) != [] host = list(cluster.metadata.get_replicas('test3rf', b'key'))[0] assert host.datacenter == 'dc1' - assert host.rack == 'r1' + assert host.rack == ('RAC1' if SCYLLA_VERSION else 'r1') cluster.shutdown() def test_token_map(self): @@ -1323,6 +1324,8 @@ def test_token(self): cluster.shutdown() +# metadata timeout (USING TIMEOUT) is Scylla only +@scylla_only class TestMetadataTimeout: """ Test of TokenMap creation and other behavior. diff --git a/tests/integration/standard/test_prepared_statements.py b/tests/integration/standard/test_prepared_statements.py index 009cd837f1..e7f3fb8b65 100644 --- a/tests/integration/standard/test_prepared_statements.py +++ b/tests/integration/standard/test_prepared_statements.py @@ -24,7 +24,7 @@ from cassandra import ConsistencyLevel, ProtocolVersion from cassandra.query import PreparedStatement, UNSET_VALUE from tests.integration import (get_server_versions, greaterthanorequalcass40, - BasicSharedKeyspaceUnitTestCase) + BasicSharedKeyspaceUnitTestCase, scylla_only) import logging import pytest @@ -406,6 +406,7 @@ def test_raise_error_on_prepared_statement_execution_dropped_table(self): with pytest.raises(InvalidRequest): self.session.execute(prepared, [0]) + @scylla_only # LWT flag in PREPARED metadata is a Scylla protocol extension def test_recognize_lwt_query(self): self.session.execute("CREATE TABLE IF NOT EXISTS preparedtests.bound_statement_test (a int PRIMARY KEY, b int)") # Prepare a non-LWT statement diff --git a/tests/integration/standard/test_query.py b/tests/integration/standard/test_query.py index 42b8173f0a..68bf6f2815 100644 --- a/tests/integration/standard/test_query.py +++ b/tests/integration/standard/test_query.py @@ -1020,6 +1020,9 @@ def test_no_connection_refused_on_timeout(self): # actually returned a WriteTimeout/WriteFailure. received_client_timeout = True continue + if exception_type == "ErrorMessage" and result.code == 0x1700: + # Cassandra 4.0+ CAS_WRITE_UNKNOWN: "CAS operation result is unknown" + continue pytest.fail("Unexpected exception %s: %s" % (exception_type, result)) diff --git a/tests/integration/standard/test_rack_aware_policy.py b/tests/integration/standard/test_rack_aware_policy.py index d2a358373d..f7615b609e 100644 --- a/tests/integration/standard/test_rack_aware_policy.py +++ b/tests/integration/standard/test_rack_aware_policy.py @@ -4,13 +4,16 @@ from cassandra.cluster import Cluster from cassandra.policies import ConstantReconnectionPolicy, RackAwareRoundRobinPolicy -from tests.integration import PROTOCOL_VERSION, get_cluster, use_multidc +from tests.integration import PROTOCOL_VERSION, get_cluster, use_multidc, scylla_only LOGGER = logging.getLogger(__name__) def setup_module(): use_multidc({'DC1': {'RC1': 2, 'RC2': 2}, 'DC2': {'RC1': 3}}) +# Cassandra fails to start this topology: "Token allocation failed: the number of racks 2 in datacenter DC1 +# is lower than its replication factor 3" +@scylla_only class RackAwareRoundRobinPolicyTests(unittest.TestCase): @classmethod def setup_class(cls): diff --git a/tests/integration/standard/test_rate_limit_exceeded.py b/tests/integration/standard/test_rate_limit_exceeded.py index 5a7fc5dc74..ad4891a49d 100644 --- a/tests/integration/standard/test_rate_limit_exceeded.py +++ b/tests/integration/standard/test_rate_limit_exceeded.py @@ -4,7 +4,7 @@ from cassandra.cluster import Cluster from cassandra.policies import ConstantReconnectionPolicy, RoundRobinPolicy, TokenAwarePolicy -from tests.integration import PROTOCOL_VERSION, use_singledc +from tests.integration import PROTOCOL_VERSION, use_singledc, scylla_only import pytest LOGGER = logging.getLogger(__name__) @@ -12,6 +12,7 @@ def setup_module(): use_singledc() +@scylla_only class TestRateLimitExceededException(unittest.TestCase): @classmethod def setup_class(cls): diff --git a/tests/integration/standard/test_shard_aware.py b/tests/integration/standard/test_shard_aware.py index 2fc36af577..c26ede9c26 100644 --- a/tests/integration/standard/test_shard_aware.py +++ b/tests/integration/standard/test_shard_aware.py @@ -25,7 +25,7 @@ from cassandra.policies import TokenAwarePolicy, RoundRobinPolicy, ConstantReconnectionPolicy from cassandra import OperationTimedOut -from tests.integration import use_cluster, get_node, PROTOCOL_VERSION +from tests.integration import use_cluster, get_node, PROTOCOL_VERSION, scylla_only from tests.util import wait_until_not_raised LOGGER = logging.getLogger(__name__) @@ -48,6 +48,7 @@ def teardown_module(): os.environ['SCYLLA_EXT_OPTS'] = _saved_scylla_ext_opts +@scylla_only class TestShardAwareIntegration(unittest.TestCase): @classmethod def setup_class(cls): diff --git a/tests/integration/standard/test_single_interface.py b/tests/integration/standard/test_single_interface.py index ad31821a7e..d82ef791da 100644 --- a/tests/integration/standard/test_single_interface.py +++ b/tests/integration/standard/test_single_interface.py @@ -14,12 +14,18 @@ import unittest +import pytest + from cassandra import ConsistencyLevel from cassandra.query import SimpleStatement from packaging.version import Version from tests.integration import use_singledc, remove_cluster, greaterthanorequalcass40, \ - CASSANDRA_VERSION, TestCluster, DEFAULT_SINGLE_INTERFACE_PORT + CASSANDRA_VERSION, TestCluster, DEFAULT_SINGLE_INTERFACE_PORT, SCYLLA_VERSION + +# Only runs on Cassandra 4.0+ anyway. use_cluster() never passes use_single_interface to CCM +# (scylla-ccm doesn't support it), so the nodes don't listen on DEFAULT_SINGLE_INTERFACE_PORT. +pytestmark = pytest.mark.skipif(SCYLLA_VERSION is None, reason='CCM single-interface clusters are not supported') def setup_module(): diff --git a/tests/integration/standard/test_tablets.py b/tests/integration/standard/test_tablets.py index 0491b15f3f..5bff43a5b1 100644 --- a/tests/integration/standard/test_tablets.py +++ b/tests/integration/standard/test_tablets.py @@ -3,7 +3,7 @@ from cassandra.cluster import Cluster, EXEC_PROFILE_DEFAULT, ExecutionProfile from cassandra.policies import ConstantReconnectionPolicy, RoundRobinPolicy, TokenAwarePolicy -from tests.integration import PROTOCOL_VERSION, use_cluster, get_cluster +from tests.integration import PROTOCOL_VERSION, use_cluster, get_cluster, scylla_only from tests.util import wait_until from tests.unit.test_host_connection_pool import LOGGER @@ -12,6 +12,7 @@ def setup_module(): use_cluster('tablets', [3], start=True, set_keyspace=False) +@scylla_only class TestTabletsIntegration: @classmethod def setup_class(cls): diff --git a/tests/integration/standard/test_types.py b/tests/integration/standard/test_types.py index 5d33f9df67..1325c95b31 100644 --- a/tests/integration/standard/test_types.py +++ b/tests/integration/standard/test_types.py @@ -39,7 +39,7 @@ from tests.integration import use_single_node, execute_until_pass, notprotocolv1, \ BasicSharedKeyspaceUnitTestCase, greaterthancass21, lessthancass30, \ greaterthanorequalcass3_10, TestCluster, requires_composite_type, \ - requires_vector_type + requires_vector_type, scylla_only from tests.integration.datatype_utils import update_datatypes, PRIMITIVE_DATATYPES, COLLECTION_TYPES, PRIMITIVE_DATATYPES_KEYS, \ get_sample, get_all_samples, get_collection_sample import pytest @@ -715,6 +715,7 @@ def test_can_insert_tuples_with_nulls(self): assert ('', None, None, b'') == result.one().t assert ('', None, None, b'') == s.execute(read).one().t + @scylla_only # Cassandra 5.0 answers with a NullPointerException server error, not InvalidRequest def test_insert_collection_with_null_fails(self): """ NULLs in list / sets / maps are forbidden. diff --git a/tests/integration/standard/test_use_keyspace.py b/tests/integration/standard/test_use_keyspace.py index bf7491dd50..d24b6eb79d 100644 --- a/tests/integration/standard/test_use_keyspace.py +++ b/tests/integration/standard/test_use_keyspace.py @@ -12,7 +12,7 @@ from cassandra.pool import HostConnection from cassandra.policies import TokenAwarePolicy, RoundRobinPolicy, ConstantReconnectionPolicy -from tests.integration import use_cluster, PROTOCOL_VERSION, local +from tests.integration import use_cluster, PROTOCOL_VERSION, local, scylla_only LOGGER = logging.getLogger(__name__) @@ -75,6 +75,7 @@ def patched_set_keyspace_blocking(*args, **kwargs): for i in range(200): session2.execute(f"SELECT * FROM set_keyspace_slow_connection WHERE pk = 1") + @scylla_only def test_use_during_background_shard_connection_open(self): self.session.execute("CREATE KEYSPACE IF NOT EXISTS use_race_old WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': 1}") self.session.execute("CREATE KEYSPACE IF NOT EXISTS use_race_new WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': 1}") From 5de98dab089d5f4230e0180ace4b1e24333a78f3 Mon Sep 17 00:00:00 2001 From: Israel Fruchter Date: Thu, 15 Oct 2026 10:54:44 +0300 Subject: [PATCH 3/5] tests: wait for the host to be up in test_reprepare_after_host_is_down The test waited only for the "Preparing all known prepared statements" log line. That's logged while the pool opens, before the host is marked up. With a slow event loop (asyncio on Cassandra in CI), the query then ran while 127.0.0.1, the only host in the whitelist profile, was still down, and failed with NoHostAvailable. Also wait for the host to be up. Co-Authored-By: Claude Opus 5.5 --- tests/integration/standard/test_query.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration/standard/test_query.py b/tests/integration/standard/test_query.py index 68bf6f2815..27ce362d55 100644 --- a/tests/integration/standard/test_query.py +++ b/tests/integration/standard/test_query.py @@ -1684,6 +1684,8 @@ def test_reprepare_after_host_is_down(self): wait_until( lambda: mock_handler.get_message_count('debug', 'Preparing all known prepared statements') >= 1, delay=0.5, max_attempts=20) + # statements are prepared while the pool opens, before the host is marked up + wait_until(lambda: self.cluster.metadata.get_host('127.0.0.1').is_up, delay=0.5, max_attempts=20) results = self.session.execute(prepared_statement, (1,), execution_profile="only_first") assert results.one() == (1, ) From 9ff3c90f4f77000fceb0ae1aeb1bdd99e4ba23e7 Mon Sep 17 00:00:00 2001 From: Israel Fruchter Date: Tue, 20 Oct 2026 14:58:30 +0300 Subject: [PATCH 4/5] tests: let test_connection_error wait for the restarted cluster test_connection_error restarts the whole cluster and then gave the session 15 seconds to reach 127.0.0.2 again. Cassandra 5.0 takes longer than that to open its native port on CI, and the default exponential reconnection backoff had grown by then (retries at +2, +4, +8, +14 s, then +28 s), so the next reconnect landed after the wait ended and the test failed with NoHostAvailable. It failed 2 of 50 local runs. Use a 1 s ConstantReconnectionPolicy for the metrics tests and wait up to 60 s. 0 of 50 local runs failed after the change. Co-Authored-By: Claude Opus 5.5 --- tests/integration/standard/test_metrics.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/integration/standard/test_metrics.py b/tests/integration/standard/test_metrics.py index 20bd92b87c..e588980233 100644 --- a/tests/integration/standard/test_metrics.py +++ b/tests/integration/standard/test_metrics.py @@ -15,7 +15,7 @@ import time from cassandra.connection import ConnectionShutdown -from cassandra.policies import HostFilterPolicy, RoundRobinPolicy, FallthroughRetryPolicy +from cassandra.policies import HostFilterPolicy, RoundRobinPolicy, FallthroughRetryPolicy, ConstantReconnectionPolicy import unittest @@ -41,7 +41,10 @@ class MetricsTests(unittest.TestCase): def setUp(self): contact_point = ['127.0.0.2'] + # constant reconnects: test_connection_error restarts the whole cluster, and the default + # exponential backoff can push the next reconnect past its wait for the cluster self.cluster = TestCluster(contact_points=contact_point, metrics_enabled=True, + reconnection_policy=ConstantReconnectionPolicy(1), execution_profiles= {EXEC_PROFILE_DEFAULT: ExecutionProfile( @@ -79,7 +82,7 @@ def test_connection_error(self): # Wait for the cluster to come back up for the next test wait_until_not_raised( lambda: self.session.execute("SELECT key FROM system.local WHERE key='local'"), - delay=0.5, max_attempts=30) + delay=0.5, max_attempts=120) assert self.cluster.metrics.stats.connection_errors > 0 From c2c4e5562c5a41dc0dcb010dbd563cfa4a4e2b89 Mon Sep 17 00:00:00 2001 From: Israel Fruchter Date: Tue, 20 Oct 2026 14:58:31 +0300 Subject: [PATCH 5/5] ci: run the integration tests against the latest Cassandra release Add a tests-cassandra job (Python 3.15, libev and asyncio) that runs tests/integration/standard/ against the newest X.Y.Z tag of apache/cassandra, found with git ls-remote. It uses JDK 11, which Cassandra 5.0 needs, and caches the CCM download per version. PROTOCOL_VERSION is left unset, so the harness default (v5 on Cassandra 4.0+) is used both by the test gates and by TestCluster. The Scylla jobs run v4, so this is the only job that covers v5. Also limit the workflow token to contents: read and don't persist the checkout credentials. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/integration-tests.yml | 56 +++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index cd5c97eda4..398075278b 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -1,5 +1,8 @@ name: Integration tests +permissions: + contents: read + on: push: branches: @@ -94,3 +97,56 @@ jobs: export PYTHON_GIL=0 fi uv run pytest -v tests/integration/standard/ tests/integration/cqlengine/ + + tests-cassandra: + name: test cassandra ${{ matrix.event_loop_manager }} (${{ matrix.python-version }}) + if: "!contains(github.event.pull_request.labels.*.name, 'disable-integration-tests')" + runs-on: ubuntu-24.04 + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + python-version: ["3.15"] + event_loop_manager: ["libev", "asyncio"] + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + # Cassandra 5.0 needs JDK 11+ + - name: Set up JDK 11 + uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 + with: + java-version: 11 + distribution: temurin + + - name: Install libev + run: sudo apt-get install libev4 libev-dev + + - name: Install uv + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true + + - name: Build driver + run: uv sync + + - name: Get latest Cassandra version + run: | + version=$(git ls-remote --tags --refs https://github.com/apache/cassandra 'cassandra-*' \ + | sed 's|.*refs/tags/cassandra-||' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1) + echo "CASSANDRA_VERSION=$version" >> "$GITHUB_ENV" + + - name: Cache Cassandra download + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.ccm/repository + key: cassandra-${{ env.CASSANDRA_VERSION }}-${{ runner.os }} + + # no PROTOCOL_VERSION: use the harness default, v5 on Cassandra 4.0+, the only job covering v5 + - name: Test with pytest + env: + EVENT_LOOP_MANAGER: ${{ matrix.event_loop_manager }} + run: uv run pytest -v tests/integration/standard/