Skip to content

Commit 10257e9

Browse files
gerzsevladvildanov
authored andcommitted
Make sure the CI actually runs RESP3 tests (#3270)
The CI tests were not running with RESP3 protocol, it was just an illusion that they do. Fix this, and also preserve coverage and test artifacts from those runs too. Some issues have surfaced after the change. The most notable issue is a bug in hiredis-py, which prevents it from being used in cluster mode at least. Make sure cluster tests do not run with hiredis-py. Also make sure some specific unit tests do not run with hiredis-py. One other issue with hiredis-py is fixed in this commit. Use a sentinel object instance to signal lack of data in hiredis-py, instead of piggybacking of `False`, which can also be returned by parsing valid RESP payloads. Some of the unit tests, mostly for modules, were failing, they are now updated so that they pass. Remove async parser from test fixture params. Leave the decision for the async parser to be used in tests to be taken based on the availability of hiredis-py, and on the protocol that is set for the tests. Otherwise when hiredis-py is available we would also run the non-hiredis tests.
1 parent 182e0b8 commit 10257e9

File tree

9 files changed

+212
-196
lines changed

9 files changed

+212
-196
lines changed

.github/workflows/integration.yaml

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ jobs:
115115
python-version: ['3.8', '3.11']
116116
test-type: ['standalone', 'cluster']
117117
connection-type: ['hiredis', 'plain']
118-
protocol: ['3']
118+
exclude:
119+
- test-type: 'cluster'
120+
connection-type: 'hiredis'
119121
env:
120122
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
121123
name: RESP3 [${{ matrix.python-version }} ${{matrix.test-type}}-${{matrix.connection-type}}]
@@ -134,9 +136,33 @@ jobs:
134136
pip install hiredis
135137
fi
136138
invoke devenv
137-
sleep 5 # time to settle
138-
invoke ${{matrix.test-type}}-tests
139-
invoke ${{matrix.test-type}}-tests --uvloop
139+
sleep 10 # time to settle
140+
invoke ${{matrix.test-type}}-tests --protocol=3
141+
invoke ${{matrix.test-type}}-tests --uvloop --protocol=3
142+
143+
- uses: actions/upload-artifact@v4
144+
if: success() || failure()
145+
with:
146+
name: pytest-results-${{matrix.test-type}}-${{matrix.connection-type}}-${{matrix.python-version}}-resp3
147+
path: '${{matrix.test-type}}*results.xml'
148+
149+
- name: Upload codecov coverage
150+
uses: codecov/codecov-action@v4
151+
with:
152+
fail_ci_if_error: false
153+
154+
- name: View Test Results
155+
uses: dorny/test-reporter@v1
156+
if: success() || failure()
157+
continue-on-error: true
158+
with:
159+
name: Test Results ${{matrix.python-version}} ${{matrix.test-type}}-${{matrix.connection-type}}-resp3
160+
path: '*.xml'
161+
reporter: java-junit
162+
list-suites: all
163+
list-tests: all
164+
max-annotations: 10
165+
fail-on-error: 'false'
140166

141167
build_and_test_package:
142168
name: Validate building and installing the package

redis/_parsers/hiredis.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
SERVER_CLOSED_CONNECTION_ERROR,
2222
)
2323

24+
# Used to signal that hiredis-py does not have enough data to parse.
25+
# Using `False` or `None` is not reliable, given that the parser can
26+
# return `False` or `None` for legitimate reasons from RESP payloads.
27+
NOT_ENOUGH_DATA = object()
28+
2429

2530
class _HiredisReaderArgs(TypedDict, total=False):
2631
protocolError: Callable[[str], Exception]
@@ -53,25 +58,26 @@ def on_connect(self, connection, **kwargs):
5358
"protocolError": InvalidResponse,
5459
"replyError": self.parse_error,
5560
"errors": connection.encoder.encoding_errors,
61+
"notEnoughData": NOT_ENOUGH_DATA,
5662
}
5763

5864
if connection.encoder.decode_responses:
5965
kwargs["encoding"] = connection.encoder.encoding
6066
self._reader = hiredis.Reader(**kwargs)
61-
self._next_response = False
67+
self._next_response = NOT_ENOUGH_DATA
6268

6369
def on_disconnect(self):
6470
self._sock = None
6571
self._reader = None
66-
self._next_response = False
72+
self._next_response = NOT_ENOUGH_DATA
6773

6874
def can_read(self, timeout):
6975
if not self._reader:
7076
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
7177

72-
if self._next_response is False:
78+
if self._next_response is NOT_ENOUGH_DATA:
7379
self._next_response = self._reader.gets()
74-
if self._next_response is False:
80+
if self._next_response is NOT_ENOUGH_DATA:
7581
return self.read_from_socket(timeout=timeout, raise_on_timeout=False)
7682
return True
7783

@@ -110,17 +116,17 @@ def read_response(self, disable_decoding=False):
110116
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
111117

112118
# _next_response might be cached from a can_read() call
113-
if self._next_response is not False:
119+
if self._next_response is not NOT_ENOUGH_DATA:
114120
response = self._next_response
115-
self._next_response = False
121+
self._next_response = NOT_ENOUGH_DATA
116122
return response
117123

118124
if disable_decoding:
119125
response = self._reader.gets(False)
120126
else:
121127
response = self._reader.gets()
122128

123-
while response is False:
129+
while response is NOT_ENOUGH_DATA:
124130
self.read_from_socket()
125131
if disable_decoding:
126132
response = self._reader.gets(False)
@@ -158,6 +164,7 @@ def on_connect(self, connection):
158164
kwargs: _HiredisReaderArgs = {
159165
"protocolError": InvalidResponse,
160166
"replyError": self.parse_error,
167+
"notEnoughData": NOT_ENOUGH_DATA,
161168
}
162169
if connection.encoder.decode_responses:
163170
kwargs["encoding"] = connection.encoder.encoding
@@ -172,7 +179,7 @@ def on_disconnect(self):
172179
async def can_read_destructive(self):
173180
if not self._connected:
174181
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
175-
if self._reader.gets():
182+
if self._reader.gets() is not NOT_ENOUGH_DATA:
176183
return True
177184
try:
178185
async with async_timeout(0):
@@ -202,7 +209,7 @@ async def read_response(
202209
response = self._reader.gets(False)
203210
else:
204211
response = self._reader.gets()
205-
while response is False:
212+
while response is NOT_ENOUGH_DATA:
206213
await self.read_from_socket()
207214
if disable_decoding:
208215
response = self._reader.gets(False)

tests/test_asyncio/conftest.py

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@
66
import pytest_asyncio
77
import redis.asyncio as redis
88
from packaging.version import Version
9-
from redis._parsers import _AsyncHiredisParser, _AsyncRESP2Parser
109
from redis.asyncio import Sentinel
1110
from redis.asyncio.client import Monitor
1211
from redis.asyncio.connection import Connection, parse_url
1312
from redis.asyncio.retry import Retry
1413
from redis.backoff import NoBackoff
15-
from redis.utils import HIREDIS_AVAILABLE
1614
from tests.conftest import REDIS_INFO
1715

1816
from .compat import mock
@@ -28,41 +26,21 @@ async def _get_info(redis_url):
2826
@pytest_asyncio.fixture(
2927
params=[
3028
pytest.param(
31-
(True, _AsyncRESP2Parser),
29+
(True,),
3230
marks=pytest.mark.skipif(
3331
'config.REDIS_INFO["cluster_enabled"]', reason="cluster mode enabled"
3432
),
3533
),
36-
(False, _AsyncRESP2Parser),
37-
pytest.param(
38-
(True, _AsyncHiredisParser),
39-
marks=[
40-
pytest.mark.skipif(
41-
'config.REDIS_INFO["cluster_enabled"]',
42-
reason="cluster mode enabled",
43-
),
44-
pytest.mark.skipif(
45-
not HIREDIS_AVAILABLE, reason="hiredis is not installed"
46-
),
47-
],
48-
),
49-
pytest.param(
50-
(False, _AsyncHiredisParser),
51-
marks=pytest.mark.skipif(
52-
not HIREDIS_AVAILABLE, reason="hiredis is not installed"
53-
),
54-
),
34+
(False,),
5535
],
5636
ids=[
57-
"single-python-parser",
58-
"pool-python-parser",
59-
"single-hiredis",
60-
"pool-hiredis",
37+
"single",
38+
"pool",
6139
],
6240
)
6341
async def create_redis(request):
6442
"""Wrapper around redis.create_redis."""
65-
single_connection, parser_cls = request.param
43+
(single_connection,) = request.param
6644

6745
teardown_clients = []
6846

@@ -78,10 +56,9 @@ async def client_factory(
7856
cluster_mode = REDIS_INFO["cluster_enabled"]
7957
if not cluster_mode:
8058
single = kwargs.pop("single_connection_client", False) or single_connection
81-
parser_class = kwargs.pop("parser_class", None) or parser_cls
8259
url_options = parse_url(url)
8360
url_options.update(kwargs)
84-
pool = redis.ConnectionPool(parser_class=parser_class, **url_options)
61+
pool = redis.ConnectionPool(**url_options)
8562
client = cls(connection_pool=pool)
8663
else:
8764
client = redis.RedisCluster.from_url(url, **kwargs)

0 commit comments

Comments
 (0)