184 lines
7.6 KiB
Python
184 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
||
"""总控平台独立运行验收。
|
||
|
||
只在本机启动临时服务,所有可写数据都进内存测试库;不会连接手机、微信或生产库。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import asyncio
|
||
import json
|
||
import socket
|
||
import sys
|
||
import threading
|
||
import time
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
import httpx
|
||
import uvicorn
|
||
|
||
APP_ROOT = Path(__file__).resolve().parents[1] / "app"
|
||
sys.path.insert(0, str(APP_ROOT))
|
||
|
||
from main import app # noqa: E402
|
||
from routers import ai_tasks as ai_tasks_router # noqa: E402
|
||
from routers import device_groups as device_groups_router # noqa: E402
|
||
from routers import integrations_admin as integrations_router # noqa: E402
|
||
from routers import kb as kb_router # noqa: E402
|
||
from routers import releases as releases_router # noqa: E402
|
||
from routers import workbench as workbench_router # noqa: E402
|
||
from services.ai_tasks import AITaskService # noqa: E402
|
||
from services.authorization_grants import AuthorizationGrantService # noqa: E402
|
||
from services.device_groups import DeviceGroupService # noqa: E402
|
||
from services.docs_catalog import DocumentCatalogService # noqa: E402
|
||
from services.integrations_admin import IntegrationAdminService # noqa: E402
|
||
from services.releases import ReleaseService # noqa: E402
|
||
from services.workbench import MemoryControlPlaneStore, WorkbenchService # noqa: E402
|
||
|
||
|
||
def _install_test_services() -> MemoryControlPlaneStore:
|
||
"""把总控各服务切到同一个内存库,避免误碰真实 Mongo。"""
|
||
store = MemoryControlPlaneStore()
|
||
integrations = IntegrationAdminService(store)
|
||
docs = DocumentCatalogService(store)
|
||
grants = AuthorizationGrantService(integrations, docs, store)
|
||
|
||
integrations_router.integration_admin_service = integrations
|
||
integrations_router.docs_catalog_service = docs
|
||
integrations_router.authorization_grant_service = grants
|
||
device_groups_router.device_group_service = DeviceGroupService(store)
|
||
releases_router.release_service = ReleaseService(store)
|
||
workbench_router.workbench_service = WorkbenchService(store)
|
||
ai_tasks_router.ai_task_service = AITaskService(
|
||
store,
|
||
online_checker=lambda _device_id: asyncio.sleep(0, result=False),
|
||
)
|
||
# 健康检查本身会尝试扫描 ADB;这里替换成空扫描,避免本次验收碰手机。
|
||
import services.adb_device as adb_device_module
|
||
|
||
class _NoDeviceADB:
|
||
async def async_scan_devices(self):
|
||
return []
|
||
|
||
adb_device_module.adb_manager = _NoDeviceADB()
|
||
return store
|
||
|
||
|
||
async def _seed(store: MemoryControlPlaneStore) -> None:
|
||
docs = integrations_router.docs_catalog_service
|
||
await docs.ensure_seeded()
|
||
await store.insert(
|
||
"release_records",
|
||
{
|
||
"_key": "smoke-release",
|
||
"channel": "stable",
|
||
"version": "3.0.0-smoke",
|
||
"version_code": 30000,
|
||
"sha256": "a" * 64,
|
||
"size": 1024,
|
||
"min_android": "8.0",
|
||
"download_url": "/static/smoke.apk",
|
||
"published_at": "2026-08-09T00:00:00+00:00",
|
||
},
|
||
)
|
||
|
||
|
||
def _start_server(port: int) -> tuple[uvicorn.Server, threading.Thread]:
|
||
config = uvicorn.Config(app, host="127.0.0.1", port=port, log_level="error", access_log=False)
|
||
server = uvicorn.Server(config)
|
||
thread = threading.Thread(target=server.run, name="console-smoke-server", daemon=True)
|
||
thread.start()
|
||
deadline = time.time() + 10
|
||
while time.time() < deadline:
|
||
try:
|
||
with socket.create_connection(("127.0.0.1", port), timeout=0.2):
|
||
return server, thread
|
||
except OSError:
|
||
time.sleep(0.05)
|
||
server.should_exit = True
|
||
raise RuntimeError("临时总控服务未能在10秒内启动")
|
||
|
||
|
||
def _check(response: httpx.Response, *, status: int = 200) -> dict[str, Any]:
|
||
assert response.status_code == status, f"{response.request.method} {response.request.url} -> {response.status_code}: {response.text[:400]}"
|
||
if "application/json" in response.headers.get("content-type", ""):
|
||
return response.json()
|
||
return {"text": response.text}
|
||
|
||
|
||
async def run_smoke(port: int) -> dict[str, Any]:
|
||
store = _install_test_services()
|
||
await _seed(store)
|
||
server, thread = _start_server(port)
|
||
base = f"http://127.0.0.1:{port}"
|
||
checks: list[str] = []
|
||
try:
|
||
async with httpx.AsyncClient(base_url=base, timeout=8) as client:
|
||
health = _check(await client.get("/health"))
|
||
checks.append("health")
|
||
root = await client.get("/")
|
||
_check(root)
|
||
assert "/assets/" in root.text and "旧页面" not in root.text
|
||
checks.append("new_console_home")
|
||
hub = await client.get("/hub")
|
||
_check(hub)
|
||
checks.append("hub")
|
||
openapi = _check(await client.get("/openapi.json"))
|
||
pairs = [(method, path) for path, item in openapi["paths"].items() for method in item]
|
||
assert len(pairs) == len(set(pairs))
|
||
checks.append("openapi_unique")
|
||
|
||
for path in (
|
||
"/api/v3/workbench/overview",
|
||
"/api/v3/devices",
|
||
"/api/v3/device-groups",
|
||
"/api/v3/releases/latest",
|
||
"/api/v3/integrations/apps",
|
||
"/api/v3/integrations/usage",
|
||
"/api/v3/integrations/docs/catalog",
|
||
"/api/v3/kb/catalog?limit=5",
|
||
"/api/v3/kb/search?q=总控&limit=5",
|
||
"/api/v3/kb/coverage",
|
||
):
|
||
body = _check(await client.get(path))
|
||
assert body.get("code") == 200, (path, body)
|
||
checks.append(path.split("?")[0])
|
||
|
||
headers = {"Idempotency-Key": "smoke-group-1", "X-Actor-Id": "smoke-operator"}
|
||
create_payload = {"name": "联调测试组", "device_ids": ["SAMPLE-DEVICE"], "description": "临时测试数据"}
|
||
first = _check(await client.post("/api/v3/device-groups", json=create_payload, headers=headers))
|
||
group_id = first["data"]["group"]["group_id"]
|
||
replay = _check(await client.post("/api/v3/device-groups", json=create_payload, headers=headers))
|
||
assert replay["data"]["group"]["group_id"] == group_id
|
||
conflict = _check(await client.post("/api/v3/device-groups", json={**create_payload, "name": "异参"}, headers=headers), status=409)
|
||
assert conflict["code"] == "idempotency_conflict"
|
||
readback = _check(await client.get(f"/api/v3/device-groups/{group_id}"))
|
||
assert readback["data"]["name"] == "联调测试组"
|
||
checks.extend(["group_write", "group_replay", "group_conflict", "group_readback"])
|
||
|
||
app_body = _check(await client.post("/api/v3/integrations/apps", json={"name": "联调第三方", "organization": "SAMPLE"}, headers={"X-Actor": "smoke-operator"}))
|
||
app_id = app_body["data"]["app_id"]
|
||
listed = _check(await client.get("/api/v3/integrations/apps"))
|
||
assert any(item["app_id"] == app_id for item in listed["data"]["items"])
|
||
checks.extend(["integration_write", "integration_readback"])
|
||
finally:
|
||
server.should_exit = True
|
||
thread.join(timeout=5)
|
||
|
||
return {"pass": len(checks), "checks": checks, "port": port, "health": health}
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument("--port", type=int, default=18765)
|
||
args = parser.parse_args()
|
||
result = asyncio.run(run_smoke(args.port))
|
||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|