Skip to content

Commit 04391fd

Browse files
authored
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 017f767 commit 04391fd

File tree

9 files changed

+212
-196
lines changed

9 files changed

+212
-196
lines changed

.github/workflows/integration.yaml

+30-4
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

+16-9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
SERVER_CLOSED_CONNECTION_ERROR,
2020
)
2121

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

2328
class _HiredisReaderArgs(TypedDict, total=False):
2429
protocolError: Callable[[str], Exception]
@@ -51,25 +56,26 @@ def on_connect(self, connection, **kwargs):
5156
"protocolError": InvalidResponse,
5257
"replyError": self.parse_error,
5358
"errors": connection.encoder.encoding_errors,
59+
"notEnoughData": NOT_ENOUGH_DATA,
5460
}
5561

5662
if connection.encoder.decode_responses:
5763
kwargs["encoding"] = connection.encoder.encoding
5864
self._reader = hiredis.Reader(**kwargs)
59-
self._next_response = False
65+
self._next_response = NOT_ENOUGH_DATA
6066

6167
def on_disconnect(self):
6268
self._sock = None
6369
self._reader = None
64-
self._next_response = False
70+
self._next_response = NOT_ENOUGH_DATA
6571

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

70-
if self._next_response is False:
76+
if self._next_response is NOT_ENOUGH_DATA:
7177
self._next_response = self._reader.gets()
72-
if self._next_response is False:
78+
if self._next_response is NOT_ENOUGH_DATA:
7379
return self.read_from_socket(timeout=timeout, raise_on_timeout=False)
7480
return True
7581

@@ -108,17 +114,17 @@ def read_response(self, disable_decoding=False):
108114
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
109115

110116
# _next_response might be cached from a can_read() call
111-
if self._next_response is not False:
117+
if self._next_response is not NOT_ENOUGH_DATA:
112118
response = self._next_response
113-
self._next_response = False
119+
self._next_response = NOT_ENOUGH_DATA
114120
return response
115121

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

121-
while response is False:
127+
while response is NOT_ENOUGH_DATA:
122128
self.read_from_socket()
123129
if disable_decoding:
124130
response = self._reader.gets(False)
@@ -156,6 +162,7 @@ def on_connect(self, connection):
156162
kwargs: _HiredisReaderArgs = {
157163
"protocolError": InvalidResponse,
158164
"replyError": self.parse_error,
165+
"notEnoughData": NOT_ENOUGH_DATA,
159166
}
160167
if connection.encoder.decode_responses:
161168
kwargs["encoding"] = connection.encoder.encoding
@@ -170,7 +177,7 @@ def on_disconnect(self):
170177
async def can_read_destructive(self):
171178
if not self._connected:
172179
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
173-
if self._reader.gets():
180+
if self._reader.gets() is not NOT_ENOUGH_DATA:
174181
return True
175182
try:
176183
async with async_timeout(0):
@@ -200,7 +207,7 @@ async def read_response(
200207
response = self._reader.gets(False)
201208
else:
202209
response = self._reader.gets()
203-
while response is False:
210+
while response is NOT_ENOUGH_DATA:
204211
await self.read_from_socket()
205212
if disable_decoding:
206213
response = self._reader.gets(False)

tests/test_asyncio/conftest.py

+6-29
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)