1
1
import asyncio
2
2
import socket
3
3
import types
4
- from unittest .mock import AsyncMock , Mock , patch
4
+ from unittest .mock import patch
5
5
6
6
import pytest
7
7
@@ -151,7 +151,10 @@ async def test_connection_parse_response_resume(r: redis.Redis):
151
151
152
152
153
153
@pytest .mark .onlynoncluster
154
- async def test_connection_hiredis_disconect_race ():
154
+ @pytest .mark .parametrize (
155
+ "parser_class" , [PythonParser , HiredisParser ], ids = ["PythonParser" , "HiredisParser" ]
156
+ )
157
+ async def test_connection_disconect_race (parser_class ):
155
158
"""
156
159
This test reproduces the case in issue #2349
157
160
where a connection is closed while the parser is reading to feed the
@@ -163,12 +166,14 @@ async def test_connection_hiredis_disconect_race():
163
166
This test verifies that a read in progress can finish even
164
167
if the `disconnect()` method is called.
165
168
"""
166
- if not HIREDIS_AVAILABLE :
167
- pytest .skip ("Hiredis not available)" )
168
- parser_class = HiredisParser
169
+ if parser_class == PythonParser :
170
+ pytest .xfail ("doesn't work yet with PythonParser" )
171
+ if parser_class == HiredisParser and not HIREDIS_AVAILABLE :
172
+ pytest .skip ("Hiredis not available" )
169
173
170
174
args = {}
171
175
args ["parser_class" ] = parser_class
176
+
172
177
conn = Connection (** args )
173
178
174
179
cond = asyncio .Condition ()
@@ -177,15 +182,20 @@ async def test_connection_hiredis_disconect_race():
177
182
# 2 == closer has closed and is waiting for close to finish
178
183
state = 0
179
184
180
- # mock read function, which wait for a close to happen before returning
181
- async def read (_ ):
185
+ # Mock read function, which wait for a close to happen before returning
186
+ # Can either be invoked as two `read()` calls (HiredisParser)
187
+ # or as a `readline()` followed by `readexact()` (PythonParser)
188
+ chunks = [b"$13\r \n " , b"Hello, World!\r \n " ]
189
+
190
+ async def read (_ = None ):
182
191
nonlocal state
183
192
async with cond :
184
- state = 1 # we are reading
185
- cond .notify ()
186
- # wait until the closing task has done
187
- await cond .wait_for (lambda : state == 2 )
188
- return b" "
193
+ if state == 0 :
194
+ state = 1 # we are reading
195
+ cond .notify ()
196
+ # wait until the closing task has done
197
+ await cond .wait_for (lambda : state == 2 )
198
+ return chunks .pop (0 )
189
199
190
200
# function closes the connection while reader is still blocked reading
191
201
async def do_close ():
@@ -197,20 +207,24 @@ async def do_close():
197
207
await conn .disconnect ()
198
208
199
209
async def do_read ():
200
- with pytest .raises (InvalidResponse ):
201
- await conn .read_response ()
210
+ return await conn .read_response ()
202
211
203
- reader = AsyncMock ()
204
- writer = AsyncMock ()
205
- writer .transport = Mock ()
212
+ reader = mock . AsyncMock ()
213
+ writer = mock . AsyncMock ()
214
+ writer .transport = mock . Mock ()
206
215
writer .transport .get_extra_info .side_effect = None
207
216
217
+ # for HiredisParser
208
218
reader .read .side_effect = read
219
+ # for PythonParser
220
+ reader .readline .side_effect = read
221
+ reader .readexactly .side_effect = read
209
222
210
223
async def open_connection (* args , ** kwargs ):
211
224
return reader , writer
212
225
213
226
with patch .object (asyncio , "open_connection" , open_connection ):
214
227
await conn .connect ()
215
228
216
- await asyncio .gather (do_read (), do_close ())
229
+ vals = await asyncio .gather (do_read (), do_close ())
230
+ assert vals == [b"Hello, World!" , None ]
0 commit comments