118 lines
4.0 KiB
Python
118 lines
4.0 KiB
Python
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' 解析逻辑是否生效"
|
||
)
|