Skip to content

Commit 287d289

Browse files
committed
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 2fe1db2 commit 287d289

12 files changed

+215
-202
lines changed

.github/workflows/integration.yaml

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

142168
build_and_test_package:
143169
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/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ def _get_client(
279279
redis_url = request.config.getoption("--redis-url")
280280
else:
281281
redis_url = from_url
282-
if "protocol" not in redis_url:
282+
if "protocol" not in redis_url and kwargs.get("protocol") is None:
283283
kwargs["protocol"] = request.config.getoption("--protocol")
284284

285285
cluster_mode = REDIS_INFO["cluster_enabled"]

tests/test_asyncio/conftest.py

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@
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.client import Monitor
1110
from redis.asyncio.connection import Connection, parse_url
1211
from redis.asyncio.retry import Retry
1312
from redis.backoff import NoBackoff
14-
from redis.utils import HIREDIS_AVAILABLE
1513
from tests.conftest import REDIS_INFO
1614

1715
from .compat import mock
@@ -27,61 +25,39 @@ async def _get_info(redis_url):
2725
@pytest_asyncio.fixture(
2826
params=[
2927
pytest.param(
30-
(True, _AsyncRESP2Parser),
28+
(True,),
3129
marks=pytest.mark.skipif(
3230
'config.REDIS_INFO["cluster_enabled"]', reason="cluster mode enabled"
3331
),
3432
),
35-
(False, _AsyncRESP2Parser),
36-
pytest.param(
37-
(True, _AsyncHiredisParser),
38-
marks=[
39-
pytest.mark.skipif(
40-
'config.REDIS_INFO["cluster_enabled"]',
41-
reason="cluster mode enabled",
42-
),
43-
pytest.mark.skipif(
44-
not HIREDIS_AVAILABLE, reason="hiredis is not installed"
45-
),
46-
],
47-
),
48-
pytest.param(
49-
(False, _AsyncHiredisParser),
50-
marks=pytest.mark.skipif(
51-
not HIREDIS_AVAILABLE, reason="hiredis is not installed"
52-
),
53-
),
33+
(False,),
5434
],
5535
ids=[
56-
"single-python-parser",
57-
"pool-python-parser",
58-
"single-hiredis",
59-
"pool-hiredis",
36+
"single",
37+
"pool",
6038
],
6139
)
6240
async def create_redis(request):
6341
"""Wrapper around redis.create_redis."""
64-
single_connection, parser_cls = request.param
42+
(single_connection,) = request.param
6543

6644
teardown_clients = []
6745

6846
async def client_factory(
6947
url: str = request.config.getoption("--redis-url"),
7048
cls=redis.Redis,
7149
flushdb=True,
72-
protocol=request.config.getoption("--protocol"),
7350
**kwargs,
7451
):
75-
if "protocol" not in url:
52+
if "protocol" not in url and kwargs.get("protocol") is None:
7653
kwargs["protocol"] = request.config.getoption("--protocol")
7754

7855
cluster_mode = REDIS_INFO["cluster_enabled"]
7956
if not cluster_mode:
8057
single = kwargs.pop("single_connection_client", False) or single_connection
81-
parser_class = kwargs.pop("parser_class", None) or parser_cls
8258
url_options = parse_url(url)
8359
url_options.update(kwargs)
84-
pool = redis.ConnectionPool(parser_class=parser_class, **url_options)
60+
pool = redis.ConnectionPool(**url_options)
8561
client = cls(connection_pool=pool)
8662
else:
8763
client = redis.RedisCluster.from_url(url, **kwargs)

tests/test_asyncio/test_connection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ async def call_with_retry(self, _, __):
6868
in_use = False
6969
return "foo"
7070

71-
mock_conn = mock.MagicMock()
71+
mock_conn = mock.AsyncMock(spec=Connection)
7272
mock_conn.retry = Retry_()
7373

7474
async def get_conn(_):

tests/test_asyncio/test_connection_pool.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,6 @@ def test_connect_from_url_tcp(self):
647647
connection = redis.Redis.from_url("redis://localhost")
648648
pool = connection.connection_pool
649649

650-
print(repr(pool))
651650
assert re.match(
652651
r"< .*?([^\.]+) \( < .*?([^\.]+) \( (.+) \) > \) >", repr(pool), re.VERBOSE
653652
).groups() == (

0 commit comments

Comments
 (0)