优化测试
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -102,5 +102,185 @@ def test_h3c_live_transceiver_rpc_and_parse() -> None:
|
||||
|
||||
transceivers, channels = parse_netconf_response(xml_str, device_name=f"h3c-{H3C_HOST}")
|
||||
|
||||
# 只要返回非空结果,就说明“连接 + filter + 解析”在真实设备上可以工作
|
||||
# 只要返回非空结果,就说明"连接 + filter + 解析"在真实设备上可以工作
|
||||
assert transceivers or channels, "H3C 设备未返回任何 transceiver/channel 数据"
|
||||
|
||||
|
||||
@pytest.mark.h3c_live
|
||||
def test_h3c_config_description_consistency() -> None:
|
||||
"""
|
||||
验证 H3C 设备上所有 transceiver 的 physical channel 的 config/description 是否正确。
|
||||
|
||||
检查规则:
|
||||
- component 名称格式如: "67.TwoHundredGigE1/0/2:1"
|
||||
- 从中提取物理端口: "1/0/2"
|
||||
- channel config/description 应该是: "1/0/2:1", "1/0/2:2", 等
|
||||
- 即:config/description 的端口部分应与 component 名称中的端口部分一致
|
||||
|
||||
目的:发现 H3C 设备上 config/description 配置错误的情况
|
||||
"""
|
||||
if not _can_connect(H3C_HOST, H3C_PORT):
|
||||
pytest.skip(f"H3C NETCONF {H3C_HOST}:{H3C_PORT} 不可达,跳过 live 测试")
|
||||
|
||||
flt = build_transceiver_filter()
|
||||
|
||||
with manager.connect(
|
||||
host=H3C_HOST,
|
||||
port=H3C_PORT,
|
||||
username=H3C_USER,
|
||||
password=H3C_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)
|
||||
|
||||
# 解析 XML 提取详细的 config 和 state 信息
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
NS = {
|
||||
"oc-platform": "http://openconfig.net/yang/platform",
|
||||
"oc-transceiver": "http://openconfig.net/yang/platform/transceiver",
|
||||
}
|
||||
|
||||
root = ET.fromstring(xml_str)
|
||||
components_path = ".//oc-platform:components/oc-platform:component"
|
||||
|
||||
inconsistencies = []
|
||||
total_channels_checked = 0
|
||||
|
||||
for comp in root.findall(components_path, NS):
|
||||
# 获取 component 名称
|
||||
name_elem = comp.find("oc-platform:name", NS)
|
||||
if name_elem is None or not name_elem.text:
|
||||
continue
|
||||
component_name = name_elem.text.strip()
|
||||
|
||||
# 只检查 TRANSCEIVER 类型
|
||||
type_elem = comp.find("oc-platform:state/oc-platform:type", NS)
|
||||
if type_elem is None or "TRANSCEIVER" not in type_elem.text:
|
||||
continue
|
||||
|
||||
# 从 component 名称中提取端口
|
||||
# 格式: "67.TwoHundredGigE1/0/2:1" -> 提取 "1/0/2"
|
||||
expected_port = _extract_port_from_component_name(component_name)
|
||||
if not expected_port:
|
||||
# 无法从 component 名称中提取端口,跳过
|
||||
continue
|
||||
|
||||
# 遍历所有 physical channels
|
||||
channels_path = (
|
||||
"oc-transceiver:transceiver/oc-transceiver:physical-channels/"
|
||||
"oc-transceiver:channel"
|
||||
)
|
||||
for ch in comp.findall(channels_path, NS):
|
||||
total_channels_checked += 1
|
||||
|
||||
# 获取 channel index
|
||||
idx_elem = ch.find("oc-transceiver:index", NS)
|
||||
channel_index = idx_elem.text if idx_elem is not None else "?"
|
||||
|
||||
# 获取 config/description
|
||||
config_desc_elem = ch.find("oc-transceiver:config/oc-transceiver:description", NS)
|
||||
config_desc = config_desc_elem.text.strip() if config_desc_elem is not None and config_desc_elem.text else None
|
||||
|
||||
# 获取 state/description
|
||||
state_desc_elem = ch.find("oc-transceiver:state/oc-transceiver:description", NS)
|
||||
state_desc = state_desc_elem.text.strip() if state_desc_elem is not None and state_desc_elem.text else None
|
||||
|
||||
# 检查 config/description
|
||||
if config_desc:
|
||||
# config/description 应该是 "端口:通道号" 格式,如 "1/0/2:1"
|
||||
if ":" in config_desc:
|
||||
config_port = config_desc.split(":", 1)[0]
|
||||
if config_port != expected_port:
|
||||
inconsistencies.append({
|
||||
"component": component_name,
|
||||
"channel_index": channel_index,
|
||||
"expected_port": expected_port,
|
||||
"config_desc": config_desc,
|
||||
"config_port": config_port,
|
||||
"issue": "config/description 端口部分与 component 名称不一致"
|
||||
})
|
||||
else:
|
||||
# config/description 缺失
|
||||
inconsistencies.append({
|
||||
"component": component_name,
|
||||
"channel_index": channel_index,
|
||||
"expected_port": expected_port,
|
||||
"config_desc": None,
|
||||
"issue": "config/description 缺失"
|
||||
})
|
||||
|
||||
# 同时检查 state/description(仅作信息收集,不作为测试失败条件)
|
||||
# 但我们会记录这些差异,以便了解 state 数据质量
|
||||
pass # state 检查移至单独的报告
|
||||
|
||||
# 生成报告
|
||||
print(f"\n=== H3C Config/Description 一致性检查报告 ===")
|
||||
print(f"检查的 channel 总数: {total_channels_checked}")
|
||||
print(f"发现不一致的 channel 数: {len(inconsistencies)}")
|
||||
|
||||
if inconsistencies:
|
||||
print("\n发现的不一致情况:")
|
||||
for item in inconsistencies:
|
||||
print(f" Component: {item['component']}")
|
||||
print(f" Channel Index: {item['channel_index']}")
|
||||
print(f" 期望端口: {item['expected_port']}")
|
||||
print(f" 实际 config/description: {item.get('config_desc', 'N/A')}")
|
||||
if 'config_port' in item:
|
||||
print(f" config 中的端口: {item['config_port']}")
|
||||
print(f" 问题: {item['issue']}")
|
||||
print()
|
||||
|
||||
# 测试失败,config/description 应该是准确的
|
||||
pytest.fail(
|
||||
f"发现 {len(inconsistencies)} 个 channel 的 config/description 与 component 名称不一致,"
|
||||
"详见上方输出"
|
||||
)
|
||||
else:
|
||||
print("\n✅ 所有 channel 的 config/description 都与 component 名称一致")
|
||||
|
||||
|
||||
def _extract_port_from_component_name(component_name: str) -> str | None:
|
||||
"""
|
||||
从 component 名称中提取物理端口。
|
||||
|
||||
例如:
|
||||
- "67.TwoHundredGigE1/0/2:1" -> "1/0/2"
|
||||
- "63.TwoHundredGigE1/0/1:1" -> "1/0/1"
|
||||
- "323.FourHundredGigE1/0/66" -> "1/0/66"
|
||||
|
||||
返回: 端口字符串,如 "1/0/2",若无法提取则返回 None
|
||||
"""
|
||||
import re
|
||||
|
||||
# 匹配 "数字/数字/数字" 格式的端口
|
||||
# 可能在 GigE 或 FourHundredGigE 等后面,可能带或不带 ":通道号"
|
||||
pattern = r"(\d+/\d+/\d+)"
|
||||
match = re.search(pattern, component_name)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def test_extract_port_from_component_name():
|
||||
"""测试从 component 名称中提取端口的辅助函数(不依赖真实设备)"""
|
||||
# 测试各种可能的命名格式
|
||||
test_cases = [
|
||||
("67.TwoHundredGigE1/0/2:1", "1/0/2"),
|
||||
("63.TwoHundredGigE1/0/1:1", "1/0/1"),
|
||||
("323.FourHundredGigE1/0/66", "1/0/66"),
|
||||
("64.TwoHundredGigE10/20/30:2", "10/20/30"),
|
||||
("SomePrefix99/88/77", "99/88/77"),
|
||||
("no_port_here", None),
|
||||
("", None),
|
||||
]
|
||||
for component_name, expected_port in test_cases:
|
||||
result = _extract_port_from_component_name(component_name)
|
||||
assert result == expected_port, (
|
||||
f"从 '{component_name}' 提取端口失败: "
|
||||
f"期望 '{expected_port}', 实际 '{result}'"
|
||||
)
|
||||
|
||||
@@ -172,3 +172,100 @@ def test_h3c_multi_component_same_serial():
|
||||
assert txs[1].serial == "SN001"
|
||||
assert txs[0].component_name != txs[1].component_name
|
||||
|
||||
|
||||
def test_prefer_config_description_over_state():
|
||||
"""
|
||||
测试当 config 和 state 的 description 不一致时,优先使用 config。
|
||||
这是为了解决 H3C 设备 state/description 有时错误的问题。
|
||||
"""
|
||||
xml = """\
|
||||
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
|
||||
<data>
|
||||
<components xmlns="http://openconfig.net/yang/platform">
|
||||
<component>
|
||||
<name>67.TwoHundredGigE1/0/2:1</name>
|
||||
<state><type>TRANSCEIVER</type></state>
|
||||
<transceiver xmlns="http://openconfig.net/yang/platform/transceiver">
|
||||
<physical-channels>
|
||||
<channel>
|
||||
<index>1</index>
|
||||
<config>
|
||||
<description>1/0/2:1</description>
|
||||
</config>
|
||||
<state>
|
||||
<description>1/0/2:1</description>
|
||||
<output-power><instant>1.53</instant></output-power>
|
||||
</state>
|
||||
</channel>
|
||||
<channel>
|
||||
<index>2</index>
|
||||
<config>
|
||||
<description>1/0/2:2</description>
|
||||
</config>
|
||||
<state>
|
||||
<description>1/0/1:2</description>
|
||||
<input-power><instant>1.07</instant></input-power>
|
||||
</state>
|
||||
</channel>
|
||||
</physical-channels>
|
||||
</transceiver>
|
||||
</component>
|
||||
</components>
|
||||
</data>
|
||||
</rpc-reply>
|
||||
"""
|
||||
_, channels = parse_netconf_response(xml, "h3c-device")
|
||||
assert len(channels) == 2
|
||||
|
||||
ch1 = next(c for c in channels if c.channel_index == 1)
|
||||
ch2 = next(c for c in channels if c.channel_index == 2)
|
||||
|
||||
# 验证 channel 1: config 和 state 一致,结果应该是 1/0/2:1
|
||||
assert ch1.logical_port == "1/0/2"
|
||||
assert ch1.logical_channel == "1/0/2:1"
|
||||
assert ch1.tx_power_dbm == 1.53
|
||||
|
||||
# 验证 channel 2: config 是 1/0/2:2,state 是错误的 1/0/1:2
|
||||
# 应该优先使用 config 的值
|
||||
assert ch2.logical_port == "1/0/2" # 从 config 的 1/0/2:2 解析
|
||||
assert ch2.logical_channel == "1/0/2:2" # 使用 config 的值
|
||||
assert ch2.rx_power_dbm == 1.07
|
||||
|
||||
# 验证两个 channel 的 port 一致(都是 1/0/2)
|
||||
assert ch1.logical_port == ch2.logical_port
|
||||
|
||||
|
||||
def test_fallback_to_state_when_config_missing():
|
||||
"""
|
||||
测试当 config/description 不存在时,fallback 到 state/description。
|
||||
"""
|
||||
xml = """\
|
||||
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
|
||||
<data>
|
||||
<components xmlns="http://openconfig.net/yang/platform">
|
||||
<component>
|
||||
<name>test-component</name>
|
||||
<state><type>TRANSCEIVER</type></state>
|
||||
<transceiver xmlns="http://openconfig.net/yang/platform/transceiver">
|
||||
<physical-channels>
|
||||
<channel>
|
||||
<index>1</index>
|
||||
<state>
|
||||
<description>1/0/5:1</description>
|
||||
</state>
|
||||
</channel>
|
||||
</physical-channels>
|
||||
</transceiver>
|
||||
</component>
|
||||
</components>
|
||||
</data>
|
||||
</rpc-reply>
|
||||
"""
|
||||
_, channels = parse_netconf_response(xml, "device")
|
||||
assert len(channels) == 1
|
||||
|
||||
ch = channels[0]
|
||||
# 没有 config,应该 fallback 到 state
|
||||
assert ch.logical_port == "1/0/5"
|
||||
assert ch.logical_channel == "1/0/5:1"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user