1313import threading
1414import time
1515import traceback
16- from collections import namedtuple
1716from types import SimpleNamespace
1817
1918import pytest
3029)
3130
3231from cheroot import (
33- connections as _connections ,
3432 errors ,
3533)
3634from cheroot .connections import ConnectionManager
@@ -1177,73 +1175,42 @@ def conn_manager():
11771175 return mgr
11781176
11791177
1180- def test_ignore_socket_oserror_increments_stats (conn_manager ):
1181- """Test the socket OSError handler for interrupt errors."""
1182- # Simulate an interrupt error
1183- exc = OSError (errors .socket_error_eintr [0 ])
1184-
1185- result = conn_manager ._ignore_socket_oserror (exc )
1186-
1187- assert result is True
1188- assert conn_manager .server .stats ['Socket Errors' ] == 1
1189-
1190-
1191- def test_ignore_socket_oserror_disabled_stats (conn_manager ):
1192- """Test the socket OSError handler for disabled_stats."""
1193- conn_manager .server .stats ['Enabled' ] = False
1194- exc = OSError (errors .socket_error_eintr [0 ])
1195-
1196- conn_manager ._ignore_socket_oserror (exc )
1197-
1198- # Should NOT increment since 'Enabled' is False
1199- assert conn_manager .server .stats ['Socket Errors' ] == 0
1200-
1201-
12021178@pytest .mark .parametrize (
1203- ('err_code' , 'expected ' ),
1179+ ('err_code' , 'should_ignore' , 'stats_enabled' , 'expect_error ' ),
12041180 (
1205- (errors .socket_error_eintr [0 ], True ),
1206- (errors .socket_errors_nonblocking [0 ], True ),
1207- (errors .socket_errors_to_ignore [0 ], True ),
1208- (999 , False ),
1181+ (errors .socket_error_eintr [0 ], True , True , True ),
1182+ (errors .socket_errors_nonblocking [0 ], True , True , True ),
1183+ (errors .socket_errors_to_ignore [0 ], True , True , True ),
1184+ (999 , False , True , True ),
1185+ (errors .socket_error_eintr [0 ], True , False , False ), # stats disabled
12091186 ),
1187+ ids = ['eintr' , 'nonblocking' , 'to-ignore' , 'unknown' , 'stats-disabled' ],
12101188)
1211- def test_ignore_socket_oserror_logic_branches (
1189+ def test_ignore_socket_oserror (
12121190 conn_manager ,
12131191 err_code ,
1214- expected ,
1192+ should_ignore ,
1193+ stats_enabled ,
1194+ expect_error ,
12151195):
1216- """Test the socket OSError handler for ignorable errors."""
1196+ """OSError handler returns correct result and increments stats appropriately."""
1197+ conn_manager .server .stats ['Enabled' ] = stats_enabled
12171198 exc = OSError (err_code )
1218- assert conn_manager ._ignore_socket_oserror (exc ) is expected
1219-
1220-
1221- def _raise_eintr (* args , ** kwargs ):
1222- """Raise an interrupt error."""
1223- raise OSError (errno .EINTR , 'Interrupted system call' )
1224-
12251199
1226- def test_from_server_socket_interrupt_error (conn_manager_with_server ):
1227- """Verify that _from_server_socket returns None on ignorable OS errors."""
1228- # Assign that function to 'accept'
1229- fake_server_socket = SimpleNamespace (
1230- accept = _raise_eintr ,
1231- )
1200+ result = conn_manager ._ignore_socket_oserror (exc )
12321201
1233- # Execute
1234- conn = conn_manager_with_server ._from_server_socket (fake_server_socket )
1235- assert conn is None
1202+ assert result is should_ignore
1203+ assert conn_manager .server .stats ['Socket Errors' ] == expect_error
12361204
12371205
12381206@pytest .fixture
1239- def conn_manager_with_server ():
1207+ def conn_manager_with_server (mocker ):
12401208 """Create a ConnectionManager with a stub server."""
12411209 mgr = ConnectionManager .__new__ (ConnectionManager )
12421210 mgr .server = SimpleNamespace (
12431211 stats = {'Enabled' : True , 'Accepts' : 0 , 'Socket Errors' : 0 },
12441212 ssl_adapter = None ,
12451213 timeout = 10 ,
1246- # Explicitly 3 args to match the modern pipeline
12471214 ConnectionClass = lambda server , s , mf : SimpleNamespace (
12481215 server = server ,
12491216 sock = s ,
@@ -1252,99 +1219,164 @@ def conn_manager_with_server():
12521219 remote_port = None ,
12531220 ),
12541221 bind_addr = ('127.0.0.1' , 8080 ),
1222+ close = mocker .Mock (),
12551223 )
12561224 return mgr
12571225
12581226
1259- @pytest .fixture
1260- def fake_socket ():
1261- """Provide a basic mock socket."""
1227+ @contextlib .contextmanager
1228+ def pipe_fd ():
1229+ """Open an OS pipe and ensure both ends are closed."""
1230+ read_fd , write_fd = os .pipe ()
1231+ try :
1232+ yield read_fd
1233+ finally :
1234+ os .close (read_fd )
1235+ os .close (write_fd )
1236+
1237+
1238+ def _make_fake_socket (error , read_fd ):
1239+ def _accept (): # noqa: WPS430
1240+ raise error
1241+
12621242 return SimpleNamespace (
1263- settimeout = lambda t : None ,
1264- fileno = lambda : 10 ,
1265- close = lambda : None ,
1266- getsockname = lambda : ('127.0.0.1' , 8080 ),
1243+ accept = _accept ,
1244+ fileno = lambda : read_fd ,
12671245 )
12681246
12691247
1270- def _dummy_fcntl (fd , op , arg = 0 ):
1271- """Return nothing instead of a real file control."""
1272- return 0
1248+ @pytest .mark .parametrize (
1249+ ('error' , 'expected_exception' ),
1250+ (
1251+ (socket .timeout (), None ),
1252+ (OSError (errno .EAGAIN , 'Resource temporarily unavailable' ), None ),
1253+ (OSError (errno .EINTR , 'Interrupted system call' ), None ),
1254+ (OSError (errno .EIO , 'Critical kernel error' ), OSError ),
1255+ ),
1256+ ids = ['timeout' , 'EAGAIN-ignored' , 'EINTR-ignored' , 'oserror-critical' ],
1257+ )
1258+ def test_from_server_socket_transport_errors (
1259+ conn_manager_with_server ,
1260+ error ,
1261+ expected_exception ,
1262+ ):
1263+ """Test errors raised during initial socket accept are handled correctly."""
1264+ with pipe_fd () as read_fd :
1265+ fake_socket = _make_fake_socket (error , read_fd )
12731266
1267+ if expected_exception :
1268+ with pytest .raises (
1269+ expected_exception ,
1270+ match = 'Critical kernel error' ,
1271+ ):
1272+ conn_manager_with_server ._from_server_socket (fake_socket )
1273+ else :
1274+ assert (
1275+ conn_manager_with_server ._from_server_socket (fake_socket )
1276+ is None
1277+ )
12741278
1275- def raise_os_error (* args , ** kwargs ):
1276- """Raise OSError in a mock."""
1277- raise OSError ('Broken' )
12781279
1280+ def _make_connection (
1281+ conn_manager ,
1282+ monkeypatch ,
1283+ provided_addr = ('1.2.3.4' , 80 ),
1284+ sock_name = ('127.0.0.1' , 80 ),
1285+ ):
1286+ """Set up a fake listener and call _from_server_socket."""
1287+ if sys .platform != 'win32' :
1288+ monkeypatch .setattr (
1289+ 'fcntl.fcntl' ,
1290+ lambda fd , cmd , * args : 0 ,
1291+ raising = False ,
1292+ )
12791293
1280- Scenario = namedtuple (
1281- 'Scenario' ,
1282- ['client_s' , 'provided_addr' , 'expected_addr' , 'expect_error' ],
1283- )
1294+ conn_manager .ConnectionClass = lambda sock , addr , server : SimpleNamespace (
1295+ sock = sock ,
1296+ server = server ,
1297+ )
1298+ accepted_socket = SimpleNamespace (
1299+ fileno = lambda : 10 ,
1300+ setblocking = lambda x : None ,
1301+ settimeout = lambda t : None ,
1302+ setsockopt = lambda * a : None ,
1303+ getsockname = lambda : sock_name ,
1304+ )
1305+ fake_listener = SimpleNamespace (
1306+ accept = lambda : (accepted_socket , provided_addr ),
1307+ )
1308+ return conn_manager ._from_server_socket (fake_listener )
12841309
12851310
12861311@pytest .mark .parametrize (
1287- 'scenario' ,
1312+ ( 'provided_addr' , 'sock_name' , 'expected_ip' , 'expected_port' ) ,
12881313 (
1289- Scenario (None , ('127.0.0.1' , 123 ), ('127.0.0.1' , 123 ), False ),
1290- Scenario (None , None , ('0.0.0.0' , 0 ), False ),
1291- Scenario (
1292- SimpleNamespace (
1293- settimeout = raise_os_error ,
1294- fileno = lambda : 11 ,
1295- close = lambda : None ,
1296- ),
1297- ('10.0.0.1' , 456 ),
1298- ('10.0.0.1' , 456 ),
1299- True ,
1300- ),
1301- ),
1302- ids = (
1303- 'standard-success' ,
1304- 'missing-addr-fallback' ,
1305- 'broken-socket-oserror' ,
1314+ (('1.2.3.4' , 80 ), ('127.0.0.1' , 80 ), '1.2.3.4' , 80 ),
1315+ (None , ('127.0.0.1' , 80 ), '0.0.0.0' , 0 ),
1316+ (None , ('::1' , 80 , 0 , 0 ), '::' , 0 ),
13061317 ),
1318+ ids = ['explicit-addr' , 'ipv4-fallback' , 'ipv6-fallback' ],
13071319)
1308- def test_from_server_socket_scenarios (
1320+ def test_from_server_socket_address_resolution (
13091321 conn_manager_with_server ,
1310- fake_socket ,
13111322 monkeypatch ,
1312- scenario ,
1313- ):
1314- """
1315- Verify high-level connection orchestration from sockets.
1316-
1317- This test ensures that the ``_from_server_socket()``
1318- pipeline correctly:
1319- 1. Accepts a connection from the server socket.
1320- 2. Configures the resulting client socket.
1321- 3. Successfully increments 'Accepts' stats on successful configuration.
1322- 4. Wraps the socket into a Connection object.
1323- """
1324- # 1. Neuter fcntl right here inside the function
1325- if hasattr (_connections , 'fcntl' ):
1326- # Patch the function
1327- monkeypatch .setattr (_connections .fcntl , 'fcntl' , _dummy_fcntl )
1328- monkeypatch .setattr (_connections .fcntl , 'F_GETFD' , 1 )
1329-
1330- # Use the provided socket or fall back to the fixture
1331- actual_client = scenario .client_s or fake_socket
1332-
1333- # Mock the server socket to return our scenario-specific data
1334- fake_server_socket = SimpleNamespace (
1335- accept = lambda : (actual_client , scenario .provided_addr ),
1323+ provided_addr ,
1324+ sock_name ,
1325+ expected_ip ,
1326+ expected_port ,
1327+ ): # pylint: disable=too-many-positional-arguments
1328+ """Remote address is resolved correctly from accepted socket or fallback."""
1329+ conn = _make_connection (
1330+ conn_manager_with_server ,
1331+ monkeypatch ,
1332+ provided_addr ,
1333+ sock_name ,
1334+ )
1335+ assert conn is not None
1336+ assert conn .remote_addr == expected_ip
1337+ assert conn .remote_port == expected_port
1338+
1339+
1340+ def _fatal_ssl_wrap (sock ):
1341+ raise errors .FatalSSLAlert ('Simulated handshake drop' )
1342+
1343+
1344+ def test_from_server_socket_ssl_failure (conn_manager_with_server , monkeypatch ):
1345+ """A FatalSSLAlert during TLS wrap closes the connection and logs it."""
1346+ server = conn_manager_with_server .server
1347+ monkeypatch .setattr (
1348+ server ,
1349+ 'ssl_adapter' ,
1350+ SimpleNamespace (wrap = _fatal_ssl_wrap ),
13361351 )
13371352
1338- if scenario .expect_error :
1339- with pytest .raises (OSError , match = 'Broken' ):
1340- conn_manager_with_server ._from_server_socket (fake_server_socket )
1341- else :
1342- conn = conn_manager_with_server ._from_server_socket (fake_server_socket )
1353+ logged_messages = []
1354+ server .error_log = lambda msg , ** kwargs : logged_messages .append (msg )
1355+
1356+ conn = _make_connection (conn_manager_with_server , monkeypatch )
1357+ assert conn is None
1358+ assert any ('lost' in msg .lower () for msg in logged_messages )
1359+
1360+
1361+ def test_connection_manager_close_logic (conn_manager_with_server , mocker ):
1362+ """Verify close() shuts down client connections but not server."""
1363+ mgr = conn_manager_with_server
1364+
1365+ mock_conn_1 = mocker .Mock ()
1366+ mock_conn_2 = mocker .Mock ()
1367+
1368+ selector = mocker .Mock ()
1369+ selector .connections = [
1370+ (None , mock_conn_1 ),
1371+ (None , mock_conn_2 ),
1372+ (None , mgr .server ),
1373+ ]
13431374
1344- assert conn is not None
1345- assert conn . sock is actual_client
1375+ mgr . _selector = selector
1376+ mgr . close ()
13461377
1347- expected_ip , expected_port = scenario .expected_addr
1348- assert conn .remote_addr == expected_ip
1349- assert conn .remote_port == expected_port
1350- assert conn_manager_with_server .server .stats ['Accepts' ] == 1
1378+ mock_conn_1 .close .assert_called_once ()
1379+ mock_conn_2 .close .assert_called_once ()
1380+ # Server is excluded from close() — only client connections are closed
1381+ mgr .server .close .assert_not_called ()
1382+ selector .close .assert_called_once ()
0 commit comments