from __future__ import annotations
import asyncio
import sys
import typing
import pytest
from eggfetch.compat.httpx import Client, AsyncClient, Request, Response
from eggfetch.compat.httpx._mock import MockTransport, _build_response
from eggfetch.compat.httpx._exceptions import RequestError
class TestAsyncContextManager:
@pytest.mark.asyncio
async def test_async_context_manager_enter_exit(self):
async with AsyncClient() as client:
assert not client.is_closed
assert client.is_closed
@pytest.mark.asyncio
async def test_async_context_manager_with_transport(self):
def handler(request):
return _build_response(200, text="ok")
async with AsyncClient(transport=MockTransport(handler)) as client:
resp = await client.get("http://test/")
assert resp.status_code == 200
class TestSyncHandlerAsyncClient:
@pytest.mark.asyncio
async def test_sync_handler_via_async_client(self):
def handler(request):
return _build_response(200, text="sync-ok")
async with AsyncClient(transport=MockTransport(handler)) as client:
resp = await client.get("http://test/")
assert resp.status_code == 200
assert resp.text == "sync-ok"
class TestAsyncHandlerAsyncClient:
@pytest.mark.asyncio
async def test_async_handler_via_async_client(self):
async def handler(request):
return _build_response(200, text="async-ok")
async with AsyncClient(async_transport=MockTransport(handler)) as client:
resp = await client.get("http://test/")
assert resp.status_code == 200
assert resp.text == "async-ok"
class TestSyncClientRejectsAsyncHandler:
def test_sync_client_with_async_handler_raises(self):
async def handler(request):
return _build_response(200)
with pytest.raises(RuntimeError, match="async"):
with Client(transport=MockTransport(handler)) as client:
client.get("http://test/")
class TestClosedClientDetection:
@pytest.mark.asyncio
async def test_async_send_after_close_raises(self):
async with AsyncClient() as client:
pass
with pytest.raises(RuntimeError, match="closed"):
req = Request("GET", "http://test/")
await client.send(req)
def test_sync_send_after_close_raises(self):
with Client() as client:
pass
with pytest.raises(RuntimeError, match="closed"):
req = Request("GET", "http://test/")
client.send(req)
class TestEventLoopDetection:
def test_sync_client_works_standalone(self):
def handler(request):
return _build_response(200, text="standalone")
with Client(transport=MockTransport(handler)) as client:
resp = client.get("http://test/")
assert resp.text == "standalone"
class TestSendTypeValidation:
def test_sync_send_rejects_string(self):
with Client() as client:
with pytest.raises(TypeError, match="Request"):
client.send("not-a-request")
@pytest.mark.asyncio
async def test_async_send_rejects_string(self):
async with AsyncClient() as client:
with pytest.raises(TypeError, match="Request"):
await client.send("not-a-request")
class TestAsyncioLeakedTaskDetection:
@pytest.mark.asyncio
async def test_no_leaked_tasks_after_send(self):
def handler(request):
return _build_response(200, text="ok")
before_tasks = asyncio.all_tasks()
async with AsyncClient(
async_transport=MockTransport(handler)
) as client:
resp = await client.get("http://test/")
assert resp.status_code == 200
after_tasks = asyncio.all_tasks()
new_tasks = after_tasks - before_tasks
assert len(new_tasks) == 0, f"Leaked tasks: {new_tasks}"
@pytest.mark.asyncio
async def test_no_leaked_tasks_after_multiple_requests(self):
def handler(request):
return _build_response(200, text="ok")
before_tasks = asyncio.all_tasks()
async with AsyncClient(
async_transport=MockTransport(handler)
) as client:
for _ in range(5):
resp = await client.get("http://test/")
assert resp.status_code == 200
after_tasks = asyncio.all_tasks()
new_tasks = after_tasks - before_tasks
assert len(new_tasks) == 0, f"Leaked tasks: {new_tasks}"
@pytest.mark.asyncio
async def test_no_leaked_tasks_after_error(self):
def handler(request):
raise ValueError("intentional error")
before_tasks = asyncio.all_tasks()
async with AsyncClient(
async_transport=MockTransport(handler)
) as client:
with pytest.raises(ValueError):
await client.get("http://test/")
after_tasks = asyncio.all_tasks()
new_tasks = after_tasks - before_tasks
assert len(new_tasks) == 0, f"Leaked tasks: {new_tasks}"
class TestEventLoopNonBlocking:
@pytest.mark.asyncio
async def test_concurrent_requests_dont_block(self):
import time
call_times = []
async def handler(request):
call_times.append(time.monotonic())
return _build_response(200, text="ok")
async with AsyncClient(
async_transport=MockTransport(handler)
) as client:
import asyncio as _asyncio
results = await _asyncio.gather(
client.get("http://test/1"),
client.get("http://test/2"),
client.get("http://test/3"),
)
assert len(results) == 3
for r in results:
assert r.status_code == 200
@pytest.mark.asyncio
async def test_async_client_works_in_running_loop(self):
def handler(request):
return _build_response(200, text="in-loop")
async with AsyncClient(
async_transport=MockTransport(handler)
) as client:
resp = await client.get("http://test/")
assert resp.text == "in-loop"
class TestClientExtensions:
def test_sync_client_extensions_stored(self):
with Client(extensions={"ext_key": "ext_val"}) as client:
assert client._extensions == {"ext_key": "ext_val"}
def test_async_client_extensions_stored(self):
client = AsyncClient(extensions={"ext_key": "ext_val"})
assert client._extensions == {"ext_key": "ext_val"}
def test_client_default_extensions_empty(self):
with Client() as client:
assert client._extensions == {}
def test_async_client_default_extensions_empty(self):
client = AsyncClient()
assert client._extensions == {}