from __future__ import annotations import asyncio from typing import Any, Dict import pytest import yaml from cryptography.fernet import Fernet @pytest.mark.asyncio async def test_async_main_starts_and_stops_cleanly(tmp_path, monkeypatch) -> None: """ 集成级测试: - 使用最小 config.yaml; - 替换 SQLiteDeviceStore / ConnectionManager / scraper_loop / uvicorn.Server; - 验证 async_main 能够启动并在 server 结束后优雅关闭 Scraper 和连接。 """ # 延迟导入以便 monkeypatch 作用于 exporter.main 模块对象 from exporter import main as exporter_main # 准备配置文件 key = Fernet.generate_key().decode() config_path = tmp_path / "config.yaml" config_data: Dict[str, Any] = { "global": { "http_listen": "127.0.0.1:0", "runtime_db_path": str(tmp_path / "devices.db"), "password_secret": key, # 其他字段使用默认值 }, "devices": [], } config_path.write_text(yaml.safe_dump(config_data), encoding="utf-8") created_store: Dict[str, Any] = {} created_cm: Dict[str, Any] = {} seen: Dict[str, Any] = {} class DummyStore: def __init__(self, db_path: str, encryptor, timeout: float = 5.0) -> None: self.db_path = db_path self.encryptor = encryptor self.timeout = timeout self.init_db_called = False self.closed = False created_store["instance"] = self def init_db(self) -> None: self.init_db_called = True def load_runtime_devices(self): return [] def save_device(self, cfg) -> None: # pragma: no cover - not used here raise NotImplementedError def delete_device(self, name: str) -> None: # pragma: no cover - not used here raise NotImplementedError def close(self) -> None: self.closed = True class DummyConnectionManager: def __init__(self, global_cfg) -> None: self.global_cfg = global_cfg self.closed = False created_cm["instance"] = self def acquire_session(self, cfg): # pragma: no cover - Scraper 不会真正调用 raise NotImplementedError def close_all(self) -> None: self.closed = True def dummy_scraper_loop( stop_event, registry, connection_manager, netconf_get_rpc, cache, health, global_cfg, ) -> None: # 记录 stop_event,并等待它被置位后退出 seen["stop_event"] = stop_event stop_event.wait(timeout=1.0) class DummyServer: def __init__(self, config) -> None: self.config = config self.served = False async def serve(self) -> None: # 立即返回以触发 shutdown 流程 await asyncio.sleep(0) self.served = True # 应用 monkeypatch monkeypatch.setattr(exporter_main, "SQLiteDeviceStore", DummyStore) monkeypatch.setattr(exporter_main, "ConnectionManager", DummyConnectionManager) monkeypatch.setattr(exporter_main, "scraper_loop", dummy_scraper_loop) monkeypatch.setattr(exporter_main.uvicorn, "Server", DummyServer) # 运行 async_main(应当能够在短时间内返回) await exporter_main.async_main(["--config", str(config_path)]) store = created_store["instance"] cm = created_cm["instance"] # 验证 DB 初始化与关闭被调用 assert store.init_db_called is True assert store.closed is True # 验证连接被关闭 assert cm.closed is True # 验证 Scraper 收到 stop_event 并退出 assert "stop_event" in seen assert seen["stop_event"].is_set()