test_alarm_flow.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. """同步映射 + 非法插接告警端到端测试
  2. 场景(PANEL_dtu_001_2, 地址=2):
  3. 1. 订阅 event / alarm / patchpanel status / response 主题
  4. 2. 清空注入 & 现有 port_state
  5. 3. SYNC_PORT_MAPPING: port 1 期望 AABBCCDDEEFF, port 5 期望 112233445566
  6. 4. 注入正确卡(port 1 = AABBCCDDEEFF, port 5 = 112233445566)-> 预期 CONNECT, 无告警
  7. 5. 注入错误卡(port 1 = DEADBEEF0001)-> 预期 MOVE + ILLEGAL_CONNECT
  8. 6. 注入空(port 5 拔出)-> 预期 DISCONNECT + ILLEGAL_DISCONNECT
  9. 7. 恢复正确卡(port 1 恢复 AABBCCDDEEFF)-> 预期 MOVE,告警清零
  10. 轮询间隔约 5s,脚本每步等待 8s 收 2 轮轮询结果。
  11. """
  12. import json
  13. import time
  14. import uuid
  15. import requests
  16. import paho.mqtt.client as mqtt
  17. DTU_HOST = "192.168.199.149"
  18. DTU_PORT = 5001
  19. BROKER = "xt.wenhq.top"
  20. BPORT = 8581
  21. BUSER, BPASS = "admin", "admin"
  22. PREFIX = "线架系统"
  23. CUSTOMER = "default_customer"
  24. DTU_ID = "dtu_001"
  25. PANEL_ID = "PANEL_dtu_001_2"
  26. DEVICE_ADDR = 2
  27. control_topic = f"{PREFIX}/{CUSTOMER}/dtu/{DTU_ID}/control"
  28. events, alarms, responses, statuses = [], [], [], []
  29. def ts():
  30. return time.strftime("%H:%M:%S")
  31. def on_connect(client, userdata, flags, rc):
  32. client.subscribe(f"{PREFIX}/{CUSTOMER}/#", qos=1)
  33. print(f"[{ts()}] 已订阅 {PREFIX}/{CUSTOMER}/#")
  34. def on_message(client, userdata, msg):
  35. try:
  36. env = json.loads(msg.payload.decode("utf-8"))
  37. pl = env.get("payload", {})
  38. except Exception:
  39. return
  40. if msg.topic.endswith("/event"):
  41. events.append(pl)
  42. print(f"[{ts()}] 🔔 EVENT panel={pl.get('panel_id')} port={pl.get('port_id')} "
  43. f"type={pl.get('event_type')} uid={pl.get('jumper_uid')} prev={pl.get('previous_jumper_uid')}")
  44. elif msg.topic.endswith("/alarm"):
  45. alarms.append(pl)
  46. print(f"[{ts()}] ⚠️ ALARM panel={pl.get('panel_id')} port={pl.get('port_id')} "
  47. f"type={pl.get('alarm_type')} sev={pl.get('severity')} "
  48. f"exp={pl.get('expected_jumper_uid')} act={pl.get('actual_jumper_uid')}")
  49. elif msg.topic.endswith("/response"):
  50. responses.append(pl)
  51. print(f"[{ts()}] ✅ RESP cmd={pl.get('command')} success={pl.get('success')} err={pl.get('error_code')}")
  52. elif "/patchpanel/" in msg.topic and msg.topic.endswith("/status"):
  53. if pl.get("panel_id") == PANEL_ID:
  54. connected = [p for p in pl.get("ports", []) if p.get("jumper_uid")]
  55. illegal = [p for p in pl.get("ports", []) if p.get("status") == "ILLEGAL"]
  56. statuses.append(pl)
  57. if connected or illegal:
  58. summary = ", ".join(f"p{p['port_id']}:{p['status']}={p.get('jumper_uid','-')}"
  59. for p in (connected + illegal))
  60. print(f"[{ts()}] 📊 面板状态: {summary}")
  61. def send_ctrl(client, command, target, params=None):
  62. env = {
  63. "msg_id": f"ctrl_{uuid.uuid4().hex[:8]}",
  64. "timestamp": int(time.time() * 1000),
  65. "dtu_id": DTU_ID, "type": "CONTROL",
  66. "payload": {"command": command, "target": target, "params": params or {}},
  67. }
  68. print(f"\n[{ts()}] ▶ 下发 {command} target={target} params={params}")
  69. client.publish(control_topic, json.dumps(env), qos=1)
  70. return env["msg_id"]
  71. def inject(cards_map):
  72. """cards_map: {port_id: hex12|None}, 全部注入到 DEVICE_ADDR"""
  73. body = {"cards": {str(DEVICE_ADDR): {str(k): v for k, v in cards_map.items()}}}
  74. r = requests.post(f"http://{DTU_HOST}:{DTU_PORT}/api/test/inject_cards",
  75. json=body, timeout=5)
  76. print(f"[{ts()}] 💉 注入 addr={DEVICE_ADDR} {cards_map} -> {r.status_code}")
  77. def clear_inject():
  78. r = requests.post(f"http://{DTU_HOST}:{DTU_PORT}/api/test/inject_cards",
  79. json={"cards": None}, timeout=5)
  80. print(f"[{ts()}] 🧹 清空注入 -> {r.status_code}")
  81. def wait(seconds, label=""):
  82. print(f"[{ts()}] ⏳ 等 {seconds}s {label}")
  83. time.sleep(seconds)
  84. def main():
  85. client = mqtt.Client(client_id="alarm_test")
  86. client.username_pw_set(BUSER, BPASS)
  87. client.on_connect = on_connect
  88. client.on_message = on_message
  89. client.connect(BROKER, BPORT, 60)
  90. client.loop_start()
  91. time.sleep(2)
  92. UID_A = "aabbccddeeff"
  93. UID_B = "112233445566"
  94. UID_WRONG = "deadbeef0001"
  95. # -------- 步骤 1: 清零 & 同步期望映射 --------
  96. print("\n===== 步骤 1: 同步期望映射(port1=A, port5=B)=====")
  97. events.clear(); alarms.clear(); responses.clear()
  98. send_ctrl(client, "SYNC_ALL_MAPPING", "all", {"mappings": [
  99. {"panel_id": PANEL_ID, "port_id": 1, "jumper_uid": UID_A.upper()},
  100. {"panel_id": PANEL_ID, "port_id": 5, "jumper_uid": UID_B.upper()},
  101. ]})
  102. wait(3, "等 SYNC 生效")
  103. # 清空所有假数据(无卡状态)
  104. inject({i: None for i in range(1, 25)})
  105. wait(6, "等一轮无卡轮询稳定 baseline")
  106. # -------- 步骤 2: 插入正确卡 --------
  107. print("\n===== 步骤 2: 插入正确卡(port1=A, port5=B)预期 CONNECT,无 ILLEGAL =====")
  108. events.clear(); alarms.clear()
  109. inject({1: UID_A, 5: UID_B, **{i: None for i in range(2, 25) if i != 5}})
  110. wait(8, "等 CONNECT 事件")
  111. connect_events = [e for e in events if e.get("event_type") == "CONNECT"]
  112. connect_ports = sorted(e.get("port_id") for e in connect_events)
  113. illegal_now = [a for a in alarms if a.get("alarm_type", "").startswith("ILLEGAL_")]
  114. print(f"[结果] CONNECT ports={connect_ports}, ILLEGAL 告警={len(illegal_now)}")
  115. assert 1 in connect_ports and 5 in connect_ports, "预期 port 1/5 CONNECT"
  116. assert len(illegal_now) == 0, f"不应有 ILLEGAL 告警: {illegal_now}"
  117. print("✅ 通过:正确卡不触发 ILLEGAL")
  118. # -------- 步骤 3: 换错卡 --------
  119. print("\n===== 步骤 3: port1 换成错卡(DEADBEEF0001)预期 MOVE + ILLEGAL_CONNECT =====")
  120. events.clear(); alarms.clear()
  121. inject({1: UID_WRONG, 5: UID_B, **{i: None for i in range(2, 25) if i != 5}})
  122. wait(8, "等 MOVE + ILLEGAL_CONNECT")
  123. move_events = [e for e in events if e.get("event_type") == "MOVE" and e.get("port_id") == 1]
  124. illegal_conn = [a for a in alarms if a.get("alarm_type") == "ILLEGAL_CONNECT" and a.get("port_id") == 1]
  125. print(f"[结果] port1 MOVE={len(move_events)}, ILLEGAL_CONNECT={len(illegal_conn)}")
  126. assert move_events, "预期 port1 MOVE 事件"
  127. assert illegal_conn, "预期 port1 ILLEGAL_CONNECT 告警"
  128. a0 = illegal_conn[0]
  129. assert a0.get("expected_jumper_uid", "").lower() == UID_A, \
  130. f"expected_jumper_uid 错: {a0.get('expected_jumper_uid')}"
  131. assert a0.get("actual_jumper_uid", "").lower() == UID_WRONG, \
  132. f"actual_jumper_uid 错: {a0.get('actual_jumper_uid')}"
  133. print(f"✅ 通过:ILLEGAL_CONNECT expected={a0.get('expected_jumper_uid')} actual={a0.get('actual_jumper_uid')} sev={a0.get('severity')}")
  134. # -------- 步骤 4: port5 拔出 --------
  135. print("\n===== 步骤 4: port5 拔出(保持 port1 错卡)预期 DISCONNECT + ILLEGAL_DISCONNECT =====")
  136. events.clear(); alarms.clear()
  137. inject({1: UID_WRONG, **{i: None for i in range(2, 25)}})
  138. wait(8, "等 DISCONNECT + ILLEGAL_DISCONNECT")
  139. disc_events = [e for e in events if e.get("event_type") == "DISCONNECT" and e.get("port_id") == 5]
  140. illegal_disc = [a for a in alarms if a.get("alarm_type") == "ILLEGAL_DISCONNECT" and a.get("port_id") == 5]
  141. print(f"[结果] port5 DISCONNECT={len(disc_events)}, ILLEGAL_DISCONNECT={len(illegal_disc)}")
  142. assert disc_events, "预期 port5 DISCONNECT 事件"
  143. assert illegal_disc, "预期 port5 ILLEGAL_DISCONNECT 告警"
  144. print(f"✅ 通过:ILLEGAL_DISCONNECT expected={illegal_disc[0].get('expected_jumper_uid')}")
  145. # -------- 步骤 5: 累计告警 3 次升级 CRITICAL --------
  146. print("\n===== 步骤 5: 累计告警观察 severity 升级(>=3 变 CRITICAL)=====")
  147. events.clear(); alarms.clear()
  148. inject({1: UID_WRONG, **{i: None for i in range(2, 25)}}) # 保持错卡+port5 拔出
  149. wait(18, "等 3 轮轮询累计告警")
  150. sev1 = [a.get("severity") for a in alarms if a.get("port_id") == 1]
  151. sev5 = [a.get("severity") for a in alarms if a.get("port_id") == 5]
  152. print(f"[结果] port1 severity 序列={sev1}, port5={sev5}")
  153. if any(s == "CRITICAL" for s in sev1 + sev5):
  154. print("✅ 通过:severity 已升级到 CRITICAL")
  155. else:
  156. print("ℹ 告警仍为 WARNING(未达 3 次阈值)")
  157. # -------- 步骤 6: 恢复正确卡 --------
  158. print("\n===== 步骤 6: 恢复正确卡 预期 MOVE + 告警清零 =====")
  159. events.clear(); alarms.clear()
  160. inject({1: UID_A, 5: UID_B, **{i: None for i in range(2, 25) if i != 5}})
  161. wait(8, "等 MOVE 恢复")
  162. move1 = [e for e in events if e.get("event_type") == "MOVE" and e.get("port_id") == 1]
  163. conn5 = [e for e in events if e.get("event_type") == "CONNECT" and e.get("port_id") == 5]
  164. illegal_still = [a for a in alarms if a.get("alarm_type", "").startswith("ILLEGAL_")]
  165. print(f"[结果] port1 MOVE={len(move1)}, port5 CONNECT={len(conn5)}, ILLEGAL={len(illegal_still)}")
  166. assert move1 or conn5, "预期 port1 恢复"
  167. print("✅ 通过:port 恢复到期望 UID,告警停止累计")
  168. # -------- 收尾: 清空注入 --------
  169. print("\n===== 收尾:清空注入 =====")
  170. clear_inject()
  171. print(f"\n===== 汇总:events={len(events)}, alarms={len(alarms)}, responses={len(responses)}, statuses={len(statuses)} =====")
  172. client.loop_stop()
  173. client.disconnect()
  174. if __name__ == "__main__":
  175. try:
  176. main()
  177. print("\n🎉 所有步骤 PASS")
  178. except AssertionError as e:
  179. print(f"\n❌ 断言失败: {e}")
  180. raise
  181. except Exception as e:
  182. print(f"\n❌ 异常: {e}")
  183. raise