Skip to content

Commit ac9f589

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

File tree

2 files changed

+79
-0
lines changed

2 files changed

+79
-0
lines changed

tests/test_asyncio/test_connection.py

+34
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,37 @@ 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
139+
# on the socket's buffer
140+
with patch.object(socket.socket, "recv", side_effect=exc_type) as mock_recv:
141+
await conn.read_response()
142+
mock_recv.assert_called_once()
143+
144+
await conn.send_command("GET non_existent_key")
145+
resp = await conn.read_response()
146+
# If working properly, this will get a None.
147+
# If not, it will get a zero (the integer result of the previous EXISTS command).
148+
assert resp is None

tests/test_connection.py

+45
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,48 @@ 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(
127+
'exc_type',
128+
[
129+
Exception,
130+
pytest.param(
131+
BaseException,
132+
marks=pytest.mark.xfail(
133+
reason='https://github.com/redis/redis-py/issues/360',
134+
),
135+
),
136+
],
137+
)
138+
def test_read_response__interrupt_does_not_corrupt(self, exc_type):
139+
conn = Connection()
140+
141+
# A note on BaseException:
142+
# While socket.recv is not supposed to raise BaseException, gevent's version
143+
# of socket (which, when using gevent + redis-py, one would monkey-patch in)
144+
# can raise BaseException on a timer elapse, since `gevent.Timeout` derives
145+
# from BaseException. This design suggests that a timeout should
146+
# not be suppressed but rather allowed to propagate.
147+
# asyncio.exceptions.CancelledError also derives from BaseException
148+
# for same reason.
149+
#
150+
# What we should ensure, one way or another, is that the connection is
151+
# not left in a corrupted state.
152+
153+
conn.send_command("GET non_existent_key")
154+
resp = conn.read_response()
155+
assert resp is None
156+
157+
with pytest.raises(exc_type):
158+
conn.send_command("EXISTS non_existent_key")
159+
# due to the interrupt, the integer '0' result of EXISTS will remain
160+
# on the socket's buffer
161+
with patch.object(socket.socket, "recv", side_effect=exc_type) as mock_recv:
162+
_ = conn.read_response()
163+
mock_recv.assert_called_once()
164+
165+
conn.send_command("GET non_existent_key")
166+
resp = conn.read_response()
167+
# If working properly, this will get a None.
168+
# If not, it will get a zero (the integer result of the previous command).
169+
assert resp is None

0 commit comments

Comments
 (0)