from __future__ import annotations """ 与真实 Ruijie 设备联调的“活体”测试用例。 说明: - 连接参数通过环境变量注入(你已经在 .env 中配置): - RUIJIE_NETCONF_HOST - RUIJIE_NETCONF_PORT - RUIJIE_NETCONF_USER - RUIJIE_NETCONF_PASSWORD 默认行为: - 若未设置 RUIJIE_NETCONF_PASSWORD,或无法建立到指定 host:port 的 TCP 连接, 则使用 pytest.skip() 自动跳过,不影响普通单元测试/CI。 - 仅在本地联调时、显式设置上述环境变量后,此测试才会真正访问设备。 """ import os import socket import pytest from ncclient import manager from exporter.netconf_client import build_transceiver_filter, parse_netconf_response RUIJIE_HOST = os.getenv("RUIJIE_NETCONF_HOST", "127.0.0.1") RUIJIE_PORT = int(os.getenv("RUIJIE_NETCONF_PORT", "9830")) RUIJIE_USER = os.getenv("RUIJIE_NETCONF_USER", "ruijie1-admin") RUIJIE_PASSWORD = os.getenv("RUIJIE_NETCONF_PASSWORD", "") def _can_connect(host: str, port: int, timeout: float = 2.0) -> bool: """快速探测 host:port 是否可连,用于决定是否跳过 live 测试。""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.settimeout(timeout) sock.connect((host, port)) return True except OSError: return False finally: sock.close() @pytest.mark.ruijie_live def test_ruijie_live_transceiver_rpc_and_parse() -> None: """ 使用真实 Ruijie 设备验证: - ncclient 能与设备建立 NETCONF 会话; - build_transceiver_filter() 构造的 subtree filter 在设备上可用; - parse_netconf_response(..., vendor='ruijie') 能正确解析设备返回的 XML。 """ if not RUIJIE_PASSWORD: pytest.skip("RUIJIE_NETCONF_PASSWORD 未设置,跳过 Ruijie live 测试") if not _can_connect(RUIJIE_HOST, RUIJIE_PORT): pytest.skip(f"Ruijie NETCONF {RUIJIE_HOST}:{RUIJIE_PORT} 不可达,跳过 live 测试") flt = build_transceiver_filter() with manager.connect( host=RUIJIE_HOST, port=RUIJIE_PORT, username=RUIJIE_USER, password=RUIJIE_PASSWORD, hostkey_verify=False, timeout=30, allow_agent=False, look_for_keys=False, ) as m: reply = m.get(filter=("subtree", flt)) xml_str = str(reply) # vendor="ruijie" 走厂商感知解析路径 transceivers, channels = parse_netconf_response( xml_str, device_name=f"ruijie-{RUIJIE_HOST}", vendor="ruijie", ) # 只要返回非空结果,就说明 "连接 + filter + 解析" 在真实设备上可以工作 assert transceivers or channels, "Ruijie 设备未返回任何 transceiver/channel 数据" # 至少有一个 transceiver 拥有对应的 channel tx_by_component = {t.component_name: t for t in transceivers} ch_by_component = {} for ch in channels: ch_by_component.setdefault(ch.component_name, []).append(ch) has_tx_with_channel = any( comp in tx_by_component and len(ch_list) > 0 for comp, ch_list in ch_by_component.items() ) assert has_tx_with_channel, ( "Ruijie live 数据中未发现“同时存在 transceiver 与 channel”的组件," "请检查设备返回的 transceiver/physical-channels 数据是否完整" ) # 至少有一个 channel 具有 rx 或 tx power 数值 channels_with_power = [ ch for ch in channels if ch.rx_power_dbm is not None or ch.tx_power_dbm is not None ] assert channels_with_power, ( "Ruijie live 数据中未发现带 rx/tx power 的通道," "请检查设备是否开启了相关光功率采集" ) # 额外 sanity 检查:至少有一个端口 label 不以 TRANSCEIVER- 开头,验证清洗逻辑生效 ports = {t.logical_port for t in transceivers} | {c.logical_port for c in channels} assert any(not p.startswith("TRANSCEIVER-") for p in ports), ( "Ruijie live 数据中未发现清洗后的端口名," "请检查 vendor='ruijie' 解析逻辑是否生效" )