Skip to content

Commit 56447f4

Browse files
Add integration testing for UDS case and nagle
1 parent aa31c2a commit 56447f4

File tree

6 files changed

+106
-23
lines changed

6 files changed

+106
-23
lines changed

httpcore/_backends/anyio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ async def connect_unix_socket(
127127
path: str,
128128
timeout: typing.Optional[float] = None,
129129
socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None,
130-
) -> AsyncNetworkStream: # pragma: nocover
130+
) -> AsyncNetworkStream:
131131
exc_map = {
132132
TimeoutError: ConnectTimeout,
133133
OSError: ConnectError,

httpcore/_backends/asyncio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ async def connect_unix_socket(
194194
path: str,
195195
timeout: Optional[float] = None,
196196
socket_options: Optional[Iterable[SOCKET_OPTION]] = None,
197-
) -> AsyncNetworkStream: # pragma: nocover
197+
) -> AsyncNetworkStream:
198198
exc_map: Dict[Type[Exception], Type[Exception]] = {
199199
asyncio.TimeoutError: ConnectTimeout,
200200
OSError: ConnectError,

httpcore/_backends/trio.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,6 @@ async def connect_tcp(
117117
local_address: typing.Optional[str] = None,
118118
socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None,
119119
) -> AsyncNetworkStream:
120-
# By default for TCP sockets, trio enables TCP_NODELAY.
121-
# https://trio.readthedocs.io/en/stable/reference-io.html#trio.SocketStream
122-
if socket_options is None:
123-
socket_options = [] # pragma: no cover
124120
timeout_or_inf = float("inf") if timeout is None else timeout
125121
exc_map: ExceptionMapping = {
126122
trio.TooSlowError: ConnectTimeout,
@@ -132,18 +128,15 @@ async def connect_tcp(
132128
stream: trio.abc.Stream = await trio.open_tcp_stream(
133129
host=host, port=port, local_address=local_address
134130
)
135-
for option in socket_options:
136-
stream.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover
131+
self._set_socket_options(stream, socket_options)
137132
return TrioStream(stream)
138133

139134
async def connect_unix_socket(
140135
self,
141136
path: str,
142137
timeout: typing.Optional[float] = None,
143138
socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None,
144-
) -> AsyncNetworkStream: # pragma: nocover
145-
if socket_options is None:
146-
socket_options = []
139+
) -> AsyncNetworkStream:
147140
timeout_or_inf = float("inf") if timeout is None else timeout
148141
exc_map: ExceptionMapping = {
149142
trio.TooSlowError: ConnectTimeout,
@@ -153,9 +146,20 @@ async def connect_unix_socket(
153146
with map_exceptions(exc_map):
154147
with trio.fail_after(timeout_or_inf):
155148
stream: trio.abc.Stream = await trio.open_unix_socket(path)
156-
for option in socket_options:
157-
stream.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover
149+
self._set_socket_options(stream, socket_options)
158150
return TrioStream(stream)
159151

160152
async def sleep(self, seconds: float) -> None:
161153
await trio.sleep(seconds) # pragma: nocover
154+
155+
def _set_socket_options(
156+
self,
157+
stream: trio.abc.Stream,
158+
socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None,
159+
) -> None:
160+
# By default for TCP sockets, trio enables TCP_NODELAY.
161+
# https://trio.readthedocs.io/en/stable/reference-io.html#trio.SocketStream
162+
if not socket_options:
163+
return
164+
for option in socket_options:
165+
stream.setsockopt(*option) # type: ignore[attr-defined]

tests/_async/test_integration.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import os
12
import socket
23
import ssl
4+
from tempfile import gettempdir
35

46
import pytest
57
import uvicorn
@@ -70,14 +72,26 @@ async def test_socket_options(
7072
assert bool(opt) is keep_alive_enabled
7173

7274

75+
@pytest.mark.anyio
76+
async def test_socket_no_nagle(server: Server, server_url: str) -> None:
77+
async with httpcore.AsyncConnectionPool() as pool:
78+
response = await pool.request("GET", server_url)
79+
assert response.status == 200
80+
81+
stream = response.extensions["network_stream"]
82+
sock = stream.get_extra_info("socket")
83+
opt = sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY)
84+
assert bool(opt) is True
85+
86+
7387
@pytest.mark.anyio
7488
async def test_pool_recovers_from_connection_breakage(
7589
server_config: uvicorn.Config, server_url: str
7690
) -> None:
7791
async with httpcore.AsyncConnectionPool(
7892
max_connections=1, max_keepalive_connections=1, keepalive_expiry=10
7993
) as pool:
80-
with Server(config=server_config).run_in_thread():
94+
with Server(server_config).run_in_thread():
8195
response = await pool.request("GET", server_url)
8296
assert response.status == 200
8397

@@ -91,7 +105,7 @@ async def test_pool_recovers_from_connection_breakage(
91105
stream.get_extra_info("is_readable") is True
92106
), "Should break by coming readable"
93107

94-
with Server(config=server_config).run_in_thread():
108+
with Server(server_config).run_in_thread():
95109
assert len(pool.connections) == 1
96110
assert pool.connections[0] is conn, "Should be the broken connection"
97111

@@ -100,3 +114,22 @@ async def test_pool_recovers_from_connection_breakage(
100114

101115
assert len(pool.connections) == 1
102116
assert pool.connections[0] is not conn, "Should be a new connection"
117+
118+
119+
@pytest.mark.anyio
120+
async def test_unix_domain_socket(server_port, server_config, server_url):
121+
uds = f"{gettempdir()}/test_httpcore_app.sock"
122+
if os.path.exists(uds):
123+
os.remove(uds) # pragma: nocover
124+
125+
uds_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
126+
try:
127+
uds_sock.bind(uds)
128+
129+
with Server(server_config).run_in_thread(sockets=[uds_sock]):
130+
async with httpcore.AsyncConnectionPool(uds=uds) as pool:
131+
response = await pool.request("GET", server_url)
132+
assert response.status == 200
133+
finally:
134+
uds_sock.close()
135+
os.remove(uds)

tests/_sync/test_integration.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import os
12
import socket
23
import ssl
4+
from tempfile import gettempdir
35

46
import pytest
57
import uvicorn
@@ -71,13 +73,25 @@ def test_socket_options(
7173

7274

7375

76+
def test_socket_no_nagle(server: Server, server_url: str) -> None:
77+
with httpcore.ConnectionPool() as pool:
78+
response = pool.request("GET", server_url)
79+
assert response.status == 200
80+
81+
stream = response.extensions["network_stream"]
82+
sock = stream.get_extra_info("socket")
83+
opt = sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY)
84+
assert bool(opt) is True
85+
86+
87+
7488
def test_pool_recovers_from_connection_breakage(
7589
server_config: uvicorn.Config, server_url: str
7690
) -> None:
7791
with httpcore.ConnectionPool(
7892
max_connections=1, max_keepalive_connections=1, keepalive_expiry=10
7993
) as pool:
80-
with Server(config=server_config).run_in_thread():
94+
with Server(server_config).run_in_thread():
8195
response = pool.request("GET", server_url)
8296
assert response.status == 200
8397

@@ -91,7 +105,7 @@ def test_pool_recovers_from_connection_breakage(
91105
stream.get_extra_info("is_readable") is True
92106
), "Should break by coming readable"
93107

94-
with Server(config=server_config).run_in_thread():
108+
with Server(server_config).run_in_thread():
95109
assert len(pool.connections) == 1
96110
assert pool.connections[0] is conn, "Should be the broken connection"
97111

@@ -100,3 +114,22 @@ def test_pool_recovers_from_connection_breakage(
100114

101115
assert len(pool.connections) == 1
102116
assert pool.connections[0] is not conn, "Should be a new connection"
117+
118+
119+
120+
def test_unix_domain_socket(server_port, server_config, server_url):
121+
uds = f"{gettempdir()}/test_httpcore_app.sock"
122+
if os.path.exists(uds):
123+
os.remove(uds) # pragma: nocover
124+
125+
uds_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
126+
try:
127+
uds_sock.bind(uds)
128+
129+
with Server(server_config).run_in_thread(sockets=[uds_sock]):
130+
with httpcore.ConnectionPool(uds=uds) as pool:
131+
response = pool.request("GET", server_url)
132+
assert response.status == 200
133+
finally:
134+
uds_sock.close()
135+
os.remove(uds)

tests/conftest.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import socket
12
import time
23
from contextlib import contextmanager
34
from threading import Thread
4-
from typing import Generator, Iterator
5+
from typing import Any, Awaitable, Callable, Generator, Iterator, List, Optional
56

67
import pytest
78
import uvicorn
@@ -31,12 +32,17 @@ def anyio_backend(request, monkeypatch):
3132

3233
class Server(uvicorn.Server):
3334
@contextmanager
34-
def run_in_thread(self) -> Generator[None, None, None]:
35-
thread = Thread(target=self.run)
35+
def run_in_thread(
36+
self, sockets: Optional[List[socket.socket]] = None
37+
) -> Generator[None, None, None]:
38+
thread = Thread(target=lambda: self.run(sockets))
3639
thread.start()
40+
start_time = time.monotonic()
3741
try:
3842
while not self.started:
3943
time.sleep(0.01)
44+
if (time.monotonic() - start_time) > 5:
45+
raise TimeoutError() # pragma: nocover
4046
yield
4147
finally:
4248
self.should_exit = True
@@ -54,7 +60,7 @@ def server_url(server_port: int) -> str:
5460

5561

5662
@pytest.fixture
57-
def server_config(server_port: int) -> uvicorn.Config:
63+
def server_app() -> Callable[[Any, Any, Any], Awaitable[None]]:
5864
async def app(scope, receive, send):
5965
assert scope["type"] == "http"
6066
assert not (await receive()).get("more_body", False)
@@ -68,10 +74,17 @@ async def app(scope, receive, send):
6874
await send(start)
6975
await send(body)
7076

71-
return uvicorn.Config(app, port=server_port, log_level="error")
77+
return app
78+
79+
80+
@pytest.fixture
81+
def server_config(
82+
server_port: int, server_app: Callable[[Any, Any, Any], Awaitable[None]]
83+
) -> uvicorn.Config:
84+
return uvicorn.Config(server_app, port=server_port, log_level="error")
7285

7386

7487
@pytest.fixture
7588
def server(server_config: uvicorn.Config) -> Iterator[None]:
76-
with Server(config=server_config).run_in_thread():
89+
with Server(server_config).run_in_thread():
7790
yield

0 commit comments

Comments
 (0)