Skip to content

Commit f73afbf

Browse files
committed
Add regression test for redis#360
1 parent 3a121be commit f73afbf

File tree

3 files changed

+71
-0
lines changed

3 files changed

+71
-0
lines changed

tests/test_asyncio/test_connection.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,36 @@ async def test_connect_timeout_error_without_retry():
112112
await conn.connect()
113113
assert conn._connect.call_count == 1
114114
assert str(e.value) == "Timeout connecting to server"
115+
116+
117+
@pytest.mark.parametrize(
118+
'exc_type',
119+
[
120+
Exception,
121+
pytest.param(
122+
BaseException,
123+
marks=pytest.mark.xfail(
124+
reason='https://github.com/redis/redis-py/issues/360',
125+
),
126+
),
127+
],
128+
)
129+
async def test_read_response__interrupt_does_not_corrupt(exc_type):
130+
conn = Connection()
131+
132+
await conn.send_command("GET non_existent_key")
133+
resp = await conn.read_response()
134+
assert resp is None
135+
136+
with pytest.raises(exc_type):
137+
await conn.send_command("EXISTS non_existent_key")
138+
# due to the interrupt, the integer '0' result of EXISTS will remain on the socket's buffer
139+
with patch.object(socket.socket, "recv", side_effect=exc_type) as mock_recv:
140+
await conn.read_response()
141+
mock_recv.assert_called_once()
142+
143+
await conn.send_command("GET non_existent_key")
144+
resp = await conn.read_response()
145+
# If working properly, this will get a None.
146+
# If not, it will get a zero (the integer result of the previous EXISTS command).
147+
assert resp is None

tests/test_connection.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,40 @@ def test_connect_timeout_error_without_retry(self):
122122
assert conn._connect.call_count == 1
123123
assert str(e.value) == "Timeout connecting to server"
124124
self.clear(conn)
125+
126+
@pytest.mark.parametrize('exc_type', [Exception, BaseException])
127+
def test_read_response__interrupt_does_not_corrupt(self, exc_type):
128+
conn = Connection()
129+
130+
# A note on BaseException:
131+
# While socket.recv is not supposed to raise BaseException, gevent's version
132+
# of socket (which, when using gevent + redis-py, one would monkey-patch in)
133+
# can raise BaseException on a timer elapse, since `gevent.Timeout` derives
134+
# from BaseException. This design suggests that a timeout should
135+
# not be suppressed but rather allowed to propagate.
136+
# asyncio.exceptions.CancelledError also derives from BaseException
137+
# for same reason.
138+
#
139+
# The notion that one should never `expect:` or `expect BaseException`,
140+
# however, is misguided. It's idiomatic to handle it, to provide
141+
# for exception safety, as long as you re-raise.
142+
#
143+
# with gevent.Timeout(5):
144+
# res = client.exists('my_key')
145+
146+
conn.send_command("GET non_existent_key")
147+
resp = conn.read_response()
148+
assert resp is None
149+
150+
with pytest.raises(exc_type):
151+
conn.send_command("EXISTS non_existent_key")
152+
# due to the interrupt, the integer '0' result of EXISTS will remain on the socket's buffer
153+
with patch.object(socket.socket, "recv", side_effect=exc_type) as mock_recv:
154+
_ = conn.read_response()
155+
mock_recv.assert_called_once()
156+
157+
conn.send_command("GET non_existent_key")
158+
resp = conn.read_response()
159+
# If working properly, this will get a None.
160+
# If not, it will get a zero (the integer result of the previous EXISTS command).
161+
assert resp is None

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ markers =
1010
replica: replica tests
1111
experimental: run only experimental tests
1212
asyncio_mode = auto
13+
xfail_strict = true
1314

1415
[tox]
1516
minversion = 3.2.0

0 commit comments

Comments
 (0)