import xml.etree.ElementTree as ET import pytest from exporter.netconf_client import ( build_transceiver_filter, parse_netconf_response, parse_port_and_channel, ) def test_build_transceiver_filter_contains_expected_paths(): flt = build_transceiver_filter() assert " 323.FourHundredGigE1/0/66 TRANSCEIVER 45.5 PRESENT Cisco ABC123 0 1/0/66:1 -2.5 """ def test_parse_transceiver_basic(): txs, chs = parse_netconf_response(SAMPLE_XML, "dev1") assert len(txs) == 1 r = txs[0] assert r.device == "dev1" assert r.component_name == "323.FourHundredGigE1/0/66" assert r.temperature_c == 45.5 assert r.vendor == "Cisco" assert r.serial == "ABC123" assert r.logical_port == "1/0/66" assert len(chs) == 1 def test_parse_channel_and_description(): _, chs = parse_netconf_response(SAMPLE_XML, "dev1") assert len(chs) == 1 ch = chs[0] assert ch.logical_port == "1/0/66" assert ch.logical_channel == "1/0/66:1" assert ch.tx_power_dbm == -2.5 def test_missing_temperature_not_fatal(): root = ET.fromstring(SAMPLE_XML) # remove temperature node ns_platform = "{http://openconfig.net/yang/platform}" temp_elem = root.find(f".//{ns_platform}temperature") parent = root.find(f".//{ns_platform}state") if temp_elem is not None and parent is not None: parent.remove(temp_elem) xml_no_temp = ET.tostring(root, encoding="unicode") txs, _ = parse_netconf_response(xml_no_temp, "dev1") assert txs[0].temperature_c is None def test_invalid_power_filtered(): xml_invalid = SAMPLE_XML.replace("-2.5", "2147483647.00") _, chs = parse_netconf_response(xml_invalid, "dev1") assert chs[0].tx_power_dbm is None @pytest.mark.parametrize( "description, component_name, index, expected_port, expected_channel", [ (None, "comp1", 0, "comp1", "comp1:ch0"), ("", "comp1", 1, "comp1", "comp1:ch1"), ("1/0/1:1", "comp1", 2, "1/0/1", "1/0/1:1"), ("GigabitEthernet1/0/1", "comp1", 3, "GigabitEthernet1/0/1", "GigabitEthernet1/0/1:ch3"), ], ) def test_parse_port_and_channel_fallbacks( description, component_name, index, expected_port, expected_channel ): lp, lc = parse_port_and_channel(description, component_name, index) assert lp == expected_port assert lc == expected_channel def test_multiple_channels_missing_description_produce_unique_logical_channel(): xml = """\ comp1 TRANSCEIVER 0 1 """ _, channels = parse_netconf_response(xml, "dev1") assert len(channels) == 2 ch0 = next(c for c in channels if c.channel_index == 0) ch1 = next(c for c in channels if c.channel_index == 1) assert ch0.logical_port == "comp1" assert ch1.logical_port == "comp1" assert ch0.logical_channel != ch1.logical_channel def test_h3c_multi_component_same_serial(): xml_multi = """\ 63.TwoHundredGigE1/0/1:1 TRANSCEIVER SN001 64.TwoHundredGigE1/0/1:2 TRANSCEIVER SN001 """ txs, _ = parse_netconf_response(xml_multi, "h3c") assert len(txs) == 2 assert txs[0].serial == "SN001" 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 = """\ 67.TwoHundredGigE1/0/2:1 TRANSCEIVER 1 1/0/2:1 1/0/2:1 1.53 2 1/0/2:2 1/0/1:2 1.07 """ _, 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 = """\ test-component TRANSCEIVER 1 1/0/5:1 """ _, 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"