Parametrize tests with concurrency backend (#273)
* Parametrize tests with concurrency backend * Refactor server restart * Add no-backend test
This commit is contained in:
parent
1872ae873b
commit
3674058ff7
@ -633,12 +633,17 @@ class Client(BaseClient):
|
||||
# concurrency backends.
|
||||
# The sync client performs I/O on its own, so it doesn't need to support
|
||||
# arbitrary concurrency backends.
|
||||
# Therefore, we kept the `backend` parameter (for testing/mocking), but enforce
|
||||
# that the concurrency backend derives from the asyncio one.
|
||||
if not isinstance(backend, AsyncioBackend):
|
||||
raise ValueError(
|
||||
"'Client' only supports asyncio-based concurrency backends"
|
||||
)
|
||||
# Therefore, we keep the `backend` parameter (for testing/mocking), but require
|
||||
# that the concurrency backend relies on asyncio.
|
||||
|
||||
if isinstance(backend, AsyncioBackend):
|
||||
return
|
||||
|
||||
if hasattr(backend, "loop"):
|
||||
# Most likely a proxy class.
|
||||
return
|
||||
|
||||
raise ValueError("'Client' only supports asyncio-based concurrency backends")
|
||||
|
||||
def _async_request_data(
|
||||
self, data: RequestData = None
|
||||
|
||||
@ -3,8 +3,22 @@ import pytest
|
||||
import httpx
|
||||
|
||||
|
||||
async def test_get(server, backend):
|
||||
url = "http://127.0.0.1:8000/"
|
||||
async with httpx.AsyncClient(backend=backend) as client:
|
||||
response = await client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.text == "Hello, world!"
|
||||
assert response.http_version == "HTTP/1.1"
|
||||
assert response.headers
|
||||
assert repr(response) == "<Response [200 OK]>"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get(server):
|
||||
async def test_get_no_backend(server):
|
||||
"""
|
||||
Verify that the client is capable of making a simple request if not given a backend.
|
||||
"""
|
||||
url = "http://127.0.0.1:8000/"
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url)
|
||||
@ -15,25 +29,22 @@ async def test_get(server):
|
||||
assert repr(response) == "<Response [200 OK]>"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post(server):
|
||||
async def test_post(server, backend):
|
||||
url = "http://127.0.0.1:8000/"
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(backend=backend) as client:
|
||||
response = await client.post(url, data=b"Hello, world!")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_json(server):
|
||||
async def test_post_json(server, backend):
|
||||
url = "http://127.0.0.1:8000/"
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(backend=backend) as client:
|
||||
response = await client.post(url, json={"text": "Hello, world!"})
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_response(server):
|
||||
async with httpx.AsyncClient() as client:
|
||||
async def test_stream_response(server, backend):
|
||||
async with httpx.AsyncClient(backend=backend) as client:
|
||||
response = await client.request("GET", "http://127.0.0.1:8000/", stream=True)
|
||||
assert response.status_code == 200
|
||||
body = await response.read()
|
||||
@ -41,31 +52,28 @@ async def test_stream_response(server):
|
||||
assert response.content == b"Hello, world!"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_access_content_stream_response(server):
|
||||
async with httpx.AsyncClient() as client:
|
||||
async def test_access_content_stream_response(server, backend):
|
||||
async with httpx.AsyncClient(backend=backend) as client:
|
||||
response = await client.request("GET", "http://127.0.0.1:8000/", stream=True)
|
||||
assert response.status_code == 200
|
||||
with pytest.raises(httpx.ResponseNotRead):
|
||||
response.content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_request(server):
|
||||
async def test_stream_request(server, backend):
|
||||
async def hello_world():
|
||||
yield b"Hello, "
|
||||
yield b"world!"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(backend=backend) as client:
|
||||
response = await client.request(
|
||||
"POST", "http://127.0.0.1:8000/", data=hello_world()
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_raise_for_status(server):
|
||||
async with httpx.AsyncClient() as client:
|
||||
async def test_raise_for_status(server, backend):
|
||||
async with httpx.AsyncClient(backend=backend) as client:
|
||||
for status_code in (200, 400, 404, 500, 505):
|
||||
response = await client.request(
|
||||
"GET", f"http://127.0.0.1:8000/status/{status_code}"
|
||||
@ -79,56 +87,50 @@ async def test_raise_for_status(server):
|
||||
assert response.raise_for_status() is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_options(server):
|
||||
async def test_options(server, backend):
|
||||
url = "http://127.0.0.1:8000/"
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(backend=backend) as client:
|
||||
response = await client.options(url)
|
||||
assert response.status_code == 200
|
||||
assert response.text == "Hello, world!"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_head(server):
|
||||
async def test_head(server, backend):
|
||||
url = "http://127.0.0.1:8000/"
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(backend=backend) as client:
|
||||
response = await client.head(url)
|
||||
assert response.status_code == 200
|
||||
assert response.text == ""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put(server):
|
||||
async def test_put(server, backend):
|
||||
url = "http://127.0.0.1:8000/"
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(backend=backend) as client:
|
||||
response = await client.put(url, data=b"Hello, world!")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch(server):
|
||||
async def test_patch(server, backend):
|
||||
url = "http://127.0.0.1:8000/"
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(backend=backend) as client:
|
||||
response = await client.patch(url, data=b"Hello, world!")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete(server):
|
||||
async def test_delete(server, backend):
|
||||
url = "http://127.0.0.1:8000/"
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(backend=backend) as client:
|
||||
response = await client.delete(url)
|
||||
assert response.status_code == 200
|
||||
assert response.text == "Hello, world!"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_100_continue(server):
|
||||
async def test_100_continue(server, backend):
|
||||
url = "http://127.0.0.1:8000/echo_body"
|
||||
headers = {"Expect": "100-continue"}
|
||||
data = b"Echo request body"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(backend=backend) as client:
|
||||
response = await client.post(url, headers=headers, data=data)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@ -100,36 +100,32 @@ class MockDispatch(AsyncDispatcher):
|
||||
return AsyncResponse(codes.OK, content=b"Hello, world!", request=request)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redirect_301():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_redirect_301(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
response = await client.post("https://example.org/redirect_301")
|
||||
assert response.status_code == codes.OK
|
||||
assert response.url == URL("https://example.org/")
|
||||
assert len(response.history) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redirect_302():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_redirect_302(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
response = await client.post("https://example.org/redirect_302")
|
||||
assert response.status_code == codes.OK
|
||||
assert response.url == URL("https://example.org/")
|
||||
assert len(response.history) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redirect_303():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_redirect_303(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
response = await client.get("https://example.org/redirect_303")
|
||||
assert response.status_code == codes.OK
|
||||
assert response.url == URL("https://example.org/")
|
||||
assert len(response.history) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disallow_redirects():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_disallow_redirects(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
response = await client.post(
|
||||
"https://example.org/redirect_303", allow_redirects=False
|
||||
)
|
||||
@ -145,36 +141,32 @@ async def test_disallow_redirects():
|
||||
assert len(response.history) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_relative_redirect():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_relative_redirect(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
response = await client.get("https://example.org/relative_redirect")
|
||||
assert response.status_code == codes.OK
|
||||
assert response.url == URL("https://example.org/")
|
||||
assert len(response.history) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_scheme_redirect():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_no_scheme_redirect(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
response = await client.get("https://example.org/no_scheme_redirect")
|
||||
assert response.status_code == codes.OK
|
||||
assert response.url == URL("https://example.org/")
|
||||
assert len(response.history) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fragment_redirect():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_fragment_redirect(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
response = await client.get("https://example.org/relative_redirect#fragment")
|
||||
assert response.status_code == codes.OK
|
||||
assert response.url == URL("https://example.org/#fragment")
|
||||
assert len(response.history) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_redirects():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_multiple_redirects(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
response = await client.get("https://example.org/multiple_redirects?count=20")
|
||||
assert response.status_code == codes.OK
|
||||
assert response.url == URL("https://example.org/multiple_redirects")
|
||||
@ -189,16 +181,14 @@ async def test_multiple_redirects():
|
||||
assert len(response.history[1].history) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_too_many_redirects():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_too_many_redirects(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
with pytest.raises(TooManyRedirects):
|
||||
await client.get("https://example.org/multiple_redirects?count=21")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_too_many_redirects_calling_next():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_too_many_redirects_calling_next(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
url = "https://example.org/multiple_redirects?count=21"
|
||||
response = await client.get(url, allow_redirects=False)
|
||||
with pytest.raises(TooManyRedirects):
|
||||
@ -206,16 +196,14 @@ async def test_too_many_redirects_calling_next():
|
||||
response = await response.next()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redirect_loop():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_redirect_loop(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
with pytest.raises(RedirectLoop):
|
||||
await client.get("https://example.org/redirect_loop")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redirect_loop_calling_next():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_redirect_loop_calling_next(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
url = "https://example.org/redirect_loop"
|
||||
response = await client.get(url, allow_redirects=False)
|
||||
with pytest.raises(RedirectLoop):
|
||||
@ -223,9 +211,8 @@ async def test_redirect_loop_calling_next():
|
||||
response = await response.next()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cross_domain_redirect():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_cross_domain_redirect(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
url = "https://example.com/cross_domain"
|
||||
headers = {"Authorization": "abc"}
|
||||
response = await client.get(url, headers=headers)
|
||||
@ -233,9 +220,8 @@ async def test_cross_domain_redirect():
|
||||
assert "authorization" not in response.json()["headers"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_same_domain_redirect():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_same_domain_redirect(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
url = "https://example.org/cross_domain"
|
||||
headers = {"Authorization": "abc"}
|
||||
response = await client.get(url, headers=headers)
|
||||
@ -243,9 +229,8 @@ async def test_same_domain_redirect():
|
||||
assert response.json()["headers"]["authorization"] == "abc"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_body_redirect():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_body_redirect(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
url = "https://example.org/redirect_body"
|
||||
data = b"Example request body"
|
||||
response = await client.post(url, data=data)
|
||||
@ -253,9 +238,8 @@ async def test_body_redirect():
|
||||
assert response.json() == {"body": "Example request body"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cannot_redirect_streaming_body():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_cannot_redirect_streaming_body(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
url = "https://example.org/redirect_body"
|
||||
|
||||
async def streaming_body():
|
||||
@ -265,9 +249,8 @@ async def test_cannot_redirect_streaming_body():
|
||||
await client.post(url, data=streaming_body())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cross_dubdomain_redirect():
|
||||
client = AsyncClient(dispatch=MockDispatch())
|
||||
async def test_cross_dubdomain_redirect(backend):
|
||||
client = AsyncClient(dispatch=MockDispatch(), backend=backend)
|
||||
url = "https://example.com/cross_subdomain"
|
||||
response = await client.get(url)
|
||||
assert response.url == URL("https://www.example.org/cross_subdomain")
|
||||
|
||||
19
tests/concurrency.py
Normal file
19
tests/concurrency.py
Normal file
@ -0,0 +1,19 @@
|
||||
"""
|
||||
This module contains concurrency utilities that are only used in tests, thus not
|
||||
required as part of the ConcurrencyBackend API.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
|
||||
from httpx import AsyncioBackend
|
||||
|
||||
|
||||
@functools.singledispatch
|
||||
async def sleep(backend, seconds: int):
|
||||
raise NotImplementedError # pragma: no cover
|
||||
|
||||
|
||||
@sleep.register(AsyncioBackend)
|
||||
async def _sleep_asyncio(backend, seconds: int):
|
||||
await asyncio.sleep(seconds)
|
||||
@ -10,6 +10,14 @@ from cryptography.hazmat.primitives.serialization import (
|
||||
from uvicorn.config import Config
|
||||
from uvicorn.main import Server
|
||||
|
||||
from httpx import AsyncioBackend
|
||||
|
||||
|
||||
@pytest.fixture(params=[pytest.param(AsyncioBackend, marks=pytest.mark.asyncio)])
|
||||
def backend(request):
|
||||
backend_cls = request.param
|
||||
return backend_cls()
|
||||
|
||||
|
||||
async def app(scope, receive, send):
|
||||
assert scope["type"] == "http"
|
||||
@ -150,3 +158,19 @@ async def https_server(cert_pem_file, cert_private_key_file):
|
||||
finally:
|
||||
server.should_exit = True
|
||||
await task
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def restart(backend):
|
||||
async def asyncio_restart(server):
|
||||
await server.shutdown()
|
||||
await server.startup()
|
||||
|
||||
if isinstance(backend, AsyncioBackend):
|
||||
return asyncio_restart
|
||||
|
||||
# The uvicorn server runs under asyncio, so we will need to figure out
|
||||
# how to restart it under a different I/O library.
|
||||
# This will most likely require running `asyncio_restart` in the threadpool,
|
||||
# but that might not be sufficient.
|
||||
raise NotImplementedError
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
import pytest
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keepalive_connections(server):
|
||||
async def test_keepalive_connections(server, backend):
|
||||
"""
|
||||
Connections should default to staying in a keep-alive state.
|
||||
"""
|
||||
async with httpx.ConnectionPool() as http:
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
response = await http.request("GET", "http://127.0.0.1:8000/")
|
||||
await response.read()
|
||||
assert len(http.active_connections) == 0
|
||||
@ -20,12 +17,11 @@ async def test_keepalive_connections(server):
|
||||
assert len(http.keepalive_connections) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_differing_connection_keys(server):
|
||||
async def test_differing_connection_keys(server, backend):
|
||||
"""
|
||||
Connections to differing connection keys should result in multiple connections.
|
||||
"""
|
||||
async with httpx.ConnectionPool() as http:
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
response = await http.request("GET", "http://127.0.0.1:8000/")
|
||||
await response.read()
|
||||
assert len(http.active_connections) == 0
|
||||
@ -37,14 +33,13 @@ async def test_differing_connection_keys(server):
|
||||
assert len(http.keepalive_connections) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_soft_limit(server):
|
||||
async def test_soft_limit(server, backend):
|
||||
"""
|
||||
The soft_limit config should limit the maximum number of keep-alive connections.
|
||||
"""
|
||||
pool_limits = httpx.PoolLimits(soft_limit=1)
|
||||
|
||||
async with httpx.ConnectionPool(pool_limits=pool_limits) as http:
|
||||
async with httpx.ConnectionPool(pool_limits=pool_limits, backend=backend) as http:
|
||||
response = await http.request("GET", "http://127.0.0.1:8000/")
|
||||
await response.read()
|
||||
assert len(http.active_connections) == 0
|
||||
@ -56,12 +51,11 @@ async def test_soft_limit(server):
|
||||
assert len(http.keepalive_connections) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_response_holds_connection(server):
|
||||
async def test_streaming_response_holds_connection(server, backend):
|
||||
"""
|
||||
A streaming request should hold the connection open until the response is read.
|
||||
"""
|
||||
async with httpx.ConnectionPool() as http:
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
response = await http.request("GET", "http://127.0.0.1:8000/")
|
||||
assert len(http.active_connections) == 1
|
||||
assert len(http.keepalive_connections) == 0
|
||||
@ -72,12 +66,11 @@ async def test_streaming_response_holds_connection(server):
|
||||
assert len(http.keepalive_connections) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_concurrent_connections(server):
|
||||
async def test_multiple_concurrent_connections(server, backend):
|
||||
"""
|
||||
Multiple conncurrent requests should open multiple conncurrent connections.
|
||||
"""
|
||||
async with httpx.ConnectionPool() as http:
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
response_a = await http.request("GET", "http://127.0.0.1:8000/")
|
||||
assert len(http.active_connections) == 1
|
||||
assert len(http.keepalive_connections) == 0
|
||||
@ -95,25 +88,23 @@ async def test_multiple_concurrent_connections(server):
|
||||
assert len(http.keepalive_connections) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_connections(server):
|
||||
async def test_close_connections(server, backend):
|
||||
"""
|
||||
Using a `Connection: close` header should close the connection.
|
||||
"""
|
||||
headers = [(b"connection", b"close")]
|
||||
async with httpx.ConnectionPool() as http:
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
response = await http.request("GET", "http://127.0.0.1:8000/", headers=headers)
|
||||
await response.read()
|
||||
assert len(http.active_connections) == 0
|
||||
assert len(http.keepalive_connections) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_standard_response_close(server):
|
||||
async def test_standard_response_close(server, backend):
|
||||
"""
|
||||
A standard close should keep the connection open.
|
||||
"""
|
||||
async with httpx.ConnectionPool() as http:
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
response = await http.request("GET", "http://127.0.0.1:8000/")
|
||||
await response.read()
|
||||
await response.close()
|
||||
@ -121,31 +112,30 @@ async def test_standard_response_close(server):
|
||||
assert len(http.keepalive_connections) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_premature_response_close(server):
|
||||
async def test_premature_response_close(server, backend):
|
||||
"""
|
||||
A premature close should close the connection.
|
||||
"""
|
||||
async with httpx.ConnectionPool() as http:
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
response = await http.request("GET", "http://127.0.0.1:8000/")
|
||||
await response.close()
|
||||
assert len(http.active_connections) == 0
|
||||
assert len(http.keepalive_connections) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keepalive_connection_closed_by_server_is_reestablished(server):
|
||||
async def test_keepalive_connection_closed_by_server_is_reestablished(
|
||||
server, restart, backend
|
||||
):
|
||||
"""
|
||||
Upon keep-alive connection closed by remote a new connection
|
||||
should be reestablished.
|
||||
"""
|
||||
async with httpx.ConnectionPool() as http:
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
response = await http.request("GET", "http://127.0.0.1:8000/")
|
||||
await response.read()
|
||||
|
||||
# shutdown the server to close the keep-alive connection
|
||||
await server.shutdown()
|
||||
await server.startup()
|
||||
await restart(server)
|
||||
|
||||
response = await http.request("GET", "http://127.0.0.1:8000/")
|
||||
await response.read()
|
||||
@ -153,19 +143,19 @@ async def test_keepalive_connection_closed_by_server_is_reestablished(server):
|
||||
assert len(http.keepalive_connections) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keepalive_http2_connection_closed_by_server_is_reestablished(server):
|
||||
async def test_keepalive_http2_connection_closed_by_server_is_reestablished(
|
||||
server, restart, backend
|
||||
):
|
||||
"""
|
||||
Upon keep-alive connection closed by remote a new connection
|
||||
should be reestablished.
|
||||
"""
|
||||
async with httpx.ConnectionPool() as http:
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
response = await http.request("GET", "http://127.0.0.1:8000/")
|
||||
await response.read()
|
||||
|
||||
# shutdown the server to close the keep-alive connection
|
||||
await server.shutdown()
|
||||
await server.startup()
|
||||
await restart(server)
|
||||
|
||||
response = await http.request("GET", "http://127.0.0.1:8000/")
|
||||
await response.read()
|
||||
@ -173,8 +163,7 @@ async def test_keepalive_http2_connection_closed_by_server_is_reestablished(serv
|
||||
assert len(http.keepalive_connections) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connection_closed_free_semaphore_on_acquire(server):
|
||||
async def test_connection_closed_free_semaphore_on_acquire(server, restart, backend):
|
||||
"""
|
||||
Verify that max_connections semaphore is released
|
||||
properly on a disconnected connection.
|
||||
@ -184,8 +173,7 @@ async def test_connection_closed_free_semaphore_on_acquire(server):
|
||||
await response.read()
|
||||
|
||||
# Close the connection so we're forced to recycle it
|
||||
await server.shutdown()
|
||||
await server.startup()
|
||||
await restart(server)
|
||||
|
||||
response = await http.request("GET", "http://127.0.0.1:8000/")
|
||||
assert response.status_code == 200
|
||||
|
||||
@ -1,44 +1,40 @@
|
||||
import pytest
|
||||
|
||||
from httpx import HTTPConnection
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get(server):
|
||||
conn = HTTPConnection(origin="http://127.0.0.1:8000/")
|
||||
async def test_get(server, backend):
|
||||
conn = HTTPConnection(origin="http://127.0.0.1:8000/", backend=backend)
|
||||
response = await conn.request("GET", "http://127.0.0.1:8000/")
|
||||
await response.read()
|
||||
assert response.status_code == 200
|
||||
assert response.content == b"Hello, world!"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post(server):
|
||||
conn = HTTPConnection(origin="http://127.0.0.1:8000/")
|
||||
async def test_post(server, backend):
|
||||
conn = HTTPConnection(origin="http://127.0.0.1:8000/", backend=backend)
|
||||
response = await conn.request(
|
||||
"GET", "http://127.0.0.1:8000/", data=b"Hello, world!"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_https_get_with_ssl_defaults(https_server):
|
||||
async def test_https_get_with_ssl_defaults(https_server, backend):
|
||||
"""
|
||||
An HTTPS request, with default SSL configuration set on the client.
|
||||
"""
|
||||
conn = HTTPConnection(origin="https://127.0.0.1:8001/", verify=False)
|
||||
conn = HTTPConnection(
|
||||
origin="https://127.0.0.1:8001/", verify=False, backend=backend
|
||||
)
|
||||
response = await conn.request("GET", "https://127.0.0.1:8001/")
|
||||
await response.read()
|
||||
assert response.status_code == 200
|
||||
assert response.content == b"Hello, world!"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_https_get_with_sll_overrides(https_server):
|
||||
async def test_https_get_with_sll_overrides(https_server, backend):
|
||||
"""
|
||||
An HTTPS request, with SSL configuration set on the request.
|
||||
"""
|
||||
conn = HTTPConnection(origin="https://127.0.0.1:8001/")
|
||||
conn = HTTPConnection(origin="https://127.0.0.1:8001/", backend=backend)
|
||||
response = await conn.request("GET", "https://127.0.0.1:8001/", verify=False)
|
||||
await response.read()
|
||||
assert response.status_code == 200
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from httpx import AsyncClient, Client, Response
|
||||
|
||||
from .utils import MockHTTP2Backend
|
||||
@ -29,9 +27,8 @@ def test_http2_get_request():
|
||||
assert json.loads(response.content) == {"method": "GET", "path": "/", "body": ""}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_http2_get_request():
|
||||
backend = MockHTTP2Backend(app=app)
|
||||
async def test_async_http2_get_request(backend):
|
||||
backend = MockHTTP2Backend(app=app, backend=backend)
|
||||
|
||||
async with AsyncClient(backend=backend) as client:
|
||||
response = await client.get("http://example.org")
|
||||
@ -54,9 +51,8 @@ def test_http2_post_request():
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_http2_post_request():
|
||||
backend = MockHTTP2Backend(app=app)
|
||||
async def test_async_http2_post_request(backend):
|
||||
backend = MockHTTP2Backend(app=app, backend=backend)
|
||||
|
||||
async with AsyncClient(backend=backend) as client:
|
||||
response = await client.post("http://example.org", data=b"<data>")
|
||||
@ -87,9 +83,8 @@ def test_http2_multiple_requests():
|
||||
assert json.loads(response_3.content) == {"method": "GET", "path": "/3", "body": ""}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_http2_multiple_requests():
|
||||
backend = MockHTTP2Backend(app=app)
|
||||
async def test_async_http2_multiple_requests(backend):
|
||||
backend = MockHTTP2Backend(app=app, backend=backend)
|
||||
|
||||
async with AsyncClient(backend=backend) as client:
|
||||
response_1 = await client.get("http://example.org/1")
|
||||
@ -125,13 +120,12 @@ def test_http2_reconnect():
|
||||
assert json.loads(response_2.content) == {"method": "GET", "path": "/2", "body": ""}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_http2_reconnect():
|
||||
async def test_async_http2_reconnect(backend):
|
||||
"""
|
||||
If a connection has been dropped between requests, then we should
|
||||
be seemlessly reconnected.
|
||||
"""
|
||||
backend = MockHTTP2Backend(app=app)
|
||||
backend = MockHTTP2Backend(app=app, backend=backend)
|
||||
|
||||
async with AsyncClient(backend=backend) as client:
|
||||
response_1 = await client.get("http://example.org/1")
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from httpx import (
|
||||
AsyncClient,
|
||||
CertTypes,
|
||||
@ -53,14 +51,13 @@ def test_threaded_dispatch():
|
||||
assert response.json() == {"hello": "world"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_threaded_dispatch():
|
||||
async def test_async_threaded_dispatch(backend):
|
||||
"""
|
||||
Use a synchronous 'Dispatcher' class with the async client.
|
||||
Calls to the dispatcher will end up running within a thread pool.
|
||||
"""
|
||||
url = "https://example.org/"
|
||||
async with AsyncClient(dispatch=MockDispatch()) as client:
|
||||
async with AsyncClient(dispatch=MockDispatch(), backend=backend) as client:
|
||||
response = await client.get(url)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import asyncio
|
||||
import ssl
|
||||
import typing
|
||||
|
||||
@ -7,11 +6,13 @@ import h2.connection
|
||||
import h2.events
|
||||
|
||||
from httpx import AsyncioBackend, BaseStream, Request, TimeoutConfig
|
||||
from tests.concurrency import sleep
|
||||
|
||||
|
||||
class MockHTTP2Backend(AsyncioBackend):
|
||||
def __init__(self, app):
|
||||
class MockHTTP2Backend:
|
||||
def __init__(self, app, backend=None):
|
||||
self.app = app
|
||||
self.backend = AsyncioBackend() if backend is None else backend
|
||||
self.server = None
|
||||
|
||||
async def connect(
|
||||
@ -21,15 +22,20 @@ class MockHTTP2Backend(AsyncioBackend):
|
||||
ssl_context: typing.Optional[ssl.SSLContext],
|
||||
timeout: TimeoutConfig,
|
||||
) -> BaseStream:
|
||||
self.server = MockHTTP2Server(self.app)
|
||||
self.server = MockHTTP2Server(self.app, backend=self.backend)
|
||||
return self.server
|
||||
|
||||
# Defer all other attributes and methods to the underlying backend.
|
||||
def __getattr__(self, name: str) -> typing.Any:
|
||||
return getattr(self.backend, name)
|
||||
|
||||
|
||||
class MockHTTP2Server(BaseStream):
|
||||
def __init__(self, app):
|
||||
def __init__(self, app, backend):
|
||||
config = h2.config.H2Configuration(client_side=False)
|
||||
self.conn = h2.connection.H2Connection(config=config)
|
||||
self.app = app
|
||||
self.backend = backend
|
||||
self.buffer = b""
|
||||
self.requests = {}
|
||||
self.close_connection = False
|
||||
@ -40,7 +46,7 @@ class MockHTTP2Server(BaseStream):
|
||||
return "HTTP/2"
|
||||
|
||||
async def read(self, n, timeout, flag=None) -> bytes:
|
||||
await asyncio.sleep(0)
|
||||
await sleep(self.backend, 0)
|
||||
send, self.buffer = self.buffer[:n], self.buffer[n:]
|
||||
return send
|
||||
|
||||
|
||||
@ -46,9 +46,8 @@ def test_asgi():
|
||||
assert response.text == "Hello, World!"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_asgi_async():
|
||||
client = httpx.AsyncClient(app=hello_world)
|
||||
async def test_asgi_async(backend):
|
||||
client = httpx.AsyncClient(app=hello_world, backend=backend)
|
||||
response = await client.get("http://www.example.org/")
|
||||
assert response.status_code == 200
|
||||
assert response.text == "Hello, World!"
|
||||
@ -61,9 +60,8 @@ def test_asgi_upload():
|
||||
assert response.text == "example"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_asgi_upload_async():
|
||||
client = httpx.AsyncClient(app=echo_body)
|
||||
async def test_asgi_upload_async(backend):
|
||||
client = httpx.AsyncClient(app=echo_body, backend=backend)
|
||||
response = await client.post("http://www.example.org/", data=b"example")
|
||||
assert response.status_code == 200
|
||||
assert response.text == "example"
|
||||
@ -75,9 +73,8 @@ def test_asgi_exc():
|
||||
client.get("http://www.example.org/")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_asgi_exc_async():
|
||||
client = httpx.AsyncClient(app=raise_exc)
|
||||
async def test_asgi_exc_async(backend):
|
||||
client = httpx.AsyncClient(app=raise_exc, backend=backend)
|
||||
with pytest.raises(ValueError):
|
||||
await client.get("http://www.example.org/")
|
||||
|
||||
@ -88,8 +85,7 @@ def test_asgi_exc_after_response():
|
||||
client.get("http://www.example.org/")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_asgi_exc_after_response_async():
|
||||
client = httpx.AsyncClient(app=raise_exc_after_response)
|
||||
async def test_asgi_exc_after_response_async(backend):
|
||||
client = httpx.AsyncClient(app=raise_exc_after_response, backend=backend)
|
||||
with pytest.raises(ValueError):
|
||||
await client.get("http://www.example.org/")
|
||||
|
||||
@ -11,40 +11,36 @@ from httpx import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_timeout(server):
|
||||
timeout = TimeoutConfig(read_timeout=0.000001)
|
||||
async def test_read_timeout(server, backend):
|
||||
timeout = TimeoutConfig(read_timeout=1e-6)
|
||||
|
||||
async with AsyncClient(timeout=timeout) as client:
|
||||
async with AsyncClient(timeout=timeout, backend=backend) as client:
|
||||
with pytest.raises(ReadTimeout):
|
||||
await client.get("http://127.0.0.1:8000/slow_response")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_timeout(server):
|
||||
timeout = TimeoutConfig(write_timeout=0.000001)
|
||||
async def test_write_timeout(server, backend):
|
||||
timeout = TimeoutConfig(write_timeout=1e-6)
|
||||
|
||||
async with AsyncClient(timeout=timeout) as client:
|
||||
async with AsyncClient(timeout=timeout, backend=backend) as client:
|
||||
with pytest.raises(WriteTimeout):
|
||||
data = b"*" * 1024 * 1024 * 100
|
||||
await client.put("http://127.0.0.1:8000/slow_response", data=data)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_timeout(server):
|
||||
timeout = TimeoutConfig(connect_timeout=0.000001)
|
||||
async def test_connect_timeout(server, backend):
|
||||
timeout = TimeoutConfig(connect_timeout=1e-6)
|
||||
|
||||
async with AsyncClient(timeout=timeout) as client:
|
||||
async with AsyncClient(timeout=timeout, backend=backend) as client:
|
||||
with pytest.raises(ConnectTimeout):
|
||||
# See https://stackoverflow.com/questions/100841/
|
||||
await client.get("http://10.255.255.1/")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pool_timeout(server):
|
||||
pool_limits = PoolLimits(hard_limit=1, pool_timeout=0.000001)
|
||||
async def test_pool_timeout(server, backend):
|
||||
pool_limits = PoolLimits(hard_limit=1, pool_timeout=1e-6)
|
||||
|
||||
async with AsyncClient(pool_limits=pool_limits) as client:
|
||||
async with AsyncClient(pool_limits=pool_limits, backend=backend) as client:
|
||||
response = await client.get("http://127.0.0.1:8000/", stream=True)
|
||||
|
||||
with pytest.raises(PoolTimeout):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user