test: 前后端对齐—容器复跑验证+清理僵尸测试

- test_main: FastAPI 0.139 _IncludedRouter 不再 flatten,改用 TestClient 探测路由
- datareader: 文件名(sh600000_daily)/patch target(vnpy.trader.database) 对齐 lazy import 实现
- alpha_lab/analyzer/data_adapter: vnpy.alpha/alphalens 容器专用本地 skip
- 删 4 个测废弃 sanguo_web 的僵尸测试(-1267 行死代码)
- pytest.ini: asyncio_mode=auto
- frontend: package.json 加 test script(npm test 可跑)
- NAS 容器 309 passed 全绿验证(Python 3.10,本机 303+6skip)
This commit is contained in:
2026-07-11 11:45:26 +08:00
parent 928a52b96f
commit a1048690c1
11 changed files with 38 additions and 1267 deletions
+1
View File
@@ -6,6 +6,7 @@
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"test": "vitest run",
"preview": "vite preview"
},
"dependencies": {
+1
View File
@@ -1,3 +1,4 @@
[pytest]
pythonpath = .
testpaths = tests
asyncio_mode = auto
-199
View File
@@ -1,199 +0,0 @@
# WebSocket 测试指南
## 概述
Phase 3 实现了 WebSocket 实时数据推送功能,包括:
- WebSocket 连接管理
- 事件监听器(Tick, Order, Trade, Position, Account, Log, Contract
- 订阅管理
- 心跳机制
## 启动服务器
```bash
cd /Users/chufeng/.openclaw/sanguo_projects/sanguo_vnpy_v2
# 启动 FastAPI 服务器
uvicorn sanguo_web.api:app --host 0.0.0.0 --port 8000 --reload
```
## 测试方法
### 方法 1: 使用 HTML 测试页面
1. 在浏览器中打开:
```
file:///Users/chufeng/.openclaw/sanguo_projects/sanguo_vnpy_v2/tests/websocket_test.html
```
2. 点击"连接"按钮建立 WebSocket 连接
3. 选择要订阅的数据类型:
- 行情 (Tick)
- 订单 (Order)
- 成交 (Trade)
- 持仓 (Position)
- 账户 (Account)
- 日志 (Log)
- 合约 (Contract)
4. 可选:输入品种代码(如:IF2501.CFFEX,IH2501.CFFEX)并订阅
5. 查看实时消息日志
### 方法 2: 使用 Python 测试脚本
```bash
# 安装依赖
pip install websockets
# 运行测试
cd /Users/chufeng/.openclaw/sanguo_projects/sanguo_vnpy_v2
python tests/test_websocket.py
```
测试脚本会执行以下测试:
1. 基本连接测试
2. 订阅功能测试
3. 消息接收测试
4. 心跳机制测试
5. 错误处理测试
6. 认证连接测试
### 方法 3: 使用 wscat 命令行工具
```bash
# 安装 wscat
npm install -g wscat
# 连接 WebSocket
wscat -c ws://localhost:8000/ws
# 发送订阅消息
{"type":"subscribe","data":{"subscription":["tick","order","trade"]}}
# 发送心跳
{"type":"ping","data":{}}
# 订阅品种
{"type":"subscribe_symbol","data":{"symbol":["IF2501.CFFEX"]}}
```
### 方法 4: 使用 JavaScript 控制台
在任何网页中打开浏览器控制台,运行:
```javascript
// 创建 WebSocket 连接
const ws = new WebSocket('ws://localhost:8000/ws');
// 监听连接事件
ws.onopen = () => {
console.log('Connected');
// 订阅行情数据
ws.send(JSON.stringify({
type: 'subscribe',
data: { subscription: ['tick', 'order', 'trade'] }
}));
};
// 监听消息
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log('Received:', message);
};
// 发送心跳
ws.send(JSON.stringify({ type: 'ping', data: {} }));
```
## WebSocket 消息格式
### 订阅消息(客户端 -> 服务器)
```json
{
"type": "subscribe",
"data": {
"subscription": ["tick", "order", "trade"]
}
}
```
### 订阅品种(客户端 -> 服务器)
```json
{
"type": "subscribe_symbol",
"data": {
"symbol": ["IF2501.CFFEX", "IH2501.CFFEX"]
}
}
```
### 心跳消息(客户端 -> 服务器)
```json
{
"type": "ping",
"data": {}
}
```
### 推送消息(服务器 -> 客户端)
```json
{
"type": "tick",
"data": {
"vt_symbol": "IF2501.CFFEX",
"symbol": "IF2501",
"exchange": "CFFEX",
"last_price": 3500.0,
"bid_price_1": 3499.0,
"ask_price_1": 3501.0,
"volume": 12345,
"datetime": "2025-01-01T09:30:00"
}
}
```
## 订阅类型
| 类型 | 说明 | 事件类型 |
|------|------|----------|
| tick | 行情数据 | eTick. |
| order | 订单数据 | eOrder. |
| trade | 成交数据 | eTrade. |
| position | 持仓数据 | ePosition. |
| account | 账户数据 | eAccount. |
| log | 日志数据 | eLog |
| contract | 合约数据 | eContract. |
## 验收标准
Phase 3 完成验收:
- [x] WebSocket 管理器完成
- [x] WebSocket 路由完成
- [x] 事件监听器完成
- [x] 心跳机制完成
- [x] 订阅管理完成
- [x] WebSocket 集成到 FastAPI
- [x] 测试脚本和页面创建
## 故障排查
### 连接失败
1. 检查服务器是否运行
2. 检查 URL 是否正确
3. 检查防火墙设置
### 没有收到消息
1. 检查是否已订阅相应数据类型
2. 检查 VeighNa 网关是否连接
3. 检查是否有行情数据
### 心跳无响应
1. 检查服务器负载
2. 检查网络连接稳定性
+15 -4
View File
@@ -3,6 +3,8 @@
Covers Task S0.1: build_app(config_path, static_dir) loads backtest.yaml and
mounts SPA static files when the directory exists.
"""
from fastapi.testclient import TestClient
from sanguo_api.main import build_app
@@ -17,10 +19,20 @@ def _cfg(tmp_path) -> str:
return str(cfg)
def _has_route(app, path: str, method: str = "GET") -> bool:
"""路由存在性探测(FastAPI 0.139+ 兼容)。
FastAPI 0.139 起 include_router 的子路由包成 _IncludedRouter,不再 flatten
到 app.routes 顶层,因此 [r.path for r in app.routes] 看不到 /api/v1/* 路由。
用 TestClient 实际探测:404 = 路由不存在,其他状态码(405/401/200)= 存在。
前后端对齐的真实验证:前端能打的端点,后端确实挂载。
"""
return TestClient(app).request(method, path).status_code != 404
def test_build_app_has_api_routes(tmp_path):
app = build_app(_cfg(tmp_path))
paths = [getattr(r, "path", "") for r in app.routes]
assert "/api/v1/auth/login" in paths
assert _has_route(app, "/api/v1/auth/login", "POST")
def test_build_app_mounts_spa_when_static_exists(tmp_path):
@@ -35,5 +47,4 @@ def test_build_app_mounts_spa_when_static_exists(tmp_path):
def test_build_app_no_static_skips_mount(tmp_path):
"""When static dir absent, build must still succeed (no SPA mount)."""
app = build_app(_cfg(tmp_path), static_dir=str(tmp_path / "nope"))
paths = [getattr(r, "path", "") for r in app.routes]
assert "/api/v1/auth/login" in paths
assert _has_route(app, "/api/v1/auth/login", "POST")
+4 -2
View File
@@ -14,7 +14,7 @@ def test_read_parquet_daily_returns_bardata(tmp_path):
"low": [9.8, 10.8], "close": [10.2, 11.2],
"volume": [10000, 12000],
})
df.to_parquet(year_dir / "600000.parquet")
df.to_parquet(year_dir / "sh600000_daily.parquet") # 实现读 {prefix}{symbol}_daily.parquetline 24
cfg = DataConfig(
data_paths={"daily_dir": str(tmp_path)},
@@ -64,7 +64,9 @@ def test_read_db_daily_configures_vnpy_settings():
mock_db = MagicMock()
mock_db.load_bar_data.return_value = [] # Return empty list to avoid data processing
with patch('sanguo_data.datareader.get_database', return_value=mock_db):
# read_db_daily 内部 lazy importfrom vnpy.trader.database import get_database),
# 故 patch 源模块属性,非 sanguo_data.datareader(其上无 get_database
with patch('vnpy.trader.database.get_database', return_value=mock_db):
# Call read_db_daily
read_db_daily("600000", "2024-01-01", "2024-12-31", cfg)
+10
View File
@@ -65,6 +65,11 @@ def test_compute_factors_calls_prepare_and_fetch(tmp_path):
"""
import pytest
pytest.importorskip("polars")
# vnpy.alpha 仅 vnpy_v4.4.0 源码提供(容器内 sys.path 引用);本地 pip 装的 vnpy 无此子模块
try:
import vnpy.alpha # noqa: F401
except ImportError:
pytest.skip("vnpy.alpha 仅容器内可用(vnpy_v4.4.0 源码)")
from pathlib import Path
from datetime import datetime
@@ -173,6 +178,11 @@ def test_compute_factors_passes_aware_periods_to_alpha_dataset(tmp_path):
"""
import pytest
pytest.importorskip("polars")
# vnpy.alpha 仅 vnpy_v4.4.0 源码提供(容器内 sys.path 引用);本地 pip 装的 vnpy 无此子模块
try:
import vnpy.alpha # noqa: F401
except ImportError:
pytest.skip("vnpy.alpha 仅容器内可用(vnpy_v4.4.0 源码)")
from pathlib import Path
from datetime import datetime
+1
View File
@@ -106,6 +106,7 @@ def test_run_factor_analysis_calls_tears(tmp_path):
"""
import pytest
pytest.importorskip("polars")
pytest.importorskip("alphalens")
from pathlib import Path
from sanguo_factor.analyzer import run_factor_analysis
+6
View File
@@ -68,6 +68,12 @@ def test_convert_empty_bars():
def test_save_alpha_lab_data():
"""Test save_alpha_lab_data creates AlphaLab and saves data."""
import pytest
pytest.importorskip("alphalens")
try:
import vnpy.alpha # noqa: F401
except ImportError:
pytest.skip("vnpy.alpha 仅容器内可用(vnpy_v4.4.0 源码)")
import tempfile
from pathlib import Path
from sanguo_factor.data_adapter import save_alpha_lab_data
-437
View File
@@ -1,437 +0,0 @@
"""
Sanguo VeighNa Web API 测试套件
测试所有 REST API 端点
"""
import pytest
import asyncio
from httpx import AsyncClient, ASGITransport
from fastapi import status
# 导入应用
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sanguo_web.api import app
# ============================================
# Fixtures
# ============================================
@pytest.fixture
async def client():
"""创建测试客户端"""
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
yield ac
@pytest.fixture
async def auth_token(client):
"""获取认证 Token"""
response = await client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "admin123"}
)
assert response.status_code == 200
data = response.json()
return data.get("access_token")
@pytest.fixture
def auth_headers(auth_token):
"""获取认证请求头"""
return {"Authorization": f"Bearer {auth_token}"}
# ============================================
# 系统端点测试
# ============================================
class TestSystemEndpoints:
"""系统端点测试"""
@pytest.mark.asyncio
async def test_root(self, client):
"""测试根路径"""
response = await client.get("/")
assert response.status_code == 200
@pytest.mark.asyncio
async def test_health_check(self, client):
"""测试健康检查"""
response = await client.get("/health")
assert response.status_code == 200
data = response.json()
assert "status" in data
@pytest.mark.asyncio
async def test_api_root(self, client):
"""测试 API 根路径"""
response = await client.get("/api")
assert response.status_code == 200
data = response.json()
assert "name" in data
assert "version" in data
@pytest.mark.asyncio
async def test_system_info(self, client, auth_headers):
"""测试系统信息"""
response = await client.get(
"/api/v1/system/info",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "version" in data or "status" in data
# ============================================
# 认证端点测试
# ============================================
class TestAuthEndpoints:
"""认证端点测试"""
@pytest.mark.asyncio
async def test_login_success(self, client):
"""测试成功登录"""
response = await client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "admin123"}
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert "token_type" in data
assert data["token_type"] == "bearer"
@pytest.mark.asyncio
async def test_login_wrong_password(self, client):
"""测试错误密码"""
response = await client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "wrong_password"}
)
assert response.status_code == 401
@pytest.mark.asyncio
async def test_login_invalid_user(self, client):
"""测试无效用户"""
response = await client.post(
"/api/v1/auth/login",
json={"username": "invalid_user", "password": "admin123"}
)
assert response.status_code == 401
@pytest.mark.asyncio
async def test_verify_token_valid(self, client, auth_token):
"""测试有效 Token 验证"""
response = await client.post(
"/api/v1/auth/verify",
json={"token": auth_token}
)
assert response.status_code == 200
data = response.json()
assert data["valid"] is True
assert "user_info" in data
@pytest.mark.asyncio
async def test_verify_token_invalid(self, client):
"""测试无效 Token 验证"""
response = await client.post(
"/api/v1/auth/verify",
json={"token": "invalid_token"}
)
assert response.status_code == 200
data = response.json()
assert data["valid"] is False
@pytest.mark.asyncio
async def test_me(self, client, auth_headers):
"""测试获取当前用户信息"""
response = await client.get(
"/api/v1/auth/me",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "username" in data
@pytest.mark.asyncio
async def test_logout(self, client, auth_headers):
"""测试登出"""
response = await client.post(
"/api/v1/auth/logout",
headers=auth_headers
)
assert response.status_code == 200
# ============================================
# 网关端点测试
# ============================================
class TestGatewayEndpoints:
"""网关端点测试"""
@pytest.mark.asyncio
async def test_get_available_gateways(self, client, auth_headers):
"""测试获取可用网关列表"""
response = await client.get(
"/api/v1/gateway/available",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
@pytest.mark.asyncio
async def test_get_connected_gateways(self, client, auth_headers):
"""测试获取已连接网关列表"""
response = await client.get(
"/api/v1/gateway/connected",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
@pytest.mark.asyncio
async def test_get_gateway_setting(self, client, auth_headers):
"""测试获取网关配置模板"""
response = await client.get(
"/api/v1/gateway/setting/CTP",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, dict)
@pytest.mark.asyncio
async def test_connect_gateway(self, client, auth_headers):
"""测试连接网关(模拟)"""
response = await client.post(
"/api/v1/gateway/connect",
headers=auth_headers,
json={
"gateway_name": "CTP_TEST",
"gateway_type": "ctp",
"setting": {
"用户名": "test_user",
"密码": "test_pass",
"经纪商代码": "9999",
"交易服务器": "tcp://test服务器:41205",
"行情服务器": "tcp://test服务器:41213",
}
}
)
# 在 Mock 模式下可能返回 200 或 500
assert response.status_code in [200, 500]
@pytest.mark.asyncio
async def test_unauthorized_access(self, client):
"""测试未授权访问"""
response = await client.get("/api/v1/gateway/available")
assert response.status_code == 401
# ============================================
# 行情端点测试
# ============================================
class TestMarketEndpoints:
"""行情端点测试"""
@pytest.mark.asyncio
async def test_get_ticks(self, client, auth_headers):
"""测试获取行情数据"""
response = await client.get(
"/api/v1/market/ticks",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "ticks" in data
@pytest.mark.asyncio
async def test_subscribe(self, client, auth_headers):
"""测试订阅行情"""
response = await client.post(
"/api/v1/market/subscribe",
headers=auth_headers,
json={
"symbol": "IF2501",
"exchange": "CFFEX"
}
)
assert response.status_code in [200, 202]
@pytest.mark.asyncio
async def test_unsubscribe(self, client, auth_headers):
"""测试取消订阅"""
response = await client.post(
"/api/v1/market/unsubscribe",
headers=auth_headers,
json={
"symbol": "IF2501",
"exchange": "CFFEX"
}
)
assert response.status_code in [200, 202]
@pytest.mark.asyncio
async def test_get_contracts(self, client, auth_headers):
"""测试获取合约列表"""
response = await client.get(
"/api/v1/market/contracts",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "contracts" in data
# ============================================
# 交易端点测试
# ============================================
class TestTradingEndpoints:
"""交易端点测试"""
@pytest.mark.asyncio
async def test_get_accounts(self, client, auth_headers):
"""测试获取账户信息"""
response = await client.get(
"/api/v1/trading/accounts",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
@pytest.mark.asyncio
async def test_get_positions(self, client, auth_headers):
"""测试获取持仓信息"""
response = await client.get(
"/api/v1/trading/positions",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
@pytest.mark.asyncio
async def test_get_orders(self, client, auth_headers):
"""测试获取委托列表"""
response = await client.get(
"/api/v1/trading/orders",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
@pytest.mark.asyncio
async def test_get_active_orders(self, client, auth_headers):
"""测试获取活动委托"""
response = await client.get(
"/api/v1/trading/orders/active",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
@pytest.mark.asyncio
async def test_send_order(self, client, auth_headers):
"""测试发送订单(模拟)"""
response = await client.post(
"/api/v1/trading/orders",
headers=auth_headers,
json={
"symbol": "IF2501",
"exchange": "CFFEX",
"direction": "buy",
"order_type": "limit",
"volume": 1,
"price": 3500.0
}
)
# 在没有连接网关的情况下可能返回错误
assert response.status_code in [200, 500]
@pytest.mark.asyncio
async def test_get_trades(self, client, auth_headers):
"""测试获取成交记录"""
response = await client.get(
"/api/v1/trading/trades",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "trades" in data
assert "total" in data
@pytest.mark.asyncio
async def test_get_account_summary(self, client, auth_headers):
"""测试获取账户综合信息"""
response = await client.get(
"/api/v1/trading/account",
headers=auth_headers
)
assert response.status_code in [200, 404] # 可能没有账户数据
# ============================================
# 策略端点测试
# ============================================
class TestStrategyEndpoints:
"""策略端点测试"""
@pytest.mark.asyncio
async def test_get_strategies(self, client, auth_headers):
"""测试获取策略列表"""
response = await client.get(
"/api/v1/strategy/list",
headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert "strategies" in data
@pytest.mark.asyncio
async def test_init_strategy(self, client, auth_headers):
"""测试初始化策略"""
response = await client.post(
"/api/v1/strategy/test_strategy/init",
headers=auth_headers
)
assert response.status_code in [200, 404]
@pytest.mark.asyncio
async def test_start_strategy(self, client, auth_headers):
"""测试启动策略"""
response = await client.post(
"/api/v1/strategy/test_strategy/start",
headers=auth_headers
)
assert response.status_code in [200, 404]
@pytest.mark.asyncio
async def test_stop_strategy(self, client, auth_headers):
"""测试停止策略"""
response = await client.post(
"/api/v1/strategy/test_strategy/stop",
headers=auth_headers
)
assert response.status_code in [200, 404]
# ============================================
# 运行测试
# ============================================
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])
-416
View File
@@ -1,416 +0,0 @@
"""
WebSocket 客户端测试脚本
用于测试 WebSocket 实时数据推送功能
"""
import asyncio
import websockets
import json
from typing import Optional
class WebSocketTestClient:
"""WebSocket 测试客户端"""
def __init__(self, url: str = "ws://localhost:8000/ws"):
"""
初始化测试客户端
- **url**: WebSocket 服务器 URL
"""
self.url = url
self.websocket: Optional[websockets.WebSocketClientProtocol] = None
self.connected = False
async def connect(self, token: Optional[str] = None):
"""
连接到 WebSocket 服务器
- **token**: 可选的 JWT Token
"""
uri = f"{self.url}"
if token:
uri += f"?token={token}"
try:
self.websocket = await websockets.connect(uri)
self.connected = True
print(f"[+] Connected to {self.url}")
# 接收欢迎消息
welcome_msg = await self.websocket.recv()
print(f"[+] Welcome message: {welcome_msg}")
return json.loads(welcome_msg)
except Exception as e:
print(f"[-] Failed to connect: {e}")
raise
async def disconnect(self):
"""断开连接"""
if self.websocket:
await self.websocket.close()
self.connected = False
print("[+] Disconnected from server")
async def subscribe(self, subscription_types: list):
"""
订阅数据类型
- **subscription_types**: 订阅类型列表 ["tick", "order", "trade"]
"""
message = {
"type": "subscribe",
"data": {
"subscription": subscription_types
}
}
await self.send(message)
print(f"[+] Subscribed to: {subscription_types}")
async def unsubscribe(self, subscription_types: list):
"""
取消订阅
- **subscription_types**: 订阅类型列表
"""
message = {
"type": "unsubscribe",
"data": {
"subscription": subscription_types
}
}
await self.send(message)
print(f"[+] Unsubscribed from: {subscription_types}")
async def subscribe_symbol(self, symbols: list):
"""
订阅品种行情
- **symbols**: 品种代码列表 ["IF2501.CFFEX", "IH2501.CFFEX"]
"""
message = {
"type": "subscribe_symbol",
"data": {
"symbol": symbols
}
}
await self.send(message)
print(f"[+] Subscribed to symbols: {symbols}")
async def unsubscribe_symbol(self, symbols: list):
"""
取消品种订阅
- **symbols**: 品种代码列表
"""
message = {
"type": "unsubscribe_symbol",
"data": {
"symbol": symbols
}
}
await self.send(message)
print(f"[+] Unsubscribed from symbols: {symbols}")
async def send_ping(self):
"""发送心跳"""
message = {
"type": "ping",
"data": {}
}
await self.send(message)
print("[+] Ping sent")
async def send(self, message: dict):
"""
发送消息
- **message**: 消息字典
"""
if not self.websocket or not self.connected:
raise Exception("Not connected to WebSocket server")
await self.websocket.send(json.dumps(message))
async def receive(self, timeout: Optional[float] = None):
"""
接收消息
- **timeout**: 超时时间
"""
if not self.websocket or not self.connected:
raise Exception("Not connected to WebSocket server")
try:
message = await asyncio.wait_for(self.websocket.recv(), timeout=timeout)
return json.loads(message)
except asyncio.TimeoutError:
return None
async def listen(self, duration: int = 10, print_messages: bool = True):
"""
监听消息
- **duration**: 监听时长
- **print_messages**: 是否打印消息
"""
print(f"\n[*] Listening for messages ({duration}s)...")
messages = []
try:
while True:
message = await asyncio.wait_for(self.websocket.recv(), timeout=duration)
data = json.loads(message)
messages.append(data)
if print_messages:
msg_type = data.get("type", "unknown")
print(f"[*] Received {msg_type}: {json.dumps(data, ensure_ascii=False)[:200]}...")
except asyncio.TimeoutError:
print(f"\n[+] Listening finished. Received {len(messages)} messages")
return messages
async def test_basic_connection():
"""测试基本连接功能"""
print("\n" + "="*50)
print("TEST 1: Basic Connection")
print("="*50)
client = WebSocketTestClient()
try:
# 连接
await client.connect()
await asyncio.sleep(1)
# 测试心跳
await client.send_ping()
response = await client.receive(timeout=2)
if response and response.get("type") == "pong":
print("[+] Ping/Pong test passed")
# 断开
await client.disconnect()
print("[+] Test passed")
return True
except Exception as e:
print(f"[-] Test failed: {e}")
return False
async def test_subscription():
"""测试订阅功能"""
print("\n" + "="*50)
print("TEST 2: Subscription")
print("="*50)
client = WebSocketTestClient()
try:
# 连接
await client.connect()
# 订阅多个数据类型
await client.subscribe(["tick", "order", "trade", "position", "account", "log"])
await asyncio.sleep(0.5)
# 订阅品种
await client.subscribe_symbol(["IF2501.CFFEX", "IH2501.CFFEX"])
await asyncio.sleep(0.5)
# 取消订阅
await client.unsubscribe(["log"])
await asyncio.sleep(0.5)
# 断开
await client.disconnect()
print("[+] Test passed")
return True
except Exception as e:
print(f"[-] Test failed: {e}")
return False
async def test_message_reception():
"""测试消息接收功能"""
print("\n" + "="*50)
print("TEST 3: Message Reception")
print("="*50)
client = WebSocketTestClient()
try:
# 连接
await client.connect()
# 订阅所有类型
await client.subscribe(["tick", "order", "trade", "position", "account", "log", "contract"])
# 监听消息
messages = await client.listen(duration=5)
print(f"\n[+] Received messages by type:")
msg_types = {}
for msg in messages:
msg_type = msg.get("type", "unknown")
msg_types[msg_type] = msg_types.get(msg_type, 0) + 1
for msg_type, count in msg_types.items():
print(f" - {msg_type}: {count}")
# 断开
await client.disconnect()
print("[+] Test passed")
return True
except Exception as e:
print(f"[-] Test failed: {e}")
return False
async def test_heartbeat():
"""测试心跳机制"""
print("\n" + "="*50)
print("TEST 4: Heartbeat")
print("="*50)
client = WebSocketTestClient()
try:
# 连接
await client.connect()
# 发送多次心跳
for i in range(5):
await client.send_ping()
response = await client.receive(timeout=2)
if response and response.get("type") == "pong":
print(f"[+] Heartbeat {i+1}/5 successful")
await asyncio.sleep(1)
# 断开
await client.disconnect()
print("[+] Test passed")
return True
except Exception as e:
print(f"[-] Test failed: {e}")
return False
async def test_error_handling():
"""测试错误处理"""
print("\n" + "="*50)
print("TEST 5: Error Handling")
print("="*50)
client = WebSocketTestClient()
try:
# 连接
await client.connect()
# 发送未知消息类型
await client.send({
"type": "unknown_type",
"data": {}
})
response = await client.receive(timeout=2)
if response and response.get("type") == "error":
print("[+] Error response received correctly")
# 断开
await client.disconnect()
print("[+] Test passed")
return True
except Exception as e:
print(f"[-] Test failed: {e}")
return False
async def test_authenticated_connection():
"""测试认证连接"""
print("\n" + "="*50)
print("TEST 6: Authenticated Connection")
print("="*50)
# 注意:需要有效的 JWT Token
# 这里测试无效 Token 的情况
client = WebSocketTestClient()
try:
# 使用无效 Token 连接
await client.connect(token="invalid_token")
print("[-] Should have failed with invalid token")
await client.disconnect()
return False
except Exception as e:
print(f"[+] Correctly rejected invalid token: {e}")
return True
async def run_all_tests():
"""运行所有测试"""
print("\n" + "="*50)
print("WebSocket Test Suite")
print("="*50)
tests = [
("Basic Connection", test_basic_connection),
("Subscription", test_subscription),
("Message Reception", test_message_reception),
("Heartbeat", test_heartbeat),
("Error Handling", test_error_handling),
("Authenticated Connection", test_authenticated_connection),
]
results = []
for name, test_func in tests:
try:
result = await test_func()
results.append((name, result))
except Exception as e:
print(f"[-] Test '{name}' crashed: {e}")
results.append((name, False))
await asyncio.sleep(1)
# 汇总结果
print("\n" + "="*50)
print("Test Results Summary")
print("="*50)
passed = sum(1 for _, result in results if result)
total = len(results)
for name, result in results:
status = "✓ PASS" if result else "✗ FAIL"
print(f"{status}: {name}")
print(f"\nTotal: {passed}/{total} tests passed")
return passed == total
def main():
"""主函数"""
# 运行测试
success = asyncio.run(run_all_tests())
if success:
print("\n[+] All tests passed!")
exit(0)
else:
print("\n[-] Some tests failed")
exit(1)
if __name__ == "__main__":
main()
-209
View File
@@ -1,209 +0,0 @@
#!/usr/bin/env python3
"""
WebSocket 验证脚本
验证 WebSocket 模块的基本结构和功能
"""
import sys
import os
# 添加项目路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../vnpy_v4.4.0'))
def test_imports():
"""测试模块导入"""
print("Testing imports...")
try:
from sanguo_web.websocket.manager import ConnectionManager, manager
print(" ✓ ConnectionManager imported")
from sanguo_web.websocket.routes import router
print(" ✓ WebSocket router imported")
from sanguo_web.websocket.events import (
EventMonitorManager,
serialize_tick_data,
serialize_order_data,
serialize_trade_data
)
print(" ✓ Event monitors imported")
from sanguo_web.websocket import (
ConnectionManager as CM,
manager as mgr,
router as ws_router,
EventMonitorManager as EMM
)
print(" ✓ WebSocket module __init__ exports")
return True
except Exception as e:
print(f" ✗ Import failed: {e}")
return False
def test_manager():
"""测试连接管理器"""
print("\nTesting ConnectionManager...")
try:
from sanguo_web.websocket.manager import ConnectionManager
manager = ConnectionManager()
# 测试连接计数
count = manager.get_connection_count()
assert count == 0, f"Expected 0 connections, got {count}"
print(" ✓ Connection count initialized correctly")
# 测试用户连接计数
user_count = manager.get_user_connection_count("test_user")
assert user_count == 0, f"Expected 0 user connections, got {user_count}"
print(" ✓ User connection count initialized correctly")
return True
except Exception as e:
print(f" ✗ Manager test failed: {e}")
return False
def test_serialization():
"""测试数据序列化函数"""
print("\nTesting serialization functions...")
try:
from sanguo_web.websocket.events import serialize_datetime
from datetime import datetime
# 测试 datetime 序列化
dt = datetime(2025, 1, 1, 12, 30, 45)
serialized = serialize_datetime(dt)
assert serialized == "2025-01-01T12:30:45", f"Expected ISO format, got {serialized}"
print(" ✓ datetime serialization works")
# 测试 None 处理
none_result = serialize_datetime(None)
assert none_result is None, f"Expected None for None input, got {none_result}"
print(" ✓ None datetime handled correctly")
return True
except Exception as e:
print(f" ✗ Serialization test failed: {e}")
return False
def test_router():
"""测试路由"""
print("\nTesting WebSocket router...")
try:
from sanguo_web.websocket.routes import router
# 检查路由
routes = [route.path for route in router.routes]
assert "/ws" in routes, "WebSocket route not found"
print(" ✓ WebSocket /ws route exists")
assert "/ws/status" in routes, "WebSocket status route not found"
print(" ✓ WebSocket /ws/status route exists")
return True
except Exception as e:
print(f" ✗ Router test failed: {e}")
return False
def test_event_monitor_classes():
"""测试事件监听器类"""
print("\nTesting event monitor classes...")
try:
from sanguo_web.websocket.events import (
TickEventMonitor,
OrderEventMonitor,
TradeEventMonitor,
PositionEventMonitor,
AccountEventMonitor,
LogEventMonitor,
ContractEventMonitor,
EventMonitorManager
)
# 检查类是否具有必要的方法
monitors = [
TickEventMonitor,
OrderEventMonitor,
TradeEventMonitor,
PositionEventMonitor,
AccountEventMonitor,
LogEventMonitor,
ContractEventMonitor
]
for monitor_class in monitors:
# 检查是否有 stop 方法
assert hasattr(monitor_class, 'stop'), f"{monitor_class.__name__} missing stop method"
print(f"{monitor_class.__name__} has stop method")
# 检查 EventMonitorManager
assert hasattr(EventMonitorManager, 'start_all'), "EventMonitorManager missing start_all method"
assert hasattr(EventMonitorManager, 'stop_all'), "EventMonitorManager missing stop_all method"
print(" ✓ EventMonitorManager has start_all and stop_all methods")
return True
except Exception as e:
print(f" ✗ Event monitor test failed: {e}")
return False
def main():
"""主测试函数"""
print("="*50)
print("WebSocket Phase 3 Verification")
print("="*50)
tests = [
("Import Test", test_imports),
("Manager Test", test_manager),
("Serialization Test", test_serialization),
("Router Test", test_router),
("Event Monitor Test", test_event_monitor_classes),
]
results = []
for name, test_func in tests:
print(f"\n{name}:")
try:
result = test_func()
results.append((name, result))
except Exception as e:
print(f" ✗ Test crashed: {e}")
results.append((name, False))
# 汇总结果
print("\n" + "="*50)
print("Test Results")
print("="*50)
passed = sum(1 for _, result in results if result)
total = len(results)
for name, result in results:
status = "✓ PASS" if result else "✗ FAIL"
print(f"{status}: {name}")
print(f"\nTotal: {passed}/{total} tests passed")
if passed == total:
print("\n✓ All tests passed! Phase 3 implementation verified.")
return 0
else:
print(f"\n{total - passed} test(s) failed")
return 1
if __name__ == "__main__":
sys.exit(main())