22
33import asyncio
44import socket
5- import sys
5+ from contextlib import contextmanager
66from http .server import BaseHTTPRequestHandler , ThreadingHTTPServer
77from threading import Thread
88from unittest .mock import patch
1717
1818@pytest .fixture (autouse = True )
1919def clean_env (monkeypatch ):
20- if hasattr (transport , "_log_keepalive_unavailable" ):
21- transport ._log_keepalive_unavailable .cache_clear ()
2220 for suffix in ("" , "_IDLE" , "_INTERVAL" , "_COUNT" ):
2321 monkeypatch .delenv ("UNSTRUCTURED_CLIENT_TCP_KEEPALIVE" + suffix , raising = False )
2422 monkeypatch .setenv ("NO_PROXY" , "*" )
2523
2624
27- @pytest .mark .parametrize ("platform" , ["linux" , "macos" , "macos_old" , "windows" ])
28- def test_unit_platform_options (monkeypatch , platform ):
29- for name in ("TCP_KEEPIDLE" , "TCP_KEEPALIVE" , "TCP_KEEPINTVL" , "TCP_KEEPCNT" ):
30- monkeypatch .delattr (socket , name , raising = False )
31- expected = [(socket .SOL_SOCKET , socket .SO_KEEPALIVE , 1 )]
32- if platform != "windows" :
33- monkeypatch .setattr (socket , "TCP_KEEPIDLE" if platform == "linux" else "TCP_KEEPALIVE" , 100 , raising = False )
34- expected .append ((socket .IPPROTO_TCP , 100 , 60 ))
35- if platform != "macos_old" :
36- monkeypatch .setattr (socket , "TCP_KEEPINTVL" , 101 , raising = False )
37- monkeypatch .setattr (socket , "TCP_KEEPCNT" , 102 , raising = False )
38- expected += [(socket .IPPROTO_TCP , 101 , 30 ), (socket .IPPROTO_TCP , 102 , 5 )]
39- assert transport .resolve_tcp_keepalive ().socket_options () == expected
25+ DEFAULTS = transport .TCPKeepAlive (enabled = True )
26+ BASE_OPTIONS = [(socket .SOL_SOCKET , socket .SO_KEEPALIVE , 1 )]
4027
4128
42- @pytest .mark .parametrize ("value" , ["0" , "false" , "False" , " FALSE " ])
43- def test_unit_disabled (monkeypatch , value ):
44- monkeypatch .setenv ("UNSTRUCTURED_CLIENT_TCP_KEEPALIVE" , value )
45- assert transport .resolve_tcp_keepalive ().socket_options () == []
29+ def _close (client ):
30+ if isinstance (client , httpx .AsyncClient ):
31+ asyncio .run (client .aclose ())
32+ else :
33+ client .close ()
4634
4735
48- @pytest .mark .parametrize ("suffix,default" , [("IDLE" , 60 ), ("INTERVAL" , 30 ), ("COUNT" , 5 )])
49- @pytest .mark .parametrize ("value" , ["0" , "-1" , "bad" , "1.5" , "" ])
50- def test_unit_invalid_value (monkeypatch , caplog , suffix , default , value ):
51- monkeypatch .setenv ("UNSTRUCTURED_CLIENT_TCP_KEEPALIVE_" + suffix , value )
52- assert getattr (transport .resolve_tcp_keepalive (), suffix .lower ()) == default
53- assert "using default" in caplog .text
36+ @contextmanager
37+ def local_server (handler ):
38+ server = ThreadingHTTPServer (("127.0.0.1" , 0 ), handler )
39+ thread = Thread (target = server .serve_forever , daemon = True )
40+ thread .start ()
41+ try :
42+ yield server .server_port
43+ finally :
44+ server .shutdown ()
45+ server .server_close ()
46+ thread .join ()
47+
48+
49+ @pytest .mark .parametrize ("attrs_to_set,expected_options" , [
50+ ({"TCP_KEEPIDLE" : 100 , "TCP_KEEPINTVL" : 101 , "TCP_KEEPCNT" : 102 },
51+ [(socket .IPPROTO_TCP , 100 , DEFAULTS .idle ),
52+ (socket .IPPROTO_TCP , 101 , DEFAULTS .interval ),
53+ (socket .IPPROTO_TCP , 102 , DEFAULTS .count )]),
54+ ({"TCP_KEEPALIVE" : 100 , "TCP_KEEPINTVL" : 101 , "TCP_KEEPCNT" : 102 },
55+ [(socket .IPPROTO_TCP , 100 , DEFAULTS .idle ),
56+ (socket .IPPROTO_TCP , 101 , DEFAULTS .interval ),
57+ (socket .IPPROTO_TCP , 102 , DEFAULTS .count )]),
58+ ({"TCP_KEEPALIVE" : 100 }, [(socket .IPPROTO_TCP , 100 , DEFAULTS .idle )]),
59+ ({}, []),
60+ ], ids = ["linux" , "macos" , "macos_old" , "windows" ])
61+ def test_unit_platform_options (monkeypatch , attrs_to_set , expected_options ):
62+ for name in ("TCP_KEEPIDLE" , "TCP_KEEPALIVE" , "TCP_KEEPINTVL" , "TCP_KEEPCNT" ):
63+ monkeypatch .delattr (socket , name , raising = False )
64+ for name , value in attrs_to_set .items ():
65+ monkeypatch .setattr (socket , name , value , raising = False )
66+ assert transport .resolve_tcp_keepalive ().socket_options () == BASE_OPTIONS + expected_options
5467
5568
56- @pytest .mark .parametrize ("suffix,maximum,default" , [
57- ("IDLE" , 32767 , 60 ), ("INTERVAL" , 32767 , 30 ), ("COUNT" , 127 , 5 ),
69+ @pytest .mark .parametrize ("value" , ["0" , "false" , "False" , " FALSE " ])
70+ def test_unit_disabled (monkeypatch , caplog , value ):
71+ monkeypatch .setenv ("UNSTRUCTURED_CLIENT_TCP_KEEPALIVE" , value )
72+ for suffix in ("IDLE" , "INTERVAL" , "COUNT" ):
73+ monkeypatch .setenv ("UNSTRUCTURED_CLIENT_TCP_KEEPALIVE_" + suffix , "invalid" )
74+ settings = transport .resolve_tcp_keepalive ()
75+ assert settings .socket_options () == []
76+ assert settings .describe () == "off"
77+ assert not caplog .records
78+
79+
80+ @pytest .mark .parametrize ("suffix,value,expected" , [
81+ (suffix , value , expected )
82+ for suffix , maximum in (
83+ ("IDLE" , transport .MAX_IDLE ),
84+ ("INTERVAL" , transport .MAX_INTERVAL ),
85+ ("COUNT" , transport .MAX_COUNT ),
86+ )
87+ for value , expected in (
88+ * [(invalid , getattr (DEFAULTS , suffix .lower ()))
89+ for invalid in ("0" , "-1" , "bad" , "1.5" , "" , str (maximum + 1 ))],
90+ ("1" , 1 ),
91+ (str (maximum ), maximum ),
92+ )
5893])
59- @pytest .mark .parametrize ("above_maximum" , [False , True ])
60- def test_unit_value_bounds (monkeypatch , caplog , suffix , maximum , default , above_maximum ):
61- monkeypatch .setenv ("UNSTRUCTURED_CLIENT_TCP_KEEPALIVE_" + suffix ,
62- str (maximum + int (above_maximum )))
63- expected = default if above_maximum else maximum
94+ def test_unit_value_bounds (monkeypatch , caplog , suffix , value , expected ):
95+ monkeypatch .setenv ("UNSTRUCTURED_CLIENT_TCP_KEEPALIVE_" + suffix , value )
6496 assert getattr (transport .resolve_tcp_keepalive (), suffix .lower ()) == expected
65- assert ("using default" in caplog .text ) == above_maximum
97+ assert ("using default" in caplog .text ) == ( value != str ( expected ))
6698
6799
68100@pytest .mark .parametrize ("asynchronous" , [False , True ])
69101def test_unit_missing_proxy_helper_client_options (monkeypatch , asynchronous ):
70102 monkeypatch .setattr (transport , "get_environment_proxies" , None )
71103 if asynchronous :
72- options = {"verify" : False , "cert" : None , "limits" : httpx .Limits (max_connections = 3 )}
104+ options = {"verify" : False , "cert" : "client.pem" , "limits" : httpx .Limits (max_connections = 3 )}
73105 with patch .object (httpx , "AsyncClient" ) as constructor :
74- transport .create_async_http_client (timeout = 42 , transport_options = options )
106+ transport .create_async_http_client (timeout = 42 , ** options )
75107 constructor .assert_called_once_with (timeout = 42 , ** options )
76108 else :
77109 with patch .object (httpx , "Client" ) as constructor :
78110 transport .create_http_client ()
79111 constructor .assert_called_once_with ()
80112
81113
82- def test_unit_missing_proxy_helper_logs_once (monkeypatch , caplog ):
83- monkeypatch .setattr (transport , "get_environment_proxies" , None )
84- caplog .set_level ("DEBUG" , logger = transport .logger .name )
85- with transport .create_http_client ():
86- pass
87- asyncio .run (transport .create_async_http_client ().aclose ())
88- messages = [record for record in caplog .records if "TCP keepalive is unavailable" in record .message ]
89- assert len (messages ) == 1
90- assert messages [0 ].levelname == "DEBUG"
91-
92-
93114def test_unit_custom_values (monkeypatch ):
94115 for suffix , value in (("IDLE" , "5" ), ("INTERVAL" , "6" ), ("COUNT" , "7" )):
95116 monkeypatch .setenv ("UNSTRUCTURED_CLIENT_TCP_KEEPALIVE_" + suffix , value )
96- assert transport .resolve_tcp_keepalive () == transport .TCPKeepAlive (True , 5 , 6 , 7 )
117+ settings = transport .resolve_tcp_keepalive ()
118+ assert settings == transport .TCPKeepAlive (True , 5 , 6 , 7 )
119+ assert settings .describe () == "idle=5s interval=6s count=7"
97120
98121
99122def test_unit_sdk_owned_clients ():
100123 sdk = UnstructuredClient ()
101124 options = transport .resolve_tcp_keepalive ().socket_options ()
102125 assert sdk .sdk_configuration .client ._transport .base_transport ._pool ._socket_options == options
103126 assert sdk .sdk_configuration .async_client ._transport ._pool ._socket_options == options
104- sdk .sdk_configuration .client . close ( )
105- asyncio . run (sdk .sdk_configuration .async_client . aclose () )
127+ _close ( sdk .sdk_configuration .client )
128+ _close (sdk .sdk_configuration .async_client )
106129
107130
108131def test_unit_supplied_clients_untouched ():
109132 sync = httpx .Client ()
110133 asynchronous = httpx .AsyncClient ()
111134 sync_transport = sync ._transport
112135 async_transport = asynchronous ._transport
113- sdk = UnstructuredClient (client = sync , async_client = asynchronous )
114- assert sdk .sdk_configuration .client is sync
115- assert sdk .sdk_configuration .async_client is asynchronous
136+ UnstructuredClient (client = sync , async_client = asynchronous )
116137 assert sync ._transport .base_transport is sync_transport
117138 assert asynchronous ._transport is async_transport
118- assert sync_transport ._pool ._socket_options is None
119- assert asynchronous ._transport ._pool ._socket_options is None
120- sync .close ()
121- asyncio .run (asynchronous .aclose ())
139+ _close (sync )
140+ _close (asynchronous )
122141
123142
124143@pytest .mark .parametrize ("missing_proxy_helper" , [False , True ])
@@ -132,21 +151,15 @@ def test_unit_environment_proxy_routes(monkeypatch, asynchronous, missing_proxy_
132151 proxied = client ._transport_for_url (httpx .URL ("https://api.example" ))
133152 assert isinstance (proxied ._pool , httpcore .AsyncHTTPProxy if asynchronous else httpcore .HTTPProxy )
134153 assert proxied ._pool ._proxy_url .host == b"127.0.0.1"
154+ assert proxied ._pool ._socket_options is None
135155 if missing_proxy_helper :
136156 assert client ._transport ._pool ._socket_options is None
137- assert proxied ._pool ._socket_options is None
138157 assert client ._transport_for_url (httpx .URL ("https://excluded.example" )) is client ._transport
139- if asynchronous :
140- asyncio .run (client .aclose ())
141- else :
142- client .close ()
158+ _close (client )
143159
144160
145161@pytest .mark .parametrize ("asynchronous" , [False , True ])
146162def test_unit_real_socket_keepalive (asynchronous ):
147- if not hasattr (socket , "SO_KEEPALIVE" ):
148- pytest .skip ("SO_KEEPALIVE unavailable" )
149-
150163 class Handler (BaseHTTPRequestHandler ):
151164 protocol_version = "HTTP/1.1"
152165
@@ -156,39 +169,26 @@ def do_GET(self):
156169 self .end_headers ()
157170 self .wfile .write (b"ok" )
158171
159- server = ThreadingHTTPServer (("127.0.0.1" , 0 ), Handler )
160- thread = Thread (target = server .serve_forever , daemon = True )
161- thread .start ()
162- url = f"http://127.0.0.1:{ server .server_port } "
163-
164172 def check (response ):
165173 stream = response .extensions ["network_stream" ]
166174 sock = stream .get_extra_info ("socket" )
167- enabled = sock .getsockopt (socket .SOL_SOCKET , socket .SO_KEEPALIVE )
168- # Darwin reports the option bit mask rather than a normalized boolean.
169- if sys .platform == "darwin" :
170- assert enabled != 0
171- else :
172- assert enabled == 1
175+ assert sock .getsockopt (socket .SOL_SOCKET , socket .SO_KEEPALIVE )
173176
174177 async def request ():
175178 async with transport .create_async_http_client () as client :
176179 async with client .stream ("GET" , url ) as response :
177180 check (response )
178181 await response .aread ()
179182
180- try :
183+ with local_server (Handler ) as port :
184+ url = f"http://127.0.0.1:{ port } "
181185 if asynchronous :
182186 asyncio .run (request ())
183187 else :
184188 with transport .create_http_client () as client :
185189 with client .stream ("GET" , url ) as response :
186190 check (response )
187191 response .read ()
188- finally :
189- server .shutdown ()
190- server .server_close ()
191- thread .join ()
192192
193193
194194@pytest .mark .parametrize ("asynchronous" , [False , True ])
@@ -200,25 +200,17 @@ def do_CONNECT(self):
200200 destinations .append (self .path )
201201 self .send_error (502 )
202202
203- server = ThreadingHTTPServer (("127.0.0.1" , 0 ), ProxyHandler )
204- thread = Thread (target = server .serve_forever , daemon = True )
205- thread .start ()
206- monkeypatch .setenv ("HTTPS_PROXY" , f"http://127.0.0.1:{ server .server_port } " )
207- monkeypatch .setenv ("NO_PROXY" , "" )
208-
209203 async def request ():
210204 async with transport .create_async_http_client () as client :
211205 await client .get ("https://proxy-target.invalid/" )
212206
213- try :
207+ with local_server (ProxyHandler ) as port :
208+ monkeypatch .setenv ("HTTPS_PROXY" , f"http://127.0.0.1:{ port } " )
209+ monkeypatch .setenv ("NO_PROXY" , "" )
214210 with pytest .raises (httpx .ProxyError , match = "502" ):
215211 if asynchronous :
216212 asyncio .run (request ())
217213 else :
218214 with transport .create_http_client () as client :
219215 client .get ("https://proxy-target.invalid/" )
220216 assert destinations == ["proxy-target.invalid:443" ]
221- finally :
222- server .shutdown ()
223- server .server_close ()
224- thread .join ()
0 commit comments