test_panel_registration.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. """线架注册模型(地址段扫描 / UID 绑定 / 换架自动接管)单元测试。"""
  2. import sys
  3. import os
  4. from unittest.mock import patch
  5. import pytest
  6. sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  7. import app as app_module # noqa: E402
  8. from app import app # noqa: E402
  9. def make_cards(port_map):
  10. """构造 24 路读卡结果;port_map: {port_id: 'hex12'}"""
  11. cards = []
  12. for ant in range(1, 25):
  13. hex_str = (port_map.get(ant) or '').lower()
  14. cards.append({
  15. 'antenna': ant,
  16. 'card_str': hex_str,
  17. 'present': bool(hex_str),
  18. 'uid': '' if not hex_str else ':'.join(hex_str[i:i + 2] for i in range(0, 12, 2)),
  19. })
  20. return cards
  21. def read_result(cards, panel_uid=None):
  22. return {
  23. 'success': True, 'device_address': 1, 'function_code': 3,
  24. 'cards': cards, 'panel_uid': panel_uid, 'raw_data': 'TEST',
  25. }
  26. class TestPanelRegistration:
  27. def setup_method(self):
  28. self._saved = {
  29. 'stored': dict(app_module.address_config.stored_devices),
  30. 'panel_config': dict(app_module.panel_config),
  31. 'port_state': dict(app_module.port_state),
  32. 'fail': dict(app_module.panel_fail_count),
  33. 'address_range': app_module.dtu_config.get('address_range'),
  34. 'port_mappings': app_module.dtu_config.get('port_mappings'),
  35. 'last_seen': dict(app_module.device_last_seen),
  36. }
  37. self.mock_modbus = patch.object(app_module, 'modbus_client').start()
  38. self.mock_save_dev = patch.object(app_module, 'save_device_config').start()
  39. self.mock_save_dtu = patch.object(app_module, 'save_dtu_config').start()
  40. self.mock_register = patch.object(app_module, 'dtu_register').start()
  41. self.mock_panel_status = patch.object(app_module, 'dtu_publish_panel_status').start()
  42. app_module.address_config.stored_devices.clear()
  43. app_module.panel_config.clear()
  44. app_module.port_state.clear()
  45. app_module.panel_fail_count.clear()
  46. app_module.device_last_seen.clear()
  47. def teardown_method(self):
  48. patch.stopall()
  49. app_module.address_config.stored_devices.clear()
  50. app_module.address_config.stored_devices.update(self._saved['stored'])
  51. app_module.panel_config.clear()
  52. app_module.panel_config.update(self._saved['panel_config'])
  53. app_module.port_state.clear()
  54. app_module.port_state.update(self._saved['port_state'])
  55. app_module.panel_fail_count.clear()
  56. app_module.panel_fail_count.update(self._saved['fail'])
  57. app_module.device_last_seen.clear()
  58. app_module.device_last_seen.update(self._saved['last_seen'])
  59. if self._saved['address_range'] is None:
  60. app_module.dtu_config.pop('address_range', None)
  61. else:
  62. app_module.dtu_config['address_range'] = self._saved['address_range']
  63. if self._saved['port_mappings'] is None:
  64. app_module.dtu_config.pop('port_mappings', None)
  65. else:
  66. app_module.dtu_config['port_mappings'] = self._saved['port_mappings']
  67. def test_takeover_rebinds_uid_and_inherits_mappings(self):
  68. """换架: 原地址探测到新 UID -> 重绑地址、保留 panel_id/映射、自动下发卡号。"""
  69. old_uid = 'a1' * 12
  70. new_uid = 'b2' * 12
  71. pid = 'PANEL_dtu_001_5'
  72. app_module.dtu_config['address_range'] = {'start': 5, 'end': 5}
  73. app_module.dtu_config['port_mappings'] = {pid: {'3': 'AABBCCDDEEFF'}}
  74. app_module.address_config.stored_devices[old_uid] = 5
  75. app_module.panel_config[pid] = {'address': 5, 'position': 5, 'panel_uid': old_uid, 'status': 'offline'}
  76. app_module.panel_fail_count[pid] = 3
  77. self.mock_modbus.read_all_antenna_cards.return_value = read_result(
  78. make_cards({3: 'aabbccddeeff'}), panel_uid=new_uid)
  79. self.mock_modbus.set_card_mapping.return_value = {'success': True}
  80. app_module._sweep_panel_addresses()
  81. devices = app_module.address_config.stored_devices
  82. assert devices.get(new_uid) == 5
  83. assert old_uid not in devices
  84. assert app_module.panel_config[pid]['panel_uid'] == new_uid
  85. assert app_module.panel_config[pid]['status'] == 'online'
  86. assert pid not in app_module.panel_fail_count
  87. # 期望卡号已下发到硬件(继承原面板数据)
  88. args, _ = self.mock_modbus.set_card_mapping.call_args
  89. assert args[0] == 5
  90. assert args[1].get(3) == 'AABBCCDDEEFF'
  91. assert self.mock_save_dev.called
  92. assert self.mock_register.called
  93. def test_probe_same_uid_marks_back_online(self):
  94. """缺失探测回应原 UID -> 恢复在线,不改变绑定。"""
  95. uid = 'c3' * 12
  96. pid = 'PANEL_dtu_001_2'
  97. app_module.dtu_config['address_range'] = {'start': 2, 'end': 2}
  98. app_module.address_config.stored_devices[uid] = 2
  99. app_module.panel_config[pid] = {'address': 2, 'position': 2, 'panel_uid': uid, 'status': 'offline'}
  100. app_module.panel_fail_count[pid] = 3
  101. self.mock_modbus.read_all_antenna_cards.return_value = read_result(
  102. make_cards({}), panel_uid=uid)
  103. app_module._sweep_panel_addresses()
  104. assert app_module.address_config.stored_devices == {uid: 2}
  105. assert app_module.panel_config[pid]['status'] == 'online'
  106. assert pid not in app_module.panel_fail_count
  107. def test_unbound_probe_registers_new_panel(self):
  108. """未绑定地址探测到新线架 -> 绑定地址↔UID。"""
  109. new_uid = 'd4' * 12
  110. app_module.dtu_config['address_range'] = {'start': 4, 'end': 4}
  111. self.mock_modbus.read_all_antenna_cards.return_value = read_result(
  112. make_cards({}), panel_uid=new_uid)
  113. app_module._sweep_panel_addresses()
  114. assert app_module.address_config.stored_devices.get(new_uid) == 4
  115. assert 'PANEL_dtu_001_4' in app_module.panel_config
  116. assert self.mock_save_dev.called
  117. def test_missing_panel_suspends_unbound_probe(self):
  118. """有面板处于缺失/接管状态时,本轮不抽查未绑定地址。"""
  119. old_uid = 'e5' * 12
  120. pid = 'PANEL_dtu_001_2'
  121. app_module.dtu_config['address_range'] = {'start': 2, 'end': 3}
  122. app_module.address_config.stored_devices[old_uid] = 2
  123. app_module.panel_config[pid] = {'address': 2, 'position': 2, 'panel_uid': old_uid, 'status': 'offline'}
  124. app_module.panel_fail_count[pid] = 1
  125. self.mock_modbus.read_all_antenna_cards.return_value = {'error': 'timeout'}
  126. app_module._sweep_panel_addresses()
  127. probed = [c.args[0] for c in self.mock_modbus.read_all_antenna_cards.call_args_list]
  128. assert probed == [2]
  129. def test_read_failure_threshold_marks_offline(self):
  130. """连续失败达到阈值 -> 标记离线(并发布离线状态)。"""
  131. uid = 'f6' * 12
  132. pid = 'PANEL_dtu_001_3'
  133. app_module.dtu_config['address_range'] = {'start': 3, 'end': 3}
  134. app_module.address_config.stored_devices[uid] = 3
  135. app_module.panel_config[pid] = {'address': 3, 'position': 3, 'panel_uid': uid, 'status': 'online'}
  136. self.mock_modbus.read_all_antenna_cards.return_value = {'error': 'timeout'}
  137. for _ in range(app_module.PANEL_MISS_THRESHOLD):
  138. app_module._sweep_panel_addresses()
  139. assert app_module.panel_config[pid]['status'] == 'offline'
  140. assert app_module.panel_fail_count[pid] == app_module.PANEL_MISS_THRESHOLD
  141. self.mock_panel_status.assert_any_call(pid, 3, None, online=False)
  142. def test_panel_unbind_api(self):
  143. """解绑 API: 移除绑定与面板配置。"""
  144. client = app.test_client()
  145. uid = '99' * 12
  146. pid = 'PANEL_dtu_001_7'
  147. app_module.address_config.stored_devices[uid] = 7
  148. app_module.panel_config[pid] = {'address': 7, 'position': 7, 'panel_uid': uid, 'status': 'online'}
  149. resp = client.post('/api/panel/unbind', json={'address': 7})
  150. data = resp.get_json()
  151. assert resp.status_code == 200 and data['success']
  152. assert uid not in app_module.address_config.stored_devices
  153. assert pid not in app_module.panel_config
  154. assert self.mock_save_dev.called
  155. def test_update_dtu_config_address_range(self):
  156. """DTU 配置 API: 地址段写入并校验。"""
  157. client = app.test_client()
  158. resp = client.post('/api/dtu/config', json={'address_range': {'start': 5, 'end': 60}})
  159. assert resp.status_code == 200
  160. assert app_module.dtu_config['address_range'] == {'start': 5, 'end': 60}
  161. assert self.mock_save_dtu.called
  162. resp = client.post('/api/dtu/config', json={'address_range': {'start': 70, 'end': 10}})
  163. assert resp.status_code == 400
  164. assert app_module.dtu_config['address_range'] == {'start': 5, 'end': 60}
  165. def test_address_range_fallback(self):
  166. app_module.dtu_config['address_range'] = {'start': 60, 'end': 10}
  167. assert app_module._address_range() == (1, 50)
  168. app_module.dtu_config['address_range'] = {'start': 3, 'end': 9}
  169. assert app_module._address_range() == (3, 9)
  170. app_module.dtu_config['address_range'] = None
  171. assert app_module._address_range() == (1, 50)
  172. if __name__ == '__main__':
  173. pytest.main([__file__, '-v'])