diff --git a/.flake8 b/.flake8 index d2ee181447..b1bd1d0b75 100644 --- a/.flake8 +++ b/.flake8 @@ -16,6 +16,7 @@ exclude = ignore = E126 E203 + E231 E701 E704 F405 diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index a619275b47..94fe8f35b6 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -25,6 +25,8 @@ permissions: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + # this speeds up coverage with Python 3.12: https://github.com/nedbat/coveragepy/issues/1665 + COVERAGE_CORE: sysmon REDIS_IMAGE: redis:7.4-rc2 REDIS_STACK_IMAGE: redis/redis-stack-server:7.4.0-rc2 @@ -54,26 +56,28 @@ jobs: pip install -r dev_requirements.txt invoke linters - run-tests: + resp2-tests: runs-on: ubuntu-latest timeout-minutes: 60 strategy: max-parallel: 15 fail-fast: false matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', 'pypy-3.8', 'pypy-3.9'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', 'pypy-3.8', 'pypy-3.9'] test-type: ['standalone', 'cluster'] connection-type: ['hiredis', 'plain'] env: ACTIONS_ALLOW_UNSECURE_COMMANDS: true - name: Python ${{ matrix.python-version }} ${{matrix.test-type}}-${{matrix.connection-type}} tests + name: RESP2 ${{ matrix.python-version }} ${{matrix.test-type}}-${{matrix.connection-type}} steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' - - name: run tests + + - name: Run tests run: | pip install -U setuptools wheel pip install -r requirements.txt @@ -84,52 +88,48 @@ jobs: invoke devenv sleep 10 # time to settle invoke ${{matrix.test-type}}-tests + ls -1 - - uses: actions/upload-artifact@v4 - if: success() || failure() + - name: Upload test results and profiling data + uses: actions/upload-artifact@v4 with: name: pytest-results-${{matrix.test-type}}-${{matrix.connection-type}}-${{matrix.python-version}} - path: '${{matrix.test-type}}*results.xml' + path: | + ${{matrix.test-type}}*-results.xml + prof/** + profile_output* + if-no-files-found: error + retention-days: 10 - name: Upload codecov coverage uses: codecov/codecov-action@v4 with: fail_ci_if_error: false - - name: View Test Results - uses: dorny/test-reporter@v1 - if: success() || failure() - continue-on-error: true - with: - name: Test Results ${{matrix.python-version}} ${{matrix.test-type}}-${{matrix.connection-type}} - path: '*.xml' - reporter: java-junit - list-suites: all - list-tests: all - max-annotations: 10 - fail-on-error: 'false' - - resp3_tests: + resp3-tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: ['3.8', '3.11'] + python-version: ['3.8', '3.12'] test-type: ['standalone', 'cluster'] connection-type: ['hiredis', 'plain'] + event-loop: ['asyncio', 'uvloop'] exclude: - test-type: 'cluster' connection-type: 'hiredis' env: ACTIONS_ALLOW_UNSECURE_COMMANDS: true - name: RESP3 [${{ matrix.python-version }} ${{matrix.test-type}}-${{matrix.connection-type}}] + name: RESP3 ${{ matrix.python-version }} ${{matrix.test-type}}-${{matrix.connection-type}}-${{matrix.event-loop}} steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' - - name: run tests + + - name: Run tests run: | pip install -U setuptools wheel pip install -r requirements.txt @@ -139,37 +139,32 @@ jobs: fi invoke devenv sleep 10 # time to settle - invoke ${{matrix.test-type}}-tests --protocol=3 - invoke ${{matrix.test-type}}-tests --uvloop --protocol=3 + if [ "${{matrix.event-loop}}" == "uvloop" ]; then + invoke ${{matrix.test-type}}-tests --uvloop --protocol=3 + else + invoke ${{matrix.test-type}}-tests --protocol=3 + fi - - uses: actions/upload-artifact@v4 - if: success() || failure() + - name: Upload test results and profiling data + uses: actions/upload-artifact@v4 with: - name: pytest-results-${{matrix.test-type}}-${{matrix.connection-type}}-${{matrix.python-version}}-resp3 - path: '${{matrix.test-type}}*results.xml' + name: pytest-results-${{matrix.test-type}}-${{matrix.connection-type}}-${{matrix.python-version}}-${{matrix.event-loop}}-resp3 + path: | + ${{matrix.test-type}}*-results.xml + prof/** + profile_output* + if-no-files-found: error + retention-days: 10 - name: Upload codecov coverage uses: codecov/codecov-action@v4 with: fail_ci_if_error: false - - name: View Test Results - uses: dorny/test-reporter@v1 - if: success() || failure() - continue-on-error: true - with: - name: Test Results ${{matrix.python-version}} ${{matrix.test-type}}-${{matrix.connection-type}}-resp3 - path: '*.xml' - reporter: java-junit - list-suites: all - list-tests: all - max-annotations: 10 - fail-on-error: 'false' - - build_and_test_package: + build-and-test-package: name: Validate building and installing the package runs-on: ubuntu-latest - needs: [run-tests] + needs: [resp2-tests, resp3-tests] strategy: fail-fast: false matrix: @@ -183,13 +178,13 @@ jobs: run: | bash .github/workflows/install_and_test.sh ${{ matrix.extension }} - install_package_from_commit: + install-package-from-commit: name: Install package from commit hash runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', 'pypy-3.8', 'pypy-3.9'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', 'pypy-3.8', 'pypy-3.9'] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 diff --git a/.gitignore b/.gitignore index 3baa34034f..c6a4efd1ec 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ coverage.xml .venv* *.xml .coverage* +prof +profile_output* docker/stunnel/keys diff --git a/dev_requirements.txt b/dev_requirements.txt index b1e10f9682..3b0ea8fa03 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -4,15 +4,15 @@ click==8.0.4 flake8-isort==6.0.0 flake8==5.0.4 flynt~=0.69.0 -invoke==1.7.3 -mock==4.0.3 +invoke==2.2.0 +mock packaging>=20.4 pytest pytest-asyncio pytest-cov +pytest-profiling pytest-timeout ujson>=4.2.0 -urllib3<2 uvloop vulture>=2.3.0 wheel>=0.30.0 diff --git a/redis/asyncio/cluster.py b/redis/asyncio/cluster.py index 719c2d2283..40b2948a7f 100644 --- a/redis/asyncio/cluster.py +++ b/redis/asyncio/cluster.py @@ -1317,6 +1317,8 @@ async def initialize(self) -> None: port = int(primary_node[1]) host, port = self.remap_host_port(host, port) + nodes_for_slot = [] + target_node = tmp_nodes_cache.get(get_node_name(host, port)) if not target_node: target_node = ClusterNode( @@ -1324,30 +1326,26 @@ async def initialize(self) -> None: ) # add this node to the nodes cache tmp_nodes_cache[target_node.name] = target_node + nodes_for_slot.append(target_node) + + replica_nodes = slot[3:] + for replica_node in replica_nodes: + host = replica_node[0] + port = replica_node[1] + host, port = self.remap_host_port(host, port) + + target_replica_node = tmp_nodes_cache.get(get_node_name(host, port)) + if not target_replica_node: + target_replica_node = ClusterNode( + host, port, REPLICA, **self.connection_kwargs + ) + # add this node to the nodes cache + tmp_nodes_cache[target_replica_node.name] = target_replica_node + nodes_for_slot.append(target_replica_node) for i in range(int(slot[0]), int(slot[1]) + 1): if i not in tmp_slots: - tmp_slots[i] = [] - tmp_slots[i].append(target_node) - replica_nodes = [slot[j] for j in range(3, len(slot))] - - for replica_node in replica_nodes: - host = replica_node[0] - port = replica_node[1] - host, port = self.remap_host_port(host, port) - - target_replica_node = tmp_nodes_cache.get( - get_node_name(host, port) - ) - if not target_replica_node: - target_replica_node = ClusterNode( - host, port, REPLICA, **self.connection_kwargs - ) - tmp_slots[i].append(target_replica_node) - # add this node to the nodes cache - tmp_nodes_cache[target_replica_node.name] = ( - target_replica_node - ) + tmp_slots[i] = nodes_for_slot else: # Validate that 2 nodes want to use the same slot cache # setup diff --git a/redis/asyncio/connection.py b/redis/asyncio/connection.py index ec1ce5a915..2ac6637986 100644 --- a/redis/asyncio/connection.py +++ b/redis/asyncio/connection.py @@ -1000,7 +1000,7 @@ def parse_url(url: str) -> ConnectKwargs: try: kwargs[name] = parser(value) except (TypeError, ValueError): - raise ValueError(f"Invalid value for `{name}` in connection URL.") + raise ValueError(f"Invalid value for '{name}' in connection URL.") else: kwargs[name] = value diff --git a/redis/cluster.py b/redis/cluster.py index 144844ec8a..be7685e9a1 100644 --- a/redis/cluster.py +++ b/redis/cluster.py @@ -1522,6 +1522,8 @@ def _get_or_create_cluster_node(self, host, port, role, tmp_nodes_cache): target_node = ClusterNode(host, port, role) if target_node.server_type != role: target_node.server_type = role + # add this node to the nodes cache + tmp_nodes_cache[target_node.name] = target_node return target_node @@ -1585,31 +1587,26 @@ def initialize(self): port = int(primary_node[1]) host, port = self.remap_host_port(host, port) + nodes_for_slot = [] + target_node = self._get_or_create_cluster_node( host, port, PRIMARY, tmp_nodes_cache ) - # add this node to the nodes cache - tmp_nodes_cache[target_node.name] = target_node + nodes_for_slot.append(target_node) + + replica_nodes = slot[3:] + for replica_node in replica_nodes: + host = str_if_bytes(replica_node[0]) + port = int(replica_node[1]) + host, port = self.remap_host_port(host, port) + target_replica_node = self._get_or_create_cluster_node( + host, port, REPLICA, tmp_nodes_cache + ) + nodes_for_slot.append(target_replica_node) for i in range(int(slot[0]), int(slot[1]) + 1): if i not in tmp_slots: - tmp_slots[i] = [] - tmp_slots[i].append(target_node) - replica_nodes = [slot[j] for j in range(3, len(slot))] - - for replica_node in replica_nodes: - host = str_if_bytes(replica_node[0]) - port = replica_node[1] - host, port = self.remap_host_port(host, port) - - target_replica_node = self._get_or_create_cluster_node( - host, port, REPLICA, tmp_nodes_cache - ) - tmp_slots[i].append(target_replica_node) - # add this node to the nodes cache - tmp_nodes_cache[target_replica_node.name] = ( - target_replica_node - ) + tmp_slots[i] = nodes_for_slot else: # Validate that 2 nodes want to use the same slot cache # setup diff --git a/redis/commands/graph/query_result.py b/redis/commands/graph/query_result.py index 7c7f58b99f..7709081bcf 100644 --- a/redis/commands/graph/query_result.py +++ b/redis/commands/graph/query_result.py @@ -1,6 +1,5 @@ import sys from collections import OrderedDict -from distutils.util import strtobool # from prettytable import PrettyTable from redis import ResponseError @@ -571,3 +570,19 @@ async def parse_array(self, value): """ scalar = [await self.parse_scalar(value[i]) for i in range(len(value))] return scalar + + +def strtobool(val): + """ + Convert a string representation of truth to true (1) or false (0). + True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values + are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if + 'val' is anything else. + """ + val = val.lower() + if val in ("y", "yes", "t", "true", "on", "1"): + return True + elif val in ("n", "no", "f", "false", "off", "0"): + return False + else: + raise ValueError(f"invalid truth value {val!r}") diff --git a/redis/connection.py b/redis/connection.py index 6e3b3ab081..19263376d2 100644 --- a/redis/connection.py +++ b/redis/connection.py @@ -813,8 +813,26 @@ def __init__( super().__init__(**kwargs) def _connect(self): - "Wrap the socket with SSL support" + """ + Wrap the socket with SSL support, handling potential errors. + """ sock = super()._connect() + try: + return self._wrap_socket_with_ssl(sock) + except OSError: + sock.close() + raise + + def _wrap_socket_with_ssl(self, sock): + """ + Wraps the socket with SSL support. + + Args: + sock: The plain socket to wrap with SSL. + + Returns: + An SSL wrapped socket. + """ context = ssl.create_default_context() context.check_hostname = self.check_hostname context.verify_mode = self.cert_reqs @@ -957,7 +975,7 @@ def parse_url(url): try: kwargs[name] = parser(value) except (TypeError, ValueError): - raise ValueError(f"Invalid value for `{name}` in connection URL.") + raise ValueError(f"Invalid value for '{name}' in connection URL.") else: kwargs[name] = value diff --git a/requirements.txt b/requirements.txt index a716b84463..3274a80f62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -async-timeout>=4.0.2 +async-timeout>=4.0.3 diff --git a/setup.py b/setup.py index ab1cb9631f..39cd40b807 100644 --- a/setup.py +++ b/setup.py @@ -51,11 +51,12 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ], extras_require={ "hiredis": ["hiredis>=1.0.0"], - "ocsp": ["cryptography>=36.0.1", "pyopenssl==20.0.1", "requests>=2.26.0"], + "ocsp": ["cryptography>=36.0.1", "pyopenssl==23.2.1", "requests>=2.31.0"], }, ) diff --git a/tasks.py b/tasks.py index 7f26081150..79b27852e3 100644 --- a/tasks.py +++ b/tasks.py @@ -42,39 +42,39 @@ def all_tests(c): @task -def tests(c, uvloop=False, protocol=2): - """Run the redis-py test suite against the current python, - with and without hiredis. - """ +def tests(c, uvloop=False, protocol=2, profile=False): + """Run the redis-py test suite against the current python.""" print("Starting Redis tests") - standalone_tests(c, uvloop=uvloop, protocol=protocol) - cluster_tests(c, uvloop=uvloop, protocol=protocol) + standalone_tests(c, uvloop=uvloop, protocol=protocol, profile=profile) + cluster_tests(c, uvloop=uvloop, protocol=protocol, profile=profile) @task -def standalone_tests(c, uvloop=False, protocol=2): +def standalone_tests(c, uvloop=False, protocol=2, profile=False): """Run tests against a standalone redis instance""" + profile_arg = "--profile" if profile else "" if uvloop: run( - f"pytest --protocol={protocol} --cov=./ --cov-report=xml:coverage_redis.xml -W always -m 'not onlycluster' --uvloop --junit-xml=standalone-uvloop-results.xml" + f"pytest {profile_arg} --protocol={protocol} --cov=./ --cov-report=xml:coverage_redis.xml -W always -m 'not onlycluster' --uvloop --junit-xml=standalone-uvloop-results.xml" ) else: run( - f"pytest --protocol={protocol} --cov=./ --cov-report=xml:coverage_redis.xml -W always -m 'not onlycluster' --junit-xml=standalone-results.xml" + f"pytest {profile_arg} --protocol={protocol} --cov=./ --cov-report=xml:coverage_redis.xml -W always -m 'not onlycluster' --junit-xml=standalone-results.xml" ) @task -def cluster_tests(c, uvloop=False, protocol=2): +def cluster_tests(c, uvloop=False, protocol=2, profile=False): """Run tests against a redis cluster""" + profile_arg = "--profile" if profile else "" cluster_url = "redis://localhost:16379/0" if uvloop: run( - f"pytest --protocol={protocol} --cov=./ --cov-report=xml:coverage_cluster.xml -W always -m 'not onlynoncluster and not redismod' --redis-url={cluster_url} --junit-xml=cluster-uvloop-results.xml --uvloop" + f"pytest {profile_arg} --protocol={protocol} --cov=./ --cov-report=xml:coverage_cluster.xml -W always -m 'not onlynoncluster and not redismod' --redis-url={cluster_url} --junit-xml=cluster-uvloop-results.xml --uvloop" ) else: run( - f"pytest --protocol={protocol} --cov=./ --cov-report=xml:coverage_clusteclient.xml -W always -m 'not onlynoncluster and not redismod' --redis-url={cluster_url} --junit-xml=cluster-results.xml" + f"pytest {profile_arg} --protocol={protocol} --cov=./ --cov-report=xml:coverage_clusteclient.xml -W always -m 'not onlynoncluster and not redismod' --redis-url={cluster_url} --junit-xml=cluster-results.xml" ) diff --git a/tests/conftest.py b/tests/conftest.py index e3326f85d5..dd78bb6a2c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -72,6 +72,21 @@ def format_usage(self): return " | ".join(self.option_strings) +@pytest.fixture(scope="session", autouse=True) +def enable_tracemalloc(): + """ + Enable tracemalloc while tests are being executed. + """ + try: + import tracemalloc + + tracemalloc.start() + yield + tracemalloc.stop() + except ImportError: + yield + + def pytest_addoption(parser): parser.addoption( "--redis-url", @@ -246,7 +261,9 @@ def skip_ifmodversion_lt(min_version: str, module_name: str): for j in modules: if module_name == j.get("name"): version = j.get("ver") - mv = int(min_version.replace(".", "")) + mv = int( + "".join(["%02d" % int(segment) for segment in min_version.split(".")]) + ) check = version < mv return pytest.mark.skipif(check, reason="Redis module version") diff --git a/tests/test_asyncio/test_cluster.py b/tests/test_asyncio/test_cluster.py index 80b0b1bff8..a36040f11b 100644 --- a/tests/test_asyncio/test_cluster.py +++ b/tests/test_asyncio/test_cluster.py @@ -58,7 +58,6 @@ class NodeProxy: def __init__(self, addr, redis_addr): self.addr = addr self.redis_addr = redis_addr - self.send_event = asyncio.Event() self.server = None self.task = None self.n_connections = 0 @@ -83,14 +82,20 @@ async def handle(self, reader, writer): await asyncio.gather(pipe1, pipe2) finally: redis_writer.close() + await self.redis_writer.wait_closed() + writer.close() + await writer.wait_closed() async def aclose(self): - self.task.cancel() try: - await self.task + self.task.cancel() + await asyncio.wait_for(self.task, timeout=1) + self.server.close() + await self.server.wait_closed() + except asyncio.TimeoutError: + pass except asyncio.CancelledError: pass - await self.server.wait_closed() async def pipe( self, diff --git a/tests/test_asyncio/test_connect.py b/tests/test_asyncio/test_connect.py index 0df7ebb43a..943540c885 100644 --- a/tests/test_asyncio/test_connect.py +++ b/tests/test_asyncio/test_connect.py @@ -1,5 +1,4 @@ import asyncio -import logging import re import socket import ssl @@ -14,9 +13,6 @@ from ..ssl_utils import get_ssl_filename -_logger = logging.getLogger(__name__) - - _CLIENT_NAME = "test-suite-client" _CMD_SEP = b"\r\n" _SUCCESS_RESP = b"+OK" + _CMD_SEP @@ -125,7 +121,7 @@ async def test_tcp_ssl_version_mismatch(tcp_address): tcp_address, certfile=certfile, keyfile=keyfile, - ssl_version=ssl.TLSVersion.TLSv1_2, + maximum_ssl_version=ssl.TLSVersion.TLSv1_2, ) await conn.disconnect() @@ -135,7 +131,8 @@ async def _assert_connect( server_address, certfile=None, keyfile=None, - ssl_version=None, + minimum_ssl_version=ssl.TLSVersion.TLSv1_2, + maximum_ssl_version=ssl.TLSVersion.TLSv1_3, ): stop_event = asyncio.Event() finished = asyncio.Event() @@ -153,9 +150,8 @@ async def _handler(reader, writer): elif certfile: host, port = server_address context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) - if ssl_version is not None: - context.minimum_version = ssl_version - context.maximum_version = ssl_version + context.minimum_version = minimum_ssl_version + context.maximum_version = maximum_ssl_version context.load_cert_chain(certfile=certfile, keyfile=keyfile) server = await asyncio.start_server(_handler, host=host, port=port, ssl=context) else: @@ -178,23 +174,18 @@ async def _handler(reader, writer): async def _redis_request_handler(reader, writer, stop_event): - buffer = b"" command = None command_ptr = None fragment_length = None - while not stop_event.is_set() or buffer: - _logger.info(str(stop_event.is_set())) - try: - buffer += await asyncio.wait_for(reader.read(1024), timeout=0.5) - except TimeoutError: - continue + while not stop_event.is_set(): + buffer = await reader.read(1024) if not buffer: - continue + break parts = re.split(_CMD_SEP, buffer) - buffer = parts[-1] - for fragment in parts[:-1]: + for fragment in parts: fragment = fragment.decode() - _logger.info("Command fragment: %s", fragment) + if not fragment: + continue if fragment.startswith("*") and command is None: command = [None for _ in range(int(fragment[1:]))] @@ -214,10 +205,7 @@ async def _redis_request_handler(reader, writer, stop_event): continue command = " ".join(command) - _logger.info("Command %s", command) resp = _SUPPORTED_CMDS.get(command, _ERROR_RESP) - _logger.info("Response from %s", resp) writer.write(resp) await writer.drain() command = None - _logger.info("Exit handler") diff --git a/tests/test_asyncio/test_cwe_404.py b/tests/test_asyncio/test_cwe_404.py index df46cabc43..e920a3fb98 100644 --- a/tests/test_asyncio/test_cwe_404.py +++ b/tests/test_asyncio/test_cwe_404.py @@ -261,4 +261,4 @@ async def doit(): await asyncio.gather(*[doit() for _ in range(10)]) finally: - await r.close() + await r.aclose() diff --git a/tests/test_asyncio/test_lock.py b/tests/test_asyncio/test_lock.py index c052eae2a0..9eaaed6920 100644 --- a/tests/test_asyncio/test_lock.py +++ b/tests/test_asyncio/test_lock.py @@ -104,16 +104,16 @@ async def test_blocking(self, r): lock_2 = self.get_lock(r, "foo") assert lock_2.blocking - async def test_blocking_timeout(self, r, event_loop): + async def test_blocking_timeout(self, r): lock1 = self.get_lock(r, "foo") assert await lock1.acquire(blocking=False) bt = 0.2 sleep = 0.05 lock2 = self.get_lock(r, "foo", sleep=sleep, blocking_timeout=bt) - start = event_loop.time() + start = asyncio.get_running_loop().time() assert not await lock2.acquire() # The elapsed duration should be less than the total blocking_timeout - assert bt >= (event_loop.time() - start) > bt - sleep + assert bt >= (asyncio.get_running_loop().time() - start) > bt - sleep await lock1.release() async def test_context_manager(self, r): diff --git a/tests/test_asyncio/test_search.py b/tests/test_asyncio/test_search.py index 4cc00c8e5f..9d27a8c7cc 100644 --- a/tests/test_asyncio/test_search.py +++ b/tests/test_asyncio/test_search.py @@ -1511,14 +1511,14 @@ async def test_withsuffixtrie(decoded_r: redis.Redis): # create withsuffixtrie index (text fields) assert await decoded_r.ft().create_index(TextField("t", withsuffixtrie=True)) - waitForIndex(decoded_r, getattr(decoded_r.ft(), "index_name", "idx")) + await waitForIndex(decoded_r, getattr(decoded_r.ft(), "index_name", "idx")) info = await decoded_r.ft().info() assert "WITHSUFFIXTRIE" in info["attributes"][0]["flags"] assert await decoded_r.ft().dropindex("idx") # create withsuffixtrie index (tag field) assert await decoded_r.ft().create_index(TagField("t", withsuffixtrie=True)) - waitForIndex(decoded_r, getattr(decoded_r.ft(), "index_name", "idx")) + await waitForIndex(decoded_r, getattr(decoded_r.ft(), "index_name", "idx")) info = await decoded_r.ft().info() assert "WITHSUFFIXTRIE" in info["attributes"][0]["flags"] diff --git a/tests/test_connect.py b/tests/test_connect.py index d7ca04b651..b11c4446e5 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1,4 +1,3 @@ -import logging import re import socket import socketserver @@ -7,13 +6,10 @@ import pytest from redis.connection import Connection, SSLConnection, UnixDomainSocketConnection -from redis.exceptions import ConnectionError +from redis.exceptions import RedisError from .ssl_utils import get_ssl_filename -_logger = logging.getLogger(__name__) - - _CLIENT_NAME = "test-suite-client" _CMD_SEP = b"\r\n" _SUCCESS_RESP = b"+OK" + _CMD_SEP @@ -126,16 +122,16 @@ def test_tcp_ssl_version_mismatch(tcp_address): port=port, client_name=_CLIENT_NAME, ssl_ca_certs=certfile, - socket_timeout=10, + socket_timeout=3, ssl_min_version=ssl.TLSVersion.TLSv1_3, ) - with pytest.raises(ConnectionError): + with pytest.raises(RedisError): _assert_connect( conn, tcp_address, certfile=certfile, keyfile=keyfile, - ssl_version=ssl.PROTOCOL_TLSv1_2, + maximum_ssl_version=ssl.TLSVersion.TLSv1_2, ) @@ -164,14 +160,16 @@ def __init__( *args, certfile=None, keyfile=None, - ssl_version=ssl.PROTOCOL_TLS, + minimum_ssl_version=ssl.TLSVersion.TLSv1_2, + maximum_ssl_version=ssl.TLSVersion.TLSv1_3, **kw, ) -> None: self._ready_event = threading.Event() self._stop_requested = False self._certfile = certfile self._keyfile = keyfile - self._ssl_version = ssl_version + self._minimum_ssl_version = minimum_ssl_version + self._maximum_ssl_version = maximum_ssl_version super().__init__(*args, **kw) def service_actions(self): @@ -191,13 +189,11 @@ def get_request(self): if self._certfile is None: return super().get_request() newsocket, fromaddr = self.socket.accept() - connstream = ssl.wrap_socket( - newsocket, - server_side=True, - certfile=self._certfile, - keyfile=self._keyfile, - ssl_version=self._ssl_version, - ) + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + context.load_cert_chain(certfile=self._certfile, keyfile=self._keyfile) + context.minimum_version = self._minimum_ssl_version + context.maximum_version = self._maximum_ssl_version + connstream = context.wrap_socket(newsocket, server_side=True) return connstream, fromaddr @@ -228,10 +224,10 @@ def is_serving(self): class _RedisRequestHandler(socketserver.StreamRequestHandler): def setup(self): - _logger.info("%s connected", self.client_address) + pass def finish(self): - _logger.info("%s disconnected", self.client_address) + pass def handle(self): buffer = b"" @@ -249,7 +245,6 @@ def handle(self): buffer = parts[-1] for fragment in parts[:-1]: fragment = fragment.decode() - _logger.info("Command fragment: %s", fragment) if fragment.startswith("*") and command is None: command = [None for _ in range(int(fragment[1:]))] @@ -269,9 +264,6 @@ def handle(self): continue command = " ".join(command) - _logger.info("Command %s", command) resp = _SUPPORTED_CMDS.get(command, _ERROR_RESP) - _logger.info("Response %s", resp) self.request.sendall(resp) command = None - _logger.info("Exit handler")