from __future__ import annotations
import threading
import time
from types import SimpleNamespace
from exporter.config import DeviceConfig, GlobalConfig
from exporter.models import DeviceHealthState, DeviceMetricsSnapshot
from exporter.registry import DeviceRegistry, DeviceRuntimeState
from exporter.scraper import run_one_scrape_round, scrape_device, scraper_loop
class DummyConnectionManager:
def __init__(self) -> None:
self.acquired = []
self.invalidated = []
def acquire_session(self, cfg: DeviceConfig):
self.acquired.append(cfg.name)
return SimpleNamespace() # manager 对象对测试无关紧要
def mark_session_invalid(self, name: str) -> None:
self.invalidated.append(name)
def close_all(self) -> None: # pragma: no cover - not used here
pass
def test_scrape_device_success_updates_cache_and_health_and_registry():
global_cfg = GlobalConfig()
global_cfg.scrape_interval_seconds = 10
global_cfg.failure_threshold = 3
global_cfg.max_backoff_factor = 8
dev_cfg = DeviceConfig(
name="dev1",
host="1.1.1.1",
port=830,
username="u",
password="p",
)
registry = DeviceRegistry(global_scrape_interval=global_cfg.scrape_interval_seconds)
registry.register_static_device(dev_cfg)
state = registry.get_enabled_devices(time.time())[0]
cm = DummyConnectionManager()
# 构造一个包含简单 transceiver/channel 的 XML
xml_reply = """
comp1
TRANSCEIVER
40.0
PRESENT
H3C
SN001
0
1/0/1:1
-2.5
""".strip()
def fake_get_rpc(_mgr, _flt: str) -> str:
return xml_reply
cache: dict[str, DeviceMetricsSnapshot] = {}
health: dict[str, DeviceHealthState] = {}
now = time.time()
scrape_device(
now,
state,
registry,
cm,
fake_get_rpc,
cache,
health,
global_cfg,
failure_threshold=global_cfg.failure_threshold,
max_backoff_factor=global_cfg.max_backoff_factor,
)
# cache 中应有快照,且包含一个 transceiver 和一个 channel
assert "dev1" in cache
snapshot = cache["dev1"]
assert len(snapshot.transceivers) == 1
assert len(snapshot.channels) == 1
# health 状态应标记为成功
hs = health["dev1"]
assert hs.last_scrape_success is True
assert hs.last_error_type is None
# registry 的调度状态应更新(下次采集时间向后推进)
state_after = registry.get_enabled_devices(now + 100)[0]
assert state_after.next_scrape_at > now
def test_scrape_device_failure_updates_health_and_invalidates_session(monkeypatch):
global_cfg = GlobalConfig()
global_cfg.scrape_interval_seconds = 10
global_cfg.failure_threshold = 1
global_cfg.max_backoff_factor = 8
dev_cfg = DeviceConfig(
name="dev2",
host="1.1.1.2",
port=830,
username="u",
password="p",
)
registry = DeviceRegistry(global_scrape_interval=global_cfg.scrape_interval_seconds)
registry.register_static_device(dev_cfg)
state = registry.get_enabled_devices(time.time())[0]
cm = DummyConnectionManager()
def failing_get_rpc(_mgr, _flt: str) -> str:
raise RuntimeError("filter failed")
cache: dict[str, DeviceMetricsSnapshot] = {}
health: dict[str, DeviceHealthState] = {}
now = time.time()
scrape_device(
now,
state,
registry,
cm,
failing_get_rpc,
cache,
health,
global_cfg,
failure_threshold=global_cfg.failure_threshold,
max_backoff_factor=global_cfg.max_backoff_factor,
)
# cache 中不应有 dev2 的快照
assert "dev2" not in cache
# health 状态应为失败,且 error_type 为 FilterError
hs = health["dev2"]
assert hs.last_scrape_success is False
assert hs.last_error_type == "FilterError"
# 连接应被标记为无效
assert "dev2" in cm.invalidated
def test_run_one_scrape_round_invokes_scrape_for_enabled_devices(monkeypatch):
global_cfg = GlobalConfig()
global_cfg.scrape_interval_seconds = 5
global_cfg.failure_threshold = 3
global_cfg.max_backoff_factor = 8
dev_cfg = DeviceConfig(
name="dev3",
host="1.1.1.3",
port=830,
username="u",
password="p",
)
registry = DeviceRegistry(global_scrape_interval=global_cfg.scrape_interval_seconds)
registry.register_static_device(dev_cfg)
state = registry.get_enabled_devices(time.time())[0]
cm = DummyConnectionManager()
def fake_get_rpc(_mgr, _flt: str) -> str:
# 返回最小合法 XML
return """
compX
TRANSCEIVER
0
""".strip()
cache: dict[str, DeviceMetricsSnapshot] = {}
health: dict[str, DeviceHealthState] = {}
now = time.time()
# 调用 run_one_scrape_round,确保会调用到 scrape_device
run_one_scrape_round(
now,
registry,
cm,
fake_get_rpc,
cache,
health,
global_cfg,
failure_threshold=global_cfg.failure_threshold,
max_backoff_factor=global_cfg.max_backoff_factor,
)
# dev3 应该被采集一次,并产生快照
assert "dev3" in cache
assert "dev3" in health
def test_scraper_loop_covers_wait_true_and_false(monkeypatch):
"""
覆盖 scraper_loop 中 stop_event.wait 的 True/False 两个分支,
以及 while 条件的退出分支。
"""
global_cfg = GlobalConfig()
global_cfg.scrape_interval_seconds = 0 # 立即触发多轮调度
registry = DeviceRegistry(global_scrape_interval=global_cfg.scrape_interval_seconds)
cm = DummyConnectionManager()
# 使用计数器控制 run_one_scrape_round 调用次数
call_count = {"n": 0}
def fake_get_rpc(_mgr, _flt: str) -> str:
# 返回最小合法 XML
return """
compY
TRANSCEIVER
""".strip()
cache: dict[str, DeviceMetricsSnapshot] = {}
health: dict[str, DeviceHealthState] = {}
# monkeypatch run_one_scrape_round,使其在第二次调用时设置 stop_event
from exporter import scraper as scraper_mod
real_run_one = scraper_mod.run_one_scrape_round
def counting_run_one(
now: float,
registry_: DeviceRegistry,
connection_manager_,
netconf_get_rpc_,
cache_,
health_,
global_cfg_,
failure_threshold: int,
max_backoff_factor: int,
):
call_count["n"] += 1
if call_count["n"] >= 2:
# 第二次调用后设置 stop_event,确保有一次 wait 返回 False,一次返回 True
stop_event.set()
return real_run_one(
now,
registry_,
connection_manager_,
netconf_get_rpc_,
cache_,
health_,
global_cfg_,
failure_threshold,
max_backoff_factor,
)
monkeypatch.setattr(scraper_mod, "run_one_scrape_round", counting_run_one)
stop_event = threading.Event()
t = threading.Thread(
target=scraper_loop,
args=(stop_event, registry, cm, fake_get_rpc, cache, health, global_cfg),
daemon=True,
)
t.start()
t.join(timeout=5.0)
# 至少调用了两次 run_one_scrape_round(一次 wait=False,一次 wait=True)
assert call_count["n"] >= 2
def test_scrape_device_preserves_existing_health_entry():
"""
第二次采集同一设备时,health 字典中已存在条目,应走 device in health 分支。
"""
global_cfg = GlobalConfig()
dev_cfg = DeviceConfig(
name="dev4",
host="1.1.1.4",
port=830,
username="u",
password="p",
)
registry = DeviceRegistry(global_scrape_interval=global_cfg.scrape_interval_seconds)
registry.register_static_device(dev_cfg)
state = registry.get_enabled_devices(time.time())[0]
cm = DummyConnectionManager()
xml_reply = """
compZ
TRANSCEIVER
0
""".strip()
def fake_get_rpc(_mgr, _flt: str) -> str:
return xml_reply
cache: dict[str, DeviceMetricsSnapshot] = {}
health: dict[str, DeviceHealthState] = {}
now = time.time()
# 第一次采集,health 中还没有 dev4
scrape_device(
now,
state,
registry,
cm,
fake_get_rpc,
cache,
health,
global_cfg,
failure_threshold=global_cfg.failure_threshold,
max_backoff_factor=global_cfg.max_backoff_factor,
)
assert "dev4" in health
# 第二次采集,应走 device 已存在分支
scrape_device(
now + 1,
state,
registry,
cm,
fake_get_rpc,
cache,
health,
global_cfg,
failure_threshold=global_cfg.failure_threshold,
max_backoff_factor=global_cfg.max_backoff_factor,
)
# health 条目仍然存在且状态为成功
assert health["dev4"].last_scrape_success is True