Skip to content

Commit b775cc4

Browse files
committed
Add a test for WANT_READ during sendall()
1 parent 3a2d946 commit b775cc4

File tree

1 file changed

+123
-3
lines changed

1 file changed

+123
-3
lines changed

tests/test_ssl.py

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ def loopback_server_factory(socket, version=SSLv23_METHOD):
321321
return server
322322

323323

324-
def loopback(server_factory=None, client_factory=None):
324+
def loopback(server_factory=None, client_factory=None, blocking=True):
325325
"""
326326
Create a connected socket pair and force two connected SSL sockets
327327
to talk to each other via memory BIOs.
@@ -337,8 +337,8 @@ def loopback(server_factory=None, client_factory=None):
337337

338338
handshake(client, server)
339339

340-
server.setblocking(True)
341-
client.setblocking(True)
340+
server.setblocking(blocking)
341+
client.setblocking(blocking)
342342
return server, client
343343

344344

@@ -3292,11 +3292,131 @@ def test_memoryview_really_doesnt_overfill(self):
32923292
self._doesnt_overfill_test(_make_memoryview)
32933293

32943294

3295+
@pytest.fixture
3296+
def nonblocking_tls_connections_pair():
3297+
"""Return a non-blocking TLS loopback connections pair."""
3298+
return loopback(blocking=False)
3299+
3300+
3301+
@pytest.fixture
3302+
def nonblocking_tls_server_connection(nonblocking_tls_connections_pair):
3303+
"""Return a non-blocking TLS server socket connected to loopback."""
3304+
return nonblocking_tls_connections_pair[0]
3305+
3306+
3307+
@pytest.fixture
3308+
def nonblocking_tls_client_connection(nonblocking_tls_connections_pair):
3309+
"""Return a non-blocking TLS client socket connected to loopback."""
3310+
return nonblocking_tls_connections_pair[1]
3311+
3312+
32953313
class TestConnectionSendall:
32963314
"""
32973315
Tests for `Connection.sendall`.
32983316
"""
32993317

3318+
def test_want_write(
3319+
self,
3320+
monkeypatch,
3321+
nonblocking_tls_server_connection,
3322+
nonblocking_tls_client_connection,
3323+
):
3324+
msg = b"x"
3325+
garbage_size = 1024 * 1024 * 64
3326+
large_payload = b"p" * garbage_size * 2
3327+
payload_size = len(large_payload)
3328+
3329+
sent_garbage_size = 0
3330+
try:
3331+
sent_garbage_size += nonblocking_tls_client_connection.send(
3332+
msg * garbage_size,
3333+
)
3334+
except WantWriteError:
3335+
pass
3336+
for i in range(garbage_size):
3337+
try:
3338+
sent_garbage_size += nonblocking_tls_client_connection.send(
3339+
msg,
3340+
)
3341+
except WantWriteError:
3342+
break
3343+
else:
3344+
pytest.fail(
3345+
"Failed to fill socket buffer, cannot test "
3346+
"'want write' in `sendall()`"
3347+
)
3348+
garbage_payload = sent_garbage_size * msg
3349+
3350+
3351+
def consume_garbage(conn):
3352+
assert patched_ssl_write.want_write_counter >= 1
3353+
assert not consume_garbage.garbage_consumed
3354+
3355+
while len(consume_garbage.consumed) < sent_garbage_size:
3356+
try:
3357+
consume_garbage.consumed += conn.recv(
3358+
sent_garbage_size - len(consume_garbage.consumed),
3359+
)
3360+
except WantReadError:
3361+
pass
3362+
3363+
assert consume_garbage.consumed == garbage_payload
3364+
3365+
consume_garbage.garbage_consumed = True
3366+
3367+
consume_garbage.garbage_consumed = False
3368+
consume_garbage.consumed = b""
3369+
3370+
def consume_payload(conn):
3371+
try:
3372+
consume_payload.consumed += conn.recv(payload_size)
3373+
except WantReadError:
3374+
pass
3375+
consume_payload.consumed = b""
3376+
3377+
original_ssl_write = _lib.SSL_write
3378+
def patched_ssl_write(ctx, data, size):
3379+
write_result = original_ssl_write(ctx, data, size)
3380+
try:
3381+
nonblocking_tls_client_connection._raise_ssl_error(
3382+
ctx, write_result,
3383+
)
3384+
except WantWriteError:
3385+
patched_ssl_write.want_write_counter += 1
3386+
consume_data_on_server = (
3387+
consume_payload if consume_garbage.garbage_consumed
3388+
else consume_garbage
3389+
)
3390+
3391+
consume_data_on_server(nonblocking_tls_server_connection)
3392+
# NOTE: We don't re-raise it as the calling code will do
3393+
# NOTE: the same after the call.
3394+
return write_result
3395+
3396+
patched_ssl_write.want_write_counter = 0
3397+
3398+
# NOTE: Make the client think it needs a handshake so that it'll
3399+
# NOTE: attempt to `do_handshake()` on the next `SSL_write()`
3400+
# NOTE: that originates from `sendall()`:
3401+
nonblocking_tls_client_connection.set_connect_state()
3402+
try:
3403+
nonblocking_tls_client_connection.do_handshake()
3404+
except WantWriteError:
3405+
assert True # Sanity check
3406+
except:
3407+
assert False # This should never happen (see the note above)
3408+
3409+
with monkeypatch.context() as mp_ctx:
3410+
mp_ctx.setattr(_lib, "SSL_write", patched_ssl_write)
3411+
nonblocking_tls_client_connection.sendall(large_payload)
3412+
3413+
assert consume_garbage.garbage_consumed
3414+
3415+
# NOTE: Read the leftover data from the very last `SSL_write()`
3416+
consume_payload(nonblocking_tls_server_connection)
3417+
3418+
assert consume_payload.consumed == large_payload
3419+
33003420
def test_wrong_args(self):
33013421
"""
33023422
When called with arguments other than a string argument for its first

0 commit comments

Comments
 (0)