Files
2026-07-13 21:35:56 +08:00

126 lines
4.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Runnable Example: FastAPI / Web Server
**当应用是 Web 服务器时**FastAPI、Flask、Starlette),需要测试完整的 HTTP 请求管道。
**方法**:使用 `httpx.AsyncClient` 配合 `ASGITransport`,在进程内运行 ASGI 应用。这是最快且最可靠的方式——无需子进程,无需管理端口。
```python
# pixie_qa/run_app.py
import httpx
from pydantic import BaseModel
import pixie
class AppArgs(BaseModel):
user_message: str
class AppRunnable(pixie.Runnable[AppArgs]):
"""通过进程内 ASGI 传输驱动 FastAPI 应用。"""
_client: httpx.AsyncClient
@classmethod
def create(cls) -> "AppRunnable":
return cls()
async def setup(self) -> None:
from myapp.main import app # 你的 FastAPI/Starlette 应用实例
transport = httpx.ASGITransport(app=app)
self._client = httpx.AsyncClient(transport=transport, base_url="http://test")
async def run(self, args: AppArgs) -> None:
await self._client.post("/chat", json={"message": args.user_message})
async def teardown(self) -> None:
await self._client.aclose()
```
## ASGITransport 会跳过生命周期事件
`httpx.ASGITransport` **不会**触发 ASGI 生命周期事件(`startup` / `shutdown`)。如果应用在其生命周期中初始化了资源(数据库连接、缓存、服务客户端),则必须在 `setup()` 中手动复制该初始化逻辑:
```python
async def setup(self) -> None:
# 手动复制应用生命周期中的操作
from myapp.db import get_connection, init_db, seed_data
import myapp.main as app_module
conn = get_connection()
init_db(conn)
seed_data(conn)
app_module.db_conn = conn # 设置应用期望的模块级全局变量
transport = httpx.ASGITransport(app=app_module.app)
self._client = httpx.AsyncClient(transport=transport, base_url="http://test")
async def teardown(self) -> None:
await self._client.aclose()
# 清理手动初始化的资源
import myapp.main as app_module
if hasattr(app_module, "db_conn") and app_module.db_conn:
app_module.db_conn.close()
```
## 共享可变状态下的并发
如果应用使用了共享可变状态(内存 SQLite、基于文件的数据库、全局缓存),请添加信号量来串行化访问:
```python
import asyncio
class AppRunnable(pixie.Runnable[AppArgs]):
_client: httpx.AsyncClient
_sem: asyncio.Semaphore
@classmethod
def create(cls) -> "AppRunnable":
inst = cls()
inst._sem = asyncio.Semaphore(1)
return inst
async def setup(self) -> None:
from myapp.main import app
transport = httpx.ASGITransport(app=app)
self._client = httpx.AsyncClient(transport=transport, base_url="http://test")
async def run(self, args: AppArgs) -> None:
async with self._sem:
await self._client.post("/chat", json={"message": args.user_message})
async def teardown(self) -> None:
await self._client.aclose()
```
仅在需要时才使用信号量——如果应用使用以唯一 IDcall_sid、session_id)为键的按会话状态,则并发调用天然隔离,无需加锁。
## 备选方案:外部服务器配合 httpx
当应用无法直接导入时(复杂的启动流程、`__main__` 中的 `uvicorn.run()`),以子进程方式启动并通过 HTTP 访问:
```python
class AppRunnable(pixie.Runnable[AppArgs]):
_client: httpx.AsyncClient
@classmethod
def create(cls) -> "AppRunnable":
return cls()
async def setup(self) -> None:
# 假定服务器已在运行(通过 run-with-timeout.sh 启动)
self._client = httpx.AsyncClient(base_url="http://localhost:8000")
async def run(self, args: AppArgs) -> None:
await self._client.post("/chat", json={"message": args.user_message})
async def teardown(self) -> None:
await self._client.aclose()
```
在运行 `pixie trace``pixie test` 之前启动服务器:
```bash
bash resources/run-with-timeout.sh 120 uv run python -m myapp.server
sleep 3 # 等待就绪