615 lines
25 KiB
Python
615 lines
25 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
微信全功能 E2E 端到端测试脚本 — 96个action / 29个功能模块
|
||
|
||
验证完整链路: 网页/API → unified.py → 通道路由 → Agent(Hook/u2) 或 ADB引擎 → 微信APP
|
||
|
||
前置条件:
|
||
1. SDK 运行: localhost:8899 (cd sdk/app && python3 main.py)
|
||
2. 设备连接: USB ADB 或 Agent WebSocket
|
||
3. 微信已登录
|
||
4. 安全联系人: 文件传输助手 (每台微信必有)
|
||
|
||
用法:
|
||
python3 test_wechat_full_e2e.py # 运行安全测试组(只读/文件传输助手)
|
||
python3 test_wechat_full_e2e.py --full # 运行全量测试(含写操作)
|
||
python3 test_wechat_full_e2e.py --group message # 只运行消息组
|
||
python3 test_wechat_full_e2e.py --action send_message # 只运行单个action
|
||
|
||
环境变量:
|
||
SDK_BASE_URL: SDK地址 (默认 http://localhost:8899)
|
||
SDK_DEVICE_ID: 设备ID (默认自动检测)
|
||
SDK_E2E_TO_ID: 测试联系人 (默认 文件传输助手)
|
||
"""
|
||
|
||
import httpx
|
||
import asyncio
|
||
import json
|
||
import time
|
||
import os
|
||
import sys
|
||
import argparse
|
||
from typing import Dict, Any, List, Tuple
|
||
from dataclasses import dataclass, field
|
||
from datetime import datetime
|
||
|
||
BASE_URL = os.environ.get("SDK_BASE_URL", "http://localhost:8899")
|
||
DEVICE_ID = os.environ.get("SDK_DEVICE_ID", "")
|
||
TO_ID = os.environ.get("SDK_E2E_TO_ID", "文件传输助手")
|
||
TIMEOUT = 60
|
||
|
||
|
||
@dataclass
|
||
class CheckResult:
|
||
action: str
|
||
group: str
|
||
channel: str
|
||
success: bool
|
||
elapsed_ms: int
|
||
response: dict
|
||
error: str = ""
|
||
|
||
|
||
@dataclass
|
||
class CheckReport:
|
||
results: List[CheckResult] = field(default_factory=list)
|
||
start_time: float = 0
|
||
end_time: float = 0
|
||
|
||
@property
|
||
def total(self): return len(self.results)
|
||
@property
|
||
def passed(self): return sum(1 for r in self.results if r.success)
|
||
@property
|
||
def failed(self): return self.total - self.passed
|
||
@property
|
||
def success_rate(self): return f"{self.passed/self.total*100:.1f}%" if self.total else "N/A"
|
||
@property
|
||
def avg_time(self): return sum(r.elapsed_ms for r in self.results) // max(self.total, 1)
|
||
|
||
def print_summary(self):
|
||
elapsed = self.end_time - self.start_time
|
||
print("\n" + "=" * 70)
|
||
print(f" 微信全功能 E2E 测试报告")
|
||
print(f" {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||
print("=" * 70)
|
||
print(f" 总计: {self.total} 通过: {self.passed} 失败: {self.failed} 成功率: {self.success_rate}")
|
||
print(f" 总耗时: {elapsed:.1f}s 平均单操作: {self.avg_time}ms")
|
||
print("-" * 70)
|
||
|
||
groups = {}
|
||
for r in self.results:
|
||
groups.setdefault(r.group, []).append(r)
|
||
|
||
for grp, items in groups.items():
|
||
ok = sum(1 for i in items if i.success)
|
||
print(f"\n [{grp}] {ok}/{len(items)} 通过")
|
||
for r in items:
|
||
icon = "✓" if r.success else "✗"
|
||
ch = f"[{r.channel}]" if r.channel else ""
|
||
err = f" — {r.error[:60]}" if r.error else ""
|
||
print(f" {icon} {r.action:<30} {r.elapsed_ms:>5}ms {ch:<12}{err}")
|
||
|
||
print("\n" + "=" * 70)
|
||
|
||
if self.failed:
|
||
print("\n 失败详情:")
|
||
for r in self.results:
|
||
if not r.success:
|
||
print(f"\n [{r.group}] {r.action}")
|
||
print(f" 错误: {r.error}")
|
||
print(f" 响应: {json.dumps(r.response, ensure_ascii=False)[:200]}")
|
||
|
||
|
||
TS = int(time.time())
|
||
|
||
ALL_TESTS: Dict[str, List[Tuple[str, str, str, dict, bool]]] = {
|
||
"消息": [
|
||
("send_message", "POST", "/api/v3/message/send",
|
||
{"to_id": TO_ID, "content": f"[E2E] {datetime.now()}", "msg_type": "text"}, True),
|
||
("get_messages", "POST", "/api/v3/message/list",
|
||
{"conversation_id": TO_ID, "limit": 5}, True),
|
||
("forward_message", "POST", "/api/v3/message/forward",
|
||
{"to_id": TO_ID, "content": "转发测试"}, True),
|
||
("recall_message", "POST", "/api/v3/message/recall", {}, True),
|
||
("send_card", "POST", "/api/v3/message/send-card",
|
||
{"to_id": TO_ID, "card_wxid": TO_ID}, True),
|
||
],
|
||
"好友": [
|
||
("get_contacts", "GET", "/api/v3/contacts", {"limit": 10}, True),
|
||
("search_contact", "GET", "/api/v3/contacts/search", {"keyword": "文件"}, True),
|
||
("friend_info", "GET", "/api/v3/friend/info", {"user_id": TO_ID}, True),
|
||
],
|
||
"群聊": [
|
||
("get_groups", "GET", "/api/v3/group/list", {"limit": 10}, True),
|
||
("get_group_members","GET", "/api/v3/group/members", {"group_id": ""}, True),
|
||
],
|
||
"标签": [
|
||
("get_tags", "GET", "/api/v3/tag/list", {}, True),
|
||
],
|
||
"朋友圈": [
|
||
("get_moments", "POST", "/api/v3/moments/list", {"limit": 5}, True),
|
||
],
|
||
"个人": [
|
||
("get_profile", "GET", "/api/v3/profile/get", {}, True),
|
||
],
|
||
"安全": [
|
||
("account_status", "GET", "/api/v3/account/status", {}, True),
|
||
("safety_center", "GET", "/api/v3/account/safety-center", {}, True),
|
||
("restrictions", "GET", "/api/v3/account/restrictions", {}, True),
|
||
],
|
||
"收藏": [
|
||
("get_favorites", "GET", "/api/v3/favorites/list", {"limit": 5}, True),
|
||
],
|
||
"聊天设置": [
|
||
("set_chat_top", "POST", "/api/v3/chat/set-top",
|
||
{"user_id": TO_ID, "enable": True}, True),
|
||
("set_mute_chat", "POST", "/api/v3/chat/set-mute",
|
||
{"user_id": TO_ID, "enable": False}, True),
|
||
],
|
||
"视频号": [
|
||
("video_list", "GET", "/api/v3/video-channel/list", {}, True),
|
||
],
|
||
"扫一扫": [
|
||
("my_qr", "GET", "/api/v3/scan/my-qr", {}, True),
|
||
],
|
||
"支付": [
|
||
("payment_code", "GET", "/api/v3/payment/code", {}, True),
|
||
("wallet", "GET", "/api/v3/payment/wallet", {}, True),
|
||
("transactions", "GET", "/api/v3/payment/transactions", {}, True),
|
||
],
|
||
"搜索": [
|
||
("wechat_search", "GET", "/api/v3/search/wechat", {"keyword": "微信"}, True),
|
||
],
|
||
"发现": [
|
||
("top_stories", "GET", "/api/v3/discover/top-stories", {}, True),
|
||
],
|
||
"运动": [
|
||
("get_steps", "GET", "/api/v3/wechat-sport/steps", {}, True),
|
||
],
|
||
"表情": [
|
||
("stickers", "GET", "/api/v3/emoji/stickers", {}, True),
|
||
],
|
||
"Hook": [
|
||
("hook_actions", "GET", "/api/v3/hook/actions", {}, True),
|
||
],
|
||
}
|
||
|
||
WRITE_TESTS: Dict[str, List[Tuple[str, str, str, dict, bool]]] = {
|
||
"消息写": [
|
||
("batch_send", "POST", "/api/v3/message/batch-send",
|
||
{"to_ids": [TO_ID], "content": f"[批量E2E] {TS}", "msg_type": "text"}, True),
|
||
("send_voice", "POST", "/api/v3/message/voice",
|
||
{"to_id": TO_ID, "duration": 3}, True),
|
||
],
|
||
"好友写": [
|
||
("set_remark", "POST", "/api/v3/friend/set-remark",
|
||
{"user_id": TO_ID, "remark": f"E2E_{TS}"}, True),
|
||
],
|
||
"标签写": [
|
||
("create_tag", "POST", "/api/v3/tag/create",
|
||
{"tag_name": f"e2e_tag_{TS}"}, True),
|
||
("get_users_by_tag","POST", "/api/v3/tag/users",
|
||
{"tag_name": f"e2e_tag_{TS}"}, True),
|
||
],
|
||
"个人写": [
|
||
("set_signature", "POST", "/api/v3/profile/set-signature",
|
||
{"signature": f"E2E测试 {TS}"}, True),
|
||
],
|
||
"朋友圈写": [
|
||
("post_moments", "POST", "/api/v3/moments/post",
|
||
{"content": f"[E2E测试] {datetime.now().strftime('%H:%M')}"}, True),
|
||
("like_moments", "POST", "/api/v3/moments/like",
|
||
{"user_id": TO_ID, "post_index": 0}, True),
|
||
("comment_moments", "POST", "/api/v3/moments/comment",
|
||
{"user_id": TO_ID, "post_index": 0, "comment": "E2E测试评论"}, True),
|
||
("set_privacy", "POST", "/api/v3/moments/set-privacy",
|
||
{"privacy_type": "all"}, True),
|
||
],
|
||
"聊天写": [
|
||
("clear_history", "POST", "/api/v3/chat/clear-history",
|
||
{"user_id": TO_ID}, True),
|
||
],
|
||
"收藏写": [
|
||
("add_favorite", "POST", "/api/v3/favorites/add",
|
||
{"content": "E2E收藏测试", "content_type": "text"}, True),
|
||
],
|
||
"视频号写": [
|
||
("like_video", "POST", "/api/v3/video-channel/like",
|
||
{"index": 0}, True),
|
||
],
|
||
"运动写": [
|
||
("like_steps", "POST", "/api/v3/wechat-sport/like-steps",
|
||
{"user_id": TO_ID}, True),
|
||
],
|
||
"位置写": [
|
||
("send_location", "POST", "/api/v3/location/send",
|
||
{"to_id": TO_ID, "latitude": 31.23, "longitude": 121.47, "name": "测试位置"}, True),
|
||
],
|
||
"表情写": [
|
||
("send_emoji", "POST", "/api/v3/emoji/send",
|
||
{"to_id": TO_ID, "emoji_name": "微笑"}, True),
|
||
],
|
||
"设置写": [
|
||
("do_not_disturb", "POST", "/api/v3/settings/do-not-disturb",
|
||
{"enable": False}, True),
|
||
("clear_cache", "POST", "/api/v3/settings/clear-cache", {}, True),
|
||
("check_update", "POST", "/api/v3/settings/check-update", {}, True),
|
||
],
|
||
"小程序写": [
|
||
("open_miniprogram","POST", "/api/v3/miniprogram/open",
|
||
{"name": "微信支付"}, True),
|
||
],
|
||
"文件写": [
|
||
("send_file", "POST", "/api/v3/file/send",
|
||
{"to_id": TO_ID, "file_path": "/sdcard/test.txt"}, True),
|
||
],
|
||
}
|
||
|
||
# 矩阵 REST 补全(~97 端点 − 已有 50 action)
|
||
MATRIX_REST_TESTS: Dict[str, List[Tuple[str, str, str, dict, bool]]] = {
|
||
"消息补": [
|
||
("comment_reply", "POST", "/api/v3/comment/reply",
|
||
{"video_id": "1", "comment_id": "1", "content": "probe"}, True),
|
||
],
|
||
"好友补": [
|
||
("friend_add", "POST", "/api/v3/friend/add",
|
||
{"user_id": "28533368", "message": "matrix-e2e"}, True),
|
||
("friend_accept", "POST", "/api/v3/friend/accept", {"user_id": "probe"}, True),
|
||
("friend_delete", "POST", "/api/v3/friend/delete", {"user_id": "invalid_probe_wxid"}, True),
|
||
("friend_batch_add", "POST", "/api/v3/friend/batch-add",
|
||
{"user_ids": ["28533368"], "message": "batch-probe"}, True),
|
||
],
|
||
"群聊补": [
|
||
("group_create", "POST", "/api/v3/group/create",
|
||
{"group_name": f"e2e_{TS}", "member_ids": [TO_ID, TO_ID]}, True),
|
||
("group_invite", "POST", "/api/v3/group/invite",
|
||
{"group_id": "", "member_ids": [TO_ID]}, True),
|
||
("group_remove", "POST", "/api/v3/group/remove",
|
||
{"group_id": "", "member_ids": [TO_ID]}, True),
|
||
("group_set_notice", "POST", "/api/v3/group/set-notice",
|
||
{"group_id": "", "notice": "e2e notice"}, True),
|
||
("group_set_name", "POST", "/api/v3/group/set-name",
|
||
{"group_id": "", "group_name": "e2e-group"}, True),
|
||
("group_send_message", "POST", "/api/v3/group/send-message",
|
||
{"group_id": "", "content": "e2e group msg"}, True),
|
||
("group_set_welcome", "POST", "/api/v3/group/set-welcome",
|
||
{"group_id": "", "welcome_text": "welcome probe"}, True),
|
||
("group_quit", "POST", "/api/v3/group/quit", {"group_id": ""}, True),
|
||
],
|
||
"标签补": [
|
||
("tag_add", "POST", "/api/v3/tag/add", {"user_id": TO_ID, "tags": [f"e2e_tag_{TS}"]}, True),
|
||
("tag_remove", "POST", "/api/v3/tag/remove", {"user_id": TO_ID, "tags": [f"e2e_tag_{TS}"]}, True),
|
||
("tag_delete", "POST", "/api/v3/tag/delete", {"tag_name": f"e2e_tag_{TS}"}, True),
|
||
],
|
||
"朋友圈补": [
|
||
("moments_delete", "POST", "/api/v3/moments/delete", {"post_index": 0}, True),
|
||
("moments_set_cover", "POST", "/api/v3/moments/set-cover", {}, True),
|
||
("moments_share_link", "POST", "/api/v3/moments/share-link",
|
||
{"content": "https://example.com probe"}, True),
|
||
],
|
||
"个人补": [
|
||
("set_nickname", "POST", "/api/v3/profile/set-nickname", {"nickname": "e2e_probe"}, True),
|
||
("set_avatar", "POST", "/api/v3/profile/set-avatar", {"image_path": "/sdcard/test.jpg"}, True),
|
||
("set_gender", "POST", "/api/v3/profile/set-gender", {"gender": "1"}, True),
|
||
("set_region", "POST", "/api/v3/profile/set-region", {"region": "福建 厦门"}, True),
|
||
],
|
||
"安全补": [
|
||
("account_unblock", "POST", "/api/v3/account/unblock", {}, True),
|
||
("change_password", "POST", "/api/v3/account/change-password",
|
||
{"old_pwd": "old", "new_pwd": "new"}, True),
|
||
("unblock_self", "POST", "/api/v3/account/unblock-self", {}, True),
|
||
("unblock_appeal", "POST", "/api/v3/account/unblock-appeal", {}, True),
|
||
("appeal_restriction", "POST", "/api/v3/account/appeal-restriction", {}, True),
|
||
("unblock_sms", "POST", "/api/v3/account/unblock-sms", {"phone": "13800138000"}, True),
|
||
],
|
||
"支付补": [
|
||
("send_red_packet", "POST", "/api/v3/payment/red-packet",
|
||
{"to_id": TO_ID, "amount": "0.01", "message": "probe"}, True),
|
||
("payment_transfer", "POST", "/api/v3/payment/transfer",
|
||
{"to_id": TO_ID, "amount": "0.01", "description": "probe"}, True),
|
||
("payment_receive", "POST", "/api/v3/payment/receive", {"amount": "0.01", "desc": "probe"}, True),
|
||
("receive_red_packet", "POST", "/api/v3/payment/receive-red-packet", {"from_id": TO_ID}, True),
|
||
],
|
||
"视频号补": [
|
||
("video_comment", "POST", "/api/v3/video-channel/comment",
|
||
{"index": 0, "comment": "probe"}, True),
|
||
("video_follow", "POST", "/api/v3/video-channel/follow", {"index": 0}, True),
|
||
("video_share", "POST", "/api/v3/video-channel/share",
|
||
{"index": 0, "to_id": TO_ID}, True),
|
||
],
|
||
"扫一扫补": [
|
||
("scan_qr", "POST", "/api/v3/scan/qr-code", {"qr_content": "probe"}, True),
|
||
("scan_add_friend", "POST", "/api/v3/scan/add-friend", {"qr_content": "probe"}, True),
|
||
("scan_extract_qr", "POST", "/api/v3/scan/extract-qr", {"image_path": "/sdcard/test.jpg"}, True),
|
||
],
|
||
"通话补": [
|
||
("call_voice", "POST", "/api/v3/call/voice", {"to_id": TO_ID}, True),
|
||
("call_video", "POST", "/api/v3/call/video", {"to_id": TO_ID}, True),
|
||
],
|
||
"群发": [
|
||
("mass_send", "POST", "/api/v3/mass-send",
|
||
{"user_ids": [TO_ID], "content": f"mass {TS}"}, True),
|
||
],
|
||
"位置补": [
|
||
("share_realtime_location", "POST", "/api/v3/location/share-realtime",
|
||
{"to_id": TO_ID, "duration_minutes": 5}, True),
|
||
],
|
||
"文件补": [
|
||
("file_download", "POST", "/api/v3/file/download", {"user_id": TO_ID}, True),
|
||
],
|
||
"设置补": [
|
||
("settings_logout", "POST", "/api/v3/settings/logout", {}, True),
|
||
("switch_account", "POST", "/api/v3/settings/switch-account", {}, True),
|
||
],
|
||
"公众号": [
|
||
("follow_official", "POST", "/api/v3/official-account/follow", {"account_name": "probe"}, True),
|
||
],
|
||
}
|
||
|
||
|
||
async def prefetch_group_id(client: httpx.AsyncClient) -> str:
|
||
"""取 chatroom 表存在的群 ID;若无则尝试建群;仍无则返回 probe id(Intent 探针)。"""
|
||
pre = await run_test(client, "get_groups", "GET", "/api/v3/group/list", {"limit": 20}, "预检")
|
||
groups = (pre.response.get("data") or {}).get("groups") or []
|
||
for g in groups:
|
||
gid = g.get("group_id") or g.get("username") or ""
|
||
if not gid:
|
||
continue
|
||
chk = await run_test(client, "get_group_members", "GET", "/api/v3/group/members",
|
||
{"group_id": gid}, "预检")
|
||
inner = chk.response.get("data") or {}
|
||
if inner.get("members") is not None or inner.get("success"):
|
||
return gid
|
||
self_wxid = ""
|
||
try:
|
||
pr = await run_test(client, "account_status", "GET", "/api/v3/account/status", {}, "预检")
|
||
inner = pr.response.get("data") or {}
|
||
self_wxid = inner.get("wxid") or inner.get("username") or ""
|
||
except Exception:
|
||
pass
|
||
members = ["filehelper"]
|
||
if self_wxid and self_wxid not in members:
|
||
members.append(self_wxid)
|
||
if len(members) < 2:
|
||
members.append("filehelper")
|
||
cr = await run_test(
|
||
client, "group_create", "POST", "/api/v3/group/create",
|
||
{"group_name": f"e2e_{TS}", "member_ids": members[:3]}, "预检",
|
||
)
|
||
data = cr.response.get("data") or {}
|
||
gid = data.get("group_id") or (data.get("group") or {}).get("group_id") or ""
|
||
if gid:
|
||
return gid
|
||
return f"probe_e2e_{TS}@chatroom"
|
||
|
||
|
||
def _inject_group_id(params: dict, group_id: str) -> None:
|
||
if group_id and "group_id" in params and not params.get("group_id"):
|
||
params["group_id"] = group_id
|
||
|
||
|
||
def _device_list_from_api_json(data: dict) -> list:
|
||
"""兼容 data 为设备数组或 { devices: [] }"""
|
||
if not isinstance(data, dict):
|
||
return []
|
||
raw = data.get("data")
|
||
if isinstance(raw, list):
|
||
return raw
|
||
if isinstance(raw, dict) and isinstance(raw.get("devices"), list):
|
||
return raw["devices"]
|
||
if isinstance(data.get("devices"), list):
|
||
return data["devices"]
|
||
return []
|
||
|
||
|
||
async def detect_device(client: httpx.AsyncClient) -> str:
|
||
"""自动检测在线设备"""
|
||
global DEVICE_ID
|
||
if DEVICE_ID:
|
||
return DEVICE_ID
|
||
try:
|
||
resp = await client.get(f"{BASE_URL}/api/v3/devices", timeout=10)
|
||
devices = _device_list_from_api_json(resp.json())
|
||
if devices:
|
||
DEVICE_ID = devices[0].get("device_id", devices[0].get("serial", str(devices[0])))
|
||
return DEVICE_ID
|
||
except Exception:
|
||
pass
|
||
try:
|
||
resp = await client.get(f"{BASE_URL}/api/v3/adb/devices", timeout=10)
|
||
devices = _device_list_from_api_json(resp.json())
|
||
if devices:
|
||
DEVICE_ID = devices[0].get("serial", devices[0].get("device_id", str(devices[0])))
|
||
return DEVICE_ID
|
||
except Exception:
|
||
pass
|
||
DEVICE_ID = "unknown"
|
||
return DEVICE_ID
|
||
|
||
|
||
async def run_test(client: httpx.AsyncClient, action: str, method: str,
|
||
endpoint: str, params: dict, group: str) -> CheckResult:
|
||
"""执行单个测试(POST 自动兼容 body/query 两种参数模式)"""
|
||
params["device_id"] = DEVICE_ID
|
||
params["platform"] = "wechat"
|
||
start = time.time()
|
||
try:
|
||
url = f"{BASE_URL}{endpoint}"
|
||
if method == "GET":
|
||
qs = "&".join(f"{k}={v}" for k, v in params.items() if v is not None)
|
||
resp = await client.get(f"{url}?{qs}" if qs else url, timeout=TIMEOUT)
|
||
else:
|
||
resp = await client.post(url, json=params, timeout=TIMEOUT)
|
||
if resp.status_code == 422:
|
||
qs = "&".join(f"{k}={v}" for k, v in params.items() if v is not None)
|
||
resp = await client.post(f"{url}?{qs}" if qs else url, timeout=TIMEOUT)
|
||
|
||
elapsed = int((time.time() - start) * 1000)
|
||
data = resp.json()
|
||
channel = data.get("channel_used", data.get("data", {}).get("channel", "unknown"))
|
||
ok = resp.status_code == 200 and data.get("code", 200) in (200, 0, None)
|
||
if isinstance(data.get("data"), dict) and data["data"].get("success") is False:
|
||
ok = False
|
||
error = "" if ok else (data.get("message", data.get("error", str(data)))[:100])
|
||
return CheckResult(action=action, group=group, channel=str(channel),
|
||
success=ok, elapsed_ms=elapsed, response=data, error=error)
|
||
except Exception as e:
|
||
elapsed = int((time.time() - start) * 1000)
|
||
return CheckResult(action=action, group=group, channel="error",
|
||
success=False, elapsed_ms=elapsed, response={}, error=str(e)[:100])
|
||
|
||
|
||
async def check_health(client: httpx.AsyncClient) -> bool:
|
||
"""检查SDK是否在线"""
|
||
try:
|
||
resp = await client.get(f"{BASE_URL}/health", timeout=10)
|
||
data = resp.json()
|
||
print(f" SDK状态: {data.get('status', 'unknown')}")
|
||
print(f" 设备在线: {data.get('devices_online', 0)}")
|
||
return True
|
||
except Exception as e:
|
||
print(f" SDK连接失败: {e}")
|
||
return False
|
||
|
||
|
||
async def main():
|
||
parser = argparse.ArgumentParser(description="微信全功能E2E测试")
|
||
parser.add_argument("--full", action="store_true", help="运行全量测试(含写操作)")
|
||
parser.add_argument("--matrix", action="store_true", help="矩阵 REST 全端点(含 --full + 补全端点)")
|
||
parser.add_argument("--group", type=str, help="只运行指定组")
|
||
parser.add_argument("--action", type=str, help="只运行指定action")
|
||
parser.add_argument("--base-url", type=str, help="SDK地址")
|
||
parser.add_argument("--device-id", type=str, help="设备ID")
|
||
args = parser.parse_args()
|
||
|
||
global BASE_URL, DEVICE_ID
|
||
if args.base_url:
|
||
BASE_URL = args.base_url
|
||
if args.device_id:
|
||
DEVICE_ID = args.device_id
|
||
|
||
report = CheckReport()
|
||
report.start_time = time.time()
|
||
|
||
print("\n" + "=" * 70)
|
||
print(" 微信全功能 E2E 端到端测试")
|
||
print(" 96个action / 29个功能模块 / 3种控制通道")
|
||
print("=" * 70)
|
||
|
||
async with httpx.AsyncClient() as client:
|
||
print("\n[0] 环境检查")
|
||
if not await check_health(client):
|
||
print("\n 请先启动SDK: cd sdk/app && python3 main.py")
|
||
return
|
||
|
||
await detect_device(client)
|
||
print(f" 设备ID: {DEVICE_ID}")
|
||
print(f" 测试联系人: {TO_ID}")
|
||
|
||
tests = dict(ALL_TESTS)
|
||
if args.full or args.matrix:
|
||
tests.update(WRITE_TESTS)
|
||
os.environ["SDK_MATRIX_VERIFY"] = "1"
|
||
if args.matrix:
|
||
tests.update(MATRIX_REST_TESTS)
|
||
|
||
mode = "矩阵 REST 全端点" if args.matrix else ("全量(含写操作)" if args.full else "安全(只读+文件传输助手)")
|
||
print(f" 测试模式: {mode}")
|
||
|
||
if args.group:
|
||
tests = {k: v for k, v in tests.items() if k == args.group}
|
||
if not tests:
|
||
print(f"\n 未找到测试组: {args.group}")
|
||
print(f" 可用组: {', '.join(list(ALL_TESTS.keys()) + list(WRITE_TESTS.keys()))}")
|
||
return
|
||
|
||
if args.action:
|
||
filtered = {}
|
||
for grp, items in tests.items():
|
||
matching = [t for t in items if t[0] == args.action]
|
||
if matching:
|
||
filtered[grp] = matching
|
||
tests = filtered
|
||
if not tests:
|
||
print(f"\n 未找到action: {args.action}")
|
||
return
|
||
|
||
total_actions = sum(len(v) for v in tests.values())
|
||
print(f"\n 即将测试 {total_actions} 个 action ({len(tests)} 组)")
|
||
print("-" * 70)
|
||
|
||
# 预取最近消息 msgId,供 forward/recall 真机链路
|
||
last_msg_id = None
|
||
try:
|
||
pre = await run_test(client, "get_messages", "POST", "/api/v3/message/list",
|
||
{"conversation_id": "filehelper", "limit": 3}, "预检")
|
||
msgs = (pre.response.get("data") or {}).get("messages") or []
|
||
if not msgs and isinstance(pre.response.get("data"), dict):
|
||
inner = pre.response["data"].get("data") or pre.response["data"]
|
||
msgs = inner.get("messages") or []
|
||
if msgs:
|
||
last_msg_id = str(msgs[0].get("id") or msgs[0].get("msgId") or msgs[0].get("msg_svr_id") or "")
|
||
print(f" 预检 msg_svr_id={last_msg_id}")
|
||
except Exception as e:
|
||
print(f" 预检 get_messages 跳过: {e}")
|
||
|
||
group_id = ""
|
||
if args.matrix:
|
||
group_id = await prefetch_group_id(client)
|
||
print(f" 预检 group_id={group_id or '(空)'}")
|
||
|
||
for group_name, group_tests in tests.items():
|
||
print(f"\n[{group_name}] 测试中...")
|
||
for action, method, endpoint, params, safe in group_tests:
|
||
p = dict(params)
|
||
if args.matrix:
|
||
_inject_group_id(p, group_id)
|
||
if action == "forward_message" and last_msg_id:
|
||
p["msg_svr_id"] = last_msg_id
|
||
if action == "recall_message" and last_msg_id:
|
||
p["msg_svr_id"] = last_msg_id
|
||
if action == "friend_info":
|
||
p["user_id"] = "filehelper"
|
||
result = await run_test(client, action, method, endpoint, p, group_name)
|
||
report.results.append(result)
|
||
icon = "✓" if result.success else "✗"
|
||
print(f" {icon} {action:<30} {result.elapsed_ms:>5}ms [{result.channel}]")
|
||
if not result.success:
|
||
print(f" └ {result.error[:80]}")
|
||
await asyncio.sleep(1.0)
|
||
|
||
report.end_time = time.time()
|
||
report.print_summary()
|
||
|
||
report_path = os.path.join(os.path.dirname(__file__), "..",
|
||
"data", "e2e_report.json")
|
||
try:
|
||
os.makedirs(os.path.dirname(report_path), exist_ok=True)
|
||
with open(report_path, "w", encoding="utf-8") as f:
|
||
json.dump({
|
||
"timestamp": datetime.now().isoformat(),
|
||
"device_id": DEVICE_ID,
|
||
"total": report.total,
|
||
"passed": report.passed,
|
||
"failed": report.failed,
|
||
"success_rate": report.success_rate,
|
||
"avg_time_ms": report.avg_time,
|
||
"results": [
|
||
{
|
||
"action": r.action, "group": r.group,
|
||
"channel": r.channel, "success": r.success,
|
||
"elapsed_ms": r.elapsed_ms, "error": r.error
|
||
} for r in report.results
|
||
]
|
||
}, f, ensure_ascii=False, indent=2)
|
||
print(f"\n 报告已保存: {report_path}")
|
||
except Exception as e:
|
||
print(f"\n 保存报告失败: {e}")
|
||
|
||
sys.exit(0 if report.failed == 0 else 1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main())
|